From 07afab6ca33d2a9c8421f390c4d1995f6f5e6d10 Mon Sep 17 00:00:00 2001 From: Ignacio Serantes Date: Wed, 8 Apr 2026 15:47:29 +0200 Subject: [PATCH] v0.9.19 --- bagheeraview.py | 6 +- constants.py | 129 ++++++++++++++++++++++++--------- duplicatecache.py | 23 ++++-- duplicatedialog.py | 165 ++++++++++++++++++++++++++++++++++++------- filesystemwatcher.py | 9 ++- imagescanner.py | 4 +- imageviewer.py | 13 ++-- pyproject.toml | 2 +- settings.py | 96 ++++++++++++++++--------- setup.py | 2 +- 10 files changed, 336 insertions(+), 113 deletions(-) diff --git a/bagheeraview.py b/bagheeraview.py index 69837bf..26c772b 100755 --- a/bagheeraview.py +++ b/bagheeraview.py @@ -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: diff --git a/constants.py b/constants.py index c2d259e..b70fea1 100644 --- a/constants.py +++ b/constants.py @@ -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": diff --git a/duplicatecache.py b/duplicatecache.py index feb4446..02fc2bf 100644 --- a/duplicatecache.py +++ b/duplicatecache.py @@ -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 diff --git a/duplicatedialog.py b/duplicatedialog.py index a9398bd..650fe67 100644 --- a/duplicatedialog.py +++ b/duplicatedialog.py @@ -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 diff --git a/filesystemwatcher.py b/filesystemwatcher.py index da69eb9..28a2458 100644 --- a/filesystemwatcher.py +++ b/filesystemwatcher.py @@ -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 diff --git a/imagescanner.py b/imagescanner.py index a5bf30f..192f210 100644 --- a/imagescanner.py +++ b/imagescanner.py @@ -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 diff --git a/imageviewer.py b/imageviewer.py index 13a448c..7d1bd6f 100644 --- a/imageviewer.py +++ b/imageviewer.py @@ -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( diff --git a/pyproject.toml b/pyproject.toml index 21ceab9..3cafa46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bagheeraview" -version = "0.9.18" +version = "0.9.19" authors = [ { name = "Ignacio Serantes" } ] diff --git a/settings.py b/settings.py index e097687..d8d9af3 100644 --- a/settings.py +++ b/settings.py @@ -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() diff --git a/setup.py b/setup.py index e105491..7e28ebf 100644 --- a/setup.py +++ b/setup.py @@ -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 "