pixmatch 0.0.1__py3-none-any.whl → 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.
Potentially problematic release.
This version of pixmatch might be problematic. Click here for more details.
- pixmatch/__init__.py +444 -444
- pixmatch/__main__.py +48 -48
- pixmatch/utils.py +36 -36
- {pixmatch-0.0.1.dist-info → pixmatch-0.0.2.dist-info}/METADATA +93 -93
- pixmatch-0.0.2.dist-info/RECORD +8 -0
- {pixmatch-0.0.1.dist-info → pixmatch-0.0.2.dist-info}/licenses/LICENSE +18 -18
- pixmatch/gui/__init__.py +0 -837
- pixmatch/gui/pixmatch.ico +0 -0
- pixmatch/gui/utils.py +0 -13
- pixmatch/gui/widgets.py +0 -656
- pixmatch/gui/zip.png +0 -0
- pixmatch-0.0.1.dist-info/RECORD +0 -13
- {pixmatch-0.0.1.dist-info → pixmatch-0.0.2.dist-info}/WHEEL +0 -0
- {pixmatch-0.0.1.dist-info → pixmatch-0.0.2.dist-info}/top_level.txt +0 -0
pixmatch/gui/__init__.py
DELETED
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
# TODO: Validate that users don't select overlapping paths...
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from PySide6 import QtCore, QtGui, QtWidgets
|
|
10
|
-
|
|
11
|
-
from pixmatch import ImageMatcher, ImageMatch, NewGroup, NewMatch, Finished, ZipPath
|
|
12
|
-
from pixmatch.gui.utils import NO_MARGIN, MAX_SIZE_POLICY
|
|
13
|
-
from pixmatch.gui.widgets import DuplicateGroupList, DirFileSystemModel, ImageViewPane, SelectionState
|
|
14
|
-
from pixmatch.utils import human_bytes
|
|
15
|
-
|
|
16
|
-
ICON_PATH = Path(__file__).resolve().parent / 'pixmatch.ico'
|
|
17
|
-
print(f"Icon at {ICON_PATH} : {ICON_PATH.exists()}")
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def ceildiv(a, b):
|
|
23
|
-
"""The opposite of floordiv, //"""
|
|
24
|
-
return -(a // -b)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def project_version() -> str:
|
|
28
|
-
try:
|
|
29
|
-
return version("pixmatch") # e.g. "myapp"
|
|
30
|
-
except PackageNotFoundError:
|
|
31
|
-
return "0.0.0+unknown"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class WorkerSignals(QtCore.QObject):
|
|
35
|
-
"""
|
|
36
|
-
Signals from a running worker thread.
|
|
37
|
-
|
|
38
|
-
finished
|
|
39
|
-
No data
|
|
40
|
-
|
|
41
|
-
error
|
|
42
|
-
tuple (exctype, value, traceback.format_exc())
|
|
43
|
-
|
|
44
|
-
result
|
|
45
|
-
object data returned from processing, anything
|
|
46
|
-
|
|
47
|
-
progress
|
|
48
|
-
float indicating % progress
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
new_match = QtCore.Signal(tuple)
|
|
52
|
-
new_group = QtCore.Signal(object)
|
|
53
|
-
finish = QtCore.Signal()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class ProcessorThread(QtCore.QRunnable):
|
|
57
|
-
|
|
58
|
-
def __init__(self, processor, *args, **kwargs):
|
|
59
|
-
super().__init__()
|
|
60
|
-
self.args = args
|
|
61
|
-
self.kwargs = kwargs
|
|
62
|
-
self.processor = processor
|
|
63
|
-
self.signals = WorkerSignals()
|
|
64
|
-
|
|
65
|
-
# timer lives on the GUI thread; it polls the library queue
|
|
66
|
-
self._poller = QtCore.QTimer()
|
|
67
|
-
self._poller.setInterval(250)
|
|
68
|
-
self._poller.timeout.connect(self._drain_events)
|
|
69
|
-
self._poller.start()
|
|
70
|
-
|
|
71
|
-
def _drain_events(self):
|
|
72
|
-
while not self.processor.events.empty():
|
|
73
|
-
evt = self.processor.events.get_nowait()
|
|
74
|
-
if isinstance(evt, NewGroup):
|
|
75
|
-
self.signals.new_group.emit(evt.group)
|
|
76
|
-
elif isinstance(evt, NewMatch):
|
|
77
|
-
self.signals.new_match.emit((evt.group, evt.path))
|
|
78
|
-
elif isinstance(evt, Finished):
|
|
79
|
-
self._poller.stop()
|
|
80
|
-
self.signals.finish.emit()
|
|
81
|
-
|
|
82
|
-
def run(self):
|
|
83
|
-
self.processor.run(*self.args, **self.kwargs)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class MainWindow(QtWidgets.QMainWindow):
|
|
87
|
-
"""
|
|
88
|
-
The main application window emulating VisiPics' primary workflow.
|
|
89
|
-
|
|
90
|
-
Key elements:
|
|
91
|
-
- Toolbar with folder selection (stub), Load Test Image, Similarity slider
|
|
92
|
-
- Central area that shows one group/page at a time
|
|
93
|
-
- Prev/Next page navigation
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
def __init__(self, start_paths=None):
|
|
97
|
-
super().__init__()
|
|
98
|
-
self.setWindowTitle(f"PixMatch v{project_version()}")
|
|
99
|
-
self.resize(1200, 800)
|
|
100
|
-
|
|
101
|
-
# State
|
|
102
|
-
self.current_page: int = 1
|
|
103
|
-
self.processor = None
|
|
104
|
-
self.file_states = dict()
|
|
105
|
-
self.threadpool = QtCore.QThreadPool()
|
|
106
|
-
self._gui_paused = False
|
|
107
|
-
|
|
108
|
-
# UI build
|
|
109
|
-
self.build_menubar()
|
|
110
|
-
self.build_central()
|
|
111
|
-
self.build_statusbar()
|
|
112
|
-
self.build_extra()
|
|
113
|
-
|
|
114
|
-
for start_path in start_paths or []:
|
|
115
|
-
self.selected_file_path_display.addItem(str(start_path))
|
|
116
|
-
|
|
117
|
-
if start_paths:
|
|
118
|
-
self.selected_file_path_display.setCurrentRow(0)
|
|
119
|
-
|
|
120
|
-
self.setWindowIcon(QtGui.QIcon(QtGui.QPixmap(ICON_PATH)))
|
|
121
|
-
|
|
122
|
-
def build_extra(self):
|
|
123
|
-
self.exit_warning = QtWidgets.QMessageBox(self)
|
|
124
|
-
self.exit_warning.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
|
125
|
-
self.exit_warning.setWindowTitle("Close?")
|
|
126
|
-
self.exit_warning.setText("Are you sure you want to quit?")
|
|
127
|
-
self.exit_warning.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
|
|
128
|
-
self.exit_warning.setDefaultButton(QtWidgets.QMessageBox.StandardButton.No)
|
|
129
|
-
self.exit_warning.setEscapeButton(QtWidgets.QMessageBox.StandardButton.No)
|
|
130
|
-
self.exit_warning.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
131
|
-
|
|
132
|
-
def build_menubar(self) -> None:
|
|
133
|
-
"""Creates the top menu bar"""
|
|
134
|
-
menu = self.menuBar()
|
|
135
|
-
|
|
136
|
-
# region File menu
|
|
137
|
-
load_project = QtGui.QAction("Load Project...", self, enabled=False)
|
|
138
|
-
save_project = QtGui.QAction("Save Project...", self, enabled=False)
|
|
139
|
-
exit_project = QtGui.QAction("Exit", self)
|
|
140
|
-
exit_project.triggered.connect(self.on_exit)
|
|
141
|
-
|
|
142
|
-
file_menu = menu.addMenu("&File")
|
|
143
|
-
file_menu.addAction(load_project)
|
|
144
|
-
file_menu.addAction(save_project)
|
|
145
|
-
file_menu.addSeparator()
|
|
146
|
-
file_menu.addAction(exit_project)
|
|
147
|
-
# endregion
|
|
148
|
-
|
|
149
|
-
# region Edit menu
|
|
150
|
-
# TODO: When adding this, make sure not to allow marking a zip file as delete
|
|
151
|
-
mark_delete = QtGui.QAction("Delete", self, enabled=False)
|
|
152
|
-
mark_ignore = QtGui.QAction("Ignore", self, enabled=False)
|
|
153
|
-
mark_ignore_group = QtGui.QAction("Ignore Group", self, enabled=False)
|
|
154
|
-
mark_ignore_folder = QtGui.QAction("Ignore Folder", self, enabled=False)
|
|
155
|
-
mark_rename = QtGui.QAction("Rename this file...", self, enabled=False)
|
|
156
|
-
mark_move = QtGui.QAction("Move this file...", self, enabled=False)
|
|
157
|
-
mark_symlink = QtGui.QAction("Symlink this file...", self, enabled=False)
|
|
158
|
-
unmark = QtGui.QAction("Un-select", self, enabled=False)
|
|
159
|
-
|
|
160
|
-
edit_menu = menu.addMenu("&Edit")
|
|
161
|
-
edit_menu.addAction(mark_delete)
|
|
162
|
-
edit_menu.addAction(mark_ignore)
|
|
163
|
-
edit_menu.addAction(mark_ignore_group)
|
|
164
|
-
edit_menu.addAction(mark_ignore_folder)
|
|
165
|
-
edit_menu.addSeparator()
|
|
166
|
-
edit_menu.addAction(mark_rename)
|
|
167
|
-
edit_menu.addAction(mark_move)
|
|
168
|
-
edit_menu.addAction(mark_symlink)
|
|
169
|
-
edit_menu.addSeparator()
|
|
170
|
-
edit_menu.addAction(unmark)
|
|
171
|
-
# endregion
|
|
172
|
-
|
|
173
|
-
# region View menu
|
|
174
|
-
page_next = QtGui.QAction("Next page", self)
|
|
175
|
-
page_next.triggered.connect(self.on_page_up)
|
|
176
|
-
page_back = QtGui.QAction("Previous page", self)
|
|
177
|
-
page_back.triggered.connect(self.on_page_down)
|
|
178
|
-
self.preview_resized = QtGui.QAction("Preview resized", self, checked=True, checkable=True)
|
|
179
|
-
preview_full_size = QtGui.QAction("Preview full size", self, checkable=True)
|
|
180
|
-
|
|
181
|
-
preview_options_grp = QtGui.QActionGroup(self)
|
|
182
|
-
preview_options_grp.setExclusive(True)
|
|
183
|
-
preview_options_grp.addAction(preview_full_size)
|
|
184
|
-
preview_options_grp.addAction(self.preview_resized)
|
|
185
|
-
|
|
186
|
-
# TODO: I'm not sure why we would want to slow display?
|
|
187
|
-
# and show differences has never worked for me
|
|
188
|
-
# mark_rename = QtGui.QAction("Slow preview display", self)
|
|
189
|
-
# mark_move = QtGui.QAction("Show differences", self)
|
|
190
|
-
|
|
191
|
-
view_menu = menu.addMenu("&View")
|
|
192
|
-
view_menu.addAction(page_next)
|
|
193
|
-
view_menu.addAction(page_back)
|
|
194
|
-
view_menu.addSeparator()
|
|
195
|
-
view_menu.addAction(self.preview_resized)
|
|
196
|
-
view_menu.addAction(preview_full_size)
|
|
197
|
-
# endregion
|
|
198
|
-
|
|
199
|
-
# region Tools menu
|
|
200
|
-
autoselect = QtGui.QAction("Auto-select", self)
|
|
201
|
-
autoselect.setEnabled(False) # TODO:
|
|
202
|
-
|
|
203
|
-
tool_menu = menu.addMenu("&Tools")
|
|
204
|
-
tool_menu.addAction(autoselect)
|
|
205
|
-
# endregion
|
|
206
|
-
|
|
207
|
-
# region Actions menu
|
|
208
|
-
run_move = QtGui.QAction("Move", self, enabled=False)
|
|
209
|
-
run_delete = QtGui.QAction("Delete", self)
|
|
210
|
-
run_delete.triggered.connect(self.on_delete)
|
|
211
|
-
run_ignore = QtGui.QAction("Save ignored pictures", self)
|
|
212
|
-
run_ignore.triggered.connect(self.on_ignore)
|
|
213
|
-
|
|
214
|
-
actions_menu = menu.addMenu("&Actions")
|
|
215
|
-
actions_menu.addAction(run_move)
|
|
216
|
-
actions_menu.addAction(run_delete)
|
|
217
|
-
actions_menu.addAction(run_ignore)
|
|
218
|
-
# endregion
|
|
219
|
-
|
|
220
|
-
# region Options menu
|
|
221
|
-
option_hidden_folders = QtGui.QAction("Show hidden folders", self, checkable=True, enabled=False)
|
|
222
|
-
option_subfolders = QtGui.QAction("Include subfolders", self, checkable=True, checked=True, enabled=False)
|
|
223
|
-
option_rotations = QtGui.QAction("Scan for rotations", self, checkable=True, checked=True, enabled=False)
|
|
224
|
-
|
|
225
|
-
# TODO: I'm not sure what these two do... I'm willing to add them if someone needs them but I don't.
|
|
226
|
-
# ... = QtGui.QAction("Between folders only", self)
|
|
227
|
-
# ... = QtGui.QAction("Loosen filter automatically", self)
|
|
228
|
-
|
|
229
|
-
options_menu = menu.addMenu("&Options")
|
|
230
|
-
options_menu.addAction(option_hidden_folders)
|
|
231
|
-
options_menu.addAction(option_subfolders)
|
|
232
|
-
options_menu.addSeparator()
|
|
233
|
-
options_menu.addAction(option_rotations)
|
|
234
|
-
# endregion
|
|
235
|
-
|
|
236
|
-
def build_central(self) -> None:
|
|
237
|
-
"""Creates the central stacked widget area for group pages."""
|
|
238
|
-
# TODO: Add other setting tabs
|
|
239
|
-
style = QtWidgets.QApplication.instance().style()
|
|
240
|
-
|
|
241
|
-
# region General controls area (top-right)
|
|
242
|
-
# region Control buttons
|
|
243
|
-
autoselect_btn = QtWidgets.QPushButton("Auto-select")
|
|
244
|
-
autoselect_btn.setEnabled(False) # TODO:
|
|
245
|
-
|
|
246
|
-
tools = QtWidgets.QVBoxLayout()
|
|
247
|
-
tools.addWidget(autoselect_btn)
|
|
248
|
-
|
|
249
|
-
tool_box = QtWidgets.QGroupBox("Tools")
|
|
250
|
-
tool_box.setLayout(tools)
|
|
251
|
-
tool_box.setMaximumHeight(60)
|
|
252
|
-
|
|
253
|
-
move_btn = QtWidgets.QPushButton("Move")
|
|
254
|
-
move_btn.setEnabled(False) # TODO:
|
|
255
|
-
delete_btn = QtWidgets.QPushButton("Delete")
|
|
256
|
-
delete_btn.pressed.connect(self.on_delete)
|
|
257
|
-
save_ignored_btn = QtWidgets.QPushButton("Save ignored")
|
|
258
|
-
save_ignored_btn.pressed.connect(self.on_ignore)
|
|
259
|
-
|
|
260
|
-
actions = QtWidgets.QVBoxLayout()
|
|
261
|
-
actions.addWidget(move_btn)
|
|
262
|
-
actions.addWidget(delete_btn)
|
|
263
|
-
actions.addWidget(save_ignored_btn)
|
|
264
|
-
|
|
265
|
-
actions_box = QtWidgets.QGroupBox("Actions")
|
|
266
|
-
actions_box.setLayout(actions)
|
|
267
|
-
actions_box.setMaximumHeight(20 + 40 * 3)
|
|
268
|
-
|
|
269
|
-
general_controls_btns = QtWidgets.QVBoxLayout()
|
|
270
|
-
general_controls_btns.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
|
|
271
|
-
general_controls_btns.setContentsMargins(NO_MARGIN)
|
|
272
|
-
general_controls_btns.addWidget(tool_box)
|
|
273
|
-
general_controls_btns.addWidget(actions_box)
|
|
274
|
-
# endregion
|
|
275
|
-
|
|
276
|
-
# region Run Controls
|
|
277
|
-
self.stop_btn = QtWidgets.QPushButton("\u25A0")
|
|
278
|
-
self.stop_btn.setSizePolicy(MAX_SIZE_POLICY)
|
|
279
|
-
self.stop_btn.setCheckable(True)
|
|
280
|
-
self.stop_btn.setChecked(True)
|
|
281
|
-
self.stop_btn.setStyleSheet('QPushButton {font-size: 26pt; color: maroon;}')
|
|
282
|
-
self.stop_btn.clicked.connect(self.on_pause)
|
|
283
|
-
|
|
284
|
-
self.start_btn = QtWidgets.QPushButton("\u25B6")
|
|
285
|
-
self.start_btn.setSizePolicy(MAX_SIZE_POLICY)
|
|
286
|
-
self.start_btn.setCheckable(True)
|
|
287
|
-
self.start_btn.setStyleSheet('QPushButton {font-size: 32pt; color: green;}')
|
|
288
|
-
self.start_btn.clicked.connect(self.on_start)
|
|
289
|
-
|
|
290
|
-
self.pause_btn = QtWidgets.QPushButton()
|
|
291
|
-
self.pause_btn.setSizePolicy(MAX_SIZE_POLICY)
|
|
292
|
-
self.pause_btn.setCheckable(True)
|
|
293
|
-
self.pause_btn.setEnabled(False)
|
|
294
|
-
self.pause_btn.clicked.connect(self.on_pause)
|
|
295
|
-
self.pause_btn.setIcon(style.standardIcon(
|
|
296
|
-
QtWidgets.QStyle.StandardPixmap.SP_MediaPause,
|
|
297
|
-
))
|
|
298
|
-
self.pause_btn.setIconSize(QtCore.QSize(28, 28))
|
|
299
|
-
|
|
300
|
-
run_controls_options_grp = QtWidgets.QButtonGroup(self, exclusive=True)
|
|
301
|
-
run_controls_options_grp.addButton(self.stop_btn)
|
|
302
|
-
run_controls_options_grp.addButton(self.start_btn)
|
|
303
|
-
run_controls_options_grp.addButton(self.pause_btn)
|
|
304
|
-
|
|
305
|
-
run_controls = QtWidgets.QHBoxLayout()
|
|
306
|
-
run_controls.setContentsMargins(NO_MARGIN)
|
|
307
|
-
run_controls.addWidget(self.stop_btn)
|
|
308
|
-
run_controls.addWidget(self.start_btn)
|
|
309
|
-
run_controls.addWidget(self.pause_btn)
|
|
310
|
-
# endregion
|
|
311
|
-
|
|
312
|
-
labels = QtWidgets.QVBoxLayout()
|
|
313
|
-
labels.setContentsMargins(NO_MARGIN)
|
|
314
|
-
self._remaining_files_label = QtWidgets.QLabel()
|
|
315
|
-
self.set_remaining_files_label(0)
|
|
316
|
-
self._loaded_pictures_label = QtWidgets.QLabel()
|
|
317
|
-
self.set_loaded_pictures_label(0)
|
|
318
|
-
self._dup_pictures_label = QtWidgets.QLabel()
|
|
319
|
-
self.set_duplicate_images_label(0)
|
|
320
|
-
self._dup_groups_label = QtWidgets.QLabel()
|
|
321
|
-
self.set_duplicate_groups_label(0)
|
|
322
|
-
|
|
323
|
-
self._timer_label = QtWidgets.QLabel("00:00:00", alignment=QtCore.Qt.AlignmentFlag.AlignHCenter)
|
|
324
|
-
self._elapsed_secs = 0
|
|
325
|
-
self._run_timer = QtCore.QTimer(self)
|
|
326
|
-
self._run_timer.setInterval(1000) # 1s ticks
|
|
327
|
-
self._run_timer.timeout.connect(self._on_run_timer_tick)
|
|
328
|
-
|
|
329
|
-
self._label_timer = QtCore.QTimer(self)
|
|
330
|
-
self._label_timer.setInterval(50)
|
|
331
|
-
self._label_timer.timeout.connect(self._on_labels_tick)
|
|
332
|
-
|
|
333
|
-
self._progress_bar = QtWidgets.QProgressBar(value=50, textVisible=False)
|
|
334
|
-
labels.addWidget(self._remaining_files_label)
|
|
335
|
-
labels.addWidget(self._loaded_pictures_label)
|
|
336
|
-
labels.addWidget(self._dup_pictures_label)
|
|
337
|
-
labels.addWidget(self._dup_groups_label)
|
|
338
|
-
labels.addWidget(self._timer_label)
|
|
339
|
-
labels.addWidget(self._progress_bar)
|
|
340
|
-
labels.addLayout(run_controls)
|
|
341
|
-
|
|
342
|
-
# region Settings tabs
|
|
343
|
-
# region Filter tab
|
|
344
|
-
slider_labels = QtWidgets.QVBoxLayout()
|
|
345
|
-
slider_labels.setContentsMargins(NO_MARGIN)
|
|
346
|
-
slider_labels.addWidget(QtWidgets.QLabel(text="Strict", alignment=QtCore.Qt.AlignmentFlag.AlignTop))
|
|
347
|
-
slider_labels.addWidget(QtWidgets.QLabel(text="Basic", alignment=QtCore.Qt.AlignmentFlag.AlignVCenter))
|
|
348
|
-
slider_labels.addWidget(QtWidgets.QLabel(text="Loose", alignment=QtCore.Qt.AlignmentFlag.AlignBottom))
|
|
349
|
-
|
|
350
|
-
self.precision_slider = QtWidgets.QSlider(tickPosition=QtWidgets.QSlider.TickPosition.TicksLeft)
|
|
351
|
-
self.precision_slider.setMaximum(10)
|
|
352
|
-
self.precision_slider.setValue(5)
|
|
353
|
-
self.precision_slider.sliderMoved.connect(self.on_precision_adjust)
|
|
354
|
-
|
|
355
|
-
filter_tab_main = QtWidgets.QHBoxLayout()
|
|
356
|
-
filter_tab_main.setContentsMargins(NO_MARGIN)
|
|
357
|
-
filter_tab_main.addLayout(slider_labels)
|
|
358
|
-
filter_tab_main.addWidget(self.precision_slider)
|
|
359
|
-
filter_tab_main.addWidget(QtWidgets.QLabel(
|
|
360
|
-
text="The slider determines\n"
|
|
361
|
-
"how strictly the program\n"
|
|
362
|
-
"checks for similarities\n"
|
|
363
|
-
"between the images.\n"
|
|
364
|
-
"Strict means it checks if\n"
|
|
365
|
-
"an image is the same or\n"
|
|
366
|
-
"slightly different, loose\n"
|
|
367
|
-
"allows for a greater\n"
|
|
368
|
-
"amount of differences.",
|
|
369
|
-
alignment=QtCore.Qt.AlignmentFlag.AlignCenter,
|
|
370
|
-
))
|
|
371
|
-
|
|
372
|
-
filter_tab = QtWidgets.QVBoxLayout()
|
|
373
|
-
self.hash_match_chkbx = QtWidgets.QCheckBox(" Hash match")
|
|
374
|
-
self.hash_match_chkbx.setEnabled(False)
|
|
375
|
-
filter_tab.addWidget(self.hash_match_chkbx)
|
|
376
|
-
filter_tab.addLayout(filter_tab_main)
|
|
377
|
-
# endregion
|
|
378
|
-
|
|
379
|
-
such = QtWidgets.QTabWidget()
|
|
380
|
-
such.addTab(QtWidgets.QWidget(layout=filter_tab), "Filter")
|
|
381
|
-
# endregion
|
|
382
|
-
|
|
383
|
-
labels_and_such = QtWidgets.QHBoxLayout()
|
|
384
|
-
labels_and_such.setContentsMargins(NO_MARGIN)
|
|
385
|
-
labels_and_such.addWidget(QtWidgets.QWidget(layout=labels, maximumWidth=140))
|
|
386
|
-
labels_and_such.addWidget(such)
|
|
387
|
-
|
|
388
|
-
primary_controls = QtWidgets.QVBoxLayout()
|
|
389
|
-
primary_controls.setContentsMargins(NO_MARGIN)
|
|
390
|
-
primary_controls.addLayout(self.build_file_path_selection_display())
|
|
391
|
-
primary_controls.addWidget(QtWidgets.QWidget(layout=labels_and_such, fixedHeight=240))
|
|
392
|
-
|
|
393
|
-
general_controls = QtWidgets.QHBoxLayout()
|
|
394
|
-
general_controls.setContentsMargins(NO_MARGIN)
|
|
395
|
-
general_controls.addLayout(primary_controls)
|
|
396
|
-
general_controls.addWidget(QtWidgets.QWidget(layout=general_controls_btns, maximumWidth=130))
|
|
397
|
-
|
|
398
|
-
# region File system explorer
|
|
399
|
-
file_system_model = DirFileSystemModel()
|
|
400
|
-
file_system_model.setFilter(QtCore.QDir.Filter.Dirs
|
|
401
|
-
| QtCore.QDir.Filter.Drives
|
|
402
|
-
| QtCore.QDir.Filter.NoDotAndDotDot)
|
|
403
|
-
file_system_model.setRootPath("")
|
|
404
|
-
self.file_system_view = QtWidgets.QTreeView(headerHidden=True)
|
|
405
|
-
self.file_system_view.setContentsMargins(NO_MARGIN)
|
|
406
|
-
self.file_system_view.setModel(file_system_model)
|
|
407
|
-
self.file_system_view.setRootIndex(file_system_model.index(""))
|
|
408
|
-
self.file_system_view.hideColumn(1) # Size
|
|
409
|
-
self.file_system_view.hideColumn(2) # Type
|
|
410
|
-
self.file_system_view.hideColumn(3) # Date Modified
|
|
411
|
-
file_view_splitter = QtWidgets.QHBoxLayout()
|
|
412
|
-
file_view_splitter.setContentsMargins(NO_MARGIN)
|
|
413
|
-
file_view_splitter.addWidget(self.file_system_view)
|
|
414
|
-
file_view_splitter.addWidget(QtWidgets.QWidget(layout=general_controls, maximumWidth=600))
|
|
415
|
-
# endregion
|
|
416
|
-
# endregion
|
|
417
|
-
|
|
418
|
-
outer_splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal, childrenCollapsible=False)
|
|
419
|
-
inner_splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical, childrenCollapsible=False)
|
|
420
|
-
|
|
421
|
-
inner_splitter.addWidget(QtWidgets.QWidget(layout=file_view_splitter))
|
|
422
|
-
inner_splitter.addWidget(self.build_image_view_area())
|
|
423
|
-
inner_splitter.setSizes([250, 250])
|
|
424
|
-
|
|
425
|
-
self.duplicate_group_list = DuplicateGroupList(sizePolicy=MAX_SIZE_POLICY)
|
|
426
|
-
self.duplicate_group_list.groupTileStateChanged.connect(self.on_match_state_changed)
|
|
427
|
-
self.duplicate_group_list.groupTileHovered.connect(self.image_view_area.set_image)
|
|
428
|
-
self.duplicate_group_list.page_down.pressed.connect(self.on_page_down)
|
|
429
|
-
self.duplicate_group_list.page_up.pressed.connect(self.on_page_up)
|
|
430
|
-
self.duplicate_group_list.first_page.pressed.connect(self.on_page_first)
|
|
431
|
-
self.duplicate_group_list.last_page.pressed.connect(self.on_page_last)
|
|
432
|
-
outer_splitter.addWidget(self.duplicate_group_list)
|
|
433
|
-
outer_splitter.addWidget(inner_splitter)
|
|
434
|
-
|
|
435
|
-
# Create a central widget and layout to hold the splitters
|
|
436
|
-
central_widget = QtWidgets.QWidget()
|
|
437
|
-
hbox = QtWidgets.QHBoxLayout(central_widget)
|
|
438
|
-
hbox.addWidget(outer_splitter)
|
|
439
|
-
self.setCentralWidget(central_widget)
|
|
440
|
-
|
|
441
|
-
def _on_run_timer_tick(self):
|
|
442
|
-
if not self.processor:
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
# Only count time while actively running
|
|
446
|
-
if not self.processor.is_paused() and not self.processor.is_finished():
|
|
447
|
-
self._elapsed_secs += 1
|
|
448
|
-
h, rem = divmod(self._elapsed_secs, 3600)
|
|
449
|
-
m, s = divmod(rem, 60)
|
|
450
|
-
self._timer_label.setText(f"{h:02d}:{m:02d}:{s:02d}")
|
|
451
|
-
|
|
452
|
-
def _on_labels_tick(self):
|
|
453
|
-
if not self.processor:
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
self.update_labels()
|
|
457
|
-
|
|
458
|
-
def on_precision_adjust(self, e):
|
|
459
|
-
"""The precision slider has been adjusted, so update the hash checkbox"""
|
|
460
|
-
if e != 10:
|
|
461
|
-
self.hash_match_chkbx.setEnabled(False)
|
|
462
|
-
self.hash_match_chkbx.setChecked(False)
|
|
463
|
-
else:
|
|
464
|
-
self.hash_match_chkbx.setEnabled(True)
|
|
465
|
-
|
|
466
|
-
def on_pause(self, e):
|
|
467
|
-
if not e:
|
|
468
|
-
return
|
|
469
|
-
|
|
470
|
-
if not self.processor:
|
|
471
|
-
return
|
|
472
|
-
|
|
473
|
-
self.processor.pause()
|
|
474
|
-
self._run_timer.stop()
|
|
475
|
-
self._label_timer.stop()
|
|
476
|
-
|
|
477
|
-
def on_start(self, e):
|
|
478
|
-
if not e:
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
if not self.processor:
|
|
482
|
-
# This is the first time running!
|
|
483
|
-
self.processor = ImageMatcher(
|
|
484
|
-
strength=self.precision_slider.value(),
|
|
485
|
-
exact_match=self.hash_match_chkbx.isChecked(),
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
elif self.processor.is_paused() and not self.processor.is_finished():
|
|
489
|
-
self.processor.resume()
|
|
490
|
-
self.pause_btn.setEnabled(True)
|
|
491
|
-
self._run_timer.start()
|
|
492
|
-
self._label_timer.start()
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
elif self.processor.running():
|
|
496
|
-
raise RuntimeError("Somehow we're trying to run when the processor appears to be running already!")
|
|
497
|
-
|
|
498
|
-
target_paths = [
|
|
499
|
-
self.selected_file_path_display.item(i).text()
|
|
500
|
-
for i in range(self.selected_file_path_display.count())
|
|
501
|
-
]
|
|
502
|
-
|
|
503
|
-
self.thread = ProcessorThread(self.processor, target_paths)
|
|
504
|
-
self.thread.signals.new_group.connect(self.on_new_match_group_found)
|
|
505
|
-
self.thread.signals.new_match.connect(self.on_new_match_found)
|
|
506
|
-
self.thread.signals.finish.connect(self.on_finish)
|
|
507
|
-
self.threadpool.start(self.thread)
|
|
508
|
-
self.pause_btn.setEnabled(True)
|
|
509
|
-
self.precision_slider.setEnabled(False)
|
|
510
|
-
self.hash_match_chkbx.setEnabled(False)
|
|
511
|
-
self._run_timer.start()
|
|
512
|
-
self._label_timer.start()
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
def on_finish(self):
|
|
516
|
-
self.stop_btn.setChecked(True)
|
|
517
|
-
self.pause_btn.setEnabled(False)
|
|
518
|
-
self._run_timer.stop()
|
|
519
|
-
self._label_timer.stop()
|
|
520
|
-
self.thread = None
|
|
521
|
-
|
|
522
|
-
def on_delete(self, *_):
|
|
523
|
-
self.process_file_states({SelectionState.DELETE})
|
|
524
|
-
|
|
525
|
-
def on_ignore(self, *_):
|
|
526
|
-
self.process_file_states({SelectionState.IGNORE})
|
|
527
|
-
|
|
528
|
-
@property
|
|
529
|
-
def last_page(self):
|
|
530
|
-
return ceildiv(len(self.processor.matches), self.duplicate_group_list._max_rows)
|
|
531
|
-
|
|
532
|
-
def on_new_match_group_found(self, match_group: ImageMatch):
|
|
533
|
-
if self._gui_paused:
|
|
534
|
-
return
|
|
535
|
-
|
|
536
|
-
page_this_belongs_on, row_this_is = divmod(match_group.match_i, self.duplicate_group_list._max_rows)
|
|
537
|
-
self.duplicate_group_list.update_page_indicator(self.current_page, self.last_page)
|
|
538
|
-
|
|
539
|
-
if self.current_page == page_this_belongs_on + 1:
|
|
540
|
-
self.duplicate_group_list.add_group(match_group.matches)
|
|
541
|
-
|
|
542
|
-
self.set_duplicate_groups_label(len(self.processor.matches))
|
|
543
|
-
|
|
544
|
-
def on_new_match_found(self, response):
|
|
545
|
-
match_group: ImageMatch
|
|
546
|
-
new_match: ZipPath
|
|
547
|
-
match_group, new_match = response
|
|
548
|
-
|
|
549
|
-
if self._gui_paused:
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
page_this_belongs_on, row_this_is = divmod(match_group.match_i, self.duplicate_group_list._max_rows)
|
|
553
|
-
|
|
554
|
-
if self.current_page == page_this_belongs_on + 1:
|
|
555
|
-
self.duplicate_group_list._rows[row_this_is].add_tile(new_match)
|
|
556
|
-
|
|
557
|
-
self.set_duplicate_images_label(self.processor.duplicate_images)
|
|
558
|
-
|
|
559
|
-
def set_duplicate_groups_label(self, duplicate_groups):
|
|
560
|
-
self._dup_groups_label.setText(f"Duplicate groups....{duplicate_groups}")
|
|
561
|
-
|
|
562
|
-
def set_duplicate_images_label(self, duplicate_images):
|
|
563
|
-
self._dup_pictures_label.setText(f"Duplicate pictures..{duplicate_images}")
|
|
564
|
-
|
|
565
|
-
def set_remaining_files_label(self, remaining_files):
|
|
566
|
-
self._remaining_files_label.setText(f"Remaining files....{remaining_files}")
|
|
567
|
-
|
|
568
|
-
def set_loaded_pictures_label(self, loaded_pictures):
|
|
569
|
-
self._loaded_pictures_label.setText(f"Loaded pictures..{loaded_pictures}")
|
|
570
|
-
|
|
571
|
-
def update_labels(self):
|
|
572
|
-
if not self.processor:
|
|
573
|
-
return
|
|
574
|
-
|
|
575
|
-
if self.processor.found_images:
|
|
576
|
-
self._progress_bar.setMaximum(self.processor.found_images)
|
|
577
|
-
self._progress_bar.setValue(self.processor.processed_images)
|
|
578
|
-
else:
|
|
579
|
-
self._progress_bar.setMaximum(100)
|
|
580
|
-
self._progress_bar.setValue(0)
|
|
581
|
-
|
|
582
|
-
self.set_remaining_files_label(self.processor.left_to_process)
|
|
583
|
-
self.set_loaded_pictures_label(self.processor.processed_images)
|
|
584
|
-
self.set_duplicate_groups_label(len(self.processor.matches))
|
|
585
|
-
self.set_duplicate_images_label(self.processor.duplicate_images)
|
|
586
|
-
|
|
587
|
-
def on_exit(self, *_):
|
|
588
|
-
self.close()
|
|
589
|
-
|
|
590
|
-
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
|
591
|
-
"""
|
|
592
|
-
Intercept window close requests (titlebar X, Cmd/Ctrl+Q, File→Exit, etc.).
|
|
593
|
-
Only prompt if data is loaded.
|
|
594
|
-
"""
|
|
595
|
-
if self.processor:
|
|
596
|
-
if len(self.processor.matches) and not self.confirm_close():
|
|
597
|
-
# There IS data loaded and user said NO to exiting
|
|
598
|
-
event.ignore()
|
|
599
|
-
return
|
|
600
|
-
|
|
601
|
-
if self.processor.running():
|
|
602
|
-
self.processor.finish()
|
|
603
|
-
|
|
604
|
-
event.accept()
|
|
605
|
-
|
|
606
|
-
def on_page_down(self, *_):
|
|
607
|
-
if self.last_page == 1:
|
|
608
|
-
# Theres only one page....
|
|
609
|
-
return
|
|
610
|
-
|
|
611
|
-
if self.current_page == 1:
|
|
612
|
-
self.current_page = self.last_page
|
|
613
|
-
else:
|
|
614
|
-
self.current_page -= 1
|
|
615
|
-
|
|
616
|
-
self.update_group_list()
|
|
617
|
-
|
|
618
|
-
def on_page_up(self, *_):
|
|
619
|
-
if self.last_page == 1:
|
|
620
|
-
# Theres only one page....
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
if self.current_page == self.last_page:
|
|
624
|
-
self.current_page = 1
|
|
625
|
-
else:
|
|
626
|
-
self.current_page += 1
|
|
627
|
-
|
|
628
|
-
self.update_group_list()
|
|
629
|
-
|
|
630
|
-
def on_page_first(self):
|
|
631
|
-
if self.current_page != 1:
|
|
632
|
-
self.current_page = 1
|
|
633
|
-
self.update_group_list()
|
|
634
|
-
|
|
635
|
-
def on_page_last(self):
|
|
636
|
-
last_page = self.last_page
|
|
637
|
-
if self.current_page != last_page:
|
|
638
|
-
self.current_page = last_page
|
|
639
|
-
self.update_group_list()
|
|
640
|
-
|
|
641
|
-
def update_group_list(self):
|
|
642
|
-
row_count = self.duplicate_group_list._max_rows
|
|
643
|
-
self.duplicate_group_list.set_groups(
|
|
644
|
-
[m.matches
|
|
645
|
-
for m in self.processor.matches[(self.current_page - 1) * row_count:self.current_page * row_count]]
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
for group in self.duplicate_group_list._rows:
|
|
649
|
-
for tile in group.tiles():
|
|
650
|
-
set_state = self.file_states.get(tile.path)
|
|
651
|
-
if set_state:
|
|
652
|
-
tile.state = set_state
|
|
653
|
-
|
|
654
|
-
self.duplicate_group_list.update_page_indicator(self.current_page, self.last_page)
|
|
655
|
-
|
|
656
|
-
def on_match_state_changed(self, path: ZipPath, state):
|
|
657
|
-
self.file_states[path] = state
|
|
658
|
-
|
|
659
|
-
for group in self.duplicate_group_list._rows:
|
|
660
|
-
for tile in group.tiles():
|
|
661
|
-
if tile.path == path:
|
|
662
|
-
tile.state = state
|
|
663
|
-
|
|
664
|
-
# region File Path Selection display
|
|
665
|
-
def build_file_path_selection_display(self):
|
|
666
|
-
# TODO: I need better icons here but I can't find the "in"/"out" icons in VP execution data...
|
|
667
|
-
|
|
668
|
-
# region Selected File Path sort controls
|
|
669
|
-
file_path_up_control = QtWidgets.QPushButton("^+")
|
|
670
|
-
file_path_up_control.setSizePolicy(MAX_SIZE_POLICY)
|
|
671
|
-
file_path_up_control.clicked.connect(self.file_path_up_clicked)
|
|
672
|
-
file_path_down_control = QtWidgets.QPushButton("V-")
|
|
673
|
-
file_path_down_control.setSizePolicy(MAX_SIZE_POLICY)
|
|
674
|
-
file_path_down_control.clicked.connect(self.file_path_down_clicked)
|
|
675
|
-
|
|
676
|
-
file_path_sort_controls = QtWidgets.QVBoxLayout()
|
|
677
|
-
file_path_sort_controls.setContentsMargins(NO_MARGIN)
|
|
678
|
-
file_path_sort_controls.addWidget(file_path_up_control)
|
|
679
|
-
file_path_sort_controls.addWidget(file_path_down_control)
|
|
680
|
-
# endregion
|
|
681
|
-
|
|
682
|
-
# region Selected File Path selection controls
|
|
683
|
-
file_path_in_control = QtWidgets.QPushButton(">+")
|
|
684
|
-
file_path_in_control.setSizePolicy(MAX_SIZE_POLICY)
|
|
685
|
-
file_path_in_control.clicked.connect(self.file_path_in_clicked)
|
|
686
|
-
file_path_out_control = QtWidgets.QPushButton("<-")
|
|
687
|
-
file_path_out_control.setSizePolicy(MAX_SIZE_POLICY)
|
|
688
|
-
file_path_out_control.clicked.connect(self.file_path_out_clicked)
|
|
689
|
-
|
|
690
|
-
file_path_io_controls = QtWidgets.QVBoxLayout()
|
|
691
|
-
file_path_io_controls.setContentsMargins(NO_MARGIN)
|
|
692
|
-
file_path_io_controls.addWidget(file_path_in_control)
|
|
693
|
-
file_path_io_controls.addWidget(file_path_out_control)
|
|
694
|
-
# endregion
|
|
695
|
-
|
|
696
|
-
self.selected_file_path_display = QtWidgets.QListWidget()
|
|
697
|
-
|
|
698
|
-
file_path_controls = QtWidgets.QHBoxLayout()
|
|
699
|
-
file_path_controls.setContentsMargins(NO_MARGIN)
|
|
700
|
-
file_path_controls.addWidget(QtWidgets.QWidget(layout=file_path_io_controls, maximumWidth=50))
|
|
701
|
-
file_path_controls.addWidget(self.selected_file_path_display)
|
|
702
|
-
file_path_controls.addWidget(QtWidgets.QWidget(layout=file_path_sort_controls, maximumWidth=50))
|
|
703
|
-
return file_path_controls
|
|
704
|
-
|
|
705
|
-
def file_path_up_clicked(self, _):
|
|
706
|
-
for selected_index in self.selected_file_path_display.selectedIndexes():
|
|
707
|
-
row = selected_index.row()
|
|
708
|
-
if row == 0:
|
|
709
|
-
continue
|
|
710
|
-
|
|
711
|
-
item = self.selected_file_path_display.takeItem(row)
|
|
712
|
-
self.selected_file_path_display.insertItem(row - 1, item)
|
|
713
|
-
self.selected_file_path_display.setCurrentIndex(self.selected_file_path_display.indexFromItem(item))
|
|
714
|
-
|
|
715
|
-
def file_path_down_clicked(self, _):
|
|
716
|
-
for selected_index in self.selected_file_path_display.selectedIndexes():
|
|
717
|
-
row = selected_index.row()
|
|
718
|
-
if row == self.selected_file_path_display.count():
|
|
719
|
-
continue
|
|
720
|
-
|
|
721
|
-
item = self.selected_file_path_display.takeItem(row)
|
|
722
|
-
self.selected_file_path_display.insertItem(row + 1, item)
|
|
723
|
-
self.selected_file_path_display.setCurrentIndex(self.selected_file_path_display.indexFromItem(item))
|
|
724
|
-
|
|
725
|
-
def file_path_in_clicked(self, _):
|
|
726
|
-
selected_indexes = self.file_system_view.selectedIndexes()
|
|
727
|
-
for index in selected_indexes:
|
|
728
|
-
info = self.file_system_view.model().fileInfo(index)
|
|
729
|
-
target_path = info.filePath()
|
|
730
|
-
self.selected_file_path_display.addItem(target_path)
|
|
731
|
-
|
|
732
|
-
if self.processor and self.processor.running():
|
|
733
|
-
self.processor.add_path(target_path)
|
|
734
|
-
|
|
735
|
-
def file_path_out_clicked(self, _):
|
|
736
|
-
for selected_index in self.selected_file_path_display.selectedIndexes():
|
|
737
|
-
selected_item = self.selected_file_path_display.takeItem(selected_index.row())
|
|
738
|
-
|
|
739
|
-
if self.processor:
|
|
740
|
-
self.processor.remove_path(selected_item.text())
|
|
741
|
-
self.update_group_list()
|
|
742
|
-
self.update_labels()
|
|
743
|
-
# endregion
|
|
744
|
-
|
|
745
|
-
# region Image View area (bottom-right)
|
|
746
|
-
def build_image_view_area(self):
|
|
747
|
-
self.preview_resized.changed.connect(self.preview_resized_changed)
|
|
748
|
-
self.image_view_area = ImageViewPane()
|
|
749
|
-
return self.image_view_area
|
|
750
|
-
|
|
751
|
-
def preview_resized_changed(self):
|
|
752
|
-
self.image_view_area.set_index(int(not self.preview_resized.isChecked()))
|
|
753
|
-
# endregion
|
|
754
|
-
|
|
755
|
-
def build_statusbar(self) -> None:
|
|
756
|
-
"""Creates a simple status bar for hints and counts."""
|
|
757
|
-
sb = QtWidgets.QStatusBar()
|
|
758
|
-
self.setStatusBar(sb)
|
|
759
|
-
self.count_label = QtWidgets.QLabel("No groups loaded")
|
|
760
|
-
sb.addPermanentWidget(self.count_label)
|
|
761
|
-
|
|
762
|
-
def process_file_states(self, states=None):
|
|
763
|
-
self.image_view_area.clear()
|
|
764
|
-
|
|
765
|
-
if not states:
|
|
766
|
-
states = {SelectionState.DELETE, SelectionState.IGNORE}
|
|
767
|
-
|
|
768
|
-
states.add(SelectionState.KEEP)
|
|
769
|
-
|
|
770
|
-
is_paused = self.processor.is_paused()
|
|
771
|
-
if not is_paused:
|
|
772
|
-
self.processor.pause()
|
|
773
|
-
|
|
774
|
-
file_size_deleted = 0
|
|
775
|
-
file_count_deleted = 0
|
|
776
|
-
file_count_ignored = 0
|
|
777
|
-
failed_file_deletes = []
|
|
778
|
-
|
|
779
|
-
for file, set_state in self.file_states.items():
|
|
780
|
-
if set_state not in states:
|
|
781
|
-
continue
|
|
782
|
-
|
|
783
|
-
if set_state == SelectionState.DELETE:
|
|
784
|
-
# TODO: This is just for testing:
|
|
785
|
-
logger.info(f"Deleting {file}")
|
|
786
|
-
path = Path(file.path)
|
|
787
|
-
try:
|
|
788
|
-
file_size_deleted += path.stat().st_size
|
|
789
|
-
path.unlink()
|
|
790
|
-
except PermissionError:
|
|
791
|
-
logger.info(f"Failed to delete {file}, it is in use!")
|
|
792
|
-
failed_file_deletes.append(file)
|
|
793
|
-
else:
|
|
794
|
-
self.processor.remove(file)
|
|
795
|
-
file_count_deleted += 1
|
|
796
|
-
elif set_state == SelectionState.IGNORE:
|
|
797
|
-
self.processor.remove(file)
|
|
798
|
-
file_count_ignored += 1
|
|
799
|
-
elif set_state == SelectionState.KEEP:
|
|
800
|
-
pass
|
|
801
|
-
|
|
802
|
-
self.file_states = {
|
|
803
|
-
k: v
|
|
804
|
-
for k, v in self.file_states.items()
|
|
805
|
-
if v not in states or k in failed_file_deletes
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if self.current_page > self.last_page:
|
|
809
|
-
self.current_page = self.last_page
|
|
810
|
-
|
|
811
|
-
self.update_labels()
|
|
812
|
-
self.update_group_list()
|
|
813
|
-
if not is_paused:
|
|
814
|
-
self.processor.resume()
|
|
815
|
-
|
|
816
|
-
popup_text = ""
|
|
817
|
-
|
|
818
|
-
if file_count_deleted:
|
|
819
|
-
popup_text += f"Deleted {file_count_deleted} files for a savings of {human_bytes(file_size_deleted)}.\n"
|
|
820
|
-
|
|
821
|
-
if file_count_ignored:
|
|
822
|
-
popup_text += f"Removed {file_count_ignored} from matching.\n"
|
|
823
|
-
|
|
824
|
-
if popup_text:
|
|
825
|
-
dlg = QtWidgets.QMessageBox(self)
|
|
826
|
-
dlg.setWindowTitle("Result")
|
|
827
|
-
dlg.setText(popup_text)
|
|
828
|
-
dlg.exec()
|
|
829
|
-
|
|
830
|
-
def confirm_close(self) -> bool:
|
|
831
|
-
"""
|
|
832
|
-
Show a confirmation dialog asking if the user really wants to close.
|
|
833
|
-
|
|
834
|
-
Returns:
|
|
835
|
-
True if the user confirmed closing; False to keep the app open.
|
|
836
|
-
"""
|
|
837
|
-
return self.exit_warning.exec() == QtWidgets.QMessageBox.StandardButton.Yes
|