v0.9.16
This commit is contained in:
330
settings.py
330
settings.py
@@ -14,12 +14,13 @@ import os
|
||||
import shutil
|
||||
import urllib.request
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QTimer
|
||||
from PySide6.QtGui import QColor, QIcon, QFont
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox, QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
||||
QLabel, QLineEdit, QMessageBox, QProgressDialog, QPushButton, QSpinBox,
|
||||
QTabWidget, QVBoxLayout, QWidget
|
||||
QTabWidget, QVBoxLayout, QWidget, QSlider, QFileDialog, QListWidget,
|
||||
QListWidgetItem, QProgressBar
|
||||
)
|
||||
from constants import (
|
||||
APP_CONFIG, AVAILABLE_FACE_ENGINES, DEFAULT_FACE_BOX_COLOR,
|
||||
@@ -27,7 +28,7 @@ from constants import (
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL,
|
||||
AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR,
|
||||
MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
|
||||
HAVE_BAGHEERASEARCH_LIB,
|
||||
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, TAGS_MENU_MAX_ITEMS_DEFAULT,
|
||||
THUMBNAILS_FILENAME_LINES_DEFAULT,
|
||||
THUMBNAILS_REFRESH_INTERVAL_DEFAULT, THUMBNAILS_BG_COLOR_DEFAULT,
|
||||
@@ -36,10 +37,72 @@ from constants import (
|
||||
THUMBNAILS_TOOLTIP_FG_COLOR_DEFAULT, THUMBNAILS_FILENAME_FONT_SIZE_DEFAULT,
|
||||
THUMBNAILS_TAGS_LINES_DEFAULT, THUMBNAILS_TAGS_FONT_SIZE_DEFAULT,
|
||||
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_WHEEL_SPEED_DEFAULT,
|
||||
UITexts, save_app_config
|
||||
UITexts, save_app_config, HAVE_DUPLICATE_RESNET_LIBS, HAVE_IMAGEHASH
|
||||
)
|
||||
|
||||
|
||||
class DuplicateFileCounter(QThread):
|
||||
"""Thread to count images in whitelist/blacklist without freezing UI."""
|
||||
count_updated = Signal(int)
|
||||
finished = Signal(int)
|
||||
|
||||
def __init__(self, whitelist, blacklist, extensions):
|
||||
super().__init__()
|
||||
self.whitelist = whitelist
|
||||
self.blacklist = blacklist
|
||||
self.extensions = extensions
|
||||
self._abort = False
|
||||
|
||||
def stop(self):
|
||||
self._abort = True
|
||||
|
||||
def run(self):
|
||||
count = 0
|
||||
for root_path in self.whitelist:
|
||||
if self._abort:
|
||||
break
|
||||
if not os.path.exists(root_path):
|
||||
continue
|
||||
for root, dirs, files in os.walk(root_path):
|
||||
if self._abort:
|
||||
break
|
||||
abs_root = os.path.abspath(root)
|
||||
dirs[:] = [d for d in dirs if os.path.join(abs_root, d) not in self.blacklist]
|
||||
if abs_root in self.blacklist:
|
||||
continue
|
||||
for f in files:
|
||||
if self._abort:
|
||||
break
|
||||
if os.path.splitext(f)[1].lower() in self.extensions:
|
||||
if os.path.join(abs_root, f) not in self.blacklist:
|
||||
count += 1
|
||||
self.count_updated.emit(count)
|
||||
self.finished.emit(count)
|
||||
|
||||
|
||||
class PathListWidget(QListWidget):
|
||||
"""A QListWidget that accepts folder drops from external file explorers."""
|
||||
def __init__(self, add_callback, parent=None):
|
||||
super().__init__(parent)
|
||||
self.add_callback = add_callback
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dropEvent(self, event):
|
||||
for url in event.mimeData().urls():
|
||||
path = url.toLocalFile()
|
||||
if path and os.path.isdir(path):
|
||||
self.add_callback(self, path)
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
class ModelDownloader(QThread):
|
||||
"""A thread to download the MediaPipe model file without freezing the UI."""
|
||||
download_complete = Signal(bool, str) # success (bool), message (str)
|
||||
@@ -93,6 +156,7 @@ class SettingsDialog(QDialog):
|
||||
self.current_thumbs_tooltip_bg_color = THUMBNAILS_TOOLTIP_BG_COLOR_DEFAULT
|
||||
self.current_thumbs_tooltip_fg_color = THUMBNAILS_TOOLTIP_FG_COLOR_DEFAULT
|
||||
self.downloader_thread = None
|
||||
self.counter_thread = None
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
@@ -112,6 +176,9 @@ class SettingsDialog(QDialog):
|
||||
scanner_tab = QWidget()
|
||||
scanner_layout = QVBoxLayout(scanner_tab)
|
||||
|
||||
duplicates_tab = QWidget()
|
||||
duplicates_layout = QVBoxLayout(duplicates_tab)
|
||||
|
||||
# --- Thumbnails Tab ---
|
||||
|
||||
mru_tags_layout = QHBoxLayout()
|
||||
@@ -344,6 +411,129 @@ class SettingsDialog(QDialog):
|
||||
scanner_layout.addLayout(scan_full_on_start_layout)
|
||||
scanner_layout.addStretch()
|
||||
|
||||
# --- Duplicates Tab ---
|
||||
if not HAVE_IMAGEHASH:
|
||||
warning_lbl = QLabel(UITexts.SETTINGS_DUPLICATE_MISSING_LIBS)
|
||||
warning_lbl.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
||||
warning_lbl.setWordWrap(True)
|
||||
duplicates_layout.addWidget(warning_lbl)
|
||||
|
||||
method_layout = QHBoxLayout()
|
||||
method_label = QLabel(UITexts.SETTINGS_DUPLICATE_METHOD_LABEL)
|
||||
self.duplicate_method_combo = QComboBox()
|
||||
self.duplicate_method_combo.addItem(UITexts.METHOD_HISTOGRAM_HASHING, "histogram_hashing")
|
||||
self.duplicate_method_combo.addItem(UITexts.METHOD_RESNET, "resnet")
|
||||
|
||||
self.duplicate_method_combo.setEnabled(HAVE_IMAGEHASH)
|
||||
|
||||
if not HAVE_DUPLICATE_RESNET_LIBS:
|
||||
resnet_idx = self.duplicate_method_combo.findData("resnet")
|
||||
if resnet_idx != -1:
|
||||
item = self.duplicate_method_combo.model().item(resnet_idx)
|
||||
if item:
|
||||
item.setEnabled(False)
|
||||
|
||||
method_layout.addWidget(method_label)
|
||||
method_layout.addWidget(self.duplicate_method_combo)
|
||||
method_label.setToolTip(UITexts.SETTINGS_DUPLICATE_METHOD_TOOLTIP)
|
||||
self.duplicate_method_combo.setToolTip(UITexts.SETTINGS_DUPLICATE_METHOD_TOOLTIP)
|
||||
duplicates_layout.addLayout(method_layout)
|
||||
|
||||
threshold_layout = QHBoxLayout()
|
||||
threshold_label = QLabel(UITexts.SETTINGS_DUPLICATE_THRESHOLD_LABEL)
|
||||
self.duplicate_threshold_slider = QSlider(Qt.Horizontal)
|
||||
self.duplicate_threshold_slider.setRange(50, 100)
|
||||
self.duplicate_threshold_value_label = QLabel("0%")
|
||||
|
||||
self.duplicate_threshold_slider.setEnabled(HAVE_IMAGEHASH)
|
||||
self.duplicate_threshold_value_label.setFixedWidth(40)
|
||||
|
||||
threshold_layout.addWidget(threshold_label)
|
||||
threshold_layout.addWidget(self.duplicate_threshold_slider)
|
||||
threshold_layout.addWidget(self.duplicate_threshold_value_label)
|
||||
|
||||
threshold_label.setToolTip(UITexts.SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP)
|
||||
self.duplicate_threshold_slider.setToolTip(UITexts.SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP)
|
||||
|
||||
self.duplicate_threshold_slider.valueChanged.connect(
|
||||
lambda v: self.duplicate_threshold_value_label.setText(f"{v}%"))
|
||||
|
||||
def create_path_list_ui(label_text, tooltip):
|
||||
container = QWidget()
|
||||
v_layout = QVBoxLayout(container)
|
||||
v_layout.setContentsMargins(0, 0, 0, 0)
|
||||
v_layout.addWidget(QLabel(label_text))
|
||||
h_layout = QHBoxLayout()
|
||||
lst = PathListWidget(self._add_path_to_list)
|
||||
lst.setToolTip(tooltip)
|
||||
lst.setMinimumHeight(100)
|
||||
h_layout.addWidget(lst)
|
||||
btn_vbox = QVBoxLayout()
|
||||
add_btn = QPushButton()
|
||||
add_btn.setIcon(QIcon.fromTheme("list-add"))
|
||||
add_btn.setFixedWidth(30)
|
||||
rem_btn = QPushButton()
|
||||
rem_btn.setIcon(QIcon.fromTheme("list-remove"))
|
||||
rem_btn.setFixedWidth(30)
|
||||
btn_vbox.addWidget(add_btn)
|
||||
btn_vbox.addWidget(rem_btn)
|
||||
btn_vbox.addStretch()
|
||||
h_layout.addLayout(btn_vbox)
|
||||
v_layout.addLayout(h_layout)
|
||||
return container, lst, add_btn, rem_btn
|
||||
|
||||
# Whitelist
|
||||
wl_cont, self.duplicate_whitelist_list, wl_add, wl_rem = create_path_list_ui(
|
||||
UITexts.SETTINGS_DUPLICATE_WHITELIST_LABEL, UITexts.SETTINGS_DUPLICATE_WHITELIST_TOOLTIP)
|
||||
wl_add.clicked.connect(self.add_whitelist_path)
|
||||
wl_rem.clicked.connect(self.remove_whitelist_path)
|
||||
duplicates_layout.addWidget(wl_cont)
|
||||
|
||||
# Blacklist
|
||||
bl_cont, self.duplicate_blacklist_list, bl_add, bl_rem = create_path_list_ui(
|
||||
UITexts.SETTINGS_DUPLICATE_BLACKLIST_LABEL, UITexts.SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP)
|
||||
bl_add.clicked.connect(self.add_blacklist_path)
|
||||
bl_rem.clicked.connect(self.remove_blacklist_path)
|
||||
duplicates_layout.addWidget(bl_cont)
|
||||
|
||||
# Image Count Layout
|
||||
count_layout = QHBoxLayout()
|
||||
self.duplicate_scan_count_label = QLabel()
|
||||
self.duplicate_scan_count_label.setStyleSheet("color: #3498db; font-weight: bold;")
|
||||
self.duplicate_scan_progress = QProgressBar()
|
||||
self.duplicate_scan_progress.setRange(0, 0) # Indeterminate mode
|
||||
self.duplicate_scan_progress.setFixedHeight(10)
|
||||
self.duplicate_scan_progress.setFixedWidth(100)
|
||||
self.duplicate_scan_progress.hide()
|
||||
count_layout.addWidget(self.duplicate_scan_count_label)
|
||||
count_layout.addWidget(self.duplicate_scan_progress)
|
||||
count_layout.addStretch()
|
||||
duplicates_layout.addLayout(count_layout)
|
||||
|
||||
# Timer for debounced count update
|
||||
self.count_update_timer = QTimer(self)
|
||||
self.count_update_timer.setSingleShot(True)
|
||||
self.count_update_timer.setInterval(500)
|
||||
self.count_update_timer.timeout.connect(self.update_duplicate_scan_count)
|
||||
|
||||
self.duplicate_whitelist_list.model().rowsInserted.connect(lambda *args: self.count_update_timer.start())
|
||||
self.duplicate_whitelist_list.model().rowsRemoved.connect(lambda *args: self.count_update_timer.start())
|
||||
self.duplicate_blacklist_list.model().rowsInserted.connect(lambda *args: self.count_update_timer.start())
|
||||
self.duplicate_blacklist_list.model().rowsRemoved.connect(lambda *args: self.count_update_timer.start())
|
||||
|
||||
self.default_delete_to_trash_checkbox = QCheckBox(UITexts.SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL)
|
||||
self.default_delete_to_trash_checkbox.setToolTip(UITexts.SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP)
|
||||
duplicates_layout.addWidget(self.default_delete_to_trash_checkbox)
|
||||
|
||||
|
||||
duplicates_layout.addLayout(threshold_layout)
|
||||
|
||||
self.duplicate_confirm_delete_checkbox = QCheckBox(UITexts.SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL)
|
||||
self.duplicate_confirm_delete_checkbox.setToolTip(UITexts.SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP)
|
||||
duplicates_layout.addWidget(self.duplicate_confirm_delete_checkbox)
|
||||
|
||||
duplicates_layout.addStretch()
|
||||
|
||||
# --- Faces & People Tab ---
|
||||
faces_tab = QWidget()
|
||||
faces_layout = QVBoxLayout(faces_tab)
|
||||
@@ -645,6 +835,7 @@ class SettingsDialog(QDialog):
|
||||
tabs.addTab(viewer_tab, UITexts.SETTINGS_GROUP_VIEWER)
|
||||
tabs.addTab(faces_tab, UITexts.SETTINGS_GROUP_AREAS)
|
||||
tabs.addTab(scanner_tab, UITexts.SETTINGS_GROUP_SCANNER)
|
||||
tabs.addTab(duplicates_tab, UITexts.SETTINGS_GROUP_DUPLICATES)
|
||||
|
||||
# --- Button Box ---
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
@@ -737,6 +928,29 @@ class SettingsDialog(QDialog):
|
||||
show_tags = APP_CONFIG.get("thumbnails_show_tags", True)
|
||||
filmstrip_position = APP_CONFIG.get("filmstrip_position", "bottom")
|
||||
|
||||
duplicate_method = APP_CONFIG.get("duplicate_method", "histogram_hashing")
|
||||
method_idx = self.duplicate_method_combo.findData(duplicate_method)
|
||||
if method_idx != -1:
|
||||
self.duplicate_method_combo.setCurrentIndex(method_idx)
|
||||
|
||||
duplicate_threshold = APP_CONFIG.get(
|
||||
"duplicate_threshold", SCANNER_SETTINGS_DEFAULTS["duplicate_threshold"])
|
||||
self.duplicate_threshold_slider.setValue(duplicate_threshold)
|
||||
self.duplicate_threshold_value_label.setText(f"{duplicate_threshold}%")
|
||||
|
||||
default_delete_to_trash = APP_CONFIG.get("default_delete_to_trash", True)
|
||||
self.default_delete_to_trash_checkbox.setChecked(default_delete_to_trash)
|
||||
|
||||
duplicate_confirm_delete = APP_CONFIG.get("duplicate_confirm_delete", True)
|
||||
self.duplicate_confirm_delete_checkbox.setChecked(duplicate_confirm_delete)
|
||||
|
||||
duplicate_whitelist = APP_CONFIG.get("duplicate_whitelist", SCANNER_SETTINGS_DEFAULTS["duplicate_whitelist"])
|
||||
for p in [x.strip() for x in duplicate_whitelist.split(",") if x.strip()]:
|
||||
self._add_path_to_list(self.duplicate_whitelist_list, p)
|
||||
duplicate_blacklist = APP_CONFIG.get("duplicate_blacklist", SCANNER_SETTINGS_DEFAULTS["duplicate_blacklist"])
|
||||
for p in [x.strip() for x in duplicate_blacklist.split(",") if x.strip()]:
|
||||
self._add_path_to_list(self.duplicate_blacklist_list, p)
|
||||
|
||||
self.scan_max_level_spin.setValue(scan_max_level)
|
||||
self.scan_batch_size_spin.setValue(scan_batch_size)
|
||||
self.threads_spin.setValue(scan_threads)
|
||||
@@ -821,6 +1035,7 @@ class SettingsDialog(QDialog):
|
||||
self.filmstrip_pos_combo.setCurrentText(
|
||||
pos_map.get(filmstrip_position, UITexts.FILMSTRIP_BOTTOM))
|
||||
self.update_mediapipe_status()
|
||||
self.update_duplicate_scan_count()
|
||||
|
||||
def set_button_color(self, color_str):
|
||||
"""Sets the background color of the button and stores the value."""
|
||||
@@ -1068,6 +1283,15 @@ class SettingsDialog(QDialog):
|
||||
APP_CONFIG["thumbnails_show_rating"] = self.show_rating_check.isChecked()
|
||||
APP_CONFIG["thumbnails_show_tags"] = self.show_tags_check.isChecked()
|
||||
APP_CONFIG["viewer_wheel_speed"] = self.viewer_wheel_spin.value()
|
||||
APP_CONFIG["duplicate_method"] = self.duplicate_method_combo.currentData()
|
||||
APP_CONFIG["duplicate_threshold"] = self.duplicate_threshold_slider.value()
|
||||
APP_CONFIG["default_delete_to_trash"] = self.default_delete_to_trash_checkbox.isChecked()
|
||||
APP_CONFIG["duplicate_confirm_delete"] = self.duplicate_confirm_delete_checkbox.isChecked()
|
||||
wl_paths = [self.duplicate_whitelist_list.item(i).text() for i in range(self.duplicate_whitelist_list.count())]
|
||||
APP_CONFIG["duplicate_whitelist"] = ",".join(wl_paths)
|
||||
bl_paths = [self.duplicate_blacklist_list.item(i).text() for i in range(self.duplicate_blacklist_list.count())]
|
||||
APP_CONFIG["duplicate_blacklist"] = ",".join(bl_paths)
|
||||
|
||||
APP_CONFIG["viewer_auto_resize_window"] = \
|
||||
self.viewer_auto_resize_check.isChecked()
|
||||
APP_CONFIG["face_detection_engine"] = self.face_engine_combo.currentText()
|
||||
@@ -1108,3 +1332,101 @@ class SettingsDialog(QDialog):
|
||||
|
||||
def _on_downloader_finished(self):
|
||||
self.downloader_thread = None
|
||||
|
||||
def _stop_downloader_thread(self):
|
||||
if self.downloader_thread and self.downloader_thread.isRunning():
|
||||
self.downloader_thread.stop()
|
||||
self.downloader_thread.wait()
|
||||
self.downloader_thread = None
|
||||
|
||||
def done(self, r):
|
||||
self._stop_downloader_thread() # Asegura que el hilo de descarga se detenga y espere
|
||||
if self.counter_thread and self.counter_thread.isRunning():
|
||||
self.counter_thread.stop()
|
||||
self.counter_thread.wait()
|
||||
super().done(r)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._stop_downloader_thread() # Asegura que el hilo de descarga se detenga y espere
|
||||
if self.counter_thread and self.counter_thread.isRunning():
|
||||
self.counter_thread.stop()
|
||||
self.counter_thread.wait()
|
||||
super().closeEvent(event)
|
||||
|
||||
def _add_path_to_list(self, list_widget, path):
|
||||
"""Adds a path to a QListWidget with existence validation."""
|
||||
path = os.path.abspath(os.path.expanduser(path.strip()))
|
||||
if not path:
|
||||
return
|
||||
|
||||
to_remove = []
|
||||
for i in range(list_widget.count()):
|
||||
existing_p = list_widget.item(i).text()
|
||||
if existing_p == path:
|
||||
return
|
||||
|
||||
# Si una carpeta padre ya existe, no añadimos esta subcarpeta
|
||||
if path.startswith(existing_p + os.sep):
|
||||
return
|
||||
|
||||
# Si la nueva ruta es padre de una existente, marcamos la existente para borrar
|
||||
if existing_p.startswith(path + os.sep):
|
||||
to_remove.append(i)
|
||||
|
||||
# Borramos las subcarpetas innecesarias (en orden inverso para no alterar los índices)
|
||||
for i in sorted(to_remove, reverse=True):
|
||||
list_widget.takeItem(i)
|
||||
|
||||
item = QListWidgetItem(path)
|
||||
if not os.path.isdir(path):
|
||||
item.setForeground(QColor("red"))
|
||||
item.setToolTip(f"Warning: Path not found or is not a directory: {path}")
|
||||
list_widget.addItem(item)
|
||||
|
||||
def add_whitelist_path(self):
|
||||
"""Opens a directory dialog to add a folder to the whitelist."""
|
||||
dir_path = QFileDialog.getExistingDirectory(self, UITexts.SELECT)
|
||||
if dir_path:
|
||||
self._add_path_to_list(self.duplicate_whitelist_list, dir_path)
|
||||
|
||||
def remove_whitelist_path(self):
|
||||
"""Removes the selected folders from the whitelist list."""
|
||||
for item in self.duplicate_whitelist_list.selectedItems():
|
||||
self.duplicate_whitelist_list.takeItem(self.duplicate_whitelist_list.row(item))
|
||||
|
||||
def add_blacklist_path(self):
|
||||
"""Opens a directory dialog to add a folder to the blacklist."""
|
||||
dir_path = QFileDialog.getExistingDirectory(self, UITexts.SELECT)
|
||||
if dir_path:
|
||||
self._add_path_to_list(self.duplicate_blacklist_list, dir_path)
|
||||
|
||||
def remove_blacklist_path(self):
|
||||
"""Removes the selected folders from the blacklist list."""
|
||||
for item in self.duplicate_blacklist_list.selectedItems():
|
||||
self.duplicate_blacklist_list.takeItem(self.duplicate_blacklist_list.row(item))
|
||||
|
||||
def update_duplicate_scan_count(self):
|
||||
"""Calculates and updates the count of images in whitelist/blacklist using a background thread."""
|
||||
if self.counter_thread and self.counter_thread.isRunning():
|
||||
self.counter_thread.stop()
|
||||
self.counter_thread.wait()
|
||||
|
||||
whitelist_paths = [self.duplicate_whitelist_list.item(i).text()
|
||||
for i in range(self.duplicate_whitelist_list.count())]
|
||||
blacklist_paths = [self.duplicate_blacklist_list.item(i).text()
|
||||
for i in range(self.duplicate_blacklist_list.count())]
|
||||
|
||||
whitelist = [os.path.abspath(os.path.expanduser(p.strip())) for p in whitelist_paths if p.strip()]
|
||||
blacklist = {os.path.abspath(os.path.expanduser(p.strip())) for p in blacklist_paths if p.strip()}
|
||||
|
||||
if not whitelist:
|
||||
self.duplicate_scan_count_label.setText(UITexts.SETTINGS_DUPLICATE_SCAN_COUNT_LABEL.format(0))
|
||||
self.duplicate_scan_progress.hide()
|
||||
return
|
||||
|
||||
self.duplicate_scan_progress.show()
|
||||
self.counter_thread = DuplicateFileCounter(whitelist, blacklist, IMAGE_EXTENSIONS)
|
||||
self.counter_thread.count_updated.connect(
|
||||
lambda c: self.duplicate_scan_count_label.setText(UITexts.SETTINGS_DUPLICATE_SCAN_COUNT_LABEL.format(c)))
|
||||
self.counter_thread.finished.connect(lambda: self.duplicate_scan_progress.hide())
|
||||
self.counter_thread.start()
|
||||
|
||||
Reference in New Issue
Block a user