This commit is contained in:
Ignacio Serantes
2026-04-08 15:47:29 +02:00
parent bff99226b0
commit 07afab6ca3
10 changed files with 336 additions and 113 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks.
"""
__appname__ = "BagheeraView"
__version__ = "0.9.18"
__version__ = "0.9.19"
__author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net"
__license__ = "LGPL"
@@ -1839,7 +1839,7 @@ class MainWindow(QMainWindow):
if paths is None:
QMessageBox.warning(
self, UITexts.WARNING,
"Whitelist is empty. Please configure it in Settings.")
UITexts.DUPLICATE_WHITELIST_EMPTY)
return
if not paths:
@@ -4666,7 +4666,7 @@ class MainWindow(QMainWindow):
menu.addSeparator()
action_other = menu.addAction(QIcon.fromTheme("applications-other"),
"Open with other application...")
UITexts.OPEN_WITH_OTHER)
action_other.triggered.connect(
lambda: self.open_with_system_chooser(full_path))
except Exception:

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.18"
PROG_VERSION = "0.9.19-dev"
PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS ---
@@ -393,6 +393,7 @@ _UI_TEXTS = {
"SEARCH": "Search",
"SELECT": "Select",
"ERROR": "Error",
"FILE_NOT_FOUND": "File not found",
"WARNING": "Warning",
"INFO": "Info",
"LOAD": "Load",
@@ -518,25 +519,39 @@ _UI_TEXTS = {
"MENU_CLEAN_UP_HASHES": "Clean up",
"MENU_CLEAR_HASHES": "Clear hashes ({} items, {:.1f} MB on disk)",
"CONFIRM_CLEAR_HASHES_TITLE": "Confirm Clear Hashes",
"CONFIRM_CLEAR_HASHES_TEXT": "Are you sure you want to permanently delete the entire hash database?",
"CONFIRM_CLEAR_HASHES_INFO": "This will remove all calculated image hashes. They will be recalculated as you detect duplicates, which may be slow. This action cannot be undone.",
"CONFIRM_CLEAR_HASHES_TEXT": "Are you sure you want to permanently delete "
"the entire hash database?",
"CONFIRM_CLEAR_HASHES_INFO": "This will remove all calculated image hashes. "
"They will be recalculated as you detect duplicates, which may be slow. This "
"action cannot be undone.",
"SETTINGS_DUPLICATE_METHOD_LABEL": "Method:",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Select the method for duplicate detection.",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Select the method for duplicate "
"detection.",
"METHOD_HISTOGRAM_HASHING": "Histogram + Hashing",
"METHOD_RESNET": "ResNet (AI Based)",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL": "Confirm before deleting duplicates",
"SETTINGS_DUPLICATE_WHITELIST_LABEL": "Whitelist (folders to include):",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Comma-separated paths of folders to scan when using 'Detect all'.",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Comma-separated paths of folders to "
"scan when using 'Detect all'.",
"SETTINGS_DUPLICATE_BLACKLIST_LABEL": "Blacklist (folders to exclude):",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Comma-separated paths of folders to ignore during 'Detect all' scans.",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Comma-separated paths of folders to "
"ignore during 'Detect all' scans.",
"SETTINGS_DUPLICATE_SCAN_COUNT_LABEL": "Images found for 'Detect all': {}",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "Delete key sends to trash by default",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "If checked, pressing the Delete key will move files to trash. If unchecked, it will permanently delete them.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Show a confirmation dialog before moving a duplicate image to the trash.",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "Delete key sends to trash by "
"default",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "If checked, pressing the Delete "
"key will move files to trash. If unchecked, it will permanently delete them.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Show a confirmation dialog "
"before moving a duplicate image to the trash.",
"SETTINGS_DUPLICATE_THRESHOLD_LABEL": "Similarity Threshold:",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Set the similarity threshold (50-100%). Higher values mean images must be more similar to be considered duplicates.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "The 'imagehash' library is required for duplicate detection but was not found. This feature is disabled.",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Set the similarity threshold 2 "
"(50-100%). Higher values mean images must be more similar to be considered "
"duplicates.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "The 'imagehash' library is required for "
"duplicate detection but was not found. This feature is disabled.",
"MENU_DETECT_DUPLICATES": "Detect Duplicates",
"DUPLICATE_WHITELIST_EMPTY": "Whitelist is empty. Please configure it "
"in Settings.",
"DUPLICATE_DETECTION_TITLE": "Duplicate Detection",
"DUPLICATE_ALREADY_RUNNING": "Duplicate detection is already in progress.",
"DUPLICATE_NO_IMAGES": "No images loaded to detect duplicates.",
@@ -617,6 +632,8 @@ _UI_TEXTS = {
"landmarks.",
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Maximum number of recently used "
"landmark names to remember.",
"SETTINGS_PATH_NOT_FOUND_WARNING": "Warning: Path not found or is not "
"a directory: {}",
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Max face history:",
"SETTINGS_THUMBS_REFRESH_LABEL": "Thumbs refresh interval (ms):",
"MENU_VIEWER_SETTINGS": "Viewer Settings",
@@ -913,6 +930,7 @@ _UI_TEXTS = {
"SEARCH": "Buscar",
"SELECT": "Seleccionar",
"ERROR": "Error",
"FILE_NOT_FOUND": "Archivo no encontrado",
"WARNING": "Advertencia",
"INFO": "Información",
"LOAD": "Cargar",
@@ -1038,25 +1056,43 @@ _UI_TEXTS = {
"MENU_CLEAN_UP_HASHES": "Limpiar",
"MENU_CLEAR_HASHES": "Limpiar hashes ({} ítems, {:.1f} MB en disco)",
"CONFIRM_CLEAR_HASHES_TITLE": "Confirmar Limpieza de Hashes",
"CONFIRM_CLEAR_HASHES_TEXT": "¿Seguro que quieres eliminar permanentemente toda la base de datos de hashes?",
"CONFIRM_CLEAR_HASHES_INFO": "Esto eliminará todos los hashes de imágenes calculados. Se recalcularán a medida que detectes duplicados, lo que puede ser lento. Esta acción no se puede deshacer.",
"CONFIRM_CLEAR_HASHES_TEXT": "¿Seguro que quieres eliminar permanentemente "
"toda la base de datos de hashes?",
"CONFIRM_CLEAR_HASHES_INFO": "Esto eliminará todos los hashes de imágenes "
"calculados. Se recalcularán a medida que detectes duplicados, lo que puede "
"ser lento. Esta acción no se puede deshacer.",
"SETTINGS_DUPLICATE_METHOD_LABEL": "Método:",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Selecciona el método para la detección de duplicados.",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Selecciona el método para la detección "
"de duplicados.",
"METHOD_HISTOGRAM_HASHING": "Histograma + Hashing",
"METHOD_RESNET": "ResNet (Basado en IA)",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL": "Confirmar antes de borrar duplicados",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL": "Confirmar antes de borrar "
"duplicados",
"SETTINGS_DUPLICATE_WHITELIST_LABEL": "Lista blanca (carpetas a incluir):",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Rutas de carpetas separadas por comas para escanear al usar 'Detectar todos'.",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Rutas de carpetas separadas por comas "
"para escanear al usar 'Detectar todos'.",
"SETTINGS_DUPLICATE_BLACKLIST_LABEL": "Lista negra (carpetas a excluir):",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Rutas de carpetas separadas por comas para ignorar durante escaneos de 'Detectar todos'.",
"SETTINGS_DUPLICATE_SCAN_COUNT_LABEL": "Imágenes encontradas para 'Detectar todos': {}",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "La tecla Supr envía a la papelera por defecto",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "Si está marcada, al pulsar la tecla Supr se moverán los archivos a la papelera. Si no, se eliminarán permanentemente.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Muestra un diálogo de confirmación antes de mover una imagen duplicada a la papelera.",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Rutas de carpetas separadas por comas "
"para ignorar durante escaneos de 'Detectar todos'.",
"SETTINGS_DUPLICATE_SCAN_COUNT_LABEL": "Imágenes encontradas para 'Detectar "
"todos': {}",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "La tecla Supr envía a la papelera "
"por defecto",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "Si está marcada, al pulsar la "
"tecla Supr se moverán los archivos a la papelera. Si no, se eliminarán "
"permanentemente.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Muestra un diálogo de "
"confirmación antes de mover una imagen duplicada a la papelera.",
"SETTINGS_DUPLICATE_THRESHOLD_LABEL": "Umbral de Similitud:",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Establece el umbral de similitud (50-100%). Valores más altos significan que las imágenes deben ser más parecidas para considerarse duplicadas.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "La librería 'imagehash' es necesaria para la detección de duplicados pero no se ha encontrado. Esta función está desactivada.",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Establece el umbral de similitud "
"(50-100%). Valores más altos significan que las imágenes deben ser más "
"parecidas para considerarse duplicadas.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "La librería 'imagehash' es necesaria "
"para la detección de duplicados pero no se ha encontrado. Esta función "
"está desactivada.",
"MENU_DETECT_DUPLICATES": "Detectar Duplicados",
"DUPLICATE_WHITELIST_EMPTY": "La lista blanca está vacía. Por favor, "
"configúrela en Opciones.",
"DUPLICATE_DETECTION_TITLE": "Detección de Duplicados",
"DUPLICATE_ALREADY_RUNNING": "La detección de duplicados ya está en curso.",
"DUPLICATE_NO_IMAGES": "No hay imágenes cargadas para detectar duplicados.",
@@ -1143,6 +1179,8 @@ _UI_TEXTS = {
"alrededor de los lugares.",
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nombres de lugares "
"usados recientemente para recordar.",
"SETTINGS_PATH_NOT_FOUND_WARNING": "Advertencia: La ruta no existe o "
"no es un directorio: {}",
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Color de fondo de miniaturas:",
@@ -1441,6 +1479,7 @@ _UI_TEXTS = {
"SEARCH": "Buscar",
"SELECT": "Seleccionar",
"ERROR": "Erro",
"FILE_NOT_FOUND": "Ficheiro non atopado",
"WARNING": "Advertencia",
"INFO": "Información",
"LOAD": "Cargar",
@@ -1567,25 +1606,42 @@ _UI_TEXTS = {
"MENU_CLEAN_UP_HASHES": "Limpar",
"MENU_CLEAR_HASHES": "Limpar hashes ({} elementos, {:.1f} MB en disco)",
"CONFIRM_CLEAR_HASHES_TITLE": "Confirmar Limpeza de Hashes",
"CONFIRM_CLEAR_HASHES_TEXT": "Seguro que queres eliminar permanentemente toda a base de datos de hashes?",
"CONFIRM_CLEAR_HASHES_INFO": "Isto eliminará todos os hashes de imaxes calculados. Rexeneraranse a medida que detectes duplicados, o que pode ser lento. Esta acción non se pode deshacer.",
"CONFIRM_CLEAR_HASHES_TEXT": "Seguro que queres eliminar permanentemente toda "
"a base de datos de hashes?",
"CONFIRM_CLEAR_HASHES_INFO": "Isto eliminará todos os hashes de imaxes "
"calculados. Rexeneraranse a medida que detectes duplicados, o que pode ser "
"lento. Esta acción non se pode deshacer.",
"SETTINGS_DUPLICATE_METHOD_LABEL": "Método:",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Selecciona o método para a detección de duplicados.",
"SETTINGS_DUPLICATE_METHOD_TOOLTIP": "Selecciona o método para a detección "
"de duplicados.",
"METHOD_HISTOGRAM_HASHING": "Histograma + Hashing",
"METHOD_RESNET": "ResNet (Baseado en IA)",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL": "Confirmar antes de borrar duplicados",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_LABEL": "Confirmar antes de borrar "
"duplicados",
"SETTINGS_DUPLICATE_WHITELIST_LABEL": "Lista branca (cartafoles a incluír):",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Rutas de cartafoles separadas por comas para escanear ao usar 'Detectar todos'.",
"SETTINGS_DUPLICATE_WHITELIST_TOOLTIP": "Rutas de cartafoles separadas por "
"comas para escanear ao usar 'Detectar todos'.",
"SETTINGS_DUPLICATE_BLACKLIST_LABEL": "Lista negra (cartafoles a excluír):",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Rutas de cartafoles separadas por comas para ignorar durante escaneos de 'Detectar todos'.",
"SETTINGS_DUPLICATE_SCAN_COUNT_LABEL": "Imaxes atopadas para 'Detectar todos': {}",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "A tecla Supr envía á papeleira por defecto",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "Se está marcada, ao premer a tecla Supr moveranse os ficheiros á papeleira. Se non, eliminaranse permanentemente.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Amosa un diálogo de confirmación antes de mover unha imaxe duplicada á papeleira.",
"SETTINGS_DUPLICATE_BLACKLIST_TOOLTIP": "Rutas de cartafoles separadas por "
"comas para ignorar durante escaneos de 'Detectar todos'.",
"SETTINGS_DUPLICATE_SCAN_COUNT_LABEL": "Imaxes atopadas para 'Detectar "
"todos': {}",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_LABEL": "A tecla Supr envía á papeleira por "
"defecto",
"SETTINGS_DEFAULT_DELETE_TO_TRASH_TOOLTIP": "Se está marcada, ao premer a "
"tecla Supr moveranse os ficheiros á papeleira. Se non, eliminaranse "
"permanentemente.",
"SETTINGS_DUPLICATE_CONFIRM_DELETE_TOOLTIP": "Amosa un diálogo de confirmación "
"antes de mover unha imaxe duplicada á papeleira.",
"SETTINGS_DUPLICATE_THRESHOLD_LABEL": "Umbral de Similitude:",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Establece o umbral de similitude (50-100%). Valores máis altos significan que as imaxes deben ser máis parecidas para considerarse duplicadas.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "A librería 'imagehash' é necesaria para a detección de duplicados pero non se atopou. Esta función está desactivada.",
"SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP": "Establece o umbral de similitude "
"(50-100%). Valores máis altos significan que as imaxes deben ser máis "
"parecidas para considerarse duplicadas.",
"SETTINGS_DUPLICATE_MISSING_LIBS": "A librería 'imagehash' é necesaria para a "
"detección de duplicados pero non se atopou. Esta función está desactivada.",
"MENU_DETECT_DUPLICATES": "Detectar Duplicados",
"DUPLICATE_WHITELIST_EMPTY": "A lista branca está baleira. Por favor, "
"configúrea en Opcións.",
"DUPLICATE_DETECTION_TITLE": "Detección de Duplicados",
"DUPLICATE_ALREADY_RUNNING": "A detección de duplicados xa está en curso.",
"DUPLICATE_NO_IMAGES": "Non hai imaxes cargadas para detectar duplicados.",
@@ -1672,6 +1728,8 @@ _UI_TEXTS = {
"arredor dos lugares.",
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nomes de lugares "
"usados recentemente para lembrar.",
"SETTINGS_PATH_NOT_FOUND_WARNING": "Advertencia: A ruta non existe ou "
"non é un directorio: {}",
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Cor de fondo de miniaturas:",
@@ -1970,6 +2028,7 @@ _UI_TEXTS = {
# Determine which language to use for UI strings
def _get_current_language():
"""Determines the language to use for UI strings based on environment."""
lang = os.getenv("BAGHEERA_LANG") or APP_CONFIG.get("language", DEFAULT_LANGUAGE)
if lang == "system":

View File

@@ -1,3 +1,14 @@
"""
Duplicate Cache and Detection Module for Bagheera.
This module provides the core logic for detecting duplicate images using
perceptual hashing (dHash) and managing a persistent cache of these hashes
and their relationships using LMDB.
Classes:
DuplicateCache: Manages the LMDB database for hashes and exceptions.
DuplicateDetector: Background thread that performs the duplicate analysis.
"""
import os
import logging
import struct
@@ -819,7 +830,8 @@ class DuplicateDetector(QThread):
if time.perf_counter() - last_update_time > 0.05 \
or i == 0 or i == total_queries - 1:
# Scale Comparison to 75% - 100% range
comparison_progress = int(((i + 1) / total_queries) * (total_files / 2)) \
comparison_progress = int(((i + 1) / total_queries)
* (total_files / 2)) \
if total_queries > 0 else (total_files / 2)
self.progress_update.emit(
int(total_files * 1.5 + comparison_progress), total_files * 2,
@@ -856,10 +868,13 @@ class DuplicateDetector(QThread):
# Frequent UI heartbeat for large duplicate groups
if time.perf_counter() - last_update_time > 0.05:
comparison_progress = int(((i + 1) / total_queries) * (total_files / 2))
comparison_progress = int(((i + 1) / total_queries)
* (total_files / 2))
self.progress_update.emit(
int(total_files * 1.5 + comparison_progress), total_files * 2,
UITexts.DUPLICATE_MSG_ANALYZING.format(filename="..."))
int(total_files * 1.5 + comparison_progress),
total_files * 2,
UITexts.DUPLICATE_MSG_ANALYZING.format(
filename="..."))
last_update_time = time.perf_counter()
# Collect for batch update to improve performance

View File

@@ -45,6 +45,7 @@ class DuplicateManagerDialog(QDialog):
self.table_widget.setCurrentCell(0, 0)
def _setup_ui(self):
"""Sets up the user interface components for the duplicate manager."""
layout = QHBoxLayout(self)
# Left side: List of pairs
@@ -181,15 +182,79 @@ class DuplicateManagerDialog(QDialog):
def resizeEvent(self, event):
"""Resizes the images to fill available space when the dialog is resized."""
super().resizeEvent(event)
if hasattr(self, 'left_pane') and self.left_pane and \
hasattr(self, 'right_pane') and self.right_pane:
self._is_syncing = True
super().resizeEvent(event) # Call base class resizeEvent
self._apply_linked_scaling()
def _apply_linked_scaling(self):
"""Applies custom linked scaling logic to both panels."""
if not self.left_pane or not self.right_pane:
return
# Ensure images are loaded to get original dimensions.
# This also ensures pane.controller.pixmap_original is populated.
self.left_pane.controller.load_image()
self.right_pane.controller.load_image()
p_l = self.left_pane.controller.pixmap_original
p_r = self.right_pane.controller.pixmap_original
# If panels are not linked or any image is null, adjust independently
if not self.panes_linked or p_l.isNull() or p_r.isNull():
self._is_syncing = True # Avoid recursion in _sync_zoom
try:
self.load_and_fit_image_for_pane(self.left_pane)
self.load_and_fit_image_for_pane(self.right_pane)
finally:
self._is_syncing = False
return
self._is_syncing = True
try:
# Get original dimensions
w_l_orig, h_l_orig = p_l.width(), p_l.height()
w_r_orig, h_r_orig = p_r.width(), p_r.height()
# Get available viewport size for each panel
viewport_l = self.left_pane.scroll_area.viewport()
viewport_r = self.right_pane.scroll_area.viewport()
vp_w_l, vp_h_l = viewport_l.width(), viewport_l.height()
vp_w_r, vp_h_r = viewport_r.width(), viewport_r.height()
# Determine the highest resolution image
res_l = w_l_orig * h_l_orig
res_r = w_r_orig * h_r_orig
if res_l >= res_r:
high_res_pane = self.left_pane
low_res_pane = self.right_pane
high_res_w, high_res_h = w_l_orig, h_l_orig
low_res_w, low_res_h = w_r_orig, h_r_orig
vp_w_high, vp_h_high = vp_w_l, vp_h_l
else:
high_res_pane = self.right_pane
low_res_pane = self.left_pane
high_res_w, high_res_h = w_r_orig, h_r_orig
low_res_w, low_res_h = w_l_orig, h_l_orig
vp_w_high, vp_h_high = vp_w_r, vp_h_r
# Calculate zoom factor for high-res image to fit its panel
zoom_high = 1.0
if high_res_w > 0 and high_res_h > 0:
zoom_high = min(vp_w_high / high_res_w, vp_h_high / high_res_h)
high_res_pane.controller.zoom_factor = zoom_high
high_res_pane.update_view(resize_win=False)
# Calculate and apply zoom for low-res image relative to high-res
zoom_low = 1.0
if high_res_w > 0 and high_res_h > 0:
relative_scale_factor = min(low_res_w / high_res_w,
low_res_h / high_res_h)
zoom_low = zoom_high * relative_scale_factor
low_res_pane.controller.zoom_factor = zoom_low
low_res_pane.update_view(resize_win=False)
finally:
self._is_syncing = False
def wheelEvent(self, event):
"""Handles mouse wheel events for zooming (with Ctrl)."""
@@ -340,6 +405,7 @@ class DuplicateManagerDialog(QDialog):
return widget
def _populate_list(self):
"""Fills the table widget with the list of duplicate results."""
self.table_widget.setSortingEnabled(False)
self.table_widget.blockSignals(True)
self.table_widget.setRowCount(0)
@@ -508,21 +574,29 @@ class DuplicateManagerDialog(QDialog):
self.right_pane_widget.info_lbl.setStyleSheet(
"font-weight: bold; color: #aaa;")
# Force view update and proportional scaling
self._apply_linked_scaling()
def _set_pane_data(self, pane_widget, path, filename_color, dir_color,
filename_text, dir_text) -> bool:
"""Updates an ImagePane and its labels with file data."""
pane = pane_widget.pane
info_lbl = pane_widget.info_lbl
filename_lbl = pane_widget.filename_lbl
dir_lbl = pane_widget.dir_lbl
if not os.path.exists(path):
info_lbl.setText("FILE NOT FOUND")
info_lbl.setText(UITexts.FILE_NOT_FOUND)
pane.controller.update_list([], 0) # Clear pane
pane.load_and_fit_image()
pane.controller.load_image()
filename_lbl.setText("N/A")
dir_lbl.setText("N/A")
return True
# Load image into pane's controller FIRST to get accurate pixmap state
pane.controller.update_list([path], 0)
pane.controller.load_image()
# Metadata
size_bytes = os.path.getsize(path)
size_str = self._format_size(size_bytes)
@@ -534,14 +608,6 @@ class DuplicateManagerDialog(QDialog):
not pane.controller.pixmap_original.size().isValid())
disable_linking = is_animated or is_invalid
self.panes_linked = self._user_link_preference and disable_linking
self.btn_link_panes.setEnabled(disable_linking)
self.btn_link_panes.setChecked(self.panes_linked)
# Load image into pane's controller
pane.controller.update_list([path], 0)
pane.load_and_fit_image()
# Update info labels
if not pane.controller.pixmap_original.isNull():
info_lbl.setText(UITexts.DUPLICATE_INFO_FORMAT.format(
@@ -559,6 +625,7 @@ class DuplicateManagerDialog(QDialog):
return disable_linking
def _show_pane_context_menu(self, pos):
"""Displays a context menu for the pane that requested it."""
pane = self.sender()
path = pane.controller.get_current_path()
if not path or not os.path.exists(path):
@@ -618,6 +685,7 @@ class DuplicateManagerDialog(QDialog):
menu.exec(pane.mapToGlobal(pos))
def _handle_permanent_delete(self, path):
"""Prompts for and executes permanent deletion of a file."""
confirm = QMessageBox(self)
confirm.setIcon(QMessageBox.Warning)
confirm.setText(UITexts.CONFIRM_DELETE_TEXT)
@@ -629,6 +697,7 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(delete_path=path, permanent=True)
def _show_properties(self, path, pane):
"""Shows the file properties dialog for a pane's image."""
tags = pane.controller._current_tags
rating = pane.controller._current_rating
dlg = PropertiesDialog(
@@ -636,6 +705,7 @@ class DuplicateManagerDialog(QDialog):
dlg.exec()
def _on_pane_activated(self):
"""Handles pane activation to synchronize viewing state if linked."""
# When a pane is activated, ensure its zoom/scroll is the reference for linking
if self.panes_linked:
active_pane = self.sender() # The pane that emitted activated signal
@@ -650,6 +720,7 @@ class DuplicateManagerDialog(QDialog):
other_pane.set_scroll_relative(x_pct, y_pct)
def _sync_scroll(self, x_pct, y_pct):
"""Synchronizes scroll position between panes if linked."""
if not self.panes_linked:
return
source_pane = self.sender()
@@ -659,35 +730,65 @@ class DuplicateManagerDialog(QDialog):
self.left_pane.set_scroll_relative(x_pct, y_pct)
def _sync_zoom(self, factor, source_pane=None):
"""Synchronizes zoom factor between panes if linked."""
if not self.panes_linked or self._is_syncing:
return
if source_pane is None:
# El emisor es el ZoomManager, su padre es el ImagePane
# Emitter is ZoomManager, its parent is ImagePane
sender = self.sender()
source_pane = sender.parent() if sender else None
if not source_pane:
return
# Ensure both images are loaded before syncing zoom
if self.left_pane.controller.pixmap_original.isNull() or \
self.right_pane.controller.pixmap_original.isNull():
return
self._is_syncing = True
try:
# Capture current scroll percentage from source to apply to target
h_bar = source_pane.scroll_area.horizontalScrollBar()
v_bar = source_pane.scroll_area.verticalScrollBar()
x_pct = h_bar.value() / h_bar.maximum() if h_bar.maximum() > 0 else 0
y_pct = v_bar.value() / v_bar.maximum() if v_bar.maximum() > 0 else 0
p_l = self.left_pane.controller.pixmap_original
p_r = self.right_pane.controller.pixmap_original
target_pane = self.left_pane \
if source_pane == self.right_pane else self.right_pane
target_pane.zoom_manager.zoom(absolute_factor=factor)
w_l_orig, h_l_orig = p_l.width(), p_l.height()
w_r_orig, h_r_orig = p_r.width(), p_r.height()
# Re-apply relative scroll after zoom changes bounds
QTimer.singleShot(
0, lambda p=target_pane, x=x_pct, y=y_pct: p.set_scroll_relative(x, y))
if w_l_orig == 0 or h_l_orig == 0 or w_r_orig == 0 or h_r_orig == 0:
return # Avoid division by zero
# Calculate original size relationship.
# Use ratio of "master" (high-res) to "slave" (low-res)
# to maintain relative size.
res_l = w_l_orig * h_l_orig
res_r = w_r_orig * h_r_orig
if res_l >= res_r: # Left is same or higher resolution
high_res_w, high_res_h = w_l_orig, h_l_orig
low_res_w, low_res_h = w_r_orig, h_r_orig
high_res_pane = self.left_pane
low_res_pane = self.right_pane
else: # Right is higher resolution
high_res_w, high_res_h = w_r_orig, h_r_orig
low_res_w, low_res_h = w_l_orig, h_l_orig
high_res_pane = self.right_pane
low_res_pane = self.left_pane
# 'factor' is the new zoom factor of the source panel.
# Apply this to the high-res panel, then calculate low-res zoom.
if source_pane == high_res_pane:
low_res_pane.controller.zoom_factor = factor * min(
low_res_w / high_res_w, low_res_h / high_res_h)
low_res_pane.update_view(resize_win=False)
else: # source_pane == low_res_pane
high_res_pane.controller.zoom_factor = factor / min(
low_res_w / high_res_w, low_res_h / high_res_h)
high_res_pane.update_view(resize_win=False)
finally:
self._is_syncing = False
def _format_size(self, size):
"""Formats a file size in bytes to a human-readable string."""
for unit in ['B', 'KiB', 'MiB', 'GiB']:
if size < 1024:
return f"{size:.1f} {unit}"
@@ -695,16 +796,19 @@ class DuplicateManagerDialog(QDialog):
return f"{size:.1f} TiB"
def _delete_left(self):
"""Triggers deletion of the image in the left pane."""
path_to_delete = self.left_pane.controller.get_current_path()
if path_to_delete:
self._handle_action(delete_path=path_to_delete)
def _delete_right(self):
"""Triggers deletion of the image in the right pane."""
path_to_delete = self.right_pane.controller.get_current_path()
if path_to_delete:
self._handle_action(delete_path=path_to_delete)
def _toggle_link_panes(self):
"""Toggles the link state between panes."""
self._user_link_preference = self.btn_link_panes.isChecked()
self.panes_linked = self._user_link_preference
if self.panes_linked:
@@ -767,6 +871,7 @@ class DuplicateManagerDialog(QDialog):
self.table_widget.setCurrentCell(new_row, 0)
def _keep_both(self):
"""Marks the current pair as an exception to ignore in future scans."""
if self.current_dup_pair:
self.cache.mark_as_exception(
self.current_dup_pair.path1,
@@ -777,6 +882,7 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(skip=False, permanent=False)
def _skip(self):
"""Skips the current pair without marking it as an exception."""
if self.review_mode and self.current_dup_pair:
self.cache.mark_as_exception(
self.current_dup_pair.path1, self.current_dup_pair.path2, False)
@@ -793,6 +899,13 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(skip=True)
def _handle_action(self, delete_path=None, skip=False, permanent=None):
"""
Handles management actions (delete, skip, keep) for duplicate pairs.
Args:
delete_path: Path to delete, if any.
skip: Whether to skip the current pair.
"""
current_row = self.table_widget.currentRow()
if current_row < 0:
return

View File

@@ -19,7 +19,7 @@ class FileSystemWatcher(QObject):
file_modified = Signal(str)
_file_modified_from_handler = Signal(str) # Internal signal from handler thread
file_moved = Signal(str, str)
monitoring_status_changed = Signal(bool) # Nuevo: Señal para el estado de monitoreo
monitoring_status_changed = Signal(bool) # New: Signal for monitoring status
directory_moved = Signal(str, str)
directory_modified = Signal(str) # For changes that might not be specific files
@@ -158,6 +158,7 @@ class FileSystemWatcher(QObject):
self.watcher = watcher
def on_created(self, event):
"""Called when a file or directory is created."""
if event.is_directory:
self.watcher.directory_modified.emit(event.src_path)
return
@@ -165,6 +166,7 @@ class FileSystemWatcher(QObject):
self.watcher.file_created.emit(event.src_path)
def on_deleted(self, event):
"""Called when a file or directory is deleted."""
if event.is_directory:
self.watcher.directory_modified.emit(event.src_path)
return
@@ -172,6 +174,7 @@ class FileSystemWatcher(QObject):
self.watcher.file_deleted.emit(event.src_path)
def on_moved(self, event):
"""Called when a file or directory is moved or renamed."""
if event.is_directory:
self.watcher.directory_moved.emit(event.src_path, event.dest_path)
self.watcher.directory_modified.emit(event.src_path)
@@ -180,6 +183,7 @@ class FileSystemWatcher(QObject):
self.watcher.file_moved.emit(event.src_path, event.dest_path)
def on_closed(self, event):
"""Called when a file is closed."""
if event.is_directory:
self.watcher.directory_modified.emit(event.src_path)
return
@@ -187,6 +191,7 @@ class FileSystemWatcher(QObject):
self.watcher.file_modified.emit(event.src_path)
def on_modified(self, event):
"""Called when a file or directory is modified."""
if event.is_directory:
self.watcher.directory_modified.emit(event.src_path)
return
@@ -194,9 +199,11 @@ class FileSystemWatcher(QObject):
self.watcher._file_modified_from_handler.emit(event.src_path)
def _emit_modified(self, path):
"""Internal helper to emit the modified signal."""
self.watcher.file_modified.emit(path)
if path in self.watcher._modified_events_queue:
del self.watcher._modified_events_queue[path]
def _is_image_file(self, path):
"""Checks if a given path has a supported image extension."""
return os.path.splitext(path)[1].lower() in IMAGE_EXTENSIONS

View File

@@ -745,9 +745,9 @@ class ThumbnailCache(QObject):
def _get_tier_for_size(self, requested_size):
"""Determines the ideal thumbnail tier based on the requested size."""
if requested_size < 192:
if requested_size <= 128:
return 128
if requested_size < 320:
if requested_size <= 256:
return 256
return 512

View File

@@ -1168,26 +1168,25 @@ class ZoomManager(QObject):
v_point = viewport.mapFrom(self.viewer, focus_point)
c_point = self.viewer.canvas.mapFrom(viewport, v_point)
else:
# 1. Determinar el punto de enfoque en coordenadas del viewport
# 1. Determine focus point in viewport coordinates
scroll_area = self.viewer.scroll_area
viewport = scroll_area.viewport()
if focus_point is None:
v_point = viewport.rect().center()
else:
# focus_point es relativo al widget self.viewer (ImageViewer o
# ImagePane)
# focus_point is relative to the self.viewer widget
# (ImageViewer or ImagePane)
v_point = viewport.mapFrom(self.viewer, focus_point)
# 2. Mapear el punto de enfoque a coordenadas del canvas antes del zoom
# 2. Map focus point to canvas coordinates before zoom
c_point = self.viewer.canvas.mapFrom(viewport, v_point)
self.viewer.controller.zoom_factor *= factor
# Aplicar la actualización (esto redimensiona el canvas)
# Apply update (this resizes the canvas)
self.viewer.update_view(resize_win=(not self.viewer.isFullScreen()))
# 3. Ajustar las barras de desplazamiento para mantener el píxel bajo el
# cursor
# 3. Adjust scrollbars to maintain pixel under cursor
scroll_area.horizontalScrollBar().setValue(
int(c_point.x() * factor - v_point.x()))
scroll_area.verticalScrollBar().setValue(

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bagheeraview"
version = "0.9.18"
version = "0.9.19"
authors = [
{ name = "Ignacio Serantes" }
]

View File

@@ -55,7 +55,7 @@ class DuplicateFileCounter(QThread):
def stop(self):
self._abort = True
self.wait() # Add this line
self.wait()
def run(self):
count = 0
@@ -68,7 +68,8 @@ class DuplicateFileCounter(QThread):
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]
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:
@@ -422,7 +423,8 @@ class SettingsDialog(QDialog):
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_HISTOGRAM_HASHING, "histogram_hashing")
self.duplicate_method_combo.addItem(UITexts.METHOD_RESNET, "resnet")
self.duplicate_method_combo.setEnabled(HAVE_IMAGEHASH)
@@ -437,7 +439,8 @@ class SettingsDialog(QDialog):
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)
self.duplicate_method_combo.setToolTip(
UITexts.SETTINGS_DUPLICATE_METHOD_TOOLTIP)
duplicates_layout.addLayout(method_layout)
threshold_layout = QHBoxLayout()
@@ -454,7 +457,8 @@ class SettingsDialog(QDialog):
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.setToolTip(
UITexts.SETTINGS_DUPLICATE_THRESHOLD_TOOLTIP)
self.duplicate_threshold_slider.valueChanged.connect(
lambda v: self.duplicate_threshold_value_label.setText(f"{v}%"))
@@ -485,14 +489,16 @@ class SettingsDialog(QDialog):
# 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)
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)
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)
@@ -500,7 +506,8 @@ class SettingsDialog(QDialog):
# 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_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)
@@ -517,20 +524,27 @@ class SettingsDialog(QDialog):
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.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)
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)
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()
@@ -945,10 +959,12 @@ class SettingsDialog(QDialog):
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"])
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"])
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)
@@ -1286,11 +1302,15 @@ class SettingsDialog(QDialog):
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["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())]
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"] = \
@@ -1381,7 +1401,8 @@ class SettingsDialog(QDialog):
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}")
item.setToolTip(
UITexts.SETTINGS_PATH_NOT_FOUND_WARNING.format(path))
list_widget.addItem(item)
def add_whitelist_path(self):
@@ -1393,7 +1414,8 @@ class SettingsDialog(QDialog):
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))
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."""
@@ -1404,10 +1426,12 @@ class SettingsDialog(QDialog):
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))
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."""
"""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()
@@ -1417,17 +1441,23 @@ class SettingsDialog(QDialog):
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()}
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_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 = 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())
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()

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="bagheeraview",
version="0.9.18",
version="0.9.19",
author="Ignacio Serantes",
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind",
long_description="A fast image viewer built with PySide6, featuring search and "