fspachinko 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fspachinko/__init__.py +6 -0
- fspachinko/_data/configs/fspachinko.json +60 -0
- fspachinko/_data/configs/logging.json +36 -0
- fspachinko/_data/icons/add_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/close_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/file_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/folder_open_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/icon.icns +0 -0
- fspachinko/_data/icons/icon.ico +0 -0
- fspachinko/_data/icons/play_arrow_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/remove_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/save_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/save_as_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/stop_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/sync_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +1 -0
- fspachinko/_data/icons/windowIcon.png +0 -0
- fspachinko/cli/__init__.py +1 -0
- fspachinko/cli/__main__.py +19 -0
- fspachinko/cli/app.py +62 -0
- fspachinko/cli/observer.py +37 -0
- fspachinko/config/__init__.py +39 -0
- fspachinko/config/config.py +213 -0
- fspachinko/config/converter.py +163 -0
- fspachinko/config/schemas.py +96 -0
- fspachinko/core/__init__.py +20 -0
- fspachinko/core/builder.py +92 -0
- fspachinko/core/engine.py +129 -0
- fspachinko/core/quota.py +46 -0
- fspachinko/core/reporter.py +55 -0
- fspachinko/core/state.py +300 -0
- fspachinko/core/transfer.py +100 -0
- fspachinko/core/validator.py +70 -0
- fspachinko/core/walker.py +184 -0
- fspachinko/gui/__init__.py +1 -0
- fspachinko/gui/__main__.py +43 -0
- fspachinko/gui/actions.py +68 -0
- fspachinko/gui/centralwidget.py +70 -0
- fspachinko/gui/components.py +581 -0
- fspachinko/gui/mainwindow.py +153 -0
- fspachinko/gui/observer.py +54 -0
- fspachinko/gui/qthelpers.py +102 -0
- fspachinko/gui/settings.py +53 -0
- fspachinko/gui/uibuilder.py +127 -0
- fspachinko/gui/workers.py +56 -0
- fspachinko/utils/__init__.py +89 -0
- fspachinko/utils/constants.py +212 -0
- fspachinko/utils/helpers.py +143 -0
- fspachinko/utils/interfaces.py +35 -0
- fspachinko/utils/loggers.py +16 -0
- fspachinko/utils/paths.py +33 -0
- fspachinko/utils/timestamp.py +29 -0
- fspachinko-0.0.2.dist-info/METADATA +322 -0
- fspachinko-0.0.2.dist-info/RECORD +56 -0
- fspachinko-0.0.2.dist-info/WHEEL +4 -0
- fspachinko-0.0.2.dist-info/entry_points.txt +5 -0
- fspachinko-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Main module."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QSettings, Slot
|
|
7
|
+
from PySide6.QtWidgets import QFileDialog, QMainWindow, QStatusBar, QToolBar
|
|
8
|
+
|
|
9
|
+
from ..utils import GUIFileDialogFilter, GUILabel, GUIName, GUISettingsKey, GUITitle, get_stem_and_ext
|
|
10
|
+
from .actions import Actions
|
|
11
|
+
from .centralwidget import CentralWidget
|
|
12
|
+
from .qthelpers import set_qt_name
|
|
13
|
+
from .settings import ProfileManager
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from PySide6.QtGui import QCloseEvent
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MainWindow(QMainWindow):
|
|
22
|
+
"""Main application window."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""Initialize the main window."""
|
|
26
|
+
super().__init__()
|
|
27
|
+
logger.debug("Initializing GUI")
|
|
28
|
+
self.central_widget = CentralWidget()
|
|
29
|
+
self.profiles = ProfileManager()
|
|
30
|
+
self.qsettings = QSettings()
|
|
31
|
+
self._actions = Actions()
|
|
32
|
+
self.setCentralWidget(self.central_widget)
|
|
33
|
+
self.init_connections()
|
|
34
|
+
self.init_menubar()
|
|
35
|
+
self.init_toolbar()
|
|
36
|
+
self.init_statusbar()
|
|
37
|
+
self.init_settings()
|
|
38
|
+
|
|
39
|
+
def init_connections(self) -> None:
|
|
40
|
+
"""Initialize connections."""
|
|
41
|
+
self._actions.file.save.triggered.connect(self.save_profile)
|
|
42
|
+
self._actions.file.save_as.triggered.connect(self.save_profile_as_dialog)
|
|
43
|
+
self._actions.file.load.triggered.connect(self.open_profile_dialog)
|
|
44
|
+
self._actions.file.exit.triggered.connect(self.close)
|
|
45
|
+
|
|
46
|
+
self._actions.run.start.triggered.connect(self.central_widget.on_start)
|
|
47
|
+
self._actions.run.stop.triggered.connect(self.central_widget.on_stop)
|
|
48
|
+
|
|
49
|
+
def init_menubar(self) -> None:
|
|
50
|
+
"""Initialize the menu bar."""
|
|
51
|
+
menubar = self.menuBar()
|
|
52
|
+
set_qt_name(menubar, GUIName.MENUBAR)
|
|
53
|
+
|
|
54
|
+
file_menu = menubar.addMenu(GUILabel.FILEMENU)
|
|
55
|
+
set_qt_name(file_menu, GUIName.FILEMENU)
|
|
56
|
+
|
|
57
|
+
file_menu.addAction(self._actions.file.save)
|
|
58
|
+
file_menu.addAction(self._actions.file.save_as)
|
|
59
|
+
file_menu.addAction(self._actions.file.load)
|
|
60
|
+
file_menu.addSeparator()
|
|
61
|
+
file_menu.addAction(self._actions.file.autosave)
|
|
62
|
+
file_menu.addSeparator()
|
|
63
|
+
file_menu.addAction(self._actions.file.exit)
|
|
64
|
+
|
|
65
|
+
run_menu = menubar.addMenu(GUILabel.RUNMENU)
|
|
66
|
+
set_qt_name(run_menu, GUIName.RUNMENU)
|
|
67
|
+
|
|
68
|
+
run_menu.addAction(self._actions.run.start)
|
|
69
|
+
run_menu.addAction(self._actions.run.stop)
|
|
70
|
+
|
|
71
|
+
def init_toolbar(self) -> None:
|
|
72
|
+
"""Initialize the toolbar."""
|
|
73
|
+
toolbar = QToolBar(GUIName.TOOLBAR)
|
|
74
|
+
set_qt_name(toolbar, GUIName.TOOLBAR)
|
|
75
|
+
|
|
76
|
+
toolbar.addAction(self._actions.file.save)
|
|
77
|
+
toolbar.addAction(self._actions.file.save_as)
|
|
78
|
+
toolbar.addAction(self._actions.file.load)
|
|
79
|
+
toolbar.addAction(self._actions.file.autosave)
|
|
80
|
+
toolbar.addSeparator()
|
|
81
|
+
toolbar.addAction(self._actions.run.start)
|
|
82
|
+
toolbar.addAction(self._actions.run.stop)
|
|
83
|
+
toolbar.addSeparator()
|
|
84
|
+
toolbar.addAction(self._actions.file.exit)
|
|
85
|
+
|
|
86
|
+
self.addToolBar(toolbar)
|
|
87
|
+
|
|
88
|
+
def init_statusbar(self) -> None:
|
|
89
|
+
"""Initialize the status bar."""
|
|
90
|
+
statusbar = QStatusBar(self, sizeGripEnabled=True)
|
|
91
|
+
set_qt_name(statusbar, GUIName.STATUSBAR)
|
|
92
|
+
|
|
93
|
+
self.setStatusBar(statusbar)
|
|
94
|
+
|
|
95
|
+
def init_settings(self) -> None:
|
|
96
|
+
"""Initialize GUI settings manager."""
|
|
97
|
+
self.restoreGeometry(self.qsettings.value(GUISettingsKey.GEOMETRY))
|
|
98
|
+
self.restoreState(self.qsettings.value(GUISettingsKey.STATE))
|
|
99
|
+
self.profiles.set_current(str(self.qsettings.value(GUISettingsKey.PROFILE, "")))
|
|
100
|
+
self.profiles.open_profile(self)
|
|
101
|
+
self.reset_window_title()
|
|
102
|
+
|
|
103
|
+
@Slot()
|
|
104
|
+
def save_profile(self) -> None:
|
|
105
|
+
"""Save the current GUI profile."""
|
|
106
|
+
self.profiles.save_profile(self)
|
|
107
|
+
|
|
108
|
+
@Slot()
|
|
109
|
+
def save_profile_as_dialog(self) -> None:
|
|
110
|
+
"""Save a GUI profile via dialog."""
|
|
111
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
112
|
+
self,
|
|
113
|
+
GUITitle.SAVE_PROFILE,
|
|
114
|
+
self.profiles.get_current_profile_parent(),
|
|
115
|
+
GUIFileDialogFilter.JSON,
|
|
116
|
+
)
|
|
117
|
+
if filename:
|
|
118
|
+
self.profiles.set_current(filename)
|
|
119
|
+
self.save_profile()
|
|
120
|
+
self.reset_window_title()
|
|
121
|
+
|
|
122
|
+
@Slot()
|
|
123
|
+
def open_profile_dialog(self) -> None:
|
|
124
|
+
"""Load a GUI profile via dialog."""
|
|
125
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
126
|
+
self,
|
|
127
|
+
GUITitle.OPEN_PROFILE,
|
|
128
|
+
self.profiles.get_current_profile_parent(),
|
|
129
|
+
GUIFileDialogFilter.JSON,
|
|
130
|
+
)
|
|
131
|
+
if filename:
|
|
132
|
+
self.profiles.set_current(filename)
|
|
133
|
+
self.profiles.open_profile(self)
|
|
134
|
+
self.reset_window_title()
|
|
135
|
+
|
|
136
|
+
@Slot()
|
|
137
|
+
def reset_window_title(self) -> None:
|
|
138
|
+
"""Update the window title based on the current profile."""
|
|
139
|
+
profile_stem, _ = get_stem_and_ext(self.profiles.current_profile)
|
|
140
|
+
self.setWindowTitle(f"{profile_stem} - {GUITitle.WINDOW}")
|
|
141
|
+
|
|
142
|
+
def save_settings(self) -> None:
|
|
143
|
+
"""Save GUI settings on close."""
|
|
144
|
+
self.qsettings.setValue(GUISettingsKey.GEOMETRY, self.saveGeometry())
|
|
145
|
+
self.qsettings.setValue(GUISettingsKey.STATE, self.saveState())
|
|
146
|
+
self.qsettings.setValue(GUISettingsKey.PROFILE, self.profiles.current_profile)
|
|
147
|
+
|
|
148
|
+
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
|
149
|
+
"""Handle window close event."""
|
|
150
|
+
self.save_settings()
|
|
151
|
+
if self._actions.file.autosave.isChecked():
|
|
152
|
+
self.save_profile()
|
|
153
|
+
super().closeEvent(event)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Observer for GUI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QObject, Signal
|
|
6
|
+
|
|
7
|
+
from ..utils import Observer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkerSignals(QObject):
|
|
11
|
+
"""Qt worker signals."""
|
|
12
|
+
|
|
13
|
+
progress_total = Signal(int)
|
|
14
|
+
count_total = Signal()
|
|
15
|
+
progress = Signal(int)
|
|
16
|
+
finished = Signal()
|
|
17
|
+
log = Signal(str)
|
|
18
|
+
time = Signal()
|
|
19
|
+
count = Signal(int)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class GuiObserver(Observer):
|
|
24
|
+
"""GUI observer."""
|
|
25
|
+
|
|
26
|
+
signals: WorkerSignals
|
|
27
|
+
|
|
28
|
+
def on_progress_total(self, maximum: int) -> None:
|
|
29
|
+
"""Emit total progress signal."""
|
|
30
|
+
self.signals.progress_total.emit(maximum)
|
|
31
|
+
|
|
32
|
+
def on_count_total(self) -> None:
|
|
33
|
+
"""Emit total count signal."""
|
|
34
|
+
self.signals.count_total.emit()
|
|
35
|
+
|
|
36
|
+
def on_progress(self, maximum: int) -> None:
|
|
37
|
+
"""Emit progress signal."""
|
|
38
|
+
self.signals.progress.emit(maximum)
|
|
39
|
+
|
|
40
|
+
def on_finished(self) -> None:
|
|
41
|
+
"""Emit finished signal."""
|
|
42
|
+
self.signals.finished.emit()
|
|
43
|
+
|
|
44
|
+
def on_log(self, msg: str) -> None:
|
|
45
|
+
"""Emit log message signal."""
|
|
46
|
+
self.signals.log.emit(msg)
|
|
47
|
+
|
|
48
|
+
def on_time(self) -> None:
|
|
49
|
+
"""Emit time update signal."""
|
|
50
|
+
self.signals.time.emit()
|
|
51
|
+
|
|
52
|
+
def on_count(self, count: int) -> None:
|
|
53
|
+
"""Emit count update signal."""
|
|
54
|
+
self.signals.count.emit(count)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Helper functions for Qt GUI elements."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QCheckBox,
|
|
7
|
+
QComboBox,
|
|
8
|
+
QDoubleSpinBox,
|
|
9
|
+
QGroupBox,
|
|
10
|
+
QLineEdit,
|
|
11
|
+
QRadioButton,
|
|
12
|
+
QSpinBox,
|
|
13
|
+
QWidget,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..utils import strtobool
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
|
|
21
|
+
from PySide6.QtCore import QObject
|
|
22
|
+
from PySide6.QtGui import QAction
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_qt_classname(obj: QObject) -> str:
|
|
26
|
+
"""Get the class name of a QObject."""
|
|
27
|
+
return str(obj.metaObject().className())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_qt_name(w: QWidget | QAction, name: str) -> None:
|
|
31
|
+
"""Initialize a widget with a given object name."""
|
|
32
|
+
w.setObjectName(name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_qt_tips(w: QWidget | QAction, tooltip: str, statustip: str = "") -> None:
|
|
36
|
+
"""Set the tooltip and status tip for a widget."""
|
|
37
|
+
if not statustip:
|
|
38
|
+
statustip = f"{tooltip} | ({get_qt_classname(w)})"
|
|
39
|
+
|
|
40
|
+
w.setToolTip(tooltip)
|
|
41
|
+
w.setStatusTip(statustip)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_widget_value(widget: QWidget) -> Any:
|
|
45
|
+
"""Retrieve the value of a widget based on its type.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
widget (QWidget): The widget to retrieve the value from.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Any: The value of the widget, or None if not applicable.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
match widget:
|
|
55
|
+
case QLineEdit():
|
|
56
|
+
return widget.text()
|
|
57
|
+
case QComboBox():
|
|
58
|
+
return widget.currentIndex()
|
|
59
|
+
case QSpinBox() | QDoubleSpinBox():
|
|
60
|
+
return widget.value()
|
|
61
|
+
case QGroupBox() if not widget.isCheckable():
|
|
62
|
+
return None
|
|
63
|
+
case QCheckBox() | QRadioButton() | QGroupBox():
|
|
64
|
+
return widget.isChecked()
|
|
65
|
+
case _:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_widget_value(widget: QWidget, val: Any) -> None:
|
|
70
|
+
"""Set the value of a widget based on its type.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
widget (QWidget): The widget to set the value for.
|
|
74
|
+
val (Any): The value to set.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
match widget:
|
|
78
|
+
case QLineEdit():
|
|
79
|
+
widget.setText(val)
|
|
80
|
+
case QComboBox():
|
|
81
|
+
try:
|
|
82
|
+
index = int(val)
|
|
83
|
+
if 0 <= index < widget.count():
|
|
84
|
+
widget.setCurrentIndex(index)
|
|
85
|
+
except (ValueError, TypeError):
|
|
86
|
+
pass
|
|
87
|
+
case QSpinBox():
|
|
88
|
+
widget.setValue(int(val))
|
|
89
|
+
case QDoubleSpinBox():
|
|
90
|
+
widget.setValue(float(val))
|
|
91
|
+
case QCheckBox() | QRadioButton() | QGroupBox():
|
|
92
|
+
state = strtobool(val=val)
|
|
93
|
+
widget.setChecked(state)
|
|
94
|
+
case _:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def iter_custom_widget(w: QWidget) -> Iterator[tuple[str, QWidget]]:
|
|
99
|
+
"""Iterate over valid child widgets."""
|
|
100
|
+
for child in w.findChildren(QWidget):
|
|
101
|
+
if (key := child.objectName()) and not key.startswith("qt_"):
|
|
102
|
+
yield key, child
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Settings handling for GUI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from PySide6.QtWidgets import QComboBox, QWidget
|
|
8
|
+
|
|
9
|
+
from ..utils import Paths, load_json, save_json
|
|
10
|
+
from .qthelpers import get_widget_value, iter_custom_widget, set_widget_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class ProfileManager:
|
|
15
|
+
"""Class for managing GUI profiles."""
|
|
16
|
+
|
|
17
|
+
current_profile: str = field(init=False)
|
|
18
|
+
|
|
19
|
+
def set_current(self, profile: str) -> None:
|
|
20
|
+
"""Set the current profile name."""
|
|
21
|
+
self.current_profile = Paths.profile(profile)
|
|
22
|
+
|
|
23
|
+
def save_profile(self, parent: QWidget) -> None:
|
|
24
|
+
"""Recursively save settings for all child widgets."""
|
|
25
|
+
data = {}
|
|
26
|
+
for key, child in iter_custom_widget(parent):
|
|
27
|
+
if (val := get_widget_value(child)) is None:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
data[key] = val
|
|
31
|
+
if isinstance(child, QComboBox):
|
|
32
|
+
items = [child.itemText(i) for i in range(child.count())]
|
|
33
|
+
data[f"{key}_items"] = items
|
|
34
|
+
|
|
35
|
+
save_json(self.current_profile, data)
|
|
36
|
+
|
|
37
|
+
def open_profile(self, parent: QWidget) -> None:
|
|
38
|
+
"""Recursively load settings for all child widgets."""
|
|
39
|
+
data = load_json(self.current_profile)
|
|
40
|
+
|
|
41
|
+
for key, child in iter_custom_widget(parent):
|
|
42
|
+
if isinstance(child, QComboBox):
|
|
43
|
+
items = data.get(f"{key}_items")
|
|
44
|
+
if isinstance(items, Sequence):
|
|
45
|
+
child.clear()
|
|
46
|
+
child.addItems([str(i) for i in items])
|
|
47
|
+
|
|
48
|
+
if (val := data.get(key)) is not None:
|
|
49
|
+
set_widget_value(child, val)
|
|
50
|
+
|
|
51
|
+
def get_current_profile_parent(self) -> str:
|
|
52
|
+
"""Get the parent directory of the current profile."""
|
|
53
|
+
return os.path.dirname(self.current_profile)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Main module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
|
|
6
|
+
|
|
7
|
+
from ..config import ConfigModel
|
|
8
|
+
from .components import (
|
|
9
|
+
DestPathSelectorWidget,
|
|
10
|
+
DurationFilterWidget,
|
|
11
|
+
ExtensionsFilterWidget,
|
|
12
|
+
FileCountWidget,
|
|
13
|
+
FilenameWidget,
|
|
14
|
+
FolderCreatorWidget,
|
|
15
|
+
FolderSizeLimitWidget,
|
|
16
|
+
KeywordsFilterWidget,
|
|
17
|
+
ListIncludeExcludeFilterWidget,
|
|
18
|
+
LoggingWidget,
|
|
19
|
+
MinMaxFilterWidget,
|
|
20
|
+
OptionsWidget,
|
|
21
|
+
PathSelectorWidget,
|
|
22
|
+
ProgressWidget,
|
|
23
|
+
RootPathSelectorWidget,
|
|
24
|
+
SizeFilterWidget,
|
|
25
|
+
SizeLimitWidget,
|
|
26
|
+
TotalSizeLimitWidget,
|
|
27
|
+
TransferModeWidget,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class UIBuilder:
|
|
33
|
+
"""Main Widget Builder."""
|
|
34
|
+
|
|
35
|
+
root: PathSelectorWidget = field(default_factory=RootPathSelectorWidget)
|
|
36
|
+
dest: PathSelectorWidget = field(default_factory=DestPathSelectorWidget)
|
|
37
|
+
filecount: FileCountWidget = field(default_factory=FileCountWidget)
|
|
38
|
+
folders: FolderCreatorWidget = field(default_factory=FolderCreatorWidget)
|
|
39
|
+
filename: FilenameWidget = field(default_factory=FilenameWidget)
|
|
40
|
+
transfermode: TransferModeWidget = field(default_factory=TransferModeWidget)
|
|
41
|
+
keywords: ListIncludeExcludeFilterWidget = field(default_factory=KeywordsFilterWidget)
|
|
42
|
+
extensions: ListIncludeExcludeFilterWidget = field(default_factory=ExtensionsFilterWidget)
|
|
43
|
+
filesize: MinMaxFilterWidget = field(default_factory=SizeFilterWidget)
|
|
44
|
+
duration: MinMaxFilterWidget = field(default_factory=DurationFilterWidget)
|
|
45
|
+
folder_size_limit: SizeLimitWidget = field(default_factory=FolderSizeLimitWidget)
|
|
46
|
+
total_size_limit: SizeLimitWidget = field(default_factory=TotalSizeLimitWidget)
|
|
47
|
+
options: OptionsWidget = field(default_factory=OptionsWidget)
|
|
48
|
+
progress: ProgressWidget = field(default_factory=ProgressWidget)
|
|
49
|
+
logging: LoggingWidget = field(default_factory=LoggingWidget)
|
|
50
|
+
|
|
51
|
+
def build_layout(self) -> QVBoxLayout:
|
|
52
|
+
"""Set up the main UI layouts."""
|
|
53
|
+
path_widget = QWidget()
|
|
54
|
+
path_layout = QVBoxLayout(path_widget)
|
|
55
|
+
path_layout.addWidget(self.root)
|
|
56
|
+
path_layout.addWidget(self.dest)
|
|
57
|
+
|
|
58
|
+
# Left widget
|
|
59
|
+
output_widget = QWidget()
|
|
60
|
+
output_layout = QHBoxLayout(output_widget)
|
|
61
|
+
output_layout.addWidget(self.filecount)
|
|
62
|
+
output_layout.addWidget(self.transfermode)
|
|
63
|
+
output_layout.addWidget(self.folders)
|
|
64
|
+
output_layout.addWidget(self.filename)
|
|
65
|
+
|
|
66
|
+
filter_widget = QWidget()
|
|
67
|
+
filter_layout = QVBoxLayout(filter_widget)
|
|
68
|
+
filter_layout.addWidget(self.keywords)
|
|
69
|
+
filter_layout.addWidget(self.extensions)
|
|
70
|
+
|
|
71
|
+
left_widget = QWidget()
|
|
72
|
+
left_layout = QVBoxLayout(left_widget)
|
|
73
|
+
left_layout.addWidget(output_widget)
|
|
74
|
+
left_layout.addWidget(filter_widget)
|
|
75
|
+
|
|
76
|
+
# Right widget
|
|
77
|
+
size_widget = QWidget()
|
|
78
|
+
size_layout = QHBoxLayout(size_widget)
|
|
79
|
+
size_layout.addWidget(self.filesize)
|
|
80
|
+
size_layout.addWidget(self.duration)
|
|
81
|
+
|
|
82
|
+
size_limit_widget = QWidget()
|
|
83
|
+
size_limit_layout = QHBoxLayout(size_limit_widget)
|
|
84
|
+
size_limit_layout.addWidget(self.folder_size_limit)
|
|
85
|
+
size_limit_layout.addWidget(self.total_size_limit)
|
|
86
|
+
|
|
87
|
+
options_widget = QWidget()
|
|
88
|
+
options_layout = QHBoxLayout(options_widget)
|
|
89
|
+
options_layout.addWidget(self.options)
|
|
90
|
+
|
|
91
|
+
right_widget = QWidget()
|
|
92
|
+
right_layout = QVBoxLayout(right_widget)
|
|
93
|
+
right_layout.addWidget(size_widget)
|
|
94
|
+
right_layout.addWidget(size_limit_widget)
|
|
95
|
+
right_layout.addWidget(options_widget)
|
|
96
|
+
|
|
97
|
+
# Body widget
|
|
98
|
+
body_widget = QWidget()
|
|
99
|
+
body_layout = QHBoxLayout(body_widget)
|
|
100
|
+
body_layout.addWidget(left_widget)
|
|
101
|
+
body_layout.addWidget(right_widget)
|
|
102
|
+
|
|
103
|
+
# Assemble Main Layout
|
|
104
|
+
main_layout = QVBoxLayout()
|
|
105
|
+
main_layout.addWidget(path_widget)
|
|
106
|
+
main_layout.addWidget(body_widget)
|
|
107
|
+
main_layout.addWidget(self.logging)
|
|
108
|
+
main_layout.addWidget(self.progress)
|
|
109
|
+
return main_layout
|
|
110
|
+
|
|
111
|
+
def get_config(self) -> ConfigModel:
|
|
112
|
+
"""Get the current configuration from all widgets."""
|
|
113
|
+
return ConfigModel(
|
|
114
|
+
root=self.root.get_config(),
|
|
115
|
+
dest=self.dest.get_config(),
|
|
116
|
+
filecount=self.filecount.get_config(),
|
|
117
|
+
folder=self.folders.get_config(),
|
|
118
|
+
filename=self.filename.get_config(),
|
|
119
|
+
transfermode=self.transfermode.get_config(),
|
|
120
|
+
keyword=self.keywords.get_config(),
|
|
121
|
+
extension=self.extensions.get_config(),
|
|
122
|
+
filesize=self.filesize.get_config(),
|
|
123
|
+
duration=self.duration.get_config(),
|
|
124
|
+
folder_size_limit=self.folder_size_limit.get_config(),
|
|
125
|
+
total_size_limit=self.total_size_limit.get_config(),
|
|
126
|
+
options=self.options.get_config(),
|
|
127
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Workers for GUI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QThread, Slot
|
|
7
|
+
|
|
8
|
+
from ..core import build_engine
|
|
9
|
+
from .observer import GuiObserver, WorkerSignals
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..config import ConfigModel
|
|
13
|
+
from ..core import Engine
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class MainWorker:
|
|
18
|
+
"""Worker for running process."""
|
|
19
|
+
|
|
20
|
+
signals: WorkerSignals
|
|
21
|
+
engine: Engine
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_config(cls, config: ConfigModel) -> MainWorker:
|
|
25
|
+
"""Post-initialization tasks."""
|
|
26
|
+
signals = WorkerSignals()
|
|
27
|
+
observer = GuiObserver(signals)
|
|
28
|
+
engine = build_engine(config)
|
|
29
|
+
engine.set_observer(observer)
|
|
30
|
+
return cls(signals, engine)
|
|
31
|
+
|
|
32
|
+
def run(self) -> None:
|
|
33
|
+
"""Run the process."""
|
|
34
|
+
self.engine.start()
|
|
35
|
+
|
|
36
|
+
def stop(self) -> None:
|
|
37
|
+
"""Stop the process."""
|
|
38
|
+
self.engine.request_stop()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MainThread(QThread):
|
|
42
|
+
"""Worker thread for running process."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, worker: MainWorker) -> None:
|
|
45
|
+
"""Initialize the worker thread."""
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.worker = worker
|
|
48
|
+
|
|
49
|
+
@Slot()
|
|
50
|
+
def run(self) -> None:
|
|
51
|
+
"""Run the process."""
|
|
52
|
+
self.worker.run()
|
|
53
|
+
|
|
54
|
+
def stop(self) -> None:
|
|
55
|
+
"""Stop the process."""
|
|
56
|
+
self.worker.stop()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Utilities package."""
|
|
2
|
+
|
|
3
|
+
from .constants import (
|
|
4
|
+
DURATION_CMD,
|
|
5
|
+
FALSE_STRS,
|
|
6
|
+
INVALID_FILENAME_CHARS,
|
|
7
|
+
PERCENTAGE_100,
|
|
8
|
+
SIZE_MAP,
|
|
9
|
+
TIME_MAP,
|
|
10
|
+
TRUE_STRS,
|
|
11
|
+
WALKER_CACHE_LIMIT,
|
|
12
|
+
AppSetting,
|
|
13
|
+
BytesIn,
|
|
14
|
+
ByteUnit,
|
|
15
|
+
DefaultPath,
|
|
16
|
+
FileError,
|
|
17
|
+
FilenameTemplate,
|
|
18
|
+
FilenameTemplateMapKey,
|
|
19
|
+
GUIFileDialogFilter,
|
|
20
|
+
GUILabel,
|
|
21
|
+
GUIName,
|
|
22
|
+
GUISettingsKey,
|
|
23
|
+
GUITitle,
|
|
24
|
+
IconFilename,
|
|
25
|
+
ReStrFmt,
|
|
26
|
+
StateStatus,
|
|
27
|
+
TimeUnit,
|
|
28
|
+
TransferMode,
|
|
29
|
+
)
|
|
30
|
+
from .helpers import (
|
|
31
|
+
SafeDict,
|
|
32
|
+
are_paths_equal,
|
|
33
|
+
calc_unique_path_name,
|
|
34
|
+
convert_byte_to_human_readable_size,
|
|
35
|
+
convert_string_to_list,
|
|
36
|
+
get_duration,
|
|
37
|
+
get_stem_and_ext,
|
|
38
|
+
load_json,
|
|
39
|
+
remove_directory,
|
|
40
|
+
save_json,
|
|
41
|
+
strtobool,
|
|
42
|
+
)
|
|
43
|
+
from .interfaces import Observer
|
|
44
|
+
from .loggers import initialize_logging
|
|
45
|
+
from .paths import Paths
|
|
46
|
+
from .timestamp import DateTimeStamp
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"DURATION_CMD",
|
|
50
|
+
"FALSE_STRS",
|
|
51
|
+
"INVALID_FILENAME_CHARS",
|
|
52
|
+
"PERCENTAGE_100",
|
|
53
|
+
"SIZE_MAP",
|
|
54
|
+
"TIME_MAP",
|
|
55
|
+
"TRUE_STRS",
|
|
56
|
+
"WALKER_CACHE_LIMIT",
|
|
57
|
+
"AppSetting",
|
|
58
|
+
"ByteUnit",
|
|
59
|
+
"BytesIn",
|
|
60
|
+
"DateTimeStamp",
|
|
61
|
+
"DefaultPath",
|
|
62
|
+
"FileError",
|
|
63
|
+
"FilenameTemplate",
|
|
64
|
+
"FilenameTemplateMapKey",
|
|
65
|
+
"GUIFileDialogFilter",
|
|
66
|
+
"GUILabel",
|
|
67
|
+
"GUIName",
|
|
68
|
+
"GUISettingsKey",
|
|
69
|
+
"GUITitle",
|
|
70
|
+
"IconFilename",
|
|
71
|
+
"Observer",
|
|
72
|
+
"Paths",
|
|
73
|
+
"ReStrFmt",
|
|
74
|
+
"SafeDict",
|
|
75
|
+
"StateStatus",
|
|
76
|
+
"TimeUnit",
|
|
77
|
+
"TransferMode",
|
|
78
|
+
"are_paths_equal",
|
|
79
|
+
"calc_unique_path_name",
|
|
80
|
+
"convert_byte_to_human_readable_size",
|
|
81
|
+
"convert_string_to_list",
|
|
82
|
+
"get_duration",
|
|
83
|
+
"get_stem_and_ext",
|
|
84
|
+
"initialize_logging",
|
|
85
|
+
"load_json",
|
|
86
|
+
"remove_directory",
|
|
87
|
+
"save_json",
|
|
88
|
+
"strtobool",
|
|
89
|
+
]
|