Fixed hang with gifs in duplicates form
This commit is contained in:
@@ -5,11 +5,11 @@ from PySide6.QtWidgets import (
|
||||
QSplitter, QWidget, QMessageBox, QApplication, QMenu,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
|
||||
)
|
||||
from PySide6.QtGui import QIcon, QImage, QDesktopServices
|
||||
from PySide6.QtGui import QIcon, QImageReader, QImage, QDesktopServices
|
||||
from PySide6.QtCore import Qt, QTimer, QUrl
|
||||
from imageviewer import ImagePane
|
||||
from propertiesdialog import PropertiesDialog
|
||||
from constants import APP_CONFIG, UITexts
|
||||
from propertiesdialog import PropertiesDialog
|
||||
|
||||
|
||||
class DuplicateManagerDialog(QDialog):
|
||||
@@ -26,6 +26,7 @@ class DuplicateManagerDialog(QDialog):
|
||||
self.active_pane = None
|
||||
self.current_dup_pair = None # Stores the current DuplicateResult object
|
||||
self.panes_linked = True # Default to linked
|
||||
self._user_link_preference = True # Persiste la intención del usuario
|
||||
self._is_syncing = False # Guard to prevent recursion during synchronization
|
||||
|
||||
self.setWindowTitle(UITexts.DUPLICATE_MANAGER_TITLE)
|
||||
@@ -37,7 +38,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
if self.main_win and hasattr(self.main_win, 'fs_watcher'):
|
||||
self.main_win.fs_watcher.file_deleted.connect(
|
||||
self._on_file_deleted_externally)
|
||||
self.main_win.fs_watcher.file_moved.connect(self._on_file_moved_externally)
|
||||
self.main_win.fs_watcher.file_moved.connect(
|
||||
self._on_file_moved_externally)
|
||||
|
||||
if self.duplicates:
|
||||
self.table_widget.setCurrentCell(0, 0)
|
||||
@@ -59,7 +61,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
|
||||
self.table_widget = QTableWidget()
|
||||
if self.review_mode:
|
||||
self.table_widget.setColumnCount(3)
|
||||
columns = 3
|
||||
self.table_widget.setColumnCount(columns)
|
||||
self.table_widget.setHorizontalHeaderLabels(
|
||||
[UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN])
|
||||
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||
@@ -69,9 +72,10 @@ class DuplicateManagerDialog(QDialog):
|
||||
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||
2, QHeaderView.Stretch)
|
||||
else:
|
||||
self.table_widget.setColumnCount(2)
|
||||
columns = 2
|
||||
self.table_widget.setColumnCount(columns)
|
||||
self.table_widget.setHorizontalHeaderLabels(
|
||||
["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica
|
||||
["%", UITexts.CONTEXT_MENU_OPEN])
|
||||
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||
0, QHeaderView.ResizeToContents)
|
||||
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||
@@ -103,18 +107,22 @@ class DuplicateManagerDialog(QDialog):
|
||||
button_widget = QWidget()
|
||||
btn_layout = QHBoxLayout(button_widget)
|
||||
|
||||
self.btn_del_left = QPushButton(QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_LEFT)
|
||||
self.btn_del_left = QPushButton(
|
||||
QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_LEFT)
|
||||
self.btn_del_left.clicked.connect(self._delete_left)
|
||||
|
||||
self.btn_del_right = QPushButton(QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_RIGHT)
|
||||
self.btn_del_right = QPushButton(
|
||||
QIcon.fromTheme("user-trash"), UITexts.DUPLICATE_DELETE_RIGHT)
|
||||
self.btn_del_right.clicked.connect(self._delete_right)
|
||||
|
||||
self.btn_link_panes = QPushButton(QIcon.fromTheme("object-link"), UITexts.VIEWER_MENU_LINK_PANES)
|
||||
self.btn_link_panes = QPushButton(
|
||||
QIcon.fromTheme("object-link"), UITexts.VIEWER_MENU_LINK_PANES)
|
||||
self.btn_link_panes.setCheckable(True)
|
||||
self.btn_link_panes.setChecked(self.panes_linked)
|
||||
self.btn_link_panes.clicked.connect(self._toggle_link_panes)
|
||||
|
||||
self.btn_keep_both = QPushButton(QIcon.fromTheme("emblem-important"), UITexts.DUPLICATE_KEEP_BOTH)
|
||||
self.btn_keep_both = QPushButton(
|
||||
QIcon.fromTheme("emblem-important"), UITexts.DUPLICATE_KEEP_BOTH)
|
||||
self.btn_keep_both.clicked.connect(self._keep_both)
|
||||
|
||||
self.btn_skip = QPushButton(UITexts.DUPLICATE_SKIP)
|
||||
@@ -135,7 +143,9 @@ class DuplicateManagerDialog(QDialog):
|
||||
self.similarity_lbl = QLabel()
|
||||
self.similarity_lbl.setAlignment(Qt.AlignCenter)
|
||||
self.similarity_lbl.setMinimumHeight(30)
|
||||
self.similarity_lbl.setStyleSheet("font-weight: bold; color: #f39c12; font-size: 15px; background-color: #222; border: 1px solid #444; border-radius: 4px;")
|
||||
self.similarity_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #f39c12; font-size: 15px; "
|
||||
"background-color: #222; border: 1px solid #444; border-radius: 4px;")
|
||||
|
||||
main_right_layout = QVBoxLayout()
|
||||
main_right_layout.addWidget(self.comparison_widget, 1)
|
||||
@@ -156,8 +166,10 @@ class DuplicateManagerDialog(QDialog):
|
||||
"""Disconnects signals and performs cleanup when closing."""
|
||||
if self.main_win and hasattr(self.main_win, 'fs_watcher'):
|
||||
try:
|
||||
self.main_win.fs_watcher.file_deleted.disconnect(self._on_file_deleted_externally)
|
||||
self.main_win.fs_watcher.file_moved.disconnect(self._on_file_moved_externally)
|
||||
self.main_win.fs_watcher.file_deleted.disconnect(
|
||||
self._on_file_deleted_externally)
|
||||
self.main_win.fs_watcher.file_moved.disconnect(
|
||||
self._on_file_moved_externally)
|
||||
except (RuntimeError, TypeError):
|
||||
pass
|
||||
|
||||
@@ -340,21 +352,25 @@ class DuplicateManagerDialog(QDialog):
|
||||
if self.review_mode:
|
||||
# Column 0: Ignored Date
|
||||
ts = dup.timestamp if hasattr(dup, 'timestamp') and dup.timestamp else 0
|
||||
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else "-"
|
||||
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") \
|
||||
if ts else "-"
|
||||
date_item = QTableWidgetItem(date_str)
|
||||
date_item.setData(Qt.UserRole, i) # Store original index here for _load_pair
|
||||
# Store original index here for _load_pair
|
||||
date_item.setData(Qt.UserRole, i)
|
||||
date_item.setTextAlignment(Qt.AlignCenter)
|
||||
self.table_widget.setItem(row, 0, date_item)
|
||||
col_offset = 1
|
||||
else:
|
||||
col_offset = 0
|
||||
|
||||
# Columna similarity (usamos DisplayRole con int para que ordene numéricamente)
|
||||
# Columna similarity (usamos DisplayRole con int para que ordene
|
||||
# numéricamente)
|
||||
sim_item = QTableWidgetItem()
|
||||
sim_item.setData(Qt.DisplayRole, dup.similarity if dup.similarity is not None else 0)
|
||||
sim_item.setData(Qt.DisplayRole, dup.similarity
|
||||
if dup.similarity is not None else 0)
|
||||
sim_item.setTextAlignment(Qt.AlignCenter)
|
||||
if not self.review_mode:
|
||||
sim_item.setData(Qt.UserRole, i) # Guardamos el índice original en la lista duplicates
|
||||
sim_item.setData(Qt.UserRole, i)
|
||||
|
||||
# Columna 1: Nombres de ficheros
|
||||
names_item = QTableWidgetItem(f"{name1} ↔ {name2}")
|
||||
@@ -375,7 +391,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
if row < 0 or row >= self.table_widget.rowCount():
|
||||
return
|
||||
|
||||
# Obtenemos el índice real de la lista duplicates guardado en el UserRole del item
|
||||
# Obtenemos el índice real de la lista duplicates guardado en el UserRole del
|
||||
# item
|
||||
item = self.table_widget.item(row, 0)
|
||||
if not item:
|
||||
return
|
||||
@@ -387,12 +404,14 @@ class DuplicateManagerDialog(QDialog):
|
||||
similarity_color = "#f39c12" # Default (amber)
|
||||
if dup.similarity is not None:
|
||||
if dup.similarity == 100:
|
||||
similarity_color = "#2ecc71" # Green
|
||||
similarity_color = "#2ecc71" # Green
|
||||
elif dup.similarity < 80:
|
||||
similarity_color = "#e74c3c" # Red
|
||||
similarity_color = "#e74c3c" # Red
|
||||
|
||||
self.similarity_lbl.setText(f"{dup.similarity}% Similarity")
|
||||
self.similarity_lbl.setStyleSheet(f"font-weight: bold; color: {similarity_color}; font-size: 12px; margin-top: 5px;")
|
||||
self.similarity_lbl.setStyleSheet(
|
||||
f"font-weight: bold; color: {similarity_color}; "
|
||||
"font-size: 12px; margin-top: 5px;")
|
||||
self.similarity_lbl.show()
|
||||
else:
|
||||
self.similarity_lbl.hide()
|
||||
@@ -417,13 +436,26 @@ class DuplicateManagerDialog(QDialog):
|
||||
mtime1 = os.path.getmtime(path_left) if os.path.exists(path_left) else 0
|
||||
mtime2 = os.path.getmtime(path_right) if os.path.exists(path_right) else 0
|
||||
|
||||
# La imagen más reciente (mtime más alto) va a la izquierda
|
||||
# Recent image to the left, older to the right
|
||||
if mtime1 >= mtime2:
|
||||
self._set_pane_data(self.left_pane_widget, path_left, filename_color, dir_color, filename_left, dir_left)
|
||||
self._set_pane_data(self.right_pane_widget, path_right, filename_color, dir_color, filename_right, dir_right)
|
||||
dis_l = self._set_pane_data(
|
||||
self.left_pane_widget, path_left, filename_color,
|
||||
dir_color, filename_left, dir_left)
|
||||
dis_r = self._set_pane_data(
|
||||
self.right_pane_widget, path_right, filename_color,
|
||||
dir_color, filename_right, dir_right)
|
||||
else:
|
||||
self._set_pane_data(self.left_pane_widget, path_right, filename_color, dir_color, filename_right, dir_right)
|
||||
self._set_pane_data(self.right_pane_widget, path_left, filename_color, dir_color, filename_left, dir_left)
|
||||
dis_l = self._set_pane_data(
|
||||
self.left_pane_widget, path_right, filename_color,
|
||||
dir_color, filename_right, dir_right)
|
||||
dis_r = self._set_pane_data(
|
||||
self.right_pane_widget, path_left, filename_color,
|
||||
dir_color, filename_left, dir_left)
|
||||
|
||||
can_link = not (dis_l or dis_r)
|
||||
self.panes_linked = self._user_link_preference and can_link
|
||||
self.btn_link_panes.setEnabled(can_link)
|
||||
self.btn_link_panes.setChecked(self.panes_linked)
|
||||
|
||||
# Compare resolutions and highlight the best one
|
||||
p_l = self.left_pane.controller.pixmap_original
|
||||
@@ -432,7 +464,7 @@ class DuplicateManagerDialog(QDialog):
|
||||
res_l = p_l.width() * p_l.height()
|
||||
res_r = p_r.width() * p_r.height()
|
||||
|
||||
winner = 0 # 0: none, 1: left, 2: right
|
||||
winner = 0 # 0: none, 1: left, 2: right
|
||||
if res_l > res_r:
|
||||
winner = 1
|
||||
elif res_r > res_l:
|
||||
@@ -444,26 +476,40 @@ class DuplicateManagerDialog(QDialog):
|
||||
path_r = self.right_pane.controller.get_current_path()
|
||||
size_l = os.path.getsize(path_l)
|
||||
size_r = os.path.getsize(path_r)
|
||||
if size_l > size_r: winner = 1
|
||||
elif size_r > size_l: winner = 2
|
||||
except (OSError, AttributeError): pass
|
||||
if size_l > size_r:
|
||||
winner = 1
|
||||
elif size_r > size_l:
|
||||
winner = 2
|
||||
except (OSError, AttributeError):
|
||||
pass
|
||||
|
||||
if winner == 1:
|
||||
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #2ecc71;")
|
||||
self.left_pane_widget.info_lbl.setText("✓ " + self.left_pane_widget.info_lbl.text())
|
||||
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.left_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #2ecc71;")
|
||||
self.left_pane_widget.info_lbl.setText(
|
||||
"✓ " + self.left_pane_widget.info_lbl.text())
|
||||
self.right_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
elif winner == 2:
|
||||
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #2ecc71;")
|
||||
self.right_pane_widget.info_lbl.setText("✓ " + self.right_pane_widget.info_lbl.text())
|
||||
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.right_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #2ecc71;")
|
||||
self.right_pane_widget.info_lbl.setText(
|
||||
"✓ " + self.right_pane_widget.info_lbl.text())
|
||||
self.left_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
else:
|
||||
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.left_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
self.right_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
else:
|
||||
self.left_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.right_pane_widget.info_lbl.setStyleSheet("font-weight: bold; color: #aaa;")
|
||||
self.left_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
self.right_pane_widget.info_lbl.setStyleSheet(
|
||||
"font-weight: bold; color: #aaa;")
|
||||
|
||||
def _set_pane_data(self, pane_widget, path, filename_color, dir_color, filename_text, dir_text):
|
||||
def _set_pane_data(self, pane_widget, path, filename_color, dir_color,
|
||||
filename_text, dir_text) -> bool:
|
||||
pane = pane_widget.pane
|
||||
info_lbl = pane_widget.info_lbl
|
||||
filename_lbl = pane_widget.filename_lbl
|
||||
@@ -471,16 +517,27 @@ class DuplicateManagerDialog(QDialog):
|
||||
|
||||
if not os.path.exists(path):
|
||||
info_lbl.setText("FILE NOT FOUND")
|
||||
pane.controller.update_list([], 0) # Clear pane
|
||||
pane.controller.update_list([], 0) # Clear pane
|
||||
pane.load_and_fit_image()
|
||||
filename_lbl.setText("N/A")
|
||||
dir_lbl.setText("N/A")
|
||||
return
|
||||
return True
|
||||
|
||||
# Metadatos
|
||||
size_bytes = os.path.getsize(path)
|
||||
size_str = self._format_size(size_bytes)
|
||||
|
||||
# Detección de imágenes animadas o resoluciones inválidas
|
||||
reader = QImageReader(path)
|
||||
is_animated = reader.supportsAnimation() and reader.imageCount() > 1
|
||||
is_invalid = (pane.controller.pixmap_original.isNull() or
|
||||
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()
|
||||
@@ -495,9 +552,11 @@ class DuplicateManagerDialog(QDialog):
|
||||
info_lbl.setText(f"{size_str} - N/A")
|
||||
|
||||
filename_lbl.setText(filename_text)
|
||||
filename_lbl.setStyleSheet(f"font-size: 11px; font-weight: bold; color: {filename_color};")
|
||||
filename_lbl.setStyleSheet(
|
||||
f"font-size: 11px; font-weight: bold; color: {filename_color};")
|
||||
dir_lbl.setText(dir_text)
|
||||
dir_lbl.setStyleSheet(f"font-size: 9px; color: {dir_color};")
|
||||
return disable_linking
|
||||
|
||||
def _show_pane_context_menu(self, pos):
|
||||
pane = self.sender()
|
||||
@@ -508,7 +567,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
menu = QMenu(self)
|
||||
|
||||
# Open with...
|
||||
open_menu = menu.addMenu(QIcon.fromTheme("document-open"), UITexts.CONTEXT_MENU_OPEN)
|
||||
open_menu = menu.addMenu(
|
||||
QIcon.fromTheme("document-open"), UITexts.CONTEXT_MENU_OPEN)
|
||||
self.main_win.populate_open_with_submenu(open_menu, path)
|
||||
|
||||
# Open location
|
||||
@@ -521,28 +581,39 @@ class DuplicateManagerDialog(QDialog):
|
||||
menu.addSeparator()
|
||||
|
||||
# Clipboard
|
||||
clip_menu = menu.addMenu(QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
|
||||
clip_menu = menu.addMenu(
|
||||
QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
|
||||
|
||||
action_copy_image = clip_menu.addAction(QIcon.fromTheme("image-x-generic"), UITexts.VIEWER_MENU_COPY_IMAGE)
|
||||
action_copy_image.triggered.connect(lambda: QApplication.clipboard().setImage(QImage(path)))
|
||||
action_copy_image = clip_menu.addAction(
|
||||
QIcon.fromTheme("image-x-generic"), UITexts.VIEWER_MENU_COPY_IMAGE)
|
||||
action_copy_image.triggered.connect(
|
||||
lambda: QApplication.clipboard().setImage(QImage(path)))
|
||||
|
||||
action_copy_path = clip_menu.addAction(QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
|
||||
action_copy_path.triggered.connect(lambda: QApplication.clipboard().setText(path))
|
||||
action_copy_path = clip_menu.addAction(
|
||||
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
|
||||
action_copy_path.triggered.connect(
|
||||
lambda: QApplication.clipboard().setText(path))
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Trash / Delete
|
||||
action_trash = menu.addAction(QIcon.fromTheme("user-trash"), UITexts.CONTEXT_MENU_TRASH)
|
||||
action_trash.triggered.connect(lambda: self._handle_action(delete_path=path, permanent=False))
|
||||
action_trash = menu.addAction(
|
||||
QIcon.fromTheme("user-trash"), UITexts.CONTEXT_MENU_TRASH)
|
||||
action_trash.triggered.connect(
|
||||
lambda: self._handle_action(delete_path=path, permanent=False))
|
||||
|
||||
action_delete = menu.addAction(QIcon.fromTheme("edit-delete"), UITexts.CONTEXT_MENU_DELETE)
|
||||
action_delete.triggered.connect(lambda: self._handle_permanent_delete(path))
|
||||
action_delete = menu.addAction(
|
||||
QIcon.fromTheme("edit-delete"), UITexts.CONTEXT_MENU_DELETE)
|
||||
action_delete.triggered.connect(
|
||||
lambda: self._handle_permanent_delete(path))
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Properties
|
||||
action_props = menu.addAction(QIcon.fromTheme("document-properties"), UITexts.CONTEXT_MENU_PROPERTIES)
|
||||
action_props.triggered.connect(lambda: self._show_properties(path, pane))
|
||||
action_props = menu.addAction(
|
||||
QIcon.fromTheme("document-properties"), UITexts.CONTEXT_MENU_PROPERTIES)
|
||||
action_props.triggered.connect(
|
||||
lambda: self._show_properties(path, pane))
|
||||
|
||||
menu.exec(pane.mapToGlobal(pos))
|
||||
|
||||
@@ -550,7 +621,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
confirm = QMessageBox(self)
|
||||
confirm.setIcon(QMessageBox.Warning)
|
||||
confirm.setText(UITexts.CONFIRM_DELETE_TEXT)
|
||||
confirm.setInformativeText(UITexts.CONFIRM_DELETE_INFO.format(os.path.basename(path)))
|
||||
confirm.setInformativeText(
|
||||
UITexts.CONFIRM_DELETE_INFO.format(os.path.basename(path)))
|
||||
confirm.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||
confirm.setDefaultButton(QMessageBox.No)
|
||||
if confirm.exec() == QMessageBox.Yes:
|
||||
@@ -559,14 +631,16 @@ class DuplicateManagerDialog(QDialog):
|
||||
def _show_properties(self, path, pane):
|
||||
tags = pane.controller._current_tags
|
||||
rating = pane.controller._current_rating
|
||||
dlg = PropertiesDialog(path, initial_tags=tags, initial_rating=rating, parent=self)
|
||||
dlg = PropertiesDialog(
|
||||
path, initial_tags=tags, initial_rating=rating, parent=self)
|
||||
dlg.exec()
|
||||
|
||||
def _on_pane_activated(self):
|
||||
# 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
|
||||
other_pane = self.left_pane if active_pane == self.right_pane else self.right_pane
|
||||
other_pane = self.left_pane \
|
||||
if active_pane == self.right_pane else self.right_pane
|
||||
self._sync_zoom(active_pane.controller.zoom_factor, source_pane=active_pane)
|
||||
# Need to get scroll position from active_pane and apply to other
|
||||
h_bar = active_pane.scroll_area.horizontalScrollBar()
|
||||
@@ -603,17 +677,20 @@ class DuplicateManagerDialog(QDialog):
|
||||
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
|
||||
|
||||
target_pane = self.left_pane if source_pane == self.right_pane else self.right_pane
|
||||
target_pane = self.left_pane \
|
||||
if source_pane == self.right_pane else self.right_pane
|
||||
target_pane.zoom_manager.zoom(absolute_factor=factor)
|
||||
|
||||
# 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))
|
||||
QTimer.singleShot(
|
||||
0, lambda p=target_pane, x=x_pct, y=y_pct: p.set_scroll_relative(x, y))
|
||||
finally:
|
||||
self._is_syncing = False
|
||||
|
||||
def _format_size(self, size):
|
||||
for unit in ['B', 'KiB', 'MiB', 'GiB']:
|
||||
if size < 1024: return f"{size:.1f} {unit}"
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TiB"
|
||||
|
||||
@@ -628,7 +705,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
self._handle_action(delete_path=path_to_delete)
|
||||
|
||||
def _toggle_link_panes(self):
|
||||
self.panes_linked = self.btn_link_panes.isChecked()
|
||||
self._user_link_preference = self.btn_link_panes.isChecked()
|
||||
self.panes_linked = self._user_link_preference
|
||||
if self.panes_linked:
|
||||
# When linking, synchronize the other pane to the active one
|
||||
# For simplicity, let's always sync right to left if linking is enabled
|
||||
@@ -645,7 +723,8 @@ class DuplicateManagerDialog(QDialog):
|
||||
path = os.path.abspath(path)
|
||||
|
||||
# 1. Identify pairs to remove and clean up the pending DB
|
||||
pairs_to_remove = [d for d in self.duplicates if d.path1 == path or d.path2 == path]
|
||||
pairs_to_remove = [d for d in self.duplicates
|
||||
if d.path1 == path or d.path2 == path]
|
||||
if not pairs_to_remove:
|
||||
return
|
||||
|
||||
@@ -699,12 +778,16 @@ class DuplicateManagerDialog(QDialog):
|
||||
|
||||
def _skip(self):
|
||||
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)
|
||||
self.cache.mark_as_exception(
|
||||
self.current_dup_pair.path1, self.current_dup_pair.path2, False)
|
||||
# Borramos los hashes para que el detector las trate como imágenes nuevas
|
||||
# y fuerce una nueva comparación en el siguiente escaneo.
|
||||
# Usamos clear_relationships=False para no perder otras posibles coincidencias ya marcadas.
|
||||
self.cache.remove_hash_for_path(self.current_dup_pair.path1, clear_relationships=False)
|
||||
self.cache.remove_hash_for_path(self.current_dup_pair.path2, clear_relationships=False)
|
||||
# Usamos clear_relationships=False para no perder otras posibles
|
||||
# coincidencias ya marcadas.
|
||||
self.cache.remove_hash_for_path(
|
||||
self.current_dup_pair.path1, clear_relationships=False)
|
||||
self.cache.remove_hash_for_path(
|
||||
self.current_dup_pair.path2, clear_relationships=False)
|
||||
self._handle_action(skip=False, permanent=False)
|
||||
else:
|
||||
self._handle_action(skip=True)
|
||||
@@ -732,27 +815,35 @@ class DuplicateManagerDialog(QDialog):
|
||||
|
||||
# Remove all pairs containing this path from the persistent pending DB
|
||||
# because the file will be gone.
|
||||
pairs_to_unmark = [d for d in self.duplicates if d.path1 == delete_path or d.path2 == delete_path]
|
||||
pairs_to_unmark = [d for d in self.duplicates
|
||||
if d.path1 == delete_path or d.path2 == delete_path]
|
||||
for p in pairs_to_unmark:
|
||||
self.cache.mark_as_pending(p.path1, p.path2, False)
|
||||
|
||||
self.main_win.delete_file_by_path(delete_path, permanent=permanent) # Use default setting
|
||||
self.main_win.delete_file_by_path(delete_path, permanent=permanent)
|
||||
if os.path.exists(delete_path):
|
||||
QMessageBox.warning(self, UITexts.ERROR, UITexts.ERROR_DELETING_FILE.format(delete_path))
|
||||
QMessageBox.warning(
|
||||
self, UITexts.ERROR,
|
||||
UITexts.ERROR_DELETING_FILE.format(delete_path))
|
||||
return
|
||||
|
||||
# Remove all pairs containing this path because it no longer exists
|
||||
self.duplicates = [d for d in self.duplicates if d.path1 != delete_path and d.path2 != delete_path]
|
||||
self.duplicates = [d for d in self.duplicates
|
||||
if d.path1 != delete_path and d.path2 != delete_path]
|
||||
else:
|
||||
# Skip or KeepBoth:
|
||||
if not skip: # "Keep Both" case
|
||||
# It's no longer pending, it's an exception (already marked in _keep_both)
|
||||
self.cache.mark_as_pending(current_pair.path1, current_pair.path2, False)
|
||||
# Note: if it's "Skip", we do NOT remove it from pending DB, so it stays there for next time.
|
||||
if not skip: # "Keep Both" case
|
||||
# It's no longer pending, it's an exception (already marked in
|
||||
# _keep_both)
|
||||
self.cache.mark_as_pending(
|
||||
current_pair.path1, current_pair.path2, False)
|
||||
# Note: if it's "Skip", we do NOT remove it from pending DB, so it stays
|
||||
# there for next time.
|
||||
if 0 <= original_index < len(self.duplicates):
|
||||
self.duplicates.pop(original_index)
|
||||
|
||||
# Repopulate list widget to ensure all indices are correct and counter is updated
|
||||
# Repopulate list widget to ensure all indices are correct and counter is
|
||||
# updated
|
||||
self._populate_list()
|
||||
|
||||
# Try to restore selection to same position (or last item)
|
||||
|
||||
Reference in New Issue
Block a user