v0.9.18
This commit is contained in:
@@ -1847,8 +1847,8 @@ class MainWindow(QMainWindow):
|
|||||||
self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
|
self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Por defecto usamos el modo optimizado (incremental) para no repetir
|
# By default, we use optimized (incremental) mode to avoid repeating
|
||||||
# comparaciones
|
# comparisons.
|
||||||
self.start_duplicate_detection(force_full=False, custom_paths=paths)
|
self.start_duplicate_detection(force_full=False, custom_paths=paths)
|
||||||
|
|
||||||
def _gather_files_for_duplicates(self):
|
def _gather_files_for_duplicates(self):
|
||||||
@@ -3205,8 +3205,8 @@ class MainWindow(QMainWindow):
|
|||||||
current_item = self.thumbnail_model.item(model_idx)
|
current_item = self.thumbnail_model.item(model_idx)
|
||||||
|
|
||||||
if self._match_item(target, current_item):
|
if self._match_item(target, current_item):
|
||||||
# Si es una cabecera, actualizamos el texto por si cambió el
|
# If it is a header, update the text in case the counter
|
||||||
# contador
|
# changed.
|
||||||
if isinstance(target, tuple) and target[0] == 'HEADER':
|
if isinstance(target, tuple) and target[0] == 'HEADER':
|
||||||
_, (_, header_text, _) = target
|
_, (_, header_text, _) = target
|
||||||
if current_item.data(DIR_ROLE) != header_text:
|
if current_item.data(DIR_ROLE) != header_text:
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ Ahora que la carga es rápida, ¿cómo puedo implementar una precarga inteligent
|
|||||||
|
|
||||||
¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista?
|
¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista?
|
||||||
|
|
||||||
|
v0.9.18 -
|
||||||
|
· Better messages
|
||||||
|
|
||||||
v0.9.17 -
|
v0.9.17 -
|
||||||
· Fixes
|
· Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if FORCE_X11:
|
|||||||
# --- CONFIGURATION ---
|
# --- CONFIGURATION ---
|
||||||
PROG_NAME = "Bagheera Image Viewer"
|
PROG_NAME = "Bagheera Image Viewer"
|
||||||
PROG_ID = "bagheeraview"
|
PROG_ID = "bagheeraview"
|
||||||
PROG_VERSION = "0.9.18-dev"
|
PROG_VERSION = "0.9.18"
|
||||||
PROG_AUTHOR = "Ignacio Serantes"
|
PROG_AUTHOR = "Ignacio Serantes"
|
||||||
|
|
||||||
# --- CACHE SETTINGS ---
|
# --- CACHE SETTINGS ---
|
||||||
|
|||||||
@@ -712,7 +712,7 @@ class DuplicateDetector(QThread):
|
|||||||
progress = int((processed_initial / total_files) * total_files)
|
progress = int((processed_initial / total_files) * total_files)
|
||||||
self.progress_update.emit(
|
self.progress_update.emit(
|
||||||
progress, total_files * 2,
|
progress, total_files * 2,
|
||||||
UITexts.DUPLICATE_MSG_HASHING.format(filename=os.path.basename(path)))
|
UITexts.DUPLICATE_MSG_HASHING.format(filename="..."))
|
||||||
last_update_time = time.perf_counter()
|
last_update_time = time.perf_counter()
|
||||||
|
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -771,9 +771,11 @@ class DuplicateDetector(QThread):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Sub-phase: Indexing hashes into the BK-Tree for comparison
|
# Sub-phase: Indexing hashes into the BK-Tree for comparison
|
||||||
if time.perf_counter() - last_update_time > 0.05 or i == 0 or i == total_items - 1:
|
if time.perf_counter() - last_update_time > 0.05 \
|
||||||
|
or i == 0 or i == total_items - 1:
|
||||||
# Scale Indexing to 50% - 75% range of the total bar
|
# Scale Indexing to 50% - 75% range of the total bar
|
||||||
indexing_progress = int((i / total_items) * (total_files / 2)) if total_items > 0 else 0
|
indexing_progress = int((i / total_items) * (total_files / 2)) \
|
||||||
|
if total_items > 0 else 0
|
||||||
self.progress_update.emit(
|
self.progress_update.emit(
|
||||||
total_files + indexing_progress, total_files * 2,
|
total_files + indexing_progress, total_files * 2,
|
||||||
UITexts.DUPLICATE_MSG_ANALYZING.format(filename="..."))
|
UITexts.DUPLICATE_MSG_ANALYZING.format(filename="..."))
|
||||||
@@ -814,7 +816,8 @@ class DuplicateDetector(QThread):
|
|||||||
items1 = hash_map[h1]
|
items1 = hash_map[h1]
|
||||||
|
|
||||||
# Update progress more frequently during analysis phase
|
# Update progress more frequently during analysis phase
|
||||||
if time.perf_counter() - last_update_time > 0.05 or i == 0 or i == total_queries - 1:
|
if time.perf_counter() - last_update_time > 0.05 \
|
||||||
|
or i == 0 or i == total_queries - 1:
|
||||||
# Scale Comparison to 75% - 100% range
|
# 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)
|
if total_queries > 0 else (total_files / 2)
|
||||||
@@ -853,10 +856,10 @@ class DuplicateDetector(QThread):
|
|||||||
|
|
||||||
# Frequent UI heartbeat for large duplicate groups
|
# Frequent UI heartbeat for large duplicate groups
|
||||||
if time.perf_counter() - last_update_time > 0.05:
|
if time.perf_counter() - last_update_time > 0.05:
|
||||||
phase2_progress = int(((i + 1) / total_queries) * total_files)
|
comparison_progress = int(((i + 1) / total_queries) * (total_files / 2))
|
||||||
self.progress_update.emit(
|
self.progress_update.emit(
|
||||||
total_files + phase2_progress, total_files * 2,
|
int(total_files * 1.5 + comparison_progress), total_files * 2,
|
||||||
UITexts.DUPLICATE_MSG_ANALYZING.format(filename=os.path.basename(p1)))
|
UITexts.DUPLICATE_MSG_ANALYZING.format(filename="..."))
|
||||||
last_update_time = time.perf_counter()
|
last_update_time = time.perf_counter()
|
||||||
|
|
||||||
# Collect for batch update to improve performance
|
# Collect for batch update to improve performance
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
self.main_win = main_win
|
self.main_win = main_win
|
||||||
self.review_mode = review_mode
|
self.review_mode = review_mode
|
||||||
|
|
||||||
self.active_pane = None
|
self.active_pane = None # Track the focused pane
|
||||||
self.current_dup_pair = None # Stores the current DuplicateResult object
|
self.current_dup_pair = None # Stores the current DuplicateResult object
|
||||||
self.panes_linked = True # Default to linked
|
self.panes_linked = True # Default to linked
|
||||||
self._user_link_preference = True # Persiste la intención del usuario
|
self._user_link_preference = True # Persists user intent
|
||||||
self._is_syncing = False # Guard to prevent recursion during synchronization
|
self._is_syncing = False # Guard to prevent recursion during synchronization
|
||||||
|
|
||||||
self.setWindowTitle(UITexts.DUPLICATE_MANAGER_TITLE)
|
self.setWindowTitle(UITexts.DUPLICATE_MANAGER_TITLE)
|
||||||
@@ -194,7 +194,7 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
def wheelEvent(self, event):
|
def wheelEvent(self, event):
|
||||||
"""Handles mouse wheel events for zooming (with Ctrl)."""
|
"""Handles mouse wheel events for zooming (with Ctrl)."""
|
||||||
if event.modifiers() & Qt.ControlModifier and self.active_pane:
|
if event.modifiers() & Qt.ControlModifier and self.active_pane:
|
||||||
# Calcular el punto de enfoque relativo al pane activo
|
# Calculate the focus point relative to the active pane.
|
||||||
focus_pos = self.active_pane.mapFromGlobal(event.globalPosition().toPoint())
|
focus_pos = self.active_pane.mapFromGlobal(event.globalPosition().toPoint())
|
||||||
if event.angleDelta().y() > 0:
|
if event.angleDelta().y() > 0:
|
||||||
self.active_pane.zoom_manager.zoom(1.1, focus_point=focus_pos)
|
self.active_pane.zoom_manager.zoom(1.1, focus_point=focus_pos)
|
||||||
@@ -364,8 +364,7 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
col_offset = 0
|
col_offset = 0
|
||||||
|
|
||||||
# Columna similarity (usamos DisplayRole con int para que ordene
|
# Similarity column (using DisplayRole with int for numerical sorting).
|
||||||
# numéricamente)
|
|
||||||
sim_item = QTableWidgetItem()
|
sim_item = QTableWidgetItem()
|
||||||
sim_item.setData(Qt.DisplayRole, dup.similarity
|
sim_item.setData(Qt.DisplayRole, dup.similarity
|
||||||
if dup.similarity is not None else 0)
|
if dup.similarity is not None else 0)
|
||||||
@@ -373,7 +372,7 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
if not self.review_mode:
|
if not self.review_mode:
|
||||||
sim_item.setData(Qt.UserRole, i)
|
sim_item.setData(Qt.UserRole, i)
|
||||||
|
|
||||||
# Columna 1: Nombres de ficheros
|
# Column 1: File names
|
||||||
names_item = QTableWidgetItem(f"{name1} ↔ {name2}")
|
names_item = QTableWidgetItem(f"{name1} ↔ {name2}")
|
||||||
|
|
||||||
self.table_widget.setItem(row, col_offset, sim_item)
|
self.table_widget.setItem(row, col_offset, sim_item)
|
||||||
@@ -392,8 +391,8 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
if row < 0 or row >= self.table_widget.rowCount():
|
if row < 0 or row >= self.table_widget.rowCount():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Obtenemos el índice real de la lista duplicates guardado en el UserRole del
|
# Get the real index of the duplicates list stored in the UserRole of
|
||||||
# item
|
# the item.
|
||||||
item = self.table_widget.item(row, 0)
|
item = self.table_widget.item(row, 0)
|
||||||
if not item:
|
if not item:
|
||||||
return
|
return
|
||||||
@@ -407,7 +406,7 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
if dup.similarity == 100:
|
if dup.similarity == 100:
|
||||||
similarity_color = "#2ecc71" # Green
|
similarity_color = "#2ecc71" # Green
|
||||||
elif dup.similarity < 80:
|
elif dup.similarity < 80:
|
||||||
similarity_color = "#e74c3c" # Red
|
similarity_color = "#e74c3c" # Red
|
||||||
|
|
||||||
self.similarity_lbl.setText(f"{dup.similarity}% Similarity")
|
self.similarity_lbl.setText(f"{dup.similarity}% Similarity")
|
||||||
self.similarity_lbl.setStyleSheet(
|
self.similarity_lbl.setStyleSheet(
|
||||||
@@ -524,11 +523,11 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
dir_lbl.setText("N/A")
|
dir_lbl.setText("N/A")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Metadatos
|
# Metadata
|
||||||
size_bytes = os.path.getsize(path)
|
size_bytes = os.path.getsize(path)
|
||||||
size_str = self._format_size(size_bytes)
|
size_str = self._format_size(size_bytes)
|
||||||
|
|
||||||
# Detección de imágenes animadas o resoluciones inválidas
|
# Detection of animated images or invalid resolutions
|
||||||
reader = QImageReader(path)
|
reader = QImageReader(path)
|
||||||
is_animated = reader.supportsAnimation() and reader.imageCount() > 1
|
is_animated = reader.supportsAnimation() and reader.imageCount() > 1
|
||||||
is_invalid = (pane.controller.pixmap_original.isNull() or
|
is_invalid = (pane.controller.pixmap_original.isNull() or
|
||||||
@@ -755,7 +754,7 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
if d.path1 == old_path or d.path2 == old_path:
|
if d.path1 == old_path or d.path2 == old_path:
|
||||||
p1 = new_path if d.path1 == old_path else d.path1
|
p1 = new_path if d.path1 == old_path else d.path1
|
||||||
p2 = new_path if d.path2 == old_path else d.path2
|
p2 = new_path if d.path2 == old_path else d.path2
|
||||||
# Actualizamos la tupla con nombre usando _replace
|
# Update the named tuple using _replace
|
||||||
self.duplicates[i] = d._replace(path1=p1, path2=p2)
|
self.duplicates[i] = d._replace(path1=p1, path2=p2)
|
||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
@@ -781,10 +780,10 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
if self.review_mode and self.current_dup_pair:
|
if self.review_mode and self.current_dup_pair:
|
||||||
self.cache.mark_as_exception(
|
self.cache.mark_as_exception(
|
||||||
self.current_dup_pair.path1, self.current_dup_pair.path2, False)
|
self.current_dup_pair.path1, self.current_dup_pair.path2, False)
|
||||||
# Borramos los hashes para que el detector las trate como imágenes nuevas
|
# Clear hashes so the detector treats them as new images and
|
||||||
# y fuerce una nueva comparación en el siguiente escaneo.
|
# forces a new comparison in the next scan. We use
|
||||||
# Usamos clear_relationships=False para no perder otras posibles
|
# clear_relationships=False to preserve other possible matches
|
||||||
# coincidencias ya marcadas.
|
# already identified.
|
||||||
self.cache.remove_hash_for_path(
|
self.cache.remove_hash_for_path(
|
||||||
self.current_dup_pair.path1, clear_relationships=False)
|
self.current_dup_pair.path1, clear_relationships=False)
|
||||||
self.cache.remove_hash_for_path(
|
self.cache.remove_hash_for_path(
|
||||||
|
|||||||
12
settings.py
12
settings.py
@@ -1195,7 +1195,7 @@ class SettingsDialog(QDialog):
|
|||||||
elif self.download_model_btn:
|
elif self.download_model_btn:
|
||||||
self.download_model_btn.hide()
|
self.download_model_btn.hide()
|
||||||
|
|
||||||
# --- Mascotas (Pets) ---
|
# --- Pets ---
|
||||||
if not AVAILABLE_PET_ENGINES:
|
if not AVAILABLE_PET_ENGINES:
|
||||||
self.pet_engine_combo.setEnabled(False)
|
self.pet_engine_combo.setEnabled(False)
|
||||||
self.pet_tags_edit.setEnabled(False)
|
self.pet_tags_edit.setEnabled(False)
|
||||||
@@ -1341,14 +1341,14 @@ class SettingsDialog(QDialog):
|
|||||||
self.downloader_thread = None
|
self.downloader_thread = None
|
||||||
|
|
||||||
def done(self, r):
|
def done(self, r):
|
||||||
self._stop_downloader_thread() # Asegura que el hilo de descarga se detenga y espere
|
self._stop_downloader_thread() # Ensure downloader thread stops and waits.
|
||||||
if self.counter_thread and self.counter_thread.isRunning():
|
if self.counter_thread and self.counter_thread.isRunning():
|
||||||
self.counter_thread.stop()
|
self.counter_thread.stop()
|
||||||
self.counter_thread.wait()
|
self.counter_thread.wait()
|
||||||
super().done(r)
|
super().done(r)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self._stop_downloader_thread() # Asegura que el hilo de descarga se detenga y espere
|
self._stop_downloader_thread() # Ensure downloader thread stops and waits.
|
||||||
if self.counter_thread and self.counter_thread.isRunning():
|
if self.counter_thread and self.counter_thread.isRunning():
|
||||||
self.counter_thread.stop()
|
self.counter_thread.stop()
|
||||||
self.counter_thread.wait()
|
self.counter_thread.wait()
|
||||||
@@ -1366,15 +1366,15 @@ class SettingsDialog(QDialog):
|
|||||||
if existing_p == path:
|
if existing_p == path:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si una carpeta padre ya existe, no añadimos esta subcarpeta
|
# If a parent folder already exists, do not add this subfolder.
|
||||||
if path.startswith(existing_p + os.sep):
|
if path.startswith(existing_p + os.sep):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si la nueva ruta es padre de una existente, marcamos la existente para borrar
|
# If the new path is a parent of an existing one, mark it for removal.
|
||||||
if existing_p.startswith(path + os.sep):
|
if existing_p.startswith(path + os.sep):
|
||||||
to_remove.append(i)
|
to_remove.append(i)
|
||||||
|
|
||||||
# Borramos las subcarpetas innecesarias (en orden inverso para no alterar los índices)
|
# Remove unnecessary subfolders (reverse order to not alter indices).
|
||||||
for i in sorted(to_remove, reverse=True):
|
for i in sorted(to_remove, reverse=True):
|
||||||
list_widget.takeItem(i)
|
list_widget.takeItem(i)
|
||||||
|
|
||||||
|
|||||||
@@ -1341,8 +1341,8 @@ class FaceNameInputWidget(QWidget):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.main_win = main_win
|
self.main_win = main_win
|
||||||
self.region_type = region_type
|
self.region_type = region_type
|
||||||
# Usamos deque para gestionar el historial de forma eficiente con un máximo
|
# Use deque to manage history efficiently with a configurable maximum
|
||||||
# configurable de elementos.
|
# number of items.
|
||||||
max_items = APP_CONFIG.get("faces_menu_max_items",
|
max_items = APP_CONFIG.get("faces_menu_max_items",
|
||||||
FACES_MENU_MAX_ITEMS_DEFAULT)
|
FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||||
if self.region_type == "Pet":
|
if self.region_type == "Pet":
|
||||||
@@ -1372,7 +1372,7 @@ class FaceNameInputWidget(QWidget):
|
|||||||
self.name_combo.setToolTip(UITexts.FACE_NAME_TOOLTIP)
|
self.name_combo.setToolTip(UITexts.FACE_NAME_TOOLTIP)
|
||||||
self.name_combo.lineEdit().setClearButtonEnabled(True)
|
self.name_combo.lineEdit().setClearButtonEnabled(True)
|
||||||
|
|
||||||
# 2. Completer para la funcionalidad de autocompletado.
|
# 2. Completer for autocomplete functionality.
|
||||||
self.completer = QCompleter(self)
|
self.completer = QCompleter(self)
|
||||||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
self.completer.setFilterMode(Qt.MatchContains)
|
self.completer.setFilterMode(Qt.MatchContains)
|
||||||
|
|||||||
Reference in New Issue
Block a user