1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

Compare commits

...

17 Commits

Author SHA1 Message Date
KenanZhu 1b172ad396 style(*): some small style changes 2025-11-30 18:46:12 +08:00
KenanZhu 05c9d433f4 hotfix(LibRenew): fix the serious bug that the renew process always failed
in this hotfix, we fix the renew bug because we
do not refresh the page after renew the seat,
this will make the subsequent operators, such as
logout ... unable to in progress.
2025-11-30 18:42:20 +08:00
KenanZhu 65ca40438d fix(ALMainWindow.ui): fix the convert error of ALMainWindow.ui
we fix the convert error of ALMainWindow.ui by
using pyside6-uic to convert the ui file to python code
2025-11-29 22:31:18 +08:00
KenanZhu 0a8763add5 feat(gui): breaking changes - Timer Task Management
1. we add menu actions 'manual' and 'about', so
you can click actions to open manual and about dialog.
2. we introduce timer task management feature, so
you can add, delete timer tasks to auto run task.
3. other style improvement in gui...
2025-11-29 20:03:45 +08:00
KenanZhu c5e589f3d1 fix(ALConfigWidget): optimize the logic when delete user list item 2025-11-29 19:52:22 +08:00
KenanZhu 5e5deba773 fix(LibReserve): fix the mistakely passed parameter 'reserve_info'
we forget to pass the username because the
'reserve_info' do not contain the username
2025-11-28 15:15:39 +08:00
KenanZhu 842fb434f4 feat(AutoLib): new feature 'Auto Renew' 2025-11-28 15:03:51 +08:00
KenanZhu 6cabddf0cd fix(operators): optimized the reserve information pre-check and more readable output 2025-11-28 15:00:09 +08:00
KenanZhu 0322558339 fix(operators): the operations's result message only show in their output queue 2025-11-28 14:58:13 +08:00
KenanZhu 703ee527ae fix(LibChecker): fix the checker of check in and renew
we only check the reservations and their status in
today's record, and return the checked renewable
record for the upcoming new feature 'Auto-Renew'
2025-11-28 14:54:37 +08:00
KenanZhu 9a925fecb6 fix(operators): fix some type hint, and add imports for LibRenew 2025-11-28 14:53:08 +08:00
KenanZhu 189fddfb6a fix(LibReserve): more fast operations of reserve 2025-11-28 14:46:17 +08:00
KenanZhu c2d53a8b78 chore(*): refactor the project structure 2025-11-25 08:48:18 +08:00
KenanZhu b99431476a hotfix(LibChecker): optimize the reserve records check process 2025-11-22 15:12:40 +08:00
KenanZhu 977c0835b7 hotfix(ALMainWindow): fix the config file paths initialization 2025-11-22 15:11:25 +08:00
KenanZhu cd565ec57d feat(gui.SeatMapWidget): add seat select map widget 2025-11-22 14:29:01 +08:00
KenanZhu 9f17474c1b fix(gui): optimize the config files' status management 2025-11-22 14:27:40 +08:00
39 changed files with 3599 additions and 958 deletions
+10 -6
View File
@@ -8,10 +8,14 @@ build/
dist/
model/*.onnx
driver/*.exe
gui/configs/*.json
gui/translators/qtbase_zh_CN.qm
gui/AutoLibraryResources.py
gui/AutoLibraryResource.py
gui/Ui_ALMainWindow.py
gui/Ui_ALConfigWidget.py
src/gui/configs/*.json
src/gui/translators/qtbase_zh_CN.qm
src/gui/AutoLibraryResources.py
src/gui/AutoLibraryResource.py
src/gui/Ui_ALMainWindow.py
src/gui/Ui_ALConfigWidget.py
src/gui/Ui_ALTimerTaskWidget.py
src/gui/Ui_ALAddTimerTaskDialog.py
src/gui/Ui_ALAboutDialog.py
Main.spec
-34
View File
@@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import queue
from LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
pass
-317
View File
@@ -1,317 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
)
from PySide6.QtWidgets import (
QMainWindow, QMenu
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon
)
from .Ui_ALMainWindow import Ui_ALMainWindow
from .ALConfigWidget import ALConfigWidget
from . import AutoLibraryResource
from AutoLib import AutoLib
from ConfigReader import ConfigReader
class AutoLibWorker(QThread):
finishedSignal = Signal()
showTraceSignal = Signal(str)
showMsgSignal = Signal(str)
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__()
self.__input_queue = input_queue
self.__output_queue = output_queue
self.__config_paths = config_paths
self.__stopped = False
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self.showTraceSignal.emit(
"配置文件路径不存在, 请检查配置文件路径是否正确。"
)
return False
return True
def run(
self
):
auto_lib = None
try:
if not self.checkTimeAvailable():
self.showTraceSignal.emit(
"当前时间不在图书馆开放时间内。\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return
if not self.checkConfigPaths():
return
self.showTraceSignal.emit("AutoLibrary 开始运行")
auto_lib = AutoLib(
self.__input_queue,
self.__output_queue,
)
auto_lib.run(
ConfigReader(self.__config_paths["system"]),
ConfigReader(self.__config_paths["users"]),
)
except Exception as e:
self.showTraceSignal.emit(
f"AutoLibrary 运行时发生异常 : {e}"
)
finally:
if auto_lib:
auto_lib.close()
self.showTraceSignal.emit("AutoLibrary 运行结束")
self.finishedSignal.emit()
def stop(
self
):
self.__stopped = True
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
def __init__(
self
):
super().__init__()
self.__class_name = self.__class__.__name__
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
}
self.__alConfigWidget = None
self.__auto_lib_thread = None
self.modifyUi()
self.connectSignals()
self.startMsgPolling()
def modifyUi(
self
):
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
self.setWindowIcon(icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
def connectSignals(
self
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
if self.__timer and self.__timer.isActive():
self.__timer.stop()
if self.__alConfigWidget:
self.__alConfigWidget.close()
super().closeEvent(event)
def appendToTextEdit(
self,
text: str
):
cursor = self.MessageIOTextEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text + "\n")
self.MessageIOTextEdit.setTextCursor(cursor)
self.MessageIOTextEdit.ensureCursorVisible()
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def startMsgPolling(
self
):
self.__timer = QTimer()
self.__timer.timeout.connect(self.pollMsgQueue)
self.__timer.start(100)
def setControlButtons(
self,
config_button_enabled: bool,
start_button_enabled: bool,
stop_button_enabled: bool
):
self.ConfigButton.setEnabled(config_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
self.StopButton.setEnabled(stop_button_enabled)
@Slot()
def showMsg(
self,
msg: str
):
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
@Slot()
def showTrace(
self,
msg: str
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
@Slot()
def pollMsgQueue(
self
):
try:
while True:
msg = self.__output_queue.get_nowait()
self.appendToTextEdit(msg)
except queue.Empty:
pass
@Slot(dict)
def onConfigWidgetClosed(
self,
config_paths: dict
):
if self.__alConfigWidget:
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.ConfigButton.setEnabled(True)
self.StartButton.setEnabled(True)
self.StopButton.setEnabled(False)
self.__config_paths = config_paths
@Slot()
def onConfigButtonClicked(
self
):
if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(
self,
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.setWindowFlags(Qt.Window)
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
@Slot()
def onStartButtonClicked(
self
):
self.setControlButtons(False, False, True)
if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker(
self.__input_queue,
self.__output_queue,
self.__config_paths,
)
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
self.__auto_lib_thread.start()
@Slot()
def onStopButtonClicked(
self
):
if self.__auto_lib_thread:
self.showTrace("正在停止操作......")
self.__auto_lib_thread.stop()
self.__auto_lib_thread.wait()
self.showTrace("操作已停止")
self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace)
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(True, True, False)
@Slot()
def onSendButtonClicked(
self
):
msg = self.MessageEdit.text().strip()
if not msg:
return
self.showMsg(msg)
self.MessageEdit.clear()
+1 -2
View File
@@ -1,6 +1,5 @@
# AutoLibrary
请访问[AutoLibrary 网站](http://autolibrary.cv)
请访问[AutoLibrary 网站](http://autolibrary.cv)\
Please access the [AutoLibrary Website](http://autolibrary.cv)
BIN
View File
Binary file not shown.
View File
+1 -1
View File
@@ -9,7 +9,7 @@ See the LICENSE file for details.
"""
import queue
from MsgBase import MsgBase
from base.MsgBase import MsgBase
class LibOperator(MsgBase):
View File
+8
View File
@@ -0,0 +1,8 @@
"""
Base module for the AutoLibrary project.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.\
- LibOperator: Base class for library operators.
"""
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import sys
import platform
from PySide6.QtGui import (
QIcon
)
from PySide6.QtWidgets import (
QDialog, QApplication
)
from PySide6.QtCore import (
QTimer, Qt
)
from gui.AppInfo import AL_VERSION
from gui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui import AutoLibraryResource
class ALAboutDialog(QDialog, Ui_ALAboutDialog):
def __init__(
self,
parent=None
):
super().__init__(parent)
self.setupUi(self)
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary.ico").pixmap(48, 48))
info_text = self.generateAboutText()
self.AboutInfoEdit.setHtml(info_text)
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals(
self
):
self.CopyButton.clicked.connect(self.copyAboutInfo)
def generateAboutText(
self
):
os_info = self.getOSInfo()
about_text = f"""
<h4>Version Information:</h4>
Version: {AL_VERSION}<br>
Python version: {platform.python_version()}<br>
Qt version: {self.getQtVersion()}<br>
<h4>Author Information:</h4>
Developer: KenanZhu<br>
Contact: nanoki_zh@163.com<br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
<h4>Project Information:</h4>
License: MIT License<br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
<h4>System Information:</h4>
Processor: {platform.processor()}<br>
Operating system: {os_info['system']}<br>
System version: {os_info['version']}<br>
System architecture: {os_info['architecture']}<br>
"""
return about_text
def getOSInfo(
self
):
system = platform.system()
version = platform.version()
architecture = platform.architecture()[0]
if system == "Windows":
try:
version = platform.win32_ver()[1]
except:
pass
elif system == "Darwin":
try:
version = platform.mac_ver()[0]
except:
pass
elif system == "Linux":
try:
import distro # try to get Linux distro info
version = f"{distro.name()} {distro.version()}"
except ImportError:
pass
return {
'system': system,
'version': version,
'architecture': architecture
}
def getQtVersion(
self
):
try:
from PySide6.QtCore import qVersion
return qVersion()
except:
return "Unknown"
def copyAboutInfo(
self
):
about_text = self.AboutInfoEdit.toPlainText()
clipboard = QApplication.clipboard()
clipboard.setText(about_text)
original_text = self.CopyButton.text()
self.CopyButton.setText("已复制")
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
+140
View File
@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAboutDialog</class>
<widget class="QDialog" name="ALAboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>关于 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALAboutDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="LogoLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="LogoIconLabel">
<property name="minimumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="LogoTextLabel">
<property name="font">
<font>
<pointsize>24</pointsize>
<bold>true</bold>
</font>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>AutoLibrary</string>
</property>
<property name="margin">
<number>0</number>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTextEdit" name="AboutInfoEdit">
<property name="font">
<font>
<family>Courier New</family>
<bold>false</bold>
</font>
</property>
<property name="lineWrapMode">
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="CopyButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>复制</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+148
View File
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import uuid
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QDateTime
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
class TimerTaskStatus(Enum):
PENDING = "等待中"
READY = "已就绪"
RUNNING = "执行中"
EXECUTED = "已执行"
OUTDATED = "已过期"
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
def modifyUi(
self
):
self.TimerTypeComboBox.setCurrentIndex(0)
self.SpecificTimerWidget = QWidget()
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
self.SpecificDateTimeEdit = QDateTimeEdit()
self.SpecificDateTimeEdit.setCalendarPopup(True)
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0)
self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0)
self.RelativeDaySpinBox.setMaximum(365)
self.RelativeDaySpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0)
self.RelativeHourSpinBox = QSpinBox()
self.RelativeHourSpinBox.setMinimum(0)
self.RelativeHourSpinBox.setMaximum(23)
self.RelativeHourSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1)
self.RelativeMinuteSpinBox = QSpinBox()
self.RelativeMinuteSpinBox.setMinimum(0)
self.RelativeMinuteSpinBox.setMaximum(59)
self.RelativeMinuteSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2)
self.RelativeSecondSpinBox = QSpinBox()
self.RelativeSecondSpinBox.setMinimum(0)
self.RelativeSecondSpinBox.setMaximum(59)
self.RelativeSecondSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
def connectSignals(
self
):
self.CancelButton.clicked.connect(self.reject)
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
def getTimerTask(
self
) -> dict:
added_time = datetime.now()
if not self.TaskNameLineEdit.text():
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
else:
name = self.TaskNameLineEdit.text()
timer_type_index = self.TimerTypeComboBox.currentIndex()
silent = not self.ShowBeforeRunRadioButton.isChecked()
if timer_type_index == 0:
execute_time = self.SpecificDateTimeEdit.dateTime()
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
else:
execute_time = datetime.now() + timedelta(
days = self.RelativeDaySpinBox.value(),
hours = self.RelativeHourSpinBox.value(),
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
return {
"name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
"add_time": added_time,
"status": TimerTaskStatus.PENDING,
"executed": False
}
@Slot(int)
def onTimerTypeComboBoxIndexChanged(
self,
index: int
):
self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1)
+249
View File
@@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAddTimerTaskDialog</class>
<widget class="QDialog" name="ALAddTimerTaskDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>添加定时任务 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TaskNameLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TaskNameLabel">
<property name="minimumSize">
<size>
<width>60</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>任务名称:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="TaskNameLineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="TimerConfigGroupBox">
<property name="title">
<string>定时设置</string>
</property>
<layout class="QVBoxLayout" name="TimerConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TimerTypeLabel">
<property name="text">
<string>定时类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTypeComboBox">
<item>
<property name="text">
<string>特定时间</string>
</property>
</item>
<item>
<property name="text">
<string>相对时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="TaskConfigGroupBox">
<property name="title">
<string>运行设置</string>
</property>
<layout class="QGridLayout" name="TaskConfigLayout">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="text">
<string>静默运行</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ControLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="CancelButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ConfirmButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -18,10 +18,12 @@ from PySide6.QtWidgets import (
)
from PySide6.QtGui import QCloseEvent
from .Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.SeatMapWidget import SeatMapWidget
from gui.SeatMapTable import seats_maps
from ConfigReader import ConfigReader
from ConfigWriter import ConfigWriter
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -32,27 +34,24 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self,
parent = None,
config_paths = {
"system":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
"users":
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
"system": "",
"users": ""
}
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
self.__config_paths = config_paths
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
if not self.__system_config_data:
self.initlizeDefaultConfig("system")
if not self.__users_config_data:
self.initlizeDefaultConfig("users")
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
self.__config_data = {"system": {}, "users": {}}
self.__seat_map_widget = None
self.modifyUi()
self.connectSignals()
self.initlizeFloorRoomMap()
self.initlizeDefaultConfigPaths()
if not self.initlizeConfigs():
self.close()
def modifyUi(
@@ -69,6 +68,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
self.SelectSeatsButton.clicked.connect(self.onSelectSeatsButtonClicked)
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
@@ -129,42 +129,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
def initlizeDefaultConfigPaths(
self
) -> dict:
):
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
return {
self.__default_config_paths = {
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
}
def initlizeDefaultConfig(
self,
which: str
):
default_config_paths = self.initlizeDefaultConfigPaths()
if which == "system":
self.__system_config_data = self.defaultSystemConfig()
self.__config_paths["system"] = default_config_paths["system"]
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
elif which == "users":
self.__users_config_data = self.defaultUsersConfig()
self.__config_paths["users"] = default_config_paths["users"]
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
if which == "system":
file_type = "系统配置文件"
elif which == "users":
file_type = "用户配置文件"
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"{file_type}已初始化, \n"\
f" 文件路径: {self.__config_paths[which]}"
)
def initlizeConfigToWidget(
self,
which: str,
@@ -180,6 +154,63 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
def initlizeConfig(
self,
which: str
) -> bool:
msg = ""
is_success = True
if which == "system":
system_config_path = self.__config_paths[which]
if not os.path.exists(system_config_path):
self.__config_data[which] = self.defaultSystemConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveSystemConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"系统配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
is_success = False
else:
self.__config_data[which] = self.loadSystemConfig(system_config_path)
if self.__config_data[which] is None:
is_success = False
elif which == "users":
users_config_path = self.__config_paths[which]
if not os.path.exists(users_config_path):
self.__config_data[which] = self.defaultUsersConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveUsersConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else:
is_success = False
else:
self.__config_data[which] = self.loadUsersConfig(users_config_path)
if self.__config_data[which] is None:
is_success = False
if msg:
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"配置文件初始化完成: \n{msg}"
)
return is_success
def initlizeConfigs(
self
) -> bool:
is_success = True
for which in ["system", "users"]:
if not self.__config_paths[which]:
self.__config_paths[which] = self.__default_config_paths[which]
if not self.initlizeConfig(which):
is_success = False
break
self.initlizeConfigToWidget(which, self.__config_data[which])
return is_success
def defaultSystemConfig(
self
) -> dict:
@@ -275,6 +306,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.MaxEndTimeDiffSpinBox.setValue(30)
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
self.SatisfyDurationCheckBox.setChecked(False)
self.ExpectRenewDurationSpinBox.setValue(1.0)
self.MaxRenewTimeDiffSpinBox.setValue(30)
self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserConfigFromUserInfoWidget(
@@ -286,7 +320,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"password": self.PasswordEdit.text(),
"reserve_info": {
"begin_time":{},
"end_time": {}
"end_time": {},
"renew_time": {}
}
}
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
@@ -302,6 +337,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
user_config["reserve_info"]["renew_time"]["expect_duration"] = self.ExpectRenewDurationSpinBox.value()
user_config["reserve_info"]["renew_time"]["max_diff"] = self.MaxRenewTimeDiffSpinBox.value()
user_config["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked()
return user_config
@@ -340,6 +378,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
self.ExpectRenewDurationSpinBox.setValue(user_config["reserve_info"]["renew_time"]["expect_duration"])
self.MaxRenewTimeDiffSpinBox.setValue(user_config["reserve_info"]["renew_time"]["max_diff"])
self.PreferLateRenewTimeCheckBox.setChecked(not user_config["reserve_info"]["renew_time"]["prefer_early"])
except:
QMessageBox.warning(
self,
@@ -448,21 +489,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> bool:
if users_config_path:
self.__users_config_data = self.defaultUsersConfig()
self.__config_data["users"] = self.defaultUsersConfig()
for index in range(self.UserListWidget.count()):
user_config = self.collectUserConfigFromUserListWidget(index)
if user_config:
self.__users_config_data["users"].append(user_config)
self.__config_data["users"]["users"].append(user_config)
if not self.saveUsersConfig(
users_config_path,
self.__users_config_data
self.__config_data["users"]
):
return False
if system_config_path:
self.__system_config_data = self.collectSystemConfigFromWidget()
self.__config_data["system"] = self.collectSystemConfigFromWidget()
if not self.saveSystemConfig(
system_config_path,
self.__system_config_data
self.__config_data["system"]
):
return False
return True
@@ -486,12 +527,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
system_config = self.loadSystemConfig(config_path)
users_config = self.loadUsersConfig(config_path)
if system_config is not None:
self.__system_config_data.update(system_config)
self.setSystemConfigToWidget(self.__system_config_data)
self.__config_data["system"].update(system_config)
self.setSystemConfigToWidget(self.__config_data["system"])
return True
if users_config is not None:
self.__users_config_data.update(users_config)
self.fillUsersList(self.__users_config_data)
self.__config_data["users"].update(users_config)
self.fillUsersList(self.__config_data["users"])
return True
except:
return False
@@ -534,7 +575,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"prefer_early": True
},
"expect_duration": 2.0,
"satisfy_duration": False
"satisfy_duration": False,
"renew_time": {
"expect_duration": 1.0,
"max_diff": 30,
"prefer_early": True
}
}
}
user_item = QListWidgetItem(new_user["username"])
@@ -550,8 +596,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
current_item = self.UserListWidget.currentItem()
if current_item:
self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
self.UserListWidget.setCurrentItem(None)
current_index = self.UserListWidget.row(current_item)
self.UserListWidget.takeItem(current_index)
if current_index < self.UserListWidget.count():
self.UserListWidget.setCurrentRow(current_index)
else:
self.UserListWidget.setCurrentItem(None)
@Slot()
def onShowPasswordCheckBoxChecked(
@@ -574,6 +624,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.RoomComboBox.addItems(self.__floor_room_map[floor])
self.RoomComboBox.setCurrentIndex(0)
@Slot()
def onSeatMapWidgetClosed(
self,
selected_seats: list[str]
):
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
self.__seat_map_widget.deleteLater()
self.__seat_map_widget = None
if len(selected_seats) == 0:
return
self.SeatIDEdit.setText(",".join(selected_seats))
@Slot()
def onSelectSeatsButtonClicked(
self
):
floor = self.FloorComboBox.currentText()
room = self.RoomComboBox.currentText()
floor_idx = self.__floor_rmap[floor]
room_idx = self.__room_rmap[room]
if self.__seat_map_widget is None:
self.__seat_map_widget = SeatMapWidget(
self,
floor,
room,
seats_maps[floor_idx][room_idx]
)
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed)
self.__seat_map_widget.show()
self.__seat_map_widget.raise_()
self.__seat_map_widget.activateWindow()
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(","))
@Slot()
def onUserListWidgetCurrentItemChanged(
self,
@@ -707,6 +792,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"", users_config_path
):
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
else:
msg += f"用户配置文件导出失败: \n'{users_config_path}'\n"
if msg:
QMessageBox.information(
self,
@@ -746,21 +833,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
exist_files.append(users_config_path)
reply = QMessageBox.information(
self,
"信息 - AutoLibrary",
"提示 - AutoLibrary",
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
return
self.__system_config_data = self.defaultSystemConfig()
self.__users_config_data = self.defaultUsersConfig()
self.__config_data["system"] = self.defaultSystemConfig()
self.__config_data["users"] = self.defaultUsersConfig()
self.__config_paths = {
"system": system_config_path,
"users": users_config_path
}
self.initlizeConfigToWidget("system", self.__system_config_data)
self.initlizeConfigToWidget("users", self.__users_config_data)
self.initlizeConfigToWidget("system", self.__config_data["system"])
self.initlizeConfigToWidget("users", self.__config_data["users"])
@Slot()
def onConfirmButtonClicked(
@@ -768,16 +855,16 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
):
if self.UserListWidget.currentItem() is not None:
user = self.collectUserConfigFromUserInfoWidget()
if user:
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
user_config = self.collectUserConfigFromUserInfoWidget()
if user_config:
self.UserListWidget.currentItem().setData(Qt.UserRole, user_config)
if self.saveConfigs(
self.__config_paths["system"],
self.__config_paths["users"]
):
QMessageBox.information(
self,
"信息 - AutoLibrary",
"提示 - AutoLibrary",
"配置文件保存成功 !\n"
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
f"用户配置文件路径: \n{self.__config_paths['users']}"
File diff suppressed because it is too large Load Diff
+534
View File
@@ -0,0 +1,534 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread, QUrl,
)
from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
)
from gui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskWidget import ALTimerTaskWidget
from gui.ALAboutDialog import ALAboutDialog
from gui import AutoLibraryResource
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
class AutoLibWorker(QThread):
finishedSignal = Signal()
showTraceSignal = Signal(str)
showMsgSignal = Signal(str)
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__()
self.__input_queue = input_queue
self.__output_queue = output_queue
self.__config_paths = config_paths
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self.showTraceSignal.emit(
"配置文件路径不存在, 请检查配置文件路径是否正确。"
)
return False
return True
def run(
self
):
auto_lib = None
try:
if not self.checkTimeAvailable():
self.showTraceSignal.emit(
"当前时间不在图书馆开放时间内。\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return
if not self.checkConfigPaths():
return
self.showTraceSignal.emit("AutoLibrary 开始运行")
auto_lib = AutoLib(
self.__input_queue,
self.__output_queue,
)
auto_lib.run(
ConfigReader(self.__config_paths["system"]),
ConfigReader(self.__config_paths["users"]),
)
except Exception as e:
self.showTraceSignal.emit(
f"AutoLibrary 运行时发生异常 : {e}"
)
finally:
if auto_lib:
auto_lib.close()
self.showTraceSignal.emit("AutoLibrary 运行结束")
self.finishedSignal.emit()
class TimerTaskWorker(AutoLibWorker):
finishedSignal_TimerWorker = Signal(dict)
def __init__(
self,
timer_task: dict,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(
input_queue,
output_queue,
config_paths,
)
self.__timer_task = timer_task
self.__stopped = False
def run(
self
):
self.showTraceSignal.emit(
f"定时任务 {self.__timer_task['name']} 开始运行"
)
super().run()
self.showTraceSignal.emit(
f"定时任务 {self.__timer_task['name']} 运行结束"
)
self.finishedSignal_TimerWorker.emit(self.__timer_task)
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict)
def __init__(
self
):
super().__init__()
self.__class_name = self.__class__.__name__
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__timer_task_queue = queue.Queue()
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")),
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
}
self.__alTimerTaskWidget = None
self.__alConfigWidget = None
self.__alAboutDialog = None
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
self.modifyUi()
self.setupTray()
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
def modifyUi(
self
):
self.icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
self.setWindowIcon(self.icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
def onAboutActionTriggered(
self
):
if self.__alAboutDialog is None:
self.__alAboutDialog = ALAboutDialog(self)
self.__alAboutDialog.show()
def onManualActionTriggered(
self
):
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html")
QDesktopServices.openUrl(url)
def setupTray(
self
):
if not QSystemTrayIcon.isSystemTrayAvailable():
self.showTraceSignal.emit(
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
)
return
self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show()
def hideToTray(
self
):
self.hide()
self.TrayIcon.showMessage(
"AutoLibrary",
"\n已最小化到托盘",
QSystemTrayIcon.MessageIcon.Information,
2000
)
def onTrayIconActivated(
self,
reason: QSystemTrayIcon.ActivationReason
):
if reason == QSystemTrayIcon.DoubleClick:
self.showNormal()
def connectSignals(
self
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
if self.__timer and self.__timer.isActive():
self.__timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive():
self.__timer_task_timer.stop()
if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskWidget:
self.__alTimerTaskWidget.close()
self.__alTimerTaskWidget.deleteLater()
if self.__alConfigWidget:
self.__alConfigWidget.close()
self.__alConfigWidget.deleteLater()
super().closeEvent(event)
def appendToTextEdit(
self,
text: str
):
cursor = self.MessageIOTextEdit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(text + "\n")
self.MessageIOTextEdit.setTextCursor(cursor)
self.MessageIOTextEdit.ensureCursorVisible()
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def startMsgPolling(
self
):
self.__timer = QTimer()
self.__timer.timeout.connect(self.pollMsgQueue)
self.__timer.start(100)
def startTimerTaskPolling(
self
):
self.__timer_task_timer = QTimer()
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
self.__timer_task_timer.start(500)
def pollTimerTaskQueue(
self
):
if self.__is_running_timer_task:
return
try:
while not self.__is_running_timer_task:
timer_task = self.__timer_task_queue.get_nowait()
self.timerTaskIsRunning.emit(timer_task)
self.__timer_task_timer.stop()
self.__is_running_timer_task = True
self.setControlButtons(False, False, True)
if not timer_task["silent"]:
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n已开始执行定时任务: \n{timer_task['name']}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showNormal()
self.__current_timer_task_thread = TimerTaskWorker(
timer_task,
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.showTraceSignal.connect(self.showTrace)
self.__current_timer_task_thread.showMsgSignal.connect(self.showMsg)
self.__current_timer_task_thread.start()
except queue.Empty:
self.__is_running_timer_task = False
pass
def setControlButtons(
self,
config_button_enabled: bool,
start_button_enabled: bool,
stop_button_enabled: bool
):
self.ConfigButton.setEnabled(config_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
self.StopButton.setEnabled(stop_button_enabled)
@Slot()
def showMsg(
self,
msg: str
):
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
@Slot()
def showTrace(
self,
msg: str
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
@Slot()
def pollMsgQueue(
self
):
try:
while True:
msg = self.__output_queue.get_nowait()
self.appendToTextEdit(msg)
except queue.Empty:
pass
@Slot()
def onTimerTaskWidgetClosed(
self
):
self.TimerTaskWidgetButton.setEnabled(True)
@Slot(dict)
def onConfigWidgetClosed(
self,
config_paths: dict
):
if self.__alConfigWidget:
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.ConfigButton.setEnabled(True)
self.StartButton.setEnabled(True)
self.StopButton.setEnabled(False)
self.__config_paths = config_paths
@Slot(dict)
def onTimerTaskReady(
self,
timer_task: dict
):
self.__timer_task_queue.put(timer_task)
@Slot(dict)
def onTimerTaskFinished(
self,
timer_task: dict
):
self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.showTraceSignal.disconnect(self.showTrace)
self.__current_timer_task_thread.showMsgSignal.disconnect(self.showMsg)
self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None
self.setControlButtons(True, True, False)
self.__is_running_timer_task = False
self.__timer_task_timer.start(500)
timer_task["executed"] = True
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行完成",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showTrace(f"定时任务 {timer_task['name']} 执行完成, uuid: {timer_task['task_uuid']}")
self.timerTaskIsExecuted.emit(timer_task)
@Slot()
def onTimerTaskWidgetButtonClicked(
self
):
if self.__alTimerTaskWidget is None:
self.__alTimerTaskWidget = ALTimerTaskWidget(self)
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.__alTimerTaskWidget.timerTaskReady.connect(self.onTimerTaskReady)
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
self.__alTimerTaskWidget.setWindowFlags(Qt.Window)
self.__alTimerTaskWidget.show()
self.__alTimerTaskWidget.raise_()
self.__alTimerTaskWidget.activateWindow()
self.TimerTaskWidgetButton.setEnabled(False)
@Slot()
def onConfigButtonClicked(
self
):
if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget(
self,
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.setWindowFlags(Qt.Window)
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
self.ConfigButton.setEnabled(False)
@Slot()
def onStartButtonClicked(
self
):
self.setControlButtons(False, False, True)
if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker(
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
self.__auto_lib_thread.start()
@Slot()
def onStopButtonClicked(
self
):
if self.__auto_lib_thread:
self.showTrace("正在停止操作......")
self.__auto_lib_thread.wait(2000)
self.showTrace("操作已停止")
self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace)
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(True, True, False)
@Slot()
def onSendButtonClicked(
self
):
msg = self.MessageEdit.text().strip()
if not msg:
return
self.showMsg(msg)
self.MessageEdit.clear()
@@ -50,11 +50,33 @@
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="TimerTaskWidgetButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="document-open-recent"/>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="minimumSize">
<size>
<width>1280</width>
<width>1000</width>
<height>0</height>
</size>
</property>
@@ -237,6 +259,9 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="text">
<string>发送</string>
</property>
<property name="icon">
<iconset theme="document-send"/>
</property>
</widget>
</item>
</layout>
@@ -245,7 +270,7 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
</widget>
<widget class="QMenuBar" name="MenuBar">
<property name="enabled">
<bool>false</bool>
<bool>true</bool>
</property>
<property name="geometry">
<rect>
@@ -258,12 +283,33 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="nativeMenuBar">
<bool>true</bool>
</property>
<widget class="QMenu" name="HelpMenu">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="title">
<string>帮助</string>
</property>
<addaction name="ManualAction"/>
<addaction name="AboutAction"/>
</widget>
<addaction name="HelpMenu"/>
</widget>
<widget class="QStatusBar" name="StatusBar">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
<action name="ManualAction">
<property name="text">
<string>在线手册</string>
</property>
</action>
<action name="AboutAction">
<property name="text">
<string>关于</string>
</property>
</action>
</widget>
<resources/>
<connections/>
+313
View File
@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus
class TimerTaskItemWidget(QWidget):
def __init__(
self,
parent = None,
timer_task: dict = None
):
super().__init__(parent)
self.__timer_task = timer_task
self.modifyUi()
def modifyUi(
self
):
self.ItemWidgetLayout = QHBoxLayout(self)
self.ItemWidgetLayout.setSpacing(10)
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
self.TaskInfoLayout = QVBoxLayout()
self.TaskInfoLayout.setSpacing(5)
TaskNameLabel = QLabel(self.__timer_task["name"])
TaskNameLabelFont = TaskNameLabel.font()
TaskNameLabelFont.setBold(True)
TaskNameLabel.setFont(TaskNameLabelFont)
TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel)
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
ExecuteTimeLabel.setStyleSheet("color: gray;")
ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
self.ItemWidgetLayout.addStretch()
match self.__timer_task["status"]:
case TimerTaskStatus.PENDING:
TaskStatusText = "等待中"
TaskStatusColor = "#FF9800"
case TimerTaskStatus.READY:
TaskStatusText = "已就绪"
TaskStatusColor = "#316BFF"
case TimerTaskStatus.RUNNING:
TaskStatusText = "执行中"
TaskStatusColor = "#2294FF"
case TimerTaskStatus.EXECUTED:
TaskStatusText = "已执行"
TaskStatusColor = "#4CAF50"
case TimerTaskStatus.OUTDATED:
TaskStatusText = "已过期"
TaskStatusColor = "#FF5722"
TaskStatusLabel = QLabel(TaskStatusText)
TaskStatusLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskStatusColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskStatusLabel.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
TaskModeLabel = QLabel(TaskModeText)
TaskModeLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskModeColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskModeLabel.setFixedSize(60, 25)
self.ItemWidgetLayout.addWidget(TaskModeLabel)
self.DeleteButton = QPushButton("删除")
self.DeleteButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.DeleteButton)
if self.__timer_task["status"] == TimerTaskStatus.READY\
or self.__timer_task["status"] == TimerTaskStatus.RUNNING:
self.DeleteButton.setEnabled(False)
self.setFixedHeight(55)
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
timerTasksChanged = Signal(list)
timerTaskReady = Signal(dict)
timerTaskWidgetClosed = Signal()
def __init__(
self,
parent = None
):
super().__init__(parent)
self.__timer_tasks = []
self.__check_timer = None
self.setupUi(self)
self.connectSignals()
self.setupTimer()
def setupTimer(
self
):
self.__check_timer = QTimer(self)
self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500)
def connectSignals(
self
):
self.AddTimerTaskButton.clicked.connect(self.addTask)
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
def closeEvent(
self,
event: QCloseEvent
):
self.hide()
self.timerTaskWidgetClosed.emit()
event.ignore()
def updateStat(
self
):
pending = 0
in_queue = 0
executed = 0
total = len(self.__timer_tasks)
for timer_task in self.__timer_tasks:
if timer_task["status"] == TimerTaskStatus.PENDING:
pending += 1
elif timer_task["status"] == TimerTaskStatus.READY\
or timer_task["status"] == TimerTaskStatus.RUNNING:
in_queue += 1
elif timer_task["status"] == TimerTaskStatus.EXECUTED:
executed += 1
self.TotalTaskLabel.setText(f"总任务:{total}")
self.PendingTaskLabel.setText(f"待执行:{pending}")
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
def updateTimerTaskList(
self
):
self.TimerTasksListWidget.clear()
self.__timer_tasks.sort(
key = lambda x: x["execute_time"]
)
for timer_task in self.__timer_tasks:
item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task)
widget = TimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
def addTask(
self
):
dialog = ALAddTimerTaskWidget(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask()
self.__timer_tasks.append(timer_task)
self.updateTimerTaskList()
self.updateStat()
def deleteTask(
self,
task_uuid: str
):
self.__timer_tasks = [
x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid
]
self.updateTimerTaskList()
self.updateStat()
def clearAllTasks(
self
):
if not self.__timer_tasks:
return
result = QMessageBox.question(
self,
"确认 - AutoLibrary",
"是否要清除所有定时任务 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if result is QMessageBox.StandardButton.No:
return
in_queue_tasks = [
x for x in self.__timer_tasks
if x["status"] == TimerTaskStatus.READY
or x["status"] == TimerTaskStatus.RUNNING
]
in_queue_count = len(in_queue_tasks)
if in_queue_count > 0:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
)
self.__timer_tasks = in_queue_tasks
self.updateTimerTaskList()
self.updateStat()
def checkTasks(
self
):
now = datetime.now()
for timer_task in self.__timer_tasks:
if timer_task["execute_time"] > now:
continue
if timer_task["status"] is not TimerTaskStatus.PENDING:
continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5):
timer_task["status"] = TimerTaskStatus.OUTDATED
else:
timer_task["status"] = TimerTaskStatus.READY
self.timerTaskReady.emit(timer_task)
self.updateTimerTaskList()
self.updateStat()
@Slot(dict)
def onTimerTaskIsRunning(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.RUNNING
self.updateTimerTaskList()
self.updateStat()
@Slot(dict)
def onTimerTaskIsExecuted(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.EXECUTED
self.updateTimerTaskList()
self.updateStat()
+239
View File
@@ -0,0 +1,239 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALTimerTaskWidget</class>
<widget class="QWidget" name="ALTimerTaskWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>定时任务 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTaskStatusLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TotalTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>总任务:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="PendingTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #FF9800
}</string>
</property>
<property name="text">
<string>待执行:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="InQueueTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #2294FF
}</string>
</property>
<property name="text">
<string>队列中:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="ExecutedTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #4CAF50
}</string>
</property>
<property name="text">
<string>已执行:0</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="TimerTaskSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="TimerTasksListWidget">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="TimerTaskEditLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ClearAllTimerTasksButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>清除全部</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="AddTimerTaskButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加任务</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="TimerTaskEditSpaceFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+1
View File
@@ -0,0 +1 @@
AL_VERSION = "1.0.0-beta"
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import (
Qt, Signal
)
from PySide6.QtWidgets import (
QFrame, QLabel
)
class SeatFrame(QFrame):
clicked = Signal(str)
def __init__(
self,
seat_number,
parent=None
):
super().__init__(parent)
self.__seat_number = seat_number
self.__is_selected = False
self.setupUi()
def setupUi(
self
):
self.setFixedSize(60, 40)
self.setFrameStyle(QFrame.Box | QFrame.Plain)
self.setLineWidth(2)
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
self.label = QLabel(self.__seat_number, self)
self.label.setAlignment(Qt.AlignCenter)
self.label.setGeometry(0, 0, 60, 40)
def mousePressEvent(
self,
event
):
if event.button() == Qt.LeftButton:
self.toggleSelection()
self.clicked.emit(self.__seat_number)
def isSelected(
self
):
return self.__is_selected
def toggleSelection(self):
self.__is_selected = not self.__is_selected
if self.__is_selected:
self.setStyleSheet("""
QFrame {
background-color: #4CAF50;
border: 2px solid #388E3C;
border-radius: 5px;
color: white;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
else:
self.setStyleSheet("""
QFrame {
background-color: #4196EB;
border: 2px solid #4196EB;
border-radius: 5px;
}
QLabel {
color: #F0F0F0;
font-weight: bold;
}
""")
+270
View File
@@ -0,0 +1,270 @@
seats_maps = {
"2": {
"1": """
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
,,,,,,,,,,,039C,039D,,040C,040D,,041C,041D,,042C,042D,,043C,043D,,044C,044D,,,,,,,,,
038B,038D,,037B,037D,,036B,036D,,,,,,,,,,,,,,,,,,,,,,045C,045A,,046C,046A,,047C,047A
038A,038C,,037A,037C,,036A,036C,,,,,,,,,,,,,,,,,,,,,,045D,045B,,046D,046B,,047D,047B
035B,035D,,034B,034D,,033B,033D,,,,,,,,,,,,,,,,,,,,,,048C,048A,,049C,049A,,050C,050A
035A,035C,,034A,034C,,033A,033C,,,,,,,,,,,,,,,,,,,,,,048D,048B,,049D,049B,,050D,050B
032B,032D,,031B,031D,,030B,030D,,,,,,,,,,,,,,,,,,,,,,051C,051A,,052C,052A,,053C,053A
032A,032C,,031A,031C,,030A,030C,,,,,,,,,,,,,,,,,,,,,,051D,051B,,052D,052B,,053D,053B
029B,029D,,028B,028D,,027B,027D,,,,,,,,,,,,,,,,,,,,,,054C,054A,,055C,055A,,056C,056A
029A,029C,,028A,028C,,027A,027C,,,,,,,,,,,,,,,,,,,,,,054D,054B,,055D,055B,,056D,056B
026B,026D,,025B,025D,,024B,024D,,,,,,,,,,,,,,,,,,,,,,057C,057A,,058C,058A,,059C,059A
026A,026C,,025A,025C,,024A,024C,,,,,,,,,,,,,,,,,,,,,,057D,057B,,058D,058B,,059D,059B
023B,023D,,022B,022D,,021B,021D,,,,,,,,,,,,,,,,,,,,,,060C,060A,,061C,061A,,062C,062A
023A,023C,,022A,022C,,021A,021C,,,,,,,,,,,,,,,,,,,,,,060D,060B,,061D,061B,,062D,062B
020B,020D,,019B,019D,,018B,018D,,,,,,,,,,,,,,,,,,,,,,063C,063A,,064C,064A,,065C,065A
020A,020C,,019A,019C,,018A,018C,,,,,,,,,,,,,,,,,,,,,,063D,063B,,064D,064B,,065D,065B
,,,,,,,,,,,017D,017C,,014D,014C,,011D,011C,,008D,008C,,005D,005C,,002D,002C,001D,001C,,,,,,,
,,,,,,,,,,,017B,017A,,014B,014A,,011B,011A,,008B,008A,,005B,005A,,002B,002A,001B,001A,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,073D,073C,,015D,015C,,012D,012C,,,,,006D,006C,,003D,003C,,,,,,,,,
,,,,,,,,,,,073B,073A,,015B,015A,,012B,012A,,,,,006B,006A,,003B,003A,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,072D,072C,,016D,016C,,013D,013C,,,,,007D,007C,,004D,004C,,,,,,,,,
,,,,,,,,,,,072B,072A,,016B,016A,,013B,013A,,,,,007B,007A,,004B,004A,,,,,,,,,
,,,,,,,,,,,071D,071C,,070D,070C,,069D,069C,,068D,068C,,067D,067C,,066D,066C,,,,,,,,,
,,,,,,,,,,,071B,071A,,070B,070A,,069B,069A,,068B,068A,,067B,067A,,066B,066A,,,,,,,,,
""",
"2": """
023B,023D,024B,024D,,,,,,,,,,,,,,,
023A,023C,024A,024C,,,,,,,,,,,,,,,
022B,022D,032D,032C,,,,,,,,,,,,,,,
022A,022C,032B,032A,,,,,,,,,,,,,,,
021B,021D,,,,,,,,,,,,,,,,,
021A,021C,,,,,,,,,,,,,,,,,
020B,020D,,,,,,,,,,,,,,,,,
020A,020C,,,,,,,,,,,,,,,,,
019B,019D,,,,,,,,,,,,,,,,,
019A,019C,,,,,,,,,,,,,,,,,
018B,018D,,,,,,,,,,,,,,,,,
018A,018C,,,,,,,,,,,,,,,,,
017B,017D,,,,,,,,,,,,,,,,,
017A,017C,,,,,,,,,,,,,,,,,
016B,016D,,,,,,,,,,,,,,,,,
016A,016C,,,,,031A,031C,,,,,,,,,,,
015B,015D,,,,,030B,030D,,,,,,,,,,,
015A,015C,,,,,030A,030C,,,,,,,,,,,
014B,014D,,,,,029B,029D,,,,,,,,,,,
014A,014C,,,,,029A,029C,,,,,,,,,,,
013B,013D,,,,,028B,028D,,,,,,,,,,,
013A,013C,,,,,028A,028C,,,,,,,,,,,
012B,012D,,,,,027B,027D,,,,,,,,,,,
012A,012C,,,,,027A,027C,,,,,,,,,,,
011B,011D,,,,,026B,026D,,,,,,,,,,,
011A,011C,,,,,026A,026C,,,,,,,,,,,
010B,010D,,,,,025B,025D,,,,,,,,,,,
010A,010C,,,,,,,,,,,,,,,,,
009B,009D,,,,,,,,,,,,,,,,,
009A,009C,,,,,,,,,,,,,,,,,
008B,008D,,,,,,,,,,,,,,,,,
008A,008C,,,,,,,,,,,,,,,,,
007B,007D,,,,,,,,,,,,,,,,,
007A,007C,,,,,,,,,,,,,,,,,
006B,006D,,,,,,,,,,,,,,,,,
006A,006C,,,,,,,,,,,,,,,,,
005B,005D,,,,,,,,,,,,,,,,,
005A,005C,,,,,,,,,,,,,,,,,
004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,
004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,
"""
},
"3": {
"3": """
,,007B,007D,,,,,,,,008C,008A,,
,,007A,007C,,,,,,,,008D,008B,,
,,006B,006D,,,,,,,,009C,009A,,
,,006A,006C,,,,,,,,009D,009B,,
,,005B,005D,,,,,,,,010C,010a,,
,,005A,005C,,,,,,,,010D,010B,,
,,004B,004D,,,,,,,,011C,011A,,
,,004A,004C,,,,,,,,011D,011B,,
,,003B,003D,,,,,,,,012C,012A,,
,,003A,003C,,,,,,,,012D,012B,,
,,002B,002D,,,,,,,,013C,013A,,
,,002A,002C,,,,,,,,013D,013B,,
,,001B,001D,,,,,,,,014C,014A,,
,,001A,001C,,,,,,,,014D,014B,,
""",
"4": """
,,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,056D,056C,057D,057C,,
,,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,056B,056A,057B,057A,,
036B,036D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058C,058A,,060C,060A
036A,036C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058D,058B,,060D,060B
035B,035D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059C,059A,,061C,061A
035A,035C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059D,059B,,061D,061B
034B,034D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062C,062A
034A,034C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062D,062B
033B,033D,,,,,,,,,,,,080B,080D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,063C,063A
033A,033C,,,,,,,,,,,,080A,080C,,081A,081B,082A,082B,083A,083B,084A,084B,085A,085B,086A,086B,087A,,,,,,,,,,,,,,,,,,063D,063B
032B,032D,,,,,,,,,,,,079B,079D,,081C,081D,082C,082D,083C,083D,084C,084D,085C,085D,086C,086D,087C,,,,,,,,,,,,,,,,,,064C,064A
032A,032C,,,,,,,,,,,,079A,079C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,064D,064B
031B,031D,,,,,,,,,,,,078B,078D,,,,,,,,,,,,,,088A,088C,,,,,,,,,,,,,,,,,065C,065A
031A,031C,,,,,,,,,,,,078A,078C,,,,,,,,,,,,,,088B,088D,,,,,,,,,,,,,,,,,065D,065B
030B,030D,,,,,,,,,,,,077B,077D,,,,,,,,,,,,,,089A,089C,,,,,,,,,,,,,,,,,066C,066A
030A,030C,,,,,,,,,,,,077A,077C,,,,,,,,,,,,,,089B,089D,,,,,,,,,,,,,,,,,066D,066B
029B,029D,,,,,,,,,,,,076B,076D,,,,,,,,,,,,,,090A,090C,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,076A,076C,,,,,,,,,,,,,,090B,090D,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,075B,075D,,,,,,,,,,,,,,091A,091C,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,075A,075C,,,,,,,,,,,,,,091B,091D,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,074B,074D,,,,,,,,,,,,,,092A,092C,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,092B,092D,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,073D,073C,072D,072C,071D,071C,070D,070C,069D,069C,068D,068C,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,073B,073A,072B,072A,071B,071A,070B,070A,069B,069A,068B,068A,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024B,024D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
024A,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023B,023D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
023A,023C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067C,,
022B,022D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067B,,
022A,022C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067A,,
,,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,
,,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007a,006B,006A,005B,005A,004B,004A,003b,003A,002B,002A,001B,001A,,,,
"""
},
"4": {
"5": """
,,,,,,,,042A,042B,045A,045B,048A,048B,051A,051B,054A,054B,057A,057B,060A,060B,,,,,,
,,,,,,,,042C,042D,045C,045D,048C,048D,051C,051D,054C,054D,057C,057D,060C,060D,,,,,,
,,,,,,,,041A,041B,044A,044B,047A,047B,050A,050B,053A,053B,056A,056B,059A,059B,,,,,,
,,,,,,,,041C,041D,044C,044D,047C,047D,050C,050D,053C,053D,056C,056D,059C,059D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,040A,040B,043A,043B,046A,046B,049A,049B,052A,052B,055A,055B,058A,058B,,,,,,
,,,,,,,,040C,040D,043C,043D,046C,046D,049C,049D,052C,052D,055C,055D,058C,058D,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,039B,039D,038B,038D,,037B,037D,,,,,,,,,,,,,,,,,,,,,
,039A,039C,038A,038C,,037A,037C,,,,,,,,,,,,,,,,,,,,,
,036B,036D,035B,035D,,034B,034D,,,,,,,,,,,,,,,,,,,,,
,036A,036C,035A,035C,,034A,034C,,,,,,,,,,,,,,,,,,,,,
,033B,033D,032B,032D,,031B,031D,,,,,,,,,,,,,,,,,,,,,
,033A,033C,032A,032C,,031A,031C,,,,,,,,,,,,,,,,,,,,,
,030B,030D,029B,029D,,028B,028D,,,,,,,,,,,,,,,,,,,,,
,030A,030C,029A,029C,,028A,028C,,,,,,,,,,,,,,,,,,,,,
,027B,027D,026B,026D,,025B,025D,,,,,,,,,,,,,,,,,,,,,
,027A,027C,026A,026C,,025A,025C,,,,,,,,,,,,,,,,,,,,,
,024B,024D,023B,023D,,022B,022D,,,,,,,,,,,,,,,,,,,,,
,024A,024C,023A,023C,,022A,022C,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,019D,019C,016D,016C,013D,013C,010D,010C,007D,007C,004D,004C,001D,001C,,,,,,
,,,,,,,,019B,019A,016B,016A,013B,013A,010B,010A,007B,007A,004B,004A,001B,001A,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,020D,020C,017D,017C,014D,014C,011D,011C,008D,008C,005D,005C,002D,002C,,,,,,
,,,,,,,,020B,020A,017B,017A,014B,014A,011B,011A,008B,008A,005B,005A,002B,002A,,,,,,
,,,,,,,,021D,021C,018D,018C,015D,015C,012D,012C,009D,009C,006D,006C,003D,003C,,,,,,
,,,,,,,,021B,021A,018B,018A,015B,015A,012B,012A,009B,009A,006B,006A,003B,003A,,,,,,
""",
"6": """
,,,026C,026D,027D,027C,028D,028C,029D,029C,030D,030C,031D,031C,032D,032C,033D,033C,035D,035C,036D,036C,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,046D,046C
,,,026A,026B,027B,027A,028B,028A,029B,029A,030B,030A,031B,031A,032B,032A,033B,033A,035B,035A,036B,036A,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,046B,046A
025D,025C,,,,,,,,,,,,,,,,034D,034C,,,,,,,,,,,,,,,,,,,,,,,047C,047A
025B,025A,,,,,,,,,,,,,,,,034B,034A,,,,,,,,,,,,,,,,,,,,,,,047D,047B
024D,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,048C,048A
024B,024A,,,,,,,,,,,,,,050D,050C,052D,052C,054D,054C,056D,056C,058D,058C,060D,060C,,,,,,,,,,,,,,,048D,048B
023D,023C,,,,,,,,,,,,,,050B,050A,052B,052A,054B,054A,056B,056A,058B,058A,060B,060A,,,,,,,,,,,,,,,,
023B,023A,,,,,,,,,,,,,,049D,049C,051D,051C,053D,053C,055D,055C,057D,057C,059D,059C,,,,,,,,,,,,,,,,
022D,022C,,,,,,,,,,,,,,049B,049A,051B,051A,053B,053A,055B,055A,057B,057A,059B,059A,,,,,,,,,,,,,,,,
022B,022A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021D,021C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
021B,021A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020D,020C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
020B,020A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019D,019C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
019B,019A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015D,015C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
015B,015A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014D,014C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
014B,014A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013D,013C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
013B,013A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012D,012C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
012B,012A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011D,011C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
011B,011A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010D,010C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
010B,010A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009D,009C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
009B,009A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
008D,008C,,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,,,,,,,,,,,,,,,,
008B,008A,,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,,,,,,,,,,,,,,,,
""",
"7": """
,,,,,,,,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,,,,,,,,,,,,
,,,,,,,,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
},
"5": {
"8": """
,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,056C,056A,,,,,,,,,,,,,,,,,,,,,,,,,,
045B,045D,,,,,,,,,,,,,,,,,,,,,,056D,056B,,,,,,,,,,,,,,,,,,,,,,,,,,
045A,045C,,,,,,,,,,,,,,,,,,,,,,057C,057A,,,,,,,,,,,,,,,,,,,,,,,,,,
044B,044D,,,,,,,,,,,,,,,,,,,,,,057D,057B,,,,,,,,,,,,,,,,,,,,,,,,,,
044A,044C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043B,043D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
043A,043C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042B,042D,,,,,,,,,,,,,,,,,070B,070D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
042A,042C,,,,,,,,,,,,,,,,,070A,070C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041B,041D,,,,,,,,,,,,,,,,,069B,069D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
041A,041C,,,,,,,,,,,,,,,,,069A,069C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
040B,040D,,,,,,,,,,,,,,,,,068B,068D,,071A,071B,072A,072B,073A,073B,074A,074B,075A,075B,076A,076B,077A,077B,,,,,,,,,,,,,,,,
040A,040C,,,,,,,,,,,,,,,,,068A,068C,,071C,071D,072C,072D,073C,073D,074C,074D,075C,075D,076C,076D,077C,077D,,,,,,,,,,,,,,,,
039B,039D,,,,,,,,,,,,,,,,,067B,067D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
039A,039C,,,,,,,,,,,,,,,,,067A,067C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038B,038D,,,,,,,,,,,,,,,,,066B,066D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
038A,038C,,,,,,,,,,,,,,,,,066A,066C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037B,037D,,,,,,,,,,,,,,,,,065B,065D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
037A,037C,,,,,,,,,,,,,,,,,065A,065C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036B,036D,,,,,,,,,,,,,,,,,064B,064D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
036A,036C,,,,,,,,,,,,,,,,,064A,064C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035B,035D,,,,,,,,,,,,,,,,,063B,063D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
035A,035C,,,,,,,,,,,,,,,,,063A,063C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034B,034D,,,,,,,,,,,,,,,,,062B,062D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
034A,034C,,,,,,,,,,,,,,,,,062A,062C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
033B,033D,,,,,,,,,,,,,,,,,,,061D,061C,,060D,060C,,059D,059C,,058D,058C,,,,,,,,,,,,,,,,,,,,
033A,033C,,,,,,,,,,,,,,,,,,,061B,061A,,060B,060A,,059B,059A,,058B,058A,,,,,,,,,,,,,,,,,,,,
032B,032D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
032A,032C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031B,031D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
031A,031C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030B,030D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
030A,030C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029B,029D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
029A,029C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028B,028D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
028A,028C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027B,027D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026B,026D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
026A,026C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,024D,024C,023D,023C,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
,,,024B,024A,023B,023A,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
"""
}
}
+252
View File
@@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import (
Qt, Slot, Signal, QEvent
)
from PySide6.QtWidgets import (
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
QPushButton,
)
from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent
)
from gui.SeatFrame import SeatFrame
class SeatMapWidget(QWidget):
seatMapWidgetClosed = Signal(list)
def __init__(
self,
parent: QWidget = None,
floor: str = "",
room: str = "",
seats_data: dict = {},
):
super().__init__(parent)
self.__floor = floor
self.__room = room
self.__seats_data = seats_data
self.__selected_seats = []
self.__seat_frames = {}
self.setupUi()
self.connectSignals()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
def setupUi(
self
):
self.setWindowFlags(Qt.WindowType.Window)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = QGraphicsView(self)
self.SeatMapGraphicsScene = QGraphicsScene(self)
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.createSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
)
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.SeatMapWidgetControlLayout = QHBoxLayout()
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def closeEvent(
self,
event: QCloseEvent
):
self.seatMapWidgetClosed.emit(self.__selected_seats)
super().closeEvent(event)
def eventFilter(
self,
watched,
event
):
if (watched is self.SeatMapGraphicsView.viewport() and
event.type() == QEvent.Type.Wheel and
event.modifiers() == Qt.KeyboardModifier.ControlModifier
):
self.zoomGraphicsView(event)
return True
return super().eventFilter(watched, event)
def zoomGraphicsView(
self,
event: QWheelEvent
):
delta = event.angleDelta().y()
zoom_factor = 1.2 if delta > 0 else 1/1.2
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
def createSeatMap(
self
):
rows = self.__seats_data.strip().split("\n")
for row_idx, row in enumerate(rows):
col_idx = 0
seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number:
if seat_number:
seat_widget = SeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget
else:
spacer = QFrame()
spacer.setFixedSize(20, 30)
spacer.setStyleSheet("background-color: transparent; border: none;")
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
col_idx += 1
self.SeatsContainerLayout.setSpacing(20)
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
self.SeatsContainerWidget.adjustSize()
def selectSeat(
self,
seat_number: str
):
if len(self.__selected_seats) >= 1:
return
seat_number = self.formatSeatNumber(seat_number)
if seat_number not in self.__seat_frames:
return
widget = self.__seat_frames[seat_number]
if widget.isSelected():
return
widget.toggleSelection()
self.__selected_seats.append(seat_number)
def selectSeats(
self,
selected_seats: list
):
self.clearSelections()
for seat_number in selected_seats:
self.selectSeat(seat_number)
def getSelectedSeats(
self
) -> list[str]:
return self.__selected_seats
def clearSelections(
self
):
seats_to_clear = self.__selected_seats.copy()
for seat_number in seats_to_clear:
if seat_number not in self.__seat_frames:
continue
widget = self.__seat_frames[seat_number]
if widget.isSelected():
widget.toggleSelection()
self.__selected_seats = []
@Slot(str)
def onSeatClicked(
self,
seat_number: str
):
if seat_number in self.__selected_seats:
self.__selected_seats.remove(seat_number)
else:
if len(self.__selected_seats) < 1:
self.__selected_seats.append(seat_number)
else:
self.__seat_frames[seat_number].toggleSelection()
@Slot()
def onConfirmButtonClicked(
self
):
self.close()
@Slot()
def onCancelButtonClicked(
self
):
self.clearSelections()
self.close()

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 785 KiB

+20 -16
View File
@@ -16,14 +16,15 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.edge.service import Service
from MsgBase import MsgBase
from LibChecker import LibChecker
from LibLogin import LibLogin
from LibLogout import LibLogout
from LibReserve import LibReserve
from LibCheckin import LibCheckin
from base.MsgBase import MsgBase
from operators.LibChecker import LibChecker
from operators.LibLogin import LibLogin
from operators.LibLogout import LibLogout
from operators.LibReserve import LibReserve
from operators.LibCheckin import LibCheckin
from operators.LibRenew import LibRenew
from ConfigReader import ConfigReader
from utils.ConfigReader import ConfigReader
class AutoLib(MsgBase):
@@ -114,6 +115,7 @@ class AutoLib(MsgBase):
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad(
@@ -185,37 +187,39 @@ class AutoLib(MsgBase):
# reserve
if run_mode["auto_reserve"]:
if self.__lib_checker.canReserve(reserve_info.get("date")):
if self.__lib_reserve.reserve(reserve_info):
self._showTrace(f"用户 {username} 预约成功 !")
if self.__lib_reserve.reserve(username, reserve_info):
result = 0
else:
self._showTrace(f"用户 {username} 预约失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2
# checkin
if run_mode["auto_checkin"] and result == 2:
if self.__lib_checker.canCheckin(reserve_info.get("date")):
if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username):
self._showTrace(f"用户 {username} 签到成功 !")
result = 0
else:
self._showTrace(f"用户 {username} 签到失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2
# renewal
if run_mode["auto_renewal"] and result == 2:
if self.__lib_checker.canRenew(reserve_info.get("date")):
pass
if record := self.__lib_checker.canRenew():
if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record):
result = 0
else:
result = 1
else:
result = 1
else:
self._showTrace(f"用户 {username} 无法续约,已跳过")
result = 2
# logout
if not self.__lib_logout.logout(
username,
username
):
# if logout is failed, we must make sure the host to be reloaded
# otherwise, the next login may fail
+58 -30
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibChecker(LibOperator):
@@ -25,7 +25,7 @@ class LibChecker(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -160,23 +160,6 @@ class LibChecker(LibOperator):
}
def __decodeReserveRecords(
self,
reservations
) -> list:
records = []
for reservation in reservations:
record = self.__decodeReserveRecord(reservation)
if record["date"] == "":
record = None
if record["time"] == {"begin": "", "end": ""}:
record = None
records.append(record)
return records
def __loadReserveRecords(
self
) -> list:
@@ -240,11 +223,15 @@ class LibChecker(LibOperator):
reservations = self.__loadReserveRecords()
if reservations is None:
return None
records = self.__decodeReserveRecords(reservations[checked_count:])
for record in records:
for reservation in reservations[checked_count:]:
record = self.__decodeReserveRecord(reservation)
checked_count += 1
if record is None:
continue
if record["date"] == "":
continue
if record["time"] == {"begin": "", "end": ""}:
continue
# record date is later than the given date, check the next one
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
datetime.strptime(wanted_date, "%Y-%m-%d").date():
@@ -283,11 +270,11 @@ class LibChecker(LibOperator):
def canCheckin(
self,
date: str
self
) -> bool:
# have a reserved record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "已预约")
if record is not None:
begin_time = record["time"]["begin"]
@@ -320,11 +307,11 @@ class LibChecker(LibOperator):
def canRenew(
self,
date: str
) -> bool:
self
):
# have a using record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "使用中")
if record is not None:
end_time = record["time"]["end"]
@@ -338,9 +325,50 @@ class LibChecker(LibOperator):
)
if abs(time_diff_seconds) < 120*60:
self._showTrace(f"{trace_msg}, 可以续约")
return True
return record
else:
self._showTrace(f"{trace_msg}, 无法续约")
return False
return None
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return None
def postRenewCheck(
self,
record: dict
):
"""
Check if the renew operation is successful
Args:
record (dict): The expected record after renewal
Returns:
bool: True if the renew operation is successful, False otherwise
"""
# because the special circumstance that the renew operation
# do not show the success message or anything else,
# we need to check the record data to make sure the renew operation is successful.
# only check the given record date
date = record["date"]
act_record = self.__getReserveRecord(date, "使用中")
if act_record is not None:
if act_record["time"]["begin"] == record["time"]["begin"] and\
act_record["time"]["end"] == record["time"]["end"]:
self._showTrace(f"\n"\
f" 续约成功 !\n"\
f" 日 期 {date}\n"\
f" 时 间 {act_record["time"]["begin"]} - {act_record["time"]["end"]}\n"\
f" 位 置 {act_record["info"]["location"]}\n"
f" 状 态 {act_record["info"]["status"]}"
)
return True
else:
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
)
return False
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
return False
+17 -8
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckin(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -54,7 +54,6 @@ class LibCheckin(LibOperator):
except:
self._showTrace("签到时发生未知错误 !")
return False
print(result_message_element)
result_message = result_message_element.text
if "签到成功" in result_message:
try:
@@ -71,16 +70,21 @@ class LibCheckin(LibOperator):
f" {details[1]}\n"\
f" {details[2]}\n"\
f" {details[3]}\n"\
f" {details[4]}")
f" {details[4]}"
)
else:
self._showTrace(
self._showTrace(f"\n"\
" 签到成功 !\n"\
" 未获取到签到详情 !")
" 未获取到签到详情 !"
)
ok_btn.click()
return True
else:
failure_reason = result_message.replace("签到失败", "").strip()
self._showTrace(f"签到失败: {failure_reason}")
self._showTrace(f"\n"\
" 签到失败 !\n"\
f" {failure_reason}"
)
ok_btn.click()
return False
@@ -104,4 +108,9 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
checkin_btn.click()
return self._waitResponseLoad()
if self._waitResponseLoad():
self._showTrace(f"用户 {username} 签到成功 !")
return True
else:
self._showTrace(f"用户 {username} 签到失败 !")
return False
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibCheckout(LibOperator):
@@ -25,7 +25,7 @@ class LibCheckout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+2 -2
View File
@@ -17,7 +17,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogin(LibOperator):
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+2 -2
View File
@@ -13,7 +13,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibLogout(LibOperator):
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+216
View File
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import time
import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator
class LibRenew(LibOperator):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver: any
):
super().__init__(input_queue, output_queue)
self.__driver = driver
def _waitResponseLoad(
self
) -> bool:
self.__driver.refresh()
return True
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __waitRenewDialog(
self
) -> bool:
try:
WebDriverWait(self.__driver, 2).until(
EC.visibility_of_element_located((By.ID, "extendDiv"))
)
head_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv p.messageHead"))
)
result_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
head_message = head_message.text.strip()
if "警告" in head_message:
result_message = result_message.text.strip()
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" {result_message}")
return False
try:
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
)
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
return True
def __selectNearstTime(
self,
record: dict,
reserve_info: dict
) -> bool:
"""
TODO : this function is too long and too ugly
we need to refactor it to make it more readable.
but may be it is not a good idea to refactor it. :) who knows...
"""
end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60
renew_ok_btn = self.__driver.find_element(
By.CSS_SELECTOR, "#extendDiv .btnOK"
)
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !")
return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
)
# update the actual renew end time
record["time"]["end"] = best_time_opt.text.strip()
renew_ok_btn.click()
return True
self._showTrace(
"无法选择最近的可用续约时间 !" \
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(
f"当前可供续约的时间有: {free_times}"
)
return False
except:
self._showTrace("查询可用续约时间时发生未知错误 !")
return False
def renew(
self,
username: str,
record: dict,
reserve_info: dict
) -> bool:
if self.__driver is None:
self._showTrace("未提供有效 WebDriver 实例 !")
return False
try:
renew_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnExtend"))
)
except:
self._showTrace(f"用户 {username} 续约界面加载失败 !")
return False
if "disabled" in renew_btn.get_attribute("class"):
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
renew_btn.click()
if not self.__waitRenewDialog():
self._showTrace(f"用户 {username} 续约失败 !")
# After the renewal, the webpage will display a mask overlay,
# so we need to refresh the page for subsequent operations.
self.__driver.refresh()
return False
if not self.__selectNearstTime(record, reserve_info):
self._showTrace(f"用户 {username} 续约失败 !")
self.__driver.refresh()
return False
if self._waitResponseLoad():
return True
+107 -33
View File
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from LibOperator import LibOperator
from base.LibOperator import LibOperator
class LibReserve(LibOperator):
@@ -25,7 +25,7 @@ class LibReserve(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -88,11 +88,13 @@ class LibReserve(LibOperator):
f" {contents[1]}\n"\
f" {contents[2]}\n"\
f" {contents[3]}\n"\
f" 签到时间 {contents[5]}")
f" 签到时间 {contents[5]}"
)
else:
self._showTrace(f"\n"\
f" 预约成功 !\n"\
f" 未找获取到详细信息")
self._showTrace("\n"\
" 预约成功 !\n"\
" 未找获取到详细信息"
)
return True
except:
self._showTrace(f"预约结果加载失败 !")
@@ -187,12 +189,13 @@ class LibReserve(LibOperator):
reserve_info: dict
) -> bool:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
if reserve_info.get("satisfy_duration") is None:
reserve_info["satisfy_duration"] = True
self._showTrace("预约满足时长要求未指定, 默认满足")
if reserve_info["satisfy_duration"]:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True
@@ -234,7 +237,7 @@ class LibReserve(LibOperator):
# if end time is earlier than begin_time, exchange them
if end_mins < begin_mins:
self._showTrace(
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 自动交换"
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
)
reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time
@@ -261,10 +264,9 @@ class LibReserve(LibOperator):
if end_mins - begin_mins > 8*60:
self._showTrace(
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
f"{int((end_mins - begin_mins)/60)} 小时 "
f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时"
)
reserve_info["expect_duration"] = 8
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
return True
@@ -324,11 +326,52 @@ class LibReserve(LibOperator):
return False
def __clickElementByJS(
self,
trigger_locator_id: str,
option_query_selector: str,
fail_msg: str,
success_msg: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_locator_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_query_selector}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
self._showTrace(success_msg)
else:
self._showTrace(fail_msg)
return result
def __selectDate(
self,
date_str: str
) -> bool:
if self.__clickElementByJS(
trigger_locator_id="onDate_select",
option_query_selector=f"p#options_onDate a[value='{date_str}']",
success_msg=f"日期 {date_str} 选择成功 !",
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "onDate_select"),
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
@@ -342,12 +385,20 @@ class LibReserve(LibOperator):
place: str
) -> bool:
actual_place = "1" if place == "图书馆" else "1"
place = "1" # the library only have this place :)
display_place = "图书馆"
if self.__clickElementByJS(
trigger_locator_id="display_building",
option_query_selector=f"p#options_building a[value='{place}']",
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "display_building"),
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{actual_place}']"),
success_msg=f"预约场所 {place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {place} 不可用"
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"),
success_msg=f"预约场所 {display_place} 选择成功 !",
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
)
@@ -357,6 +408,13 @@ class LibReserve(LibOperator):
) -> bool:
display_floor = self.__floor_map.get(floor)
if self.__clickElementByJS(
trigger_locator_id="floor_select",
option_query_selector=f"p#options_floor a[value='{floor}']",
success_msg=f"楼层 {display_floor} 选择成功 !",
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
):
return True
return self.__clickElement(
trigger_locator=(By.ID, "floor_select"),
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
@@ -371,12 +429,24 @@ class LibReserve(LibOperator):
) -> bool:
display_room = self.__room_map.get(room)
return self.__clickElement(
trigger_locator=(By.ID, f"room_{room}"),
option_locator=None,
success_msg=f"房间 {display_room} 选择成功 !",
fail_msg=f"选择房间失败 ! : {display_room} 不可用"
)
# find room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# select room
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, f"room_{room}"))
).click()
self._showTrace(f"房间 {display_room} 选择成功 !")
return True
except:
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
return False
def __selectSeat(
@@ -509,6 +579,7 @@ class LibReserve(LibOperator):
expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time)
# select the begin time
@@ -522,11 +593,18 @@ class LibReserve(LibOperator):
return False
else:
actual_begin_time = self.__minsToTime(expect_begin_mins)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True.
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration:
expect_end_mins = int(expect_begin_mins + expct_duration*60)
expect_end_mins = int(actual_begin_mins + expct_duration*60)
if expect_end_mins > self.__timeToMins("23:30"):
expect_end_mins = self.__timeToMins("23:30")
self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace(
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
@@ -551,6 +629,7 @@ class LibReserve(LibOperator):
def reserve(
self,
username: str,
reserve_info: dict
) -> bool:
@@ -572,22 +651,13 @@ class LibReserve(LibOperator):
except:
self._showTrace(f"加载预约选座页面失败 !")
return False
# date, place, floor
# date, place, floor, room
if not self.__selectDate(reserve_info["date"]):
return False
if not self.__selectPlace(reserve_info["place"]):
return False
if not self.__selectFloor(reserve_info["floor"]):
return False
# room find
try:
WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "findRoom"))
).click()
except:
self._showTrace("加载房间/区域失败 !")
return False
# room
if not self.__selectRoom(reserve_info["room"]):
return False
else:
@@ -615,4 +685,8 @@ class LibReserve(LibOperator):
self._showTrace(f"预约提交失败 !")
if not submit_reserve and have_hover_on_page:
self.__driver.refresh()
if reserve_success:
self._showTrace(f"用户 {username} 预约成功 !")
else:
self._showTrace(f"用户 {username} 预约失败 !")
return reserve_success
+12
View File
@@ -0,0 +1,12 @@
"""
Operators module for the AutoLibrary project.
Here are the classes and modules in this package:
- AutoLib: AutoLibrary operator.
- LibLogin: Library operator for logging in.
- LibLogout: Library operator for logging out.
- LibReserve: Library operator for reserving seat.
- LibCheckin: Library operator for checking in seat.
- LibCheckout: Library operator for checking out seat.
- LibRenew: Library operator for renewing seat.
"""
+7
View File
@@ -0,0 +1,7 @@
"""
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigReader: Configuration reader class for the AutoLibrary project.
- ConfigWriter: Configuration writer class for the AutoLibrary project.
"""