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,581 @@
|
|
|
1
|
+
"""GUI components in PySide6."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QDir, QObject, QUrl, Signal, Slot
|
|
8
|
+
from PySide6.QtGui import QDesktopServices, QIcon
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QCheckBox,
|
|
11
|
+
QComboBox,
|
|
12
|
+
QDoubleSpinBox,
|
|
13
|
+
QFileDialog,
|
|
14
|
+
QFormLayout,
|
|
15
|
+
QGridLayout,
|
|
16
|
+
QGroupBox,
|
|
17
|
+
QHBoxLayout,
|
|
18
|
+
QLineEdit,
|
|
19
|
+
QMenu,
|
|
20
|
+
QProgressBar,
|
|
21
|
+
QPushButton,
|
|
22
|
+
QRadioButton,
|
|
23
|
+
QSpinBox,
|
|
24
|
+
QTextBrowser,
|
|
25
|
+
QTextEdit,
|
|
26
|
+
QVBoxLayout,
|
|
27
|
+
QWidget,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ..config import (
|
|
31
|
+
DirectoryModel,
|
|
32
|
+
FilecountModel,
|
|
33
|
+
FilenameModel,
|
|
34
|
+
ListIncludeExcludeModel,
|
|
35
|
+
MinMaxModel,
|
|
36
|
+
OptionsModel,
|
|
37
|
+
SizeLimitModel,
|
|
38
|
+
TransferModeModel,
|
|
39
|
+
)
|
|
40
|
+
from ..core import get_available_transfer_modes
|
|
41
|
+
from ..utils import ByteUnit, FilenameTemplate, IconFilename, Paths, TimeUnit
|
|
42
|
+
from .qthelpers import set_qt_name, set_qt_tips
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from collections.abc import Sequence
|
|
46
|
+
|
|
47
|
+
from PySide6.QtGui import QDragEnterEvent, QDropEvent
|
|
48
|
+
|
|
49
|
+
from .workers import WorkerSignals
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BaseGroupBox(QGroupBox):
|
|
55
|
+
"""Base class for group boxes with common functionality."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, title: str, name: str, *, checkable: bool = False) -> None:
|
|
58
|
+
"""Initialize the base group box."""
|
|
59
|
+
super().__init__(title=title)
|
|
60
|
+
set_qt_name(self, name)
|
|
61
|
+
self.setCheckable(checkable)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PathSelectorWidget(BaseGroupBox):
|
|
65
|
+
"""Handles logic for selecting a path."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, title: str, name: str, items: Sequence[str]) -> None:
|
|
68
|
+
"""Initialize the path selector widget."""
|
|
69
|
+
super().__init__(title, name)
|
|
70
|
+
self.setAcceptDrops(True)
|
|
71
|
+
|
|
72
|
+
title_lower = self.title().casefold()
|
|
73
|
+
|
|
74
|
+
self.combo = QComboBox()
|
|
75
|
+
self.combo.addItems(items)
|
|
76
|
+
set_qt_name(self.combo, f"{name}_combo")
|
|
77
|
+
set_qt_tips(self.combo, f"Select or enter a path for {title_lower}.")
|
|
78
|
+
|
|
79
|
+
icon_browse = QIcon(Paths.icon(IconFilename.BROWSE))
|
|
80
|
+
icon_open = QIcon(Paths.icon(IconFilename.OPEN_DIR))
|
|
81
|
+
icon_delete = QIcon(Paths.icon(IconFilename.REMOVE))
|
|
82
|
+
|
|
83
|
+
self.btn_browse = QPushButton()
|
|
84
|
+
self.btn_browse.setIcon(icon_browse)
|
|
85
|
+
self.btn_browse.clicked.connect(self.browse)
|
|
86
|
+
set_qt_name(self.btn_browse, f"{name}_browse_btn")
|
|
87
|
+
set_qt_tips(self.btn_browse, f"Browse for {title_lower} folder.")
|
|
88
|
+
|
|
89
|
+
self.btn_open = QPushButton()
|
|
90
|
+
self.btn_open.setIcon(icon_open)
|
|
91
|
+
self.btn_open.clicked.connect(self.open)
|
|
92
|
+
set_qt_name(self.btn_open, f"{name}_open_btn")
|
|
93
|
+
set_qt_tips(self.btn_open, f"Open current {title_lower} folder in file explorer.")
|
|
94
|
+
|
|
95
|
+
self.btn_delete = QPushButton()
|
|
96
|
+
self.btn_delete.setIcon(icon_delete)
|
|
97
|
+
self.btn_delete.clicked.connect(self.delete_curr_item)
|
|
98
|
+
set_qt_name(self.btn_delete, f"{name}_delete_btn")
|
|
99
|
+
set_qt_tips(self.btn_delete, f"Delete current {title_lower} entry.")
|
|
100
|
+
|
|
101
|
+
layout = QHBoxLayout(self)
|
|
102
|
+
layout.addWidget(self.combo, stretch=1)
|
|
103
|
+
layout.addWidget(self.btn_browse)
|
|
104
|
+
layout.addWidget(self.btn_delete)
|
|
105
|
+
layout.addWidget(self.btn_open)
|
|
106
|
+
|
|
107
|
+
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
|
108
|
+
"""Handle drag enter event for folder paths."""
|
|
109
|
+
if event.mimeData().hasUrls():
|
|
110
|
+
event.acceptProposedAction()
|
|
111
|
+
|
|
112
|
+
def dropEvent(self, event: QDropEvent) -> None: # noqa: N802
|
|
113
|
+
"""Handle drop event for folder paths."""
|
|
114
|
+
for url in event.mimeData().urls():
|
|
115
|
+
path = url.toLocalFile()
|
|
116
|
+
if os.path.isdir(path):
|
|
117
|
+
if self.combo.findText(path) == -1:
|
|
118
|
+
self.combo.addItem(path)
|
|
119
|
+
self.combo.setCurrentText(path)
|
|
120
|
+
|
|
121
|
+
@Slot()
|
|
122
|
+
def browse(self) -> None:
|
|
123
|
+
"""Return the browse button."""
|
|
124
|
+
d = QFileDialog.getExistingDirectory(self, f"Select {self.title()}")
|
|
125
|
+
if d:
|
|
126
|
+
if self.combo.findText(d) == -1:
|
|
127
|
+
self.combo.addItem(d)
|
|
128
|
+
self.combo.setCurrentText(d)
|
|
129
|
+
|
|
130
|
+
@Slot()
|
|
131
|
+
def delete_curr_item(self) -> None:
|
|
132
|
+
"""Delete the currently selected item."""
|
|
133
|
+
if self.combo.count() > 0:
|
|
134
|
+
self.combo.removeItem(self.combo.currentIndex())
|
|
135
|
+
|
|
136
|
+
@Slot()
|
|
137
|
+
def open(self) -> None:
|
|
138
|
+
"""Open the currently selected path in file explorer."""
|
|
139
|
+
try:
|
|
140
|
+
QDesktopServices.openUrl(QUrl.fromLocalFile(self.combo.currentText()))
|
|
141
|
+
except Exception:
|
|
142
|
+
logger.exception("Failed to open path")
|
|
143
|
+
|
|
144
|
+
def get_config(self) -> str:
|
|
145
|
+
"""Return clean data for the config."""
|
|
146
|
+
return self.combo.currentText()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class RootPathSelectorWidget(PathSelectorWidget):
|
|
150
|
+
"""Handles logic for selecting a root path."""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
"""Initialize the root path selector widget."""
|
|
154
|
+
super().__init__("Root", "root", [QDir.rootPath()])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class DestPathSelectorWidget(PathSelectorWidget):
|
|
158
|
+
"""Handles logic for selecting a destination path."""
|
|
159
|
+
|
|
160
|
+
def __init__(self) -> None:
|
|
161
|
+
"""Initialize the destination path selector widget."""
|
|
162
|
+
super().__init__("Destination", "dest", [QDir.homePath()])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class FileCountWidget(BaseGroupBox):
|
|
166
|
+
"""Handles logic for file count settings."""
|
|
167
|
+
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
"""Initialize the file count widget."""
|
|
170
|
+
name = "filecount"
|
|
171
|
+
super().__init__("File count", name)
|
|
172
|
+
|
|
173
|
+
self.radio_fixed = QRadioButton("Fixed")
|
|
174
|
+
self.radio_fixed.setChecked(True)
|
|
175
|
+
set_qt_name(self.radio_fixed, f"{name}_fixed_chk")
|
|
176
|
+
set_qt_tips(self.radio_fixed, "Select fixed file count.")
|
|
177
|
+
|
|
178
|
+
self.spin_fixed = QSpinBox(suffix=" files")
|
|
179
|
+
set_qt_name(self.spin_fixed, f"{name}_fixed_val")
|
|
180
|
+
set_qt_tips(self.spin_fixed, "Number of files to copy.")
|
|
181
|
+
|
|
182
|
+
self.radio_rand = QRadioButton("Random")
|
|
183
|
+
set_qt_name(self.radio_rand, f"{name}_rand_chk")
|
|
184
|
+
set_qt_tips(self.radio_rand, "Select random file count.")
|
|
185
|
+
|
|
186
|
+
self.spin_min_rand = QSpinBox(prefix="Min: ")
|
|
187
|
+
set_qt_name(self.spin_min_rand, f"{name}_rand_min")
|
|
188
|
+
set_qt_tips(self.spin_min_rand, "Minimum random file count.")
|
|
189
|
+
|
|
190
|
+
self.spin_max_rand = QSpinBox(prefix="Max: ")
|
|
191
|
+
set_qt_name(self.spin_max_rand, f"{name}_rand_max")
|
|
192
|
+
set_qt_tips(self.spin_max_rand, "Maximum random file count.")
|
|
193
|
+
|
|
194
|
+
self.spin_min_rand.valueChanged.connect(self.spin_max_rand.setMinimum)
|
|
195
|
+
self.spin_max_rand.valueChanged.connect(self.spin_min_rand.setMaximum)
|
|
196
|
+
|
|
197
|
+
self.radio_fixed.toggled.connect(self.spin_fixed.setEnabled)
|
|
198
|
+
self.radio_fixed.toggled.connect(lambda c: self.spin_min_rand.setDisabled(c))
|
|
199
|
+
self.radio_fixed.toggled.connect(lambda c: self.spin_max_rand.setDisabled(c))
|
|
200
|
+
|
|
201
|
+
# Initial state
|
|
202
|
+
self.spin_min_rand.setDisabled(True)
|
|
203
|
+
self.spin_max_rand.setDisabled(True)
|
|
204
|
+
|
|
205
|
+
layout = QGridLayout(self)
|
|
206
|
+
layout.addWidget(self.radio_fixed, 0, 0)
|
|
207
|
+
layout.addWidget(self.spin_fixed, 0, 1)
|
|
208
|
+
layout.addWidget(self.radio_rand, 1, 0)
|
|
209
|
+
layout.addWidget(self.spin_min_rand, 1, 1)
|
|
210
|
+
layout.addWidget(self.spin_max_rand, 2, 1)
|
|
211
|
+
|
|
212
|
+
def get_config(self) -> FilecountModel:
|
|
213
|
+
"""Return clean data for the config."""
|
|
214
|
+
return FilecountModel(
|
|
215
|
+
count=self.spin_fixed.value(),
|
|
216
|
+
is_rand_enabled=self.radio_rand.isChecked(),
|
|
217
|
+
rand_min=self.spin_min_rand.value(),
|
|
218
|
+
rand_max=self.spin_max_rand.value(),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class FolderCreatorWidget(BaseGroupBox):
|
|
223
|
+
"""Handles logic for creating folders."""
|
|
224
|
+
|
|
225
|
+
def __init__(self) -> None:
|
|
226
|
+
"""Initialize the create folders widget."""
|
|
227
|
+
name = "folder"
|
|
228
|
+
super().__init__("Create Folders", name, checkable=True)
|
|
229
|
+
|
|
230
|
+
self.spinbox_folder_count = QSpinBox(suffix=" folders")
|
|
231
|
+
set_qt_name(self.spinbox_folder_count, f"{name}_count")
|
|
232
|
+
set_qt_tips(self.spinbox_folder_count, "Number of folders to create.")
|
|
233
|
+
|
|
234
|
+
self.lineedit_folder_name = QLineEdit(placeholderText="Ex: Random_Files", clearButtonEnabled=True)
|
|
235
|
+
set_qt_name(self.lineedit_folder_name, f"{name}_name")
|
|
236
|
+
set_qt_tips(self.lineedit_folder_name, "Template for naming created folders.")
|
|
237
|
+
|
|
238
|
+
self.chk_unique_folders = QCheckBox("Ensure unique folders")
|
|
239
|
+
set_qt_name(self.chk_unique_folders, f"{name}_unique")
|
|
240
|
+
set_qt_tips(self.chk_unique_folders, "If checked, created folder names will have unique files.")
|
|
241
|
+
|
|
242
|
+
layout = QVBoxLayout(self)
|
|
243
|
+
layout.addWidget(self.spinbox_folder_count)
|
|
244
|
+
layout.addWidget(self.lineedit_folder_name)
|
|
245
|
+
layout.addWidget(self.chk_unique_folders)
|
|
246
|
+
|
|
247
|
+
def get_config(self) -> DirectoryModel:
|
|
248
|
+
"""Return clean data for the config."""
|
|
249
|
+
return DirectoryModel(
|
|
250
|
+
is_enabled=self.isChecked(),
|
|
251
|
+
is_unique=self.chk_unique_folders.isChecked(),
|
|
252
|
+
name=self.lineedit_folder_name.text(),
|
|
253
|
+
count=self.spinbox_folder_count.value() if self.isChecked() else 1,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class FilenameWidget(BaseGroupBox):
|
|
258
|
+
"""Handles logic for filename template settings."""
|
|
259
|
+
|
|
260
|
+
def __init__(self) -> None:
|
|
261
|
+
"""Initialize the filename template settings widget."""
|
|
262
|
+
name: str = "filename"
|
|
263
|
+
super().__init__("Filename", name)
|
|
264
|
+
|
|
265
|
+
self.edit_template = QLineEdit("{original}", placeholderText="Ex: {original}_{index}", clearButtonEnabled=True)
|
|
266
|
+
set_qt_name(self.edit_template, f"{name}_template")
|
|
267
|
+
set_qt_tips(self.edit_template, "Template for renaming files. Use the 'Insert Tag' button to add tags.")
|
|
268
|
+
|
|
269
|
+
self.menu_template = QMenu(title="Tags")
|
|
270
|
+
set_qt_name(self.menu_template, f"{name}_template_menu")
|
|
271
|
+
set_qt_tips(self.menu_template, "Select a tag to insert into the filename template.")
|
|
272
|
+
|
|
273
|
+
self.btn_template = QPushButton("Insert tag")
|
|
274
|
+
self.btn_template.setMenu(self.menu_template)
|
|
275
|
+
set_qt_name(self.btn_template, f"{name}_template_button")
|
|
276
|
+
set_qt_tips(self.btn_template, "Insert a tag into the template at the cursor position.")
|
|
277
|
+
|
|
278
|
+
for lbl in FilenameTemplate:
|
|
279
|
+
action = self.menu_template.addAction(lbl)
|
|
280
|
+
action.triggered.connect(lambda _, tag=lbl: self.insert_tag(tag))
|
|
281
|
+
|
|
282
|
+
layout = QHBoxLayout(self)
|
|
283
|
+
layout.addWidget(self.edit_template)
|
|
284
|
+
layout.addWidget(self.btn_template)
|
|
285
|
+
|
|
286
|
+
@Slot(str)
|
|
287
|
+
def insert_tag(self, tag: str) -> None:
|
|
288
|
+
"""Insert a tag into the template at the cursor position."""
|
|
289
|
+
self.edit_template.insert(tag)
|
|
290
|
+
self.edit_template.setFocus()
|
|
291
|
+
|
|
292
|
+
def get_config(self) -> FilenameModel:
|
|
293
|
+
"""Return clean data for the config."""
|
|
294
|
+
val = self.edit_template.text() or "{original}"
|
|
295
|
+
return FilenameModel(template=val)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TransferModeWidget(BaseGroupBox):
|
|
299
|
+
"""Handles logic for mode settings."""
|
|
300
|
+
|
|
301
|
+
def __init__(self) -> None:
|
|
302
|
+
"""Initialize the mode settings widget."""
|
|
303
|
+
name = "transfermode"
|
|
304
|
+
super().__init__("Transfer Mode", name)
|
|
305
|
+
|
|
306
|
+
self.combo_mode = QComboBox()
|
|
307
|
+
self.combo_mode.addItems(get_available_transfer_modes())
|
|
308
|
+
set_qt_name(self.combo_mode, f"{name}_mode")
|
|
309
|
+
set_qt_tips(self.combo_mode, "Select the transfer mode to use.")
|
|
310
|
+
|
|
311
|
+
layout = QVBoxLayout(self)
|
|
312
|
+
layout.addWidget(self.combo_mode)
|
|
313
|
+
|
|
314
|
+
def get_config(self) -> TransferModeModel:
|
|
315
|
+
"""Return clean data for the config."""
|
|
316
|
+
return TransferModeModel(transfer_mode=self.combo_mode.currentText())
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ListIncludeExcludeFilterWidget(BaseGroupBox):
|
|
320
|
+
"""Handles the Include/Exclude pattern for Keywords and Extensions."""
|
|
321
|
+
|
|
322
|
+
def __init__(self, title: str, name: str) -> None:
|
|
323
|
+
"""Initialize the dual list widget."""
|
|
324
|
+
super().__init__(title, name, checkable=True)
|
|
325
|
+
|
|
326
|
+
title_lower = self.title().casefold()
|
|
327
|
+
|
|
328
|
+
self.filter_edit = QLineEdit(placeholderText="comma,separated,items", clearButtonEnabled=True)
|
|
329
|
+
set_qt_name(self.filter_edit, f"{name}_text")
|
|
330
|
+
set_qt_tips(self.filter_edit, f"Enter {title_lower} separated by commas.")
|
|
331
|
+
|
|
332
|
+
self.filter_include_radio = QRadioButton("Include")
|
|
333
|
+
self.filter_include_radio.setChecked(True)
|
|
334
|
+
set_qt_name(self.filter_include_radio, f"{name}_include")
|
|
335
|
+
set_qt_tips(self.filter_include_radio, f"Include only items matching the {title_lower} filter.")
|
|
336
|
+
|
|
337
|
+
self.filter_exclude_radio = QRadioButton("Exclude")
|
|
338
|
+
set_qt_name(self.filter_exclude_radio, f"{name}_exclude")
|
|
339
|
+
set_qt_tips(self.filter_exclude_radio, f"Exclude items matching the {title_lower} filter.")
|
|
340
|
+
|
|
341
|
+
layout = QHBoxLayout(self)
|
|
342
|
+
layout.addWidget(self.filter_edit)
|
|
343
|
+
layout.addWidget(self.filter_include_radio)
|
|
344
|
+
layout.addWidget(self.filter_exclude_radio)
|
|
345
|
+
|
|
346
|
+
def get_config(self) -> ListIncludeExcludeModel:
|
|
347
|
+
"""Return clean data for the config."""
|
|
348
|
+
return ListIncludeExcludeModel(
|
|
349
|
+
is_enabled=self.isChecked(),
|
|
350
|
+
should_include=self.filter_include_radio.isChecked(),
|
|
351
|
+
text=self.filter_edit.text(),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class ExtensionsFilterWidget(ListIncludeExcludeFilterWidget):
|
|
356
|
+
"""Handles the Include/Exclude pattern for file extensions."""
|
|
357
|
+
|
|
358
|
+
def __init__(self) -> None:
|
|
359
|
+
"""Initialize the extensions filter widget."""
|
|
360
|
+
super().__init__("Extensions", "extension")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class KeywordsFilterWidget(ListIncludeExcludeFilterWidget):
|
|
364
|
+
"""Handles the Include/Exclude pattern for keywords."""
|
|
365
|
+
|
|
366
|
+
def __init__(self) -> None:
|
|
367
|
+
"""Initialize the keywords filter widget."""
|
|
368
|
+
super().__init__("Keywords", "keyword")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class MinMaxFilterWidget(BaseGroupBox):
|
|
372
|
+
"""Handles logic for ranges (Min/Max), e.g., Size or Duration."""
|
|
373
|
+
|
|
374
|
+
def __init__(self, title: str, name: str, items: Sequence[str | ByteUnit | TimeUnit]) -> None:
|
|
375
|
+
"""Initialize the range filter widget."""
|
|
376
|
+
super().__init__(title, name, checkable=True)
|
|
377
|
+
|
|
378
|
+
self.spin_min = QDoubleSpinBox(prefix="Min ")
|
|
379
|
+
set_qt_name(self.spin_min, f"{name}_minimum")
|
|
380
|
+
set_qt_tips(self.spin_min, f"Minimum value for the {name} filter.")
|
|
381
|
+
|
|
382
|
+
self.spin_max = QDoubleSpinBox(prefix="Max ")
|
|
383
|
+
set_qt_name(self.spin_max, f"{name}_maximum")
|
|
384
|
+
set_qt_tips(self.spin_max, f"Maximum value for the {name} filter.")
|
|
385
|
+
|
|
386
|
+
self.spin_min.valueChanged.connect(self.spin_max.setMinimum)
|
|
387
|
+
self.spin_max.valueChanged.connect(self.spin_min.setMaximum)
|
|
388
|
+
|
|
389
|
+
self.combo_unit = QComboBox()
|
|
390
|
+
self.combo_unit.addItems(items)
|
|
391
|
+
set_qt_name(self.combo_unit, f"{name}_unit")
|
|
392
|
+
set_qt_tips(self.combo_unit, f"Unit multiplier for the {name} filter.")
|
|
393
|
+
|
|
394
|
+
layout = QHBoxLayout(self)
|
|
395
|
+
layout.addWidget(self.spin_min)
|
|
396
|
+
layout.addWidget(self.spin_max)
|
|
397
|
+
layout.addWidget(self.combo_unit)
|
|
398
|
+
|
|
399
|
+
def get_config(self) -> MinMaxModel:
|
|
400
|
+
"""Return clean data for the config."""
|
|
401
|
+
return MinMaxModel(
|
|
402
|
+
is_enabled=self.isChecked(),
|
|
403
|
+
minimum=self.spin_min.value(),
|
|
404
|
+
maximum=self.spin_max.value(),
|
|
405
|
+
unit=self.combo_unit.currentText(),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SizeFilterWidget(MinMaxFilterWidget):
|
|
410
|
+
"""Handles logic for size range filter."""
|
|
411
|
+
|
|
412
|
+
def __init__(self) -> None:
|
|
413
|
+
"""Initialize the size filter widget."""
|
|
414
|
+
super().__init__("File Size", "filesize", tuple(ByteUnit))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class DurationFilterWidget(MinMaxFilterWidget):
|
|
418
|
+
"""Handles logic for duration range filter."""
|
|
419
|
+
|
|
420
|
+
def __init__(self) -> None:
|
|
421
|
+
"""Initialize the duration filter widget."""
|
|
422
|
+
super().__init__("Duration", "duration", tuple(TimeUnit))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class SizeLimitWidget(BaseGroupBox):
|
|
426
|
+
"""Handles logic for output folder size limit."""
|
|
427
|
+
|
|
428
|
+
def __init__(self, title: str, name: str, items: Sequence[str | ByteUnit | TimeUnit]) -> None:
|
|
429
|
+
"""Initialize the size limit widget."""
|
|
430
|
+
super().__init__(title, name, checkable=True)
|
|
431
|
+
|
|
432
|
+
self.spin_size = QDoubleSpinBox(prefix="Size ")
|
|
433
|
+
self.spin_size.setSpecialValueText("Unlimited")
|
|
434
|
+
set_qt_name(self.spin_size, f"{name}_size")
|
|
435
|
+
set_qt_tips(self.spin_size, f"{title} value for the size limit.")
|
|
436
|
+
|
|
437
|
+
self.combo_unit = QComboBox()
|
|
438
|
+
self.combo_unit.addItems(items)
|
|
439
|
+
set_qt_name(self.combo_unit, f"{name}_unit")
|
|
440
|
+
set_qt_tips(self.combo_unit, f"Unit multiplier for the {title} filter.")
|
|
441
|
+
|
|
442
|
+
layout = QHBoxLayout(self)
|
|
443
|
+
layout.addWidget(self.spin_size)
|
|
444
|
+
layout.addWidget(self.combo_unit)
|
|
445
|
+
|
|
446
|
+
def get_config(self) -> SizeLimitModel:
|
|
447
|
+
"""Return clean data for the config."""
|
|
448
|
+
return SizeLimitModel(
|
|
449
|
+
is_enabled=self.isChecked(),
|
|
450
|
+
size_limit=self.spin_size.value(),
|
|
451
|
+
unit=self.combo_unit.currentText(),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class FolderSizeLimitWidget(SizeLimitWidget):
|
|
456
|
+
"""Handles logic for per-folder size limit."""
|
|
457
|
+
|
|
458
|
+
def __init__(self) -> None:
|
|
459
|
+
"""Initialize the folder size limit widget."""
|
|
460
|
+
super().__init__("Max Folder Size", "folder_size_limit", tuple(ByteUnit))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class TotalSizeLimitWidget(SizeLimitWidget):
|
|
464
|
+
"""Handles logic for total size limit across all folders."""
|
|
465
|
+
|
|
466
|
+
def __init__(self) -> None:
|
|
467
|
+
"""Initialize the total size limit widget."""
|
|
468
|
+
super().__init__("Max Total Size", "total_size_limit", tuple(ByteUnit))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class OptionsWidget(BaseGroupBox):
|
|
472
|
+
"""Handles logic for miscellaneous options."""
|
|
473
|
+
|
|
474
|
+
def __init__(self) -> None:
|
|
475
|
+
"""Initialize the options widget."""
|
|
476
|
+
name = "options"
|
|
477
|
+
super().__init__("Options", name)
|
|
478
|
+
|
|
479
|
+
self.spin_max_per_folder = QSpinBox()
|
|
480
|
+
self.spin_max_per_folder.setSpecialValueText("Unlimited")
|
|
481
|
+
set_qt_name(self.spin_max_per_folder, f"{name}_max_per_folder")
|
|
482
|
+
set_qt_tips(self.spin_max_per_folder, "Maximum number of files allowed per input folder. 0 for unlimited.")
|
|
483
|
+
|
|
484
|
+
self.chk_follow_symlink = QCheckBox()
|
|
485
|
+
set_qt_name(self.chk_follow_symlink, f"{name}_should_follow_symlink")
|
|
486
|
+
set_qt_tips(self.chk_follow_symlink, "If checked, symbolic links will be followed during file traversal.")
|
|
487
|
+
|
|
488
|
+
self.chk_dry_run = QCheckBox()
|
|
489
|
+
set_qt_name(self.chk_dry_run, f"{name}_dry_run")
|
|
490
|
+
set_qt_tips(self.chk_dry_run, "If checked, no files will actually be copied.")
|
|
491
|
+
|
|
492
|
+
layout = QFormLayout(self)
|
|
493
|
+
layout.addRow("Max from one folder", self.spin_max_per_folder)
|
|
494
|
+
layout.addRow("Follow symbolic links", self.chk_follow_symlink)
|
|
495
|
+
layout.addRow("Dry run (simulation)", self.chk_dry_run)
|
|
496
|
+
|
|
497
|
+
def get_config(self) -> OptionsModel:
|
|
498
|
+
"""Return clean data for the config."""
|
|
499
|
+
return OptionsModel(
|
|
500
|
+
max_per_folder=self.spin_max_per_folder.value(),
|
|
501
|
+
should_follow_symlink=self.chk_follow_symlink.isChecked(),
|
|
502
|
+
is_dry_run=self.chk_dry_run.isChecked(),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class ProgressWidget(QWidget):
|
|
507
|
+
"""Progress bars and execution controls."""
|
|
508
|
+
|
|
509
|
+
def __init__(self) -> None:
|
|
510
|
+
"""Post-initialize the progress widget."""
|
|
511
|
+
super().__init__()
|
|
512
|
+
name = "progress"
|
|
513
|
+
set_qt_name(self, name)
|
|
514
|
+
|
|
515
|
+
self.progbar_total = QProgressBar(textVisible=True)
|
|
516
|
+
set_qt_name(self.progbar_total, f"{name}_total")
|
|
517
|
+
set_qt_tips(self.progbar_total, "Total progress bar, max is set at number of output folders.")
|
|
518
|
+
|
|
519
|
+
self.progbar_dir = QProgressBar(textVisible=True)
|
|
520
|
+
set_qt_name(self.progbar_dir, f"{name}_dir")
|
|
521
|
+
set_qt_tips(self.progbar_dir, "Current folder progress bar, max is set at number of files to copy.")
|
|
522
|
+
|
|
523
|
+
layout = QFormLayout(self)
|
|
524
|
+
layout.addRow("Total", self.progbar_total)
|
|
525
|
+
layout.addRow("Folder", self.progbar_dir)
|
|
526
|
+
|
|
527
|
+
@Slot()
|
|
528
|
+
def update_total_prog(self) -> None:
|
|
529
|
+
"""Update the total progress bar."""
|
|
530
|
+
self.progbar_total.setValue(self.progbar_total.value() + 1)
|
|
531
|
+
|
|
532
|
+
def reset(self) -> None:
|
|
533
|
+
"""Reset progress bars."""
|
|
534
|
+
self.progbar_total.setValue(0)
|
|
535
|
+
self.progbar_dir.setValue(0)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class LoggingWidget(QWidget):
|
|
539
|
+
"""Logging widget."""
|
|
540
|
+
|
|
541
|
+
def __init__(self) -> None:
|
|
542
|
+
"""Post-initialize the logging widget."""
|
|
543
|
+
super().__init__()
|
|
544
|
+
name = "logging"
|
|
545
|
+
set_qt_name(self, name)
|
|
546
|
+
|
|
547
|
+
self.textbrowser_log = QTextBrowser()
|
|
548
|
+
self.textbrowser_log.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
|
549
|
+
set_qt_name(self.textbrowser_log, f"{name}_log")
|
|
550
|
+
set_qt_tips(self.textbrowser_log, "Log for output messages.")
|
|
551
|
+
|
|
552
|
+
layout = QHBoxLayout(self)
|
|
553
|
+
layout.addWidget(self.textbrowser_log)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class ProgressBinder(QObject):
|
|
557
|
+
"""Class for binding progress widgets."""
|
|
558
|
+
|
|
559
|
+
count: ClassVar[Signal] = Signal(int)
|
|
560
|
+
finished: ClassVar[Signal] = Signal()
|
|
561
|
+
|
|
562
|
+
def __init__(self, progressw: ProgressWidget, loggingw: LoggingWidget) -> None:
|
|
563
|
+
"""Initialize the ProgressBinder."""
|
|
564
|
+
super().__init__()
|
|
565
|
+
self.progress = progressw
|
|
566
|
+
self.logging = loggingw
|
|
567
|
+
|
|
568
|
+
def bind(self, signals: WorkerSignals) -> None:
|
|
569
|
+
"""Bind worker signals to progress widget."""
|
|
570
|
+
signals.progress_total.connect(self.progress.progbar_total.setMaximum)
|
|
571
|
+
signals.count_total.connect(self.progress.update_total_prog)
|
|
572
|
+
signals.progress.connect(self.progress.progbar_dir.setMaximum)
|
|
573
|
+
signals.log.connect(self.logging.textbrowser_log.append)
|
|
574
|
+
signals.count.connect(self.on_count)
|
|
575
|
+
signals.finished.connect(self.finished.emit)
|
|
576
|
+
|
|
577
|
+
@Slot(int)
|
|
578
|
+
def on_count(self, count: int) -> None:
|
|
579
|
+
"""Handle directory progress count update."""
|
|
580
|
+
self.progress.progbar_dir.setValue(count)
|
|
581
|
+
self.count.emit(count)
|