diff --git a/app/common/clear_worker.py b/app/common/clear_worker.py new file mode 100644 index 0000000..74d9126 --- /dev/null +++ b/app/common/clear_worker.py @@ -0,0 +1,34 @@ +from send2trash import send2trash +import traceback + +from pathlib import Path +from PySide6.QtCore import QRunnable, Signal, QObject + + +class ClearSignalBus(QObject): + finishSignal = Signal(bool) + + +class ClearWorker(QRunnable): + def __init__(self, path: Path, deleteFolder=False) -> None: + super().__init__() + self.path = path + self.deleteFolder = deleteFolder + self.clearSignalBus = ClearSignalBus() + self.finishSignal = self.clearSignalBus.finishSignal + + def run(self): + try: + if self.deleteFolder: + send2trash(self.path) + else: + self.deleteContents() + self.finishSignal.emit(True) + except Exception as e: + traceback.print_exc() + self.finishSignal.emit(False) + + def deleteContents(self): + for item in self.path.iterdir(): + send2trash(item) + diff --git a/app/common/notification.py b/app/common/notification.py new file mode 100644 index 0000000..2e000c0 --- /dev/null +++ b/app/common/notification.py @@ -0,0 +1,32 @@ +from PySide6.QtCore import Qt +from qfluentwidgets import InfoBar, InfoBarPosition + + +class Notification: + + mainWindow = None + + def success(self, title: str, content: str = ''): + InfoBar.success( + title=title, + content=content, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=2000, + parent=self.mainWindow + ) + + def error(self, title: str, content: str = ''): + InfoBar.error( + title=title, + content=content, + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=2000, + parent=self.mainWindow + ) + + +notification = Notification() \ No newline at end of file diff --git a/app/common/script_manager.py b/app/common/script_manager.py index 1491558..14c5678 100644 --- a/app/common/script_manager.py +++ b/app/common/script_manager.py @@ -1,5 +1,5 @@ import os -from PySide6.QtCore import QProcess, Signal, QObject, Slot +from PySide6.QtCore import QProcess, QObject, Slot class ScriptManager(QObject): @@ -68,3 +68,6 @@ class ScriptManager(QObject): self._process = None self.signalBus.stopSignal.emit() + def scriptRunning(self): + return self._process is not None + diff --git a/app/common/signal_bus.py b/app/common/signal_bus.py index 7850b62..202f501 100644 --- a/app/common/signal_bus.py +++ b/app/common/signal_bus.py @@ -1,9 +1,10 @@ # coding: utf-8 -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QObject, Signal, QThreadPool from qfluentwidgets import SettingCardGroup from app.common.logger import Logger from app.common.script_manager import ScriptManager + class SignalBus(QObject): """ Signal bus """ @@ -13,6 +14,7 @@ class SignalBus(QObject): selectAllClicked = Signal() clearAllClicked = Signal() + disableStartSignal = Signal(bool) startSignal = Signal() stopSignal = Signal() loggerSignal = Signal(str) @@ -21,6 +23,9 @@ class SignalBus(QObject): super().__init__(parent) self.logger = Logger(self) self.scriptManager = ScriptManager(self) + self.threadPool = QThreadPool(self) + def scriptRunning(self) -> bool: + return self.scriptManager.scriptRunning() signalBus = SignalBus() \ No newline at end of file diff --git a/app/components/clear_messagebox.py b/app/components/clear_messagebox.py new file mode 100644 index 0000000..1bb0915 --- /dev/null +++ b/app/components/clear_messagebox.py @@ -0,0 +1,35 @@ +from PySide6.QtCore import Qt, Signal +from qfluentwidgets import MessageBox, CheckBox + + +class ClearMessageBox(MessageBox): + mainWindow = None + yesSignal = Signal() + cancelSignal = Signal() + + def __init__(self, title: str, content: str, parent=None): + parent = parent if parent is not None else self.mainWindow + if parent is None: + raise Exception("Please, assign mainWindow") + + super().__init__(title, content, parent) + self.yesButtonPressed = False + self.deleteFolder = False + + self.checkBox = CheckBox('Delete folder') + self.textLayout.insertWidget(2, self.checkBox, 0, alignment=Qt.AlignLeft) + + self.__connectSignalToSlot() + + def __connectSignalToSlot(self): + self.checkBox.checkStateChanged.connect(self.onChecked) + self.yesSignal.connect(self.onYesSignal) + + def onYesSignal(self): + self.yesButtonPressed = True + + def onChecked(self, value: bool): + self.deleteFolder = value + + def getResponse(self) -> tuple[bool, bool]: + return self.yesButtonPressed, self.deleteFolder \ No newline at end of file diff --git a/app/components/folder_setting_card.py b/app/components/folder_setting_card.py index f1891c9..b221db9 100644 --- a/app/components/folder_setting_card.py +++ b/app/components/folder_setting_card.py @@ -1,14 +1,17 @@ import os -import subprocess from pathlib import Path -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Slot from PySide6.QtGui import QIcon from PySide6.QtWidgets import QFileDialog from qfluentwidgets import SettingCard, FluentIconBase, FluentIcon, CommandBar, Action, LineEdit, LineEditButton from typing import Union -from ..common.config import cfg - +from app.common.notification import notification +from app.common.config import cfg +from app.common.signal_bus import signalBus +from app.common.clear_worker import ClearWorker +from app.components.clear_messagebox import ClearMessageBox + class FolderLineEdit(LineEdit): """ Search line edit """ @@ -26,7 +29,9 @@ class FolderLineEdit(LineEdit): class FolderSettingCard(SettingCard): - def __init__(self, configItem, icon: Union[str, QIcon, FluentIconBase], title, content=None, parent=None): + clearQueue = set() + + def __init__(self, configItem, icon: Union[str, QIcon, FluentIconBase], titleGroup, title, content=None, parent=None, clearable=True): """ Parameters ---------- @@ -46,9 +51,11 @@ class FolderSettingCard(SettingCard): parent widget """ super().__init__(icon, title, content, parent) + self.titleGroup = titleGroup self.configItem = configItem self.lineEdit = FolderLineEdit() self.lineEdit.setText(configItem.value) + self.clearable = clearable self.__initCommandBar() self.__initLayout() @@ -56,13 +63,18 @@ class FolderSettingCard(SettingCard): def __initCommandBar(self): self.commandBar = CommandBar() - explorerAction = Action(FluentIcon.FOLDER, 'Show in Explorer') - explorerAction.triggered.connect(self.openExplorer) - self.commandBar.addHiddenAction(explorerAction) + self.explorerAction = Action(FluentIcon.FOLDER, 'Show in Explorer') + self.explorerAction.triggered.connect(self.openExplorer) + self.commandBar.addHiddenAction(self.explorerAction) - browseAction = Action(FluentIcon.EDIT, 'Browse') - browseAction.triggered.connect(self.browse) - self.commandBar.addHiddenAction(browseAction) + self.browseAction = Action(FluentIcon.EDIT, 'Browse') + self.browseAction.triggered.connect(self.browse) + self.commandBar.addHiddenAction(self.browseAction) + + if self.clearable: + self.clearAction = Action(FluentIcon.DELETE, 'Clear') + self.clearAction.triggered.connect(self.showClearDialog) + self.commandBar.addHiddenAction(self.clearAction) def __initLayout(self): self.setFixedHeight(70) @@ -72,30 +84,97 @@ class FolderSettingCard(SettingCard): self.hBoxLayout.addSpacing(16) def __connectSignalToSlot(self): - self.lineEdit.textChanged.connect(self.pathValid) + self.lineEdit.textChanged.connect(self.validatePath) + if self.clearable: + signalBus.startSignal.connect(lambda: self.setDisabledClear(True)) + signalBus.stopSignal.connect(lambda: self.setDisabledClear(False)) - def pathValid(self, text): + @Slot(str) + def validatePath(self, text: str): if not text: cfg.set(self.configItem, "") self.lineEdit.setValid(True) + self.explorerAction.setDisabled(True) + self.setDisabledClear(True) return path = Path(text) if path.is_absolute() and path.is_dir() and path.exists(): cfg.set(self.configItem, text) self.lineEdit.setValid(True) + self.setDisabledClear(False) + self.explorerAction.setDisabled(False) else: self.lineEdit.setValid(False) + self.setDisabledClear(True) + self.explorerAction.setDisabled(True) + @Slot() def openExplorer(self): if self.configItem.value: - file_path = os.path.normpath(self.configItem.value) - subprocess.Popen(f'explorer /select,"{file_path}"') + path = os.path.realpath(self.configItem.value) + os.startfile(path) + @Slot() def browse(self): """ download folder card clicked slot """ folder = QFileDialog.getExistingDirectory( self, self.tr("Choose folder"), "./") if not folder or cfg.get(self.configItem) == folder: return - self.lineEdit.setText(folder) \ No newline at end of file + self.lineEdit.setText(folder) + + @Slot() + def showClearDialog(self): + path = Path(self.lineEdit.text()) + if path in self.clearQueue: + notification.error(f"{self.titleGroup}'s directory is already being cleared!") + return + + if not (path.is_absolute() and path.is_dir() and path.exists()): + notification.error(f"{self.titleGroup} directory path is not valid!") + return + + w = ClearMessageBox("Delete all files", f"Are you sure you want to delete all files in {self.titleGroup}'s directory:\n\n{path}?") + w.exec() + yesPressed, deleteFolder = w.getResponse() + if yesPressed: + self.clear(path, deleteFolder) + + @Slot() + def clear(self, path: Path, deleteFolder: bool): + signalBus.disableStartSignal.emit(True) + + self.clearQueue.add(path) + self.lineEdit.setDisabled(True) + self.clearAction.setDisabled(True) + + worker = ClearWorker(path, deleteFolder) + worker.finishSignal.connect(lambda successful: self.afterClear(path, deleteFolder, successful)) + signalBus.threadPool.start(worker) + + @Slot() + def afterClear(self, path: Path, deleteFolder: bool, successful: bool): + if successful: + if deleteFolder: + self.lineEdit.clear() + notification.success(f"Successfully deleted {self.titleGroup}'s directory") + else: + notification.success(f"Successfully cleared {self.titleGroup}'s directory") + else: + notification.error(f"Error clearing {self.titleGroup}'s directory") + + self.lineEdit.setDisabled(False) + self.clearAction.setDisabled(False) + self.clearQueue.remove(path) + if not self.clearQueue: + signalBus.disableStartSignal.emit(False) + + def setDisabledClear(self, value: bool): + if not self.clearable: + return + if not value and signalBus.scriptRunning(): + value = True + + self.clearAction.setDisabled(value) + diff --git a/app/components/navigation_action_buttons.py b/app/components/navigation_action_buttons.py index 9d32603..2ca6c3c 100644 --- a/app/components/navigation_action_buttons.py +++ b/app/components/navigation_action_buttons.py @@ -41,6 +41,7 @@ class NavigationActionButtons(QWidget): signalBus.startSignal.connect(lambda: self.startButton.setText("Stop")) signalBus.stopSignal.connect(lambda: self.startButton.setText("Start")) + signalBus.disableStartSignal.connect(lambda state: self.startButton.setDisabled(state)) def onSelectAllClicked(self): signalBus.selectAllClicked.emit() diff --git a/app/view/main_window.py b/app/view/main_window.py index ed9c239..b380413 100644 --- a/app/view/main_window.py +++ b/app/view/main_window.py @@ -10,9 +10,11 @@ from qfluentwidgets import FluentIcon as FIF from .logger_interface import LoggerInterface from .setting_interface import SettingInterface +from ..common.notification import notification from ..common.config import ZH_SUPPORT_URL, EN_SUPPORT_URL, cfg from ..common.signal_bus import signalBus from ..common import resource +from ..components.clear_messagebox import ClearMessageBox from ..components.navigation_checkbox import NavigationCheckBox from ..components.navigation_action_buttons import NavigationActionButtons from ..components.navigation_logo import NavigationLogoWidget @@ -67,6 +69,10 @@ class MainWindow(FluentWindow): self.loggerInterface, FIF.CALENDAR, self.tr('Log'), NavigationItemPosition.BOTTOM) self.addSubInterface( self.settingInterface, FIF.SETTING, self.tr('Settings'), NavigationItemPosition.BOTTOM) + + notification.mainWindow = self + ClearMessageBox.mainWindow = self + def initWindow(self): self.resize(960, 780) diff --git a/app/view/setting_interface.py b/app/view/setting_interface.py index ff6b748..ffba3c2 100644 --- a/app/view/setting_interface.py +++ b/app/view/setting_interface.py @@ -34,8 +34,10 @@ class SettingInterface(ScrollArea): self.gamePathCard = FolderSettingCard( cfg.gamePath, FIF.GAME, + 'Core', self.tr("Koikatsu directory"), - parent=self.coreGroup + parent=self.coreGroup, + clearable=False ) # createBackup @@ -44,6 +46,7 @@ class SettingInterface(ScrollArea): self.backupPathCard = FolderSettingCard( cfg.backupPath, FIF.ZIP_FOLDER, + 'Create Backup', self.tr("Backup directory"), parent=self.backupGroup ) @@ -82,7 +85,8 @@ class SettingInterface(ScrollArea): self.fckksPathCard = FolderSettingCard( cfg.fccksPath, FIF.DOWNLOAD, - self.tr("Backup directory"), + 'Filter & Convert', + self.tr("Input directory"), parent=self.fckksGroup ) self.convertCard = SwitchSettingCard( @@ -99,6 +103,7 @@ class SettingInterface(ScrollArea): self.installPathCard = FolderSettingCard( cfg.installPath, FIF.DOWNLOAD, + 'Install Chara', self.tr("Input directory"), parent=self.installGroup ) @@ -124,6 +129,7 @@ class SettingInterface(ScrollArea): self.removePathCard = FolderSettingCard( cfg.removePath, FIF.DOWNLOAD, + 'Remove Chara', self.tr("Input directory"), parent=self.removeGroup ) diff --git a/requirements.txt b/requirements.txt index b4d0182..d59bc27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ PySide6_Addons==6.7.2 PySide6_Essentials==6.7.2 PySideSix-Frameless-Window==0.4.3 pywin32==306 +Send2Trash==1.8.3 setuptools==75.1.0 shiboken6==6.7.2 wheel==0.44.0