This commit is contained in:
Ignacio Serantes
2026-03-25 12:18:19 +01:00
parent 0349155fd2
commit 56ef674d4a
9 changed files with 641 additions and 455 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks. MainWindow: The main application window containing the thumbnail grid and docks.
""" """
__appname__ = "BagheeraView" __appname__ = "BagheeraView"
__version__ = "0.9.11" __version__ = "0.9.12"
__author__ = "Ignacio Serantes" __author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net" __email__ = "kde@aynoa.net"
__license__ = "LGPL" __license__ = "LGPL"
@@ -41,10 +41,10 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import ( from PySide6.QtGui import (
QDesktopServices, QFont, QFontMetrics, QIcon, QTransform, QImageReader, QPalette, QDesktopServices, QFont, QFontMetrics, QIcon, QTransform, QImageReader, QPalette,
QStandardItemModel, QStandardItem, QColor, QPixmap, QPixmapCache, QPainter, QStandardItemModel, QStandardItem, QColor, QPixmap, QPixmapCache, QPainter,
QKeySequence, QAction, QActionGroup QKeySequence, QAction, QActionGroup, QImage
) )
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, QPoint, QUrl, QObject, QEvent, QTimer, QMimeData, QByteArray, Qt, QPoint, QUrl, QObject, QEvent, QTimer, QByteArray,
QItemSelection, QSortFilterProxyModel, QItemSelectionModel, QRect, QSize, QItemSelection, QSortFilterProxyModel, QItemSelectionModel, QRect, QSize,
QThread, QPersistentModelIndex, QModelIndex QThread, QPersistentModelIndex, QModelIndex
) )
@@ -1483,9 +1483,9 @@ class MainWindow(QMainWindow):
mw_data = data.get("main_window", {}) mw_data = data.get("main_window", {})
# Restore main window geometry and state (including docks) # Restore main window geometry and state (including docks)
if "geometry" in mw_data: # if "geometry" in mw_data:
g = mw_data["geometry"] # g = mw_data["geometry"]
self.setGeometry(g["x"], g["y"], g["w"], g["h"]) # self.setGeometry(g["x"], g["y"], g["w"], g["h"])
selected_path = mw_data.get("selected_path") selected_path = mw_data.get("selected_path")
select_paths = [selected_path] if selected_path else None select_paths = [selected_path] if selected_path else None
@@ -2621,6 +2621,8 @@ class MainWindow(QMainWindow):
# layout properties (grid size, uniform items) are in sync with the # layout properties (grid size, uniform items) are in sync with the
# selected mode before any model rebuild. # selected mode before any model rebuild.
self._suppress_updates = True self._suppress_updates = True
selected_paths = []
try:
index = self.view_mode_combo.currentIndex() index = self.view_mode_combo.currentIndex()
self._model_update_queue.clear() self._model_update_queue.clear()
@@ -2661,15 +2663,16 @@ class MainWindow(QMainWindow):
self.found_items_data.sort(key=user_sort_key, reverse=rev) self.found_items_data.sort(key=user_sort_key, reverse=rev)
# 3. Rebuild the model. Disable view updates for a massive performance boost. # 3. Rebuild the model. Disable view updates for a massive performance
# boost.
self.thumbnail_view.setUpdatesEnabled(False) self.thumbnail_view.setUpdatesEnabled(False)
target_structure = [] target_structure = []
if not is_grouped: if not is_grouped:
# OPTIMIZATION: In Flat View, rely on Proxy Model for sorting. # OPTIMIZATION: In Flat View, rely on Proxy Model for sorting.
# This avoids expensive O(N) source model reshuffling/syncing on the main # This avoids expensive O(N) source model reshuffling/syncing on the
# thread. # main thread.
sort_role = Qt.DisplayRole if sort_by_name else MTIME_ROLE sort_role = Qt.DisplayRole if sort_by_name else MTIME_ROLE
sort_order = Qt.DescendingOrder if rev else Qt.AscendingOrder sort_order = Qt.DescendingOrder if rev else Qt.AscendingOrder
@@ -2694,18 +2697,9 @@ class MainWindow(QMainWindow):
self._path_to_model_index[path] = \ self._path_to_model_index[path] = \
QPersistentModelIndex(source_index) QPersistentModelIndex(source_index)
self._suppress_updates = False
self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True)
self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \
self.tags_tabs.currentWidget() == self.filter_widget:
if not self.filter_refresh_timer.isActive():
self.filter_refresh_timer.start()
return
else: else:
# For Grouped View, we must ensure source model order matches groups/headers # For Grouped View, we must ensure source model order matches
# groups/headers
self.proxy_model.sort(-1) # Disable proxy sorting self.proxy_model.sort(-1) # Disable proxy sorting
if full_reset: if full_reset:
@@ -2784,18 +2778,21 @@ class MainWindow(QMainWindow):
new_items_batch = [new_item] new_items_batch = [new_item]
target_idx += 1 target_idx += 1
# Look ahead to see if next items are also new (not in current model) # Look ahead to see if next items are also new (not in current
# This optimization drastically reduces proxy model recalculations # model)
# This optimization drastically reduces proxy model
# recalculations
while target_idx < total_targets: while target_idx < total_targets:
next_target = target_structure[target_idx] next_target = target_structure[target_idx]
# Check if next_target matches current model position (re-sync) # Check if next_target matches current model position
# (re-sync)
if self._match_item( if self._match_item(
next_target, self.thumbnail_model.item(model_idx)): next_target, self.thumbnail_model.item(model_idx)):
break break
# If not matching, it's another new item to insert # If not matching, it's another new item to insert
if isinstance(next_target, tuple) and len(next_target) == 2 \ if isinstance(next_target, tuple) \
and next_target[0] == 'HEADER': and len(next_target) == 2 and next_target[0] == 'HEADER':
_, (h_group, h_text, _) = next_target _, (h_group, h_text, _) = next_target
n_item = QStandardItem() n_item = QStandardItem()
n_item.setData('header', ITEM_TYPE_ROLE) n_item.setData('header', ITEM_TYPE_ROLE)
@@ -2811,25 +2808,27 @@ class MainWindow(QMainWindow):
target_idx += 1 target_idx += 1
# Perform batch insertion # Perform batch insertion
# Optimization: Use appendRow/insertRow with the item directly to avoid # Optimization: Use appendRow/insertRow with the item directly
# double-signaling (rowsInserted + dataChanged) which forces the # to avoid double-signaling (rowsInserted + dataChanged) which
# ProxyModel to filter every row twice. # forces the ProxyModel to filter every row twice.
if model_idx >= self.thumbnail_model.rowCount(): if model_idx >= self.thumbnail_model.rowCount():
for item in new_items_batch: for item in new_items_batch:
self.thumbnail_model.appendRow(item) self.thumbnail_model.appendRow(item)
if item.data(ITEM_TYPE_ROLE) == 'thumbnail': if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
path = item.data(PATH_ROLE) path = item.data(PATH_ROLE)
source_index = self.thumbnail_model.indexFromItem(item) source_index = \
self._path_to_model_index[path] = QPersistentModelIndex( self.thumbnail_model.indexFromItem(item)
source_index) self._path_to_model_index[path] = \
QPersistentModelIndex(source_index)
else: else:
for i, item in enumerate(new_items_batch): for i, item in enumerate(new_items_batch):
self.thumbnail_model.insertRow(model_idx + i, item) self.thumbnail_model.insertRow(model_idx + i, item)
if item.data(ITEM_TYPE_ROLE) == 'thumbnail': if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
path = item.data(PATH_ROLE) path = item.data(PATH_ROLE)
source_index = self.thumbnail_model.index(model_idx + i, 0) source_index = self.thumbnail_model.index(
self._path_to_model_index[path] = QPersistentModelIndex( model_idx + i, 0)
source_index) self._path_to_model_index[path] = \
QPersistentModelIndex(source_index)
model_idx += len(new_items_batch) model_idx += len(new_items_batch)
@@ -2841,16 +2840,22 @@ class MainWindow(QMainWindow):
path = item.data(PATH_ROLE) path = item.data(PATH_ROLE)
if path in self._path_to_model_index: if path in self._path_to_model_index:
# Only delete if it points to this specific row (stale) # Only delete if it points to this specific row (stale)
# otherwise we might delete the index for a newly inserted item # otherwise we might delete the index for a newly
# inserted item
p_idx = self._path_to_model_index[path] p_idx = self._path_to_model_index[path]
if not p_idx.isValid() or p_idx.row() == row: if not p_idx.isValid() or p_idx.row() == row:
del self._path_to_model_index[path] del self._path_to_model_index[path]
self.thumbnail_model.removeRows( self.thumbnail_model.removeRows(
model_idx, self.thumbnail_model.rowCount() - model_idx) model_idx, self.thumbnail_model.rowCount() - model_idx)
except Exception as e:
import traceback
traceback.print_exc()
print(f"Error in rebuild_view: {e}")
finally:
self._suppress_updates = False self._suppress_updates = False
self.apply_filters() self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True) self.thumbnail_view.setUpdatesEnabled(True)
if selected_paths:
self.restore_selection(selected_paths) self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \ if self.main_dock.isVisible() and \
@@ -3452,8 +3457,6 @@ class MainWindow(QMainWindow):
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None)) self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
self.rebuild_view(full_reset=True) self.rebuild_view(full_reset=True)
self.update_tag_list()
self.save_config() self.save_config()
self.setFocus() self.setFocus()
@@ -3954,9 +3957,15 @@ class MainWindow(QMainWindow):
clipboard_menu = menu.addMenu(QIcon.fromTheme("edit-copy"), clipboard_menu = menu.addMenu(QIcon.fromTheme("edit-copy"),
UITexts.CONTEXT_MENU_CLIPBOARD) UITexts.CONTEXT_MENU_CLIPBOARD)
action_copy_url = clipboard_menu.addAction(QIcon.fromTheme("text-html"), action_copy_image = clipboard_menu.addAction(QIcon.fromTheme("image-x-generic"),
UITexts.CONTEXT_MENU_COPY_URL) UITexts.VIEWER_MENU_COPY_IMAGE)
action_copy_url.triggered.connect(self.copy_file_url) action_copy_image.triggered.connect(self.copy_image_to_clipboard)
if len(selected_indexes) > 1:
action_copy_image.setEnabled(False)
action_copy_path = clipboard_menu.addAction(
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
action_copy_path.triggered.connect(self.copy_file_path_to_clipboard)
action_copy_dir = clipboard_menu.addAction(QIcon.fromTheme("folder"), action_copy_dir = clipboard_menu.addAction(QIcon.fromTheme("folder"),
UITexts.CONTEXT_MENU_COPY_DIR) UITexts.CONTEXT_MENU_COPY_DIR)
@@ -4127,23 +4136,30 @@ class MainWindow(QMainWindow):
msg.setArguments(["", QUrl.fromLocalFile(path).toString(), {"ask": True}]) msg.setArguments(["", QUrl.fromLocalFile(path).toString(), {"ask": True}])
QDBusConnection.sessionBus().call(msg, QDBus.NoBlock) QDBusConnection.sessionBus().call(msg, QDBus.NoBlock)
def copy_file_url(self): def copy_image_to_clipboard(self):
"""Copies the file URL of the selected image to the clipboard.""" """Copies the full image of the selected thumbnail to the clipboard."""
path = self.get_current_selected_path() path = self.get_current_selected_path()
if not path: if not path:
return return
url = QUrl.fromLocalFile(path) # This is a disk read, but it's on user action.
mime = QMimeData() img = QImage(path)
mime.setUrls([url]) if not img.isNull():
mime.setText(url.toString()) QApplication.clipboard().setImage(img)
QApplication.clipboard().setMimeData(mime)
def copy_file_path_to_clipboard(self):
"""Copies the file path(s) of the selected image(s) to the clipboard."""
paths = self.get_selected_paths()
if not paths:
return
QApplication.clipboard().setText("\n".join(paths))
def copy_dir_path(self): def copy_dir_path(self):
"""Copies the directory path of the selected image to the clipboard.""" """Copies the directory path(s) of the selected image(s) to the clipboard."""
path = self.get_current_selected_path() paths = self.get_selected_paths()
if not path: if not paths:
return return
QApplication.clipboard().setText(os.path.dirname(path)) dir_paths = sorted(list(set(os.path.dirname(p) for p in paths)))
QApplication.clipboard().setText("\n".join(dir_paths))
def show_properties(self): def show_properties(self):
"""Shows the custom properties dialog for the selected file.""" """Shows the custom properties dialog for the selected file."""

View File

@@ -1,12 +1,10 @@
v0.9.11 -
· Filmstrip fixed
· Añadida una nueva área llamada Body.
· Refactorizaciones, optimizaciones y cambios a saco.
· Image viewer tiene comparisonb
Implement a bulk rename feature for the selected pet or face tags. Implement a bulk rename feature for the selected pet or face tags.
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text. Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
Me gustaría que el submenú de "Manipular" también incluyera una opción para "Escalar" la imagen, ¿cómo podría implementarlo?
Ensure that apply_filters doesn't trigger redundant updates if rebuild_view already called it.
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text. Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
Add a `shutdown` signal or method to `ScannerWorker` to allow cleaner cancellation of long-running tasks like `generate_thumbnail`. Add a `shutdown` signal or method to `ScannerWorker` to allow cleaner cancellation of long-running tasks like `generate_thumbnail`.
Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity. Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity.
@@ -30,10 +28,6 @@ Implement a "Comparison" mode to view 2 or 4 images side-by-side in the viewer.
· Si quisiera distribuir mi aplicación como un AppImage, ¿cómo empaquetaría estos plugins de KDE para que funcionen en otros sistemas? · Si quisiera distribuir mi aplicación como un AppImage, ¿cómo empaquetaría estos plugins de KDE para que funcionen en otros sistemas?
· Solucionar el problema de las ventanas de diálogo nativas, tan simple como usar PySide nativo. · Solucionar el problema de las ventanas de diálogo nativas, tan simple como usar PySide nativo.
¿Cómo puedo añadir una opción para limitar el número de hilos que `ImageScanner` puede usar para la generación de miniaturas?
Check if the `_suppress_updates` flag correctly prevents potential race conditions in `update_tag_list` when switching view modes rapidly.
Verify if `find_and_select_path` logic in `on_view_mode_changed` handles cases where the selected item is filtered out after the view mode change. Verify if `find_and_select_path` logic in `on_view_mode_changed` handles cases where the selected item is filtered out after the view mode change.
cuando se hace una nueva búsqueda que no se refresquen los tags, ni filtros, ni nada hasta que venga la primera imagen de la búsqueda nueva. Actualizar algo que se está destruyendo no tiene sentido. Lo mismo aplica si se cambia la agrupación, paramos las actualizaciones y luego, cuando acabe la agrupación activamos de nuevo los tags y los filtros y todo lo que implique un refresco de pantalla. cuando se hace una nueva búsqueda que no se refresquen los tags, ni filtros, ni nada hasta que venga la primera imagen de la búsqueda nueva. Actualizar algo que se está destruyendo no tiene sentido. Lo mismo aplica si se cambia la agrupación, paramos las actualizaciones y luego, cuando acabe la agrupación activamos de nuevo los tags y los filtros y todo lo que implique un refresco de pantalla.
@@ -69,6 +63,16 @@ 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.12 -
· Al restaurar el layout no se restaura la posición y dimensiones de los thumbnails.
· Mejoras en los menús de contexto.
v0.9.11 -
· Filmstrip fixed
· Añadida una nueva área llamada Body.
· Refactorizaciones, optimizaciones y cambios a saco.
· Image viewer tiene comparisonb
v0.9.10 - Eleven step to 1.0 v0.9.10 - Eleven step to 1.0
· Slideshow inverso · Slideshow inverso
· Más mejoras de rendimiento y seguridad · Más mejoras de rendimiento y seguridad

View File

@@ -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.11" PROG_VERSION = "0.9.12"
PROG_AUTHOR = "Ignacio Serantes" PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS --- # --- CACHE SETTINGS ---
@@ -96,7 +96,12 @@ KWINOUTPUTCONFIG_PATH = os.path.join(os.path.expanduser("~"),
# --- EXTERNAL TOOLS --- # --- EXTERNAL TOOLS ---
# Command definitions for external search tools. # Command definitions for external search tools.
try:
from bagheera_search_lib import BagheeraSearcher
HAVE_BAGHEERASEARCH_LIB = True
except ImportError:
HAVE_BAGHEERASEARCH_LIB = False
pass
BALOOSEARCH_EXEC = shutil.which("baloosearch") or shutil.which("baloosearch6") BALOOSEARCH_EXEC = shutil.which("baloosearch") or shutil.which("baloosearch6")
SEARCH_CMD = [BALOOSEARCH_EXEC, "--type", "image"] if BALOOSEARCH_EXEC else None SEARCH_CMD = [BALOOSEARCH_EXEC, "--type", "image"] if BALOOSEARCH_EXEC else None
@@ -167,8 +172,6 @@ if importlib.util.find_spec("mediapipe") is not None:
pass pass
HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None
HAVE_BAGHEERASEARCH_LIB = True
MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR, MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR,
"blaze_face_short_range.tflite") "blaze_face_short_range.tflite")
MEDIAPIPE_FACE_MODEL_URL = ( MEDIAPIPE_FACE_MODEL_URL = (
@@ -291,6 +294,8 @@ VIEWER_ACTIONS = {
"toggle_visibility": ("Show/Hide Main Window", "Window"), "toggle_visibility": ("Show/Hide Main Window", "Window"),
"toggle_crop": ("Toggle Crop Mode", "Edit"), "toggle_crop": ("Toggle Crop Mode", "Edit"),
"save_crop": ("Save Cropped Image", "File"), "save_crop": ("Save Cropped Image", "File"),
"copy_image": ("Copy Image to Clipboard", "Edit"),
"copy_path": ("Copy File Path", "Edit"),
"compare_1": ("Single View", "View"), "compare_1": ("Single View", "View"),
"compare_2": ("Compare 2 Images", "View"), "compare_2": ("Compare 2 Images", "View"),
"compare_4": ("Compare 4 Images", "View"), "compare_4": ("Compare 4 Images", "View"),
@@ -451,8 +456,8 @@ _UI_TEXTS = {
"MENU_ABOUT": "About", "MENU_ABOUT": "About",
"MENU_ABOUT_TITLE": "About {}", "MENU_ABOUT_TITLE": "About {}",
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>A simple image viewer and manager " "MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>A simple image viewer and manager "
"for KDE with Baloo support.<br><br>Created by {2} with the help of AI, and " "for KDE with Baloo support.<br><br>Created by {2} with the help of AI, but "
"mostly the good people at KDE and Qt.", "mostly thanks to the job of the good people at KDE and Qt.",
"MENU_CACHE": "Cache", "MENU_CACHE": "Cache",
"MENU_CLEAR_CACHE": "Clear cache ({} items, {:.1f} MB, {:.1f} MB on disk)", "MENU_CLEAR_CACHE": "Clear cache ({} items, {:.1f} MB, {:.1f} MB on disk)",
"MENU_CLEAN_CACHE": "Clean up invalid cache entries", "MENU_CLEAN_CACHE": "Clean up invalid cache entries",
@@ -527,7 +532,7 @@ _UI_TEXTS = {
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Thumbnails filename font size:", "SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Thumbnails filename font size:",
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Thumbnails tags font size:", "SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Thumbnails tags font size:",
"SETTINGS_SCAN_THREADS_LABEL": "Generation threads:", "SETTINGS_SCAN_THREADS_LABEL": "Generation threads:",
"SETTINGS_SCAN_THREADS_TOOLTIP": "Maximum number of simultaneous threads to" "SETTINGS_SCAN_THREADS_TOOLTIP": "Maximum number of simultaneous threads to "
"generate thumbnails.", "generate thumbnails.",
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Scan Max Level:", "SETTINGS_SCAN_MAX_LEVEL_LABEL": "Scan Max Level:",
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Scan Batch Size:", "SETTINGS_SCAN_BATCH_SIZE_LABEL": "Scan Batch Size:",
@@ -742,6 +747,15 @@ _UI_TEXTS = {
"VIEWER_MENU_TAGS": "Quick tags", "VIEWER_MENU_TAGS": "Quick tags",
"VIEWER_MENU_CROP": "Crop Mode", "VIEWER_MENU_CROP": "Crop Mode",
"VIEWER_MENU_SAVE_CROP": "Save Selection...", "VIEWER_MENU_SAVE_CROP": "Save Selection...",
"VIEWER_MENU_COPY_PATH": "Copy File Path",
"VIEWER_MENU_COPY_IMAGE": "Copy Image to Clipboard",
"VIEWER_MENU_DETECT_AREAS": "Detect areas",
"VIEWER_MENU_DETECT_FACES": "Faces",
"VIEWER_MENU_DETECT_PETS": "Pets",
"VIEWER_MENU_MANIPULATE": "Manipulate",
"VIEWER_MENU_ZOOM": "Zoom",
"VIEWER_MENU_ZOOM_IN": "Zoom In",
"VIEWER_MENU_ZOOM_OUT": "Zoom Out",
"SAVE_CROP_TITLE": "Save Cropped Image", "SAVE_CROP_TITLE": "Save Cropped Image",
"VIEWER_MENU_COMPARE": "Comparison Mode", "VIEWER_MENU_COMPARE": "Comparison Mode",
"VIEWER_MENU_COMPARE_1": "Single View", "VIEWER_MENU_COMPARE_1": "Single View",
@@ -762,7 +776,7 @@ _UI_TEXTS = {
"CONTEXT_MENU_ROTATE_RIGHT": "Right", "CONTEXT_MENU_ROTATE_RIGHT": "Right",
"CONTEXT_MENU_TRASH": "Move to Trash", "CONTEXT_MENU_TRASH": "Move to Trash",
"CONTEXT_MENU_CLIPBOARD": "Clipboard", "CONTEXT_MENU_CLIPBOARD": "Clipboard",
"CONTEXT_MENU_COPY_URL": "Copy File URL", "CONTEXT_MENU_COPY_FILE": "Copy File URL",
"CONTEXT_MENU_COPY_DIR": "Copy Directory Path", "CONTEXT_MENU_COPY_DIR": "Copy Directory Path",
"CONTEXT_MENU_PROPERTIES": "Properties", "CONTEXT_MENU_PROPERTIES": "Properties",
"CONTEXT_MENU_NO_APPS_FOUND": "No apps found", "CONTEXT_MENU_NO_APPS_FOUND": "No apps found",
@@ -881,7 +895,7 @@ _UI_TEXTS = {
"MENU_ABOUT_TITLE": "Acerca de {}", "MENU_ABOUT_TITLE": "Acerca de {}",
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor y gestor de imágenes " "MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor y gestor de imágenes "
"simple para KDE con soporte para Baloo.<br><br>Creado por {2} con la ayuda de " "simple para KDE con soporte para Baloo.<br><br>Creado por {2} con la ayuda de "
"la IA, y mayormente la buena gente de KDE y Qt.", "la IA, pero mayormente gracias al trabajo de la buena gente de KDE y Qt.",
"MENU_CACHE": "Caché", "MENU_CACHE": "Caché",
"MENU_CLEAR_CACHE": "Limpiar caché ({} ítems, {:.1f} MB, {:.1f} MB en disco)", "MENU_CLEAR_CACHE": "Limpiar caché ({} ítems, {:.1f} MB, {:.1f} MB en disco)",
"MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas", "MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas",
@@ -898,11 +912,11 @@ _UI_TEXTS = {
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:", "SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:",
"SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:", "SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:",
"SETTINGS_FACE_COLOR_LABEL": "Color del recuadro de cara:", "SETTINGS_FACE_COLOR_LABEL": "Color del recuadro de cara:",
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máx etiquetas recientes:", "SETTINGS_MRU_TAGS_COUNT_LABEL": "Máximo número de etiquetas recientes:",
"SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:", "SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:",
"SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:", "SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:",
"SETTINGS_PET_COLOR_LABEL": "Color del recuadro de mascota:", "SETTINGS_PET_COLOR_LABEL": "Color del recuadro de mascota:",
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máx historial mascotas:", "SETTINGS_PET_HISTORY_COUNT_LABEL": "Máximo historial de mascotas:",
"SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, " "SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_PET_ENGINE_TOOLTIP": "Librería utilizada para la detección de " "SETTINGS_PET_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
@@ -919,7 +933,7 @@ _UI_TEXTS = {
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de cuerpo:", "SETTINGS_BODY_TAGS_LABEL": "Etiquetas de cuerpo:",
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de cuerpos:", "SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de cuerpos:",
"SETTINGS_BODY_COLOR_LABEL": "Color del recuadro de cuerpo:", "SETTINGS_BODY_COLOR_LABEL": "Color del recuadro de cuerpo:",
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial cuerpos:", "SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máximo historial de cuerpos:",
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para cuerpos, " "SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para cuerpos, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_BODY_ENGINE_TOOLTIP": "Librería utilizada para la detección de " "SETTINGS_BODY_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
@@ -931,7 +945,7 @@ _UI_TEXTS = {
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de objeto:", "SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de objeto:",
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de objetos:", "SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de objetos:",
"SETTINGS_OBJECT_COLOR_LABEL": "Color del recuadro de objeto:", "SETTINGS_OBJECT_COLOR_LABEL": "Color del recuadro de objeto:",
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máx historial objetos:", "SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máximo historial de objetos:",
"SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para objetos, " "SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para objetos, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_OBJECT_ENGINE_TOOLTIP": "Librería utilizada para la detección " "SETTINGS_OBJECT_ENGINE_TOOLTIP": "Librería utilizada para la detección "
@@ -943,7 +957,7 @@ _UI_TEXTS = {
"SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:", "SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:",
"SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:", "SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:",
"SETTINGS_LANDMARK_COLOR_LABEL": "Color del recuadro de lugar:", "SETTINGS_LANDMARK_COLOR_LABEL": "Color del recuadro de lugar:",
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máx historial lugares:", "SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máximo historial de lugares:",
"SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para " "SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para "
"lugares/monumentos, separadas por comas.", "lugares/monumentos, separadas por comas.",
"SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Librería utilizada para la detección " "SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Librería utilizada para la detección "
@@ -952,7 +966,7 @@ _UI_TEXTS = {
"alrededor de los lugares.", "alrededor de los lugares.",
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nombres de lugares " "SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nombres de lugares "
"usados recientemente para recordar.", "usados recientemente para recordar.",
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máx historial caras:", "SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):", "SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Color de fondo de miniaturas:", "SETTINGS_THUMBS_BG_COLOR_LABEL": "Color de fondo de miniaturas:",
"SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Color del nombre de fichero:", "SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Color del nombre de fichero:",
@@ -960,12 +974,15 @@ _UI_TEXTS = {
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Color de valoración de miniaturas:", "SETTINGS_THUMBS_RATING_COLOR_LABEL": "Color de valoración de miniaturas:",
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño de fuente del nombre de " "SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño de fuente del nombre de "
"fichero:", "fichero:",
"SETTINGS_SCAN_THREADS_LABEL": "Hilos de generación:",
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de hilos simultaneos para "
"generar miniaturas.",
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño de fuente de las etiquetas:", "SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño de fuente de las etiquetas:",
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:", "SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:",
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño de Lote de Escaneo:", "SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño de Lote de Escaneo:",
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de búsqueda de archivos:", "SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de búsqueda de archivos:",
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar archivos. " "SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar archivos. "
"'Bagheera' usa la librería de BagheeraSearch. 'Baloo0 usa el commando " "'Bagheera' usa la librería de BagheeraSearch. 'Baloo' usa el commando "
"'baloosearch'", "'baloosearch'",
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo al Inicio:", "SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo al Inicio:",
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidad máxima de directorio para " "SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidad máxima de directorio para "
@@ -1177,6 +1194,15 @@ _UI_TEXTS = {
"VIEWER_MENU_TAGS": "Etiquetas rápidas", "VIEWER_MENU_TAGS": "Etiquetas rápidas",
"VIEWER_MENU_CROP": "Modo Recorte", "VIEWER_MENU_CROP": "Modo Recorte",
"VIEWER_MENU_SAVE_CROP": "Guardar Selección...", "VIEWER_MENU_SAVE_CROP": "Guardar Selección...",
"VIEWER_MENU_COPY_PATH": "Copiar Ruta del Archivo",
"VIEWER_MENU_COPY_IMAGE": "Copiar Imagen al Portapapeles",
"VIEWER_MENU_DETECT_AREAS": "Detectar áreas",
"VIEWER_MENU_DETECT_FACES": "Rostros",
"VIEWER_MENU_DETECT_PETS": "Mascotas",
"VIEWER_MENU_MANIPULATE": "Manipular",
"VIEWER_MENU_ZOOM": "Zoom",
"VIEWER_MENU_ZOOM_IN": "Acercar",
"VIEWER_MENU_ZOOM_OUT": "Alejar",
"VIEWER_MENU_COMPARE": "Modo Comparación", "VIEWER_MENU_COMPARE": "Modo Comparación",
"VIEWER_MENU_COMPARE_1": "Vista Única", "VIEWER_MENU_COMPARE_1": "Vista Única",
"VIEWER_MENU_COMPARE_2": "2 Imágenes", "VIEWER_MENU_COMPARE_2": "2 Imágenes",
@@ -1197,7 +1223,7 @@ _UI_TEXTS = {
"CONTEXT_MENU_ROTATE_RIGHT": "Derecha", "CONTEXT_MENU_ROTATE_RIGHT": "Derecha",
"CONTEXT_MENU_TRASH": "Mover a la Papelera", "CONTEXT_MENU_TRASH": "Mover a la Papelera",
"CONTEXT_MENU_CLIPBOARD": "Portapapeles", "CONTEXT_MENU_CLIPBOARD": "Portapapeles",
"CONTEXT_MENU_COPY_URL": "Copiar URL del Archivo", "CONTEXT_MENU_COPY_FILE": "Copiar URL del Archivo",
"CONTEXT_MENU_COPY_DIR": "Copiar Ruta del Directorio", "CONTEXT_MENU_COPY_DIR": "Copiar Ruta del Directorio",
"CONTEXT_MENU_PROPERTIES": "Propiedades", "CONTEXT_MENU_PROPERTIES": "Propiedades",
"CONTEXT_MENU_NO_APPS_FOUND": "No se encontraron aplicaciones", "CONTEXT_MENU_NO_APPS_FOUND": "No se encontraron aplicaciones",
@@ -1316,7 +1342,7 @@ _UI_TEXTS = {
"MENU_ABOUT_TITLE": "Acerca de {}", "MENU_ABOUT_TITLE": "Acerca de {}",
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor e xestor de imaxes " "MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor e xestor de imaxes "
"sinxelo para KDE con soporte para Baloo.<br><br>Creado por {2} coa axuda da " "sinxelo para KDE con soporte para Baloo.<br><br>Creado por {2} coa axuda da "
" IA, e maiormente a boa xente de KDE e Qt.", " IA, pero maiormente gracias ó traballo da boa xente de KDE e Qt.",
"MENU_CACHE": "Caché", "MENU_CACHE": "Caché",
"MENU_CLEAR_CACHE": "Limpar caché ({} elementos, {:.1f} MB, {:.1f} MB en " "MENU_CLEAR_CACHE": "Limpar caché ({} elementos, {:.1f} MB, {:.1f} MB en "
"disco)", "disco)",
@@ -1328,17 +1354,17 @@ _UI_TEXTS = {
"MENU_SHOW_HISTORY": "Amosar Historial", "MENU_SHOW_HISTORY": "Amosar Historial",
"MENU_SETTINGS": "Opcións", "MENU_SETTINGS": "Opcións",
"SETTINGS_GROUP_SCANNER": "Escáner", "SETTINGS_GROUP_SCANNER": "Escáner",
"SETTINGS_GROUP_AREAS": "´áreas", "SETTINGS_GROUP_AREAS": "Áreas",
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas", "SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
"SETTINGS_GROUP_VIEWER": "Visor de Imaxes", "SETTINGS_GROUP_VIEWER": "Visor de Imaxes",
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persoa:", "SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persoa:",
"SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:", "SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:",
"SETTINGS_FACE_COLOR_LABEL": "Cor do cadro de cara:", "SETTINGS_FACE_COLOR_LABEL": "Cor do cadro de cara:",
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máx etiquetas recentes:", "SETTINGS_MRU_TAGS_COUNT_LABEL": "Máximo número de etiquetas recentes:",
"SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:", "SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:",
"SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:", "SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:",
"SETTINGS_PET_COLOR_LABEL": "Cor do cadro de mascota:", "SETTINGS_PET_COLOR_LABEL": "Cor do cadro de mascota:",
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máx historial mascotas:", "SETTINGS_PET_HISTORY_COUNT_LABEL": "Máximo historial de mascotas:",
"SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, " "SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_PET_ENGINE_TOOLTIP": "Libraría utilizada para a detección de " "SETTINGS_PET_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
@@ -1355,7 +1381,7 @@ _UI_TEXTS = {
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de corpo:", "SETTINGS_BODY_TAGS_LABEL": "Etiquetas de corpo:",
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de corpos:", "SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de corpos:",
"SETTINGS_BODY_COLOR_LABEL": "Cor do cadro de corpo:", "SETTINGS_BODY_COLOR_LABEL": "Cor do cadro de corpo:",
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial corpos:", "SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máximo historial de corpos:",
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para corpos, " "SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para corpos, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_BODY_ENGINE_TOOLTIP": "Libraría utilizada para a detección de " "SETTINGS_BODY_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
@@ -1367,7 +1393,7 @@ _UI_TEXTS = {
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de obxecto:", "SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de obxecto:",
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de obxectos:", "SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de obxectos:",
"SETTINGS_OBJECT_COLOR_LABEL": "Cor do cadro de obxecto:", "SETTINGS_OBJECT_COLOR_LABEL": "Cor do cadro de obxecto:",
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máx historial obxectos:", "SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máximo historial de obxectos:",
"SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para obxectos, " "SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para obxectos, "
"separadas por comas.", "separadas por comas.",
"SETTINGS_OBJECT_ENGINE_TOOLTIP": "Libraría utilizada para a detección de " "SETTINGS_OBJECT_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
@@ -1379,7 +1405,7 @@ _UI_TEXTS = {
"SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:", "SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:",
"SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:", "SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:",
"SETTINGS_LANDMARK_COLOR_LABEL": "Cor do cadro de lugar:", "SETTINGS_LANDMARK_COLOR_LABEL": "Cor do cadro de lugar:",
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máx historial lugares:", "SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máximo historial de lugares:",
"SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para " "SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para "
"lugares/monumentos, separadas por comas.", "lugares/monumentos, separadas por comas.",
"SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Libraría utilizada para a detección " "SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Libraría utilizada para a detección "
@@ -1388,7 +1414,7 @@ _UI_TEXTS = {
"arredor dos lugares.", "arredor dos lugares.",
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nomes de lugares " "SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nomes de lugares "
"usados recentemente para lembrar.", "usados recentemente para lembrar.",
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máx historial caras:", "SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):", "SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Cor de fondo de miniaturas:", "SETTINGS_THUMBS_BG_COLOR_LABEL": "Cor de fondo de miniaturas:",
"SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Cor do nome de ficheiro:", "SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Cor do nome de ficheiro:",
@@ -1396,10 +1422,10 @@ _UI_TEXTS = {
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Cor da valoración das miniaturas:", "SETTINGS_THUMBS_RATING_COLOR_LABEL": "Cor da valoración das miniaturas:",
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño da fonte do nome de " "SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño da fonte do nome de "
"ficheiro:", "ficheiro:",
"SETTINGS_SCAN_THREADS_LABEL": "Fios de xeración:",
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de fios simultaneos para "
"xerar miniaturas.",
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño da fonte das etiquetas:", "SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño da fonte das etiquetas:",
"SETTINGS_SCAN_THREADS_LABEL": "Hilos de generación:",
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de hilos simultáneos para "
"generar miniaturas.",
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:", "SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:",
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño do Lote de Escaneo:", "SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño do Lote de Escaneo:",
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de busca de ficheiros:", "SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de busca de ficheiros:",
@@ -1615,6 +1641,15 @@ _UI_TEXTS = {
"VIEWER_MENU_TAGS": "Etiquetas rápidas", "VIEWER_MENU_TAGS": "Etiquetas rápidas",
"VIEWER_MENU_CROP": "Modo Recorte", "VIEWER_MENU_CROP": "Modo Recorte",
"VIEWER_MENU_SAVE_CROP": "Gardar Selección...", "VIEWER_MENU_SAVE_CROP": "Gardar Selección...",
"VIEWER_MENU_COPY_PATH": "Copiar Ruta do Ficheiro",
"VIEWER_MENU_COPY_IMAGE": "Copiar Imaxe ao Portapapeis",
"VIEWER_MENU_DETECT_AREAS": "Detectar áreas",
"VIEWER_MENU_DETECT_FACES": "Rostros",
"VIEWER_MENU_DETECT_PETS": "Mascotas",
"VIEWER_MENU_MANIPULATE": "Manipular",
"VIEWER_MENU_ZOOM": "Zoom",
"VIEWER_MENU_ZOOM_IN": "Achegar",
"VIEWER_MENU_ZOOM_OUT": "Afastar",
"VIEWER_MENU_COMPARE": "Modo Comparación", "VIEWER_MENU_COMPARE": "Modo Comparación",
"VIEWER_MENU_COMPARE_1": "Vista Única", "VIEWER_MENU_COMPARE_1": "Vista Única",
"VIEWER_MENU_COMPARE_2": "2 Imaxes", "VIEWER_MENU_COMPARE_2": "2 Imaxes",
@@ -1636,7 +1671,7 @@ _UI_TEXTS = {
"CONTEXT_MENU_ROTATE_RIGHT": "Dereita", "CONTEXT_MENU_ROTATE_RIGHT": "Dereita",
"CONTEXT_MENU_TRASH": "Mover á Papeleira", "CONTEXT_MENU_TRASH": "Mover á Papeleira",
"CONTEXT_MENU_CLIPBOARD": "Portapapeis", "CONTEXT_MENU_CLIPBOARD": "Portapapeis",
"CONTEXT_MENU_COPY_URL": "Copiar URL do Ficheiro", "CONTEXT_MENU_COPY_FILE": "Copiar URL do Ficheiro",
"CONTEXT_MENU_COPY_DIR": "Copiar Ruta do Directorio", "CONTEXT_MENU_COPY_DIR": "Copiar Ruta do Directorio",
"CONTEXT_MENU_PROPERTIES": "Propiedades", "CONTEXT_MENU_PROPERTIES": "Propiedades",
"CONTEXT_MENU_NO_APPS_FOUND": "Non se atoparon aplicacións", "CONTEXT_MENU_NO_APPS_FOUND": "Non se atoparon aplicacións",

View File

@@ -36,19 +36,16 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
from constants import ( from constants import (
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES, APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES,
IMAGE_EXTENSIONS, SEARCH_CMD, THUMBNAIL_SIZES, RATING_XATTR_NAME, XATTR_NAME, HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS, RATING_XATTR_NAME,
UITexts, SCANNER_SETTINGS_DEFAULTS, HAVE_BAGHEERASEARCH_LIB SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES, XATTR_NAME,
UITexts
) )
from imageviewer import ImageViewer from imageviewer import ImageViewer
from metadatamanager import XattrManager from metadatamanager import XattrManager
if HAVE_BAGHEERASEARCH_LIB: if HAVE_BAGHEERASEARCH_LIB:
try:
from bagheera_search_lib import BagheeraSearcher from bagheera_search_lib import BagheeraSearcher
except ImportError:
HAVE_BAGHEERASEARCH_LIB = False
pass
# Set up logging for better debugging # Set up logging for better debugging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -172,14 +172,14 @@ class FastTagManager:
def __init__(self, viewer): def __init__(self, viewer):
self.viewer = viewer self.viewer = viewer
self.main_win = viewer.main_win self.main_win = viewer.main_win
self.controller = viewer.controller
def show_menu(self): def show_menu(self):
"""Builds and shows a context menu for quickly adding/removing tags.""" """Builds and shows a context menu for quickly adding/removing tags."""
if not self.main_win or not self.controller.get_current_path(): controller = self.viewer.controller
if not self.main_win or not controller or not controller.get_current_path():
return return
current_path = self.controller.get_current_path() current_path = controller.get_current_path()
try: try:
raw_tags = os.getxattr(current_path, XATTR_NAME).decode('utf-8') raw_tags = os.getxattr(current_path, XATTR_NAME).decode('utf-8')
current_tags = {t.strip() for t in raw_tags.split(',') if t.strip()} current_tags = {t.strip() for t in raw_tags.split(',') if t.strip()}
@@ -233,10 +233,11 @@ class FastTagManager:
return return
tag_name = action.text() tag_name = action.text()
is_checked = action.isChecked() is_checked = action.isChecked()
current_path = self.controller.get_current_path() controller = self.viewer.controller
current_path = controller.get_current_path() if controller else None
if not current_path: if not current_path:
return return
self.controller.toggle_tag(tag_name, is_checked) controller.toggle_tag(tag_name, is_checked)
self.viewer.update_status_bar() self.viewer.update_status_bar()
if self.main_win: if self.main_win:
if is_checked: if is_checked:
@@ -289,18 +290,23 @@ class FilmStripWidget(QListWidget):
allowing the user to quickly jump to an image by clicking its thumbnail. allowing the user to quickly jump to an image by clicking its thumbnail.
It also supports dragging files out of the application. It also supports dragging files out of the application.
""" """
def __init__(self, controller, parent=None): def __init__(self, viewer, parent=None):
""" """
Initializes the FilmStripWidget. Initializes the FilmStripWidget.
Args: Args:
controller (ImageController): The controller managing the image list. viewer (ImageViewer): The viewer that owns this filmstrip.
parent (QWidget, optional): The parent widget. Defaults to None. parent (QWidget, optional): The parent widget. Defaults to None.
""" """
super().__init__(parent) super().__init__(parent)
self.controller = controller self.viewer = viewer
self.setDragEnabled(True) self.setDragEnabled(True)
@property
def controller(self):
"""Returns the controller of the active pane in the viewer."""
return self.viewer.controller
def startDrag(self, supportedActions): def startDrag(self, supportedActions):
""" """
Initiates a drag-and-drop operation for the selected image(s). Initiates a drag-and-drop operation for the selected image(s).
@@ -312,7 +318,7 @@ class FilmStripWidget(QListWidget):
urls = [] urls = []
for item in items: for item in items:
row = self.row(item) row = self.row(item)
if 0 <= row < len(self.controller.image_list): if self.controller and 0 <= row < len(self.controller.image_list):
path = self.controller.image_list[row] path = self.controller.image_list[row]
urls.append(QUrl.fromLocalFile(path)) urls.append(QUrl.fromLocalFile(path))
@@ -332,6 +338,63 @@ class FilmStripWidget(QListWidget):
drag.exec(Qt.CopyAction) drag.exec(Qt.CopyAction)
def _get_selected_paths(self):
"""Helper to get paths of all selected items."""
return [item.data(Qt.UserRole)
for item in self.selectedItems() if item.data(Qt.UserRole)]
def contextMenuEvent(self, event):
"""Shows a context menu for the selected items."""
selected_items = self.selectedItems()
if not selected_items:
return
menu = QMenu(self)
# Clipboard Submenu
clipboard_menu = menu.addMenu(
QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
# Copy Image
action_copy_image = clipboard_menu.addAction(
QIcon.fromTheme("image-x-generic"), UITexts.VIEWER_MENU_COPY_IMAGE)
action_copy_image.triggered.connect(self._copy_image_to_clipboard)
if len(selected_items) > 1:
action_copy_image.setEnabled(False)
# Copy Path
action_copy_path = clipboard_menu.addAction(
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
action_copy_path.triggered.connect(self._copy_path_to_clipboard)
# Copy Directory Path
action_copy_dir = clipboard_menu.addAction(
QIcon.fromTheme("folder"), UITexts.CONTEXT_MENU_COPY_DIR)
action_copy_dir.triggered.connect(self._copy_dir_to_clipboard)
menu.exec(event.globalPos())
def _copy_image_to_clipboard(self):
"""Copies the currently selected image to the system clipboard."""
paths = self._get_selected_paths()
if len(paths) == 1 and paths[0] and os.path.exists(paths[0]):
img = QImage(paths[0])
if not img.isNull():
QApplication.clipboard().setImage(img)
def _copy_path_to_clipboard(self):
"""Copies the file path(s) of the selected image(s) to the clipboard."""
paths = self._get_selected_paths()
if paths:
QApplication.clipboard().setText("\n".join(paths))
def _copy_dir_to_clipboard(self):
"""Copies the directory path(s) of the selected image(s) to the clipboard."""
paths = self._get_selected_paths()
if paths:
dir_paths = sorted(list(set(os.path.dirname(p) for p in paths)))
QApplication.clipboard().setText("\n".join(dir_paths))
class FaceCanvas(QLabel): class FaceCanvas(QLabel):
""" """
@@ -1222,8 +1285,8 @@ class ImagePane(QWidget):
self.scroll_area = QScrollArea() self.scroll_area = QScrollArea()
self.scroll_area.setAlignment(Qt.AlignCenter) self.scroll_area.setAlignment(Qt.AlignCenter)
self.scroll_area.setStyleSheet("background-color: #000; border: none;") self.scroll_area.setStyleSheet("background-color: #000; border: none;")
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll_area.setWidget(self.canvas) self.scroll_area.setWidget(self.canvas)
layout.addWidget(self.scroll_area) layout.addWidget(self.scroll_area)
@@ -1413,7 +1476,7 @@ class ImageViewer(QWidget):
# self.canvas = FaceCanvas(self) ... Moved to ImagePane # self.canvas = FaceCanvas(self) ... Moved to ImagePane
# self.scroll_area.setWidget(self.canvas) # self.scroll_area.setWidget(self.canvas)
self.filmstrip = FilmStripWidget(self.controller) self.filmstrip = FilmStripWidget(self)
self.filmstrip.setSpacing(2) self.filmstrip.setSpacing(2)
self.filmstrip.itemClicked.connect(self.on_filmstrip_clicked) self.filmstrip.itemClicked.connect(self.on_filmstrip_clicked)
@@ -1564,7 +1627,6 @@ class ImageViewer(QWidget):
pane.scrolled.connect(self._sync_scroll) pane.scrolled.connect(self._sync_scroll)
pane.zoom_manager.zoomed.connect(self._sync_zoom) pane.zoom_manager.zoomed.connect(self._sync_zoom)
self.filmstrip.controller = pane.controller
self.populate_filmstrip() self.populate_filmstrip()
self.sync_filmstrip_selection(pane.controller.index) self.sync_filmstrip_selection(pane.controller.index)
self.update_status_bar() self.update_status_bar()
@@ -1618,6 +1680,10 @@ class ImageViewer(QWidget):
if count == current_panes: if count == current_panes:
return return
if count > 1 and self.slideshow_manager.is_running():
self.slideshow_manager.stop()
self.update_title()
if count > current_panes: if count > current_panes:
# Add panes # Add panes
base_controller = self.active_pane.controller base_controller = self.active_pane.controller
@@ -1726,6 +1792,9 @@ class ImageViewer(QWidget):
"properties": self.show_properties, "properties": self.show_properties,
"toggle_visibility": self.toggle_main_window_visibility, "toggle_visibility": self.toggle_main_window_visibility,
"toggle_crop": self.toggle_crop_mode, "toggle_crop": self.toggle_crop_mode,
"copy_image": self.copy_image_to_clipboard,
"copy_path": self.copy_file_path_to_clipboard,
"copy_dir_path": self.copy_dir_path_to_clipboard,
"save_crop": self.save_cropped_image, "save_crop": self.save_cropped_image,
"compare_1": lambda: self.set_comparison_mode(1), "compare_1": lambda: self.set_comparison_mode(1),
"compare_2": lambda: self.set_comparison_mode(2), "compare_2": lambda: self.set_comparison_mode(2),
@@ -1735,6 +1804,11 @@ class ImageViewer(QWidget):
def _execute_action(self, action): def _execute_action(self, action):
"""Executes the method corresponding to the action name.""" """Executes the method corresponding to the action name."""
if self.slideshow_manager.is_running():
allowed_actions = ('slideshow', 'slideshow_reverse', 'close', 'fullscreen')
if action not in allowed_actions:
return
if action in self._actions: if action in self._actions:
self._actions[action]() self._actions[action]()
@@ -2383,6 +2457,81 @@ class ImageViewer(QWidget):
if self.main_win: if self.main_win:
self.main_win.update_metadata_for_path(path, metadata) self.main_win.update_metadata_for_path(path, metadata)
def copy_image_to_clipboard(self):
"""Copies the currently displayed image to the system clipboard."""
if self.controller and not self.controller.pixmap_original.isNull():
QApplication.clipboard().setImage(self.controller.pixmap_original.toImage())
def copy_file_path_to_clipboard(self):
"""Copies the current image's file path to the system clipboard."""
if self.controller:
path = self.controller.get_current_path()
if path:
QApplication.clipboard().setText(path)
def copy_dir_path_to_clipboard(self):
"""Copies the directory path of the current image to the system clipboard."""
if self.controller:
path = self.controller.get_current_path()
if path:
QApplication.clipboard().setText(os.path.dirname(path))
def _populate_open_with_menu(self, menu):
"""Populates the 'Open With' submenu."""
if self.main_win:
path = self.controller.get_current_path()
if path:
self.main_win.populate_open_with_submenu(menu, path)
def _build_menu_from_data(self, target_menu, items):
"""Builds a menu or submenu from a list of dictionary items."""
for item in items:
if item == "separator":
target_menu.addSeparator()
continue
# Handle dynamic submenus that need to be populated by a function
if "dynamic_submenu" in item:
icon = QIcon.fromTheme(item.get("icon", ""))
submenu = target_menu.addMenu(icon, item["text"])
item["dynamic_submenu"](submenu)
continue
action_name = item.get("action")
display_text = item["text"]
# Add shortcut string to display text if available
if action_name and "submenu" not in item and \
action_name in self.action_to_shortcut:
key, mods = self.action_to_shortcut[action_name]
try:
mod_val = int(mods)
except TypeError:
mod_val = mods.value
seq = QKeySequence(mod_val | key)
shortcut_str = seq.toString(QKeySequence.NativeText)
if shortcut_str:
display_text += f"\t{shortcut_str}"
icon = QIcon.fromTheme(item.get("icon", ""))
if "submenu" in item:
submenu = target_menu.addMenu(icon, item["text"])
self._build_menu_from_data(submenu, item["submenu"])
else:
action = target_menu.addAction(icon, display_text)
slot = item.get("slot")
if action_name:
action.triggered.connect(
lambda checked=False, name=action_name:
self._execute_action(name))
elif slot:
action.triggered.connect(slot)
if item.get("checkable"):
action.setCheckable(True)
action.setChecked(item.get("checked", False))
def restore_scroll_for_pane(self, pane, config): def restore_scroll_for_pane(self, pane, config):
""" """
Applies the saved scrollbar positions from a layout configuration. Applies the saved scrollbar positions from a layout configuration.
@@ -2451,11 +2600,15 @@ class ImageViewer(QWidget):
def toggle_slideshow(self): def toggle_slideshow(self):
"""Starts or stops the automatic slideshow timer.""" """Starts or stops the automatic slideshow timer."""
if len(self.panes) > 1:
return
self.slideshow_manager.toggle(reverse=False) self.slideshow_manager.toggle(reverse=False)
self.update_view(resize_win=False) self.update_view(resize_win=False)
def toggle_slideshow_reverse(self): def toggle_slideshow_reverse(self):
"""Starts or stops the automatic reverse slideshow timer.""" """Starts or stops the automatic reverse slideshow timer."""
if len(self.panes) > 1:
return
self.slideshow_manager.toggle(reverse=True) self.slideshow_manager.toggle(reverse=True)
self.update_view(resize_win=False) self.update_view(resize_win=False)
@@ -2604,53 +2757,94 @@ class ImageViewer(QWidget):
"""Builds and returns the general viewer context menu.""" """Builds and returns the general viewer context menu."""
menu = QMenu(self) menu = QMenu(self)
# Add "Open With" submenu menu_structure = []
if self.main_win: if self.slideshow_manager.is_running():
path = self.controller.get_current_path() is_fwd = self.slideshow_manager.is_forward()
if path: is_rev = self.slideshow_manager.is_reverse()
open_submenu = menu.addMenu(QIcon.fromTheme("document-open"), menu_structure = [
UITexts.CONTEXT_MENU_OPEN) {"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd
self.main_win.populate_open_with_submenu(open_submenu, path) else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
menu.addSeparator() "icon": "media-playback-stop" if is_fwd else "media-playback-start"},
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
"action": "slideshow_reverse",
"icon": "media-playback-stop" if is_rev else "media-seek-backward"},
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
]
menu_items = [ else:
{"text": UITexts.VIEWER_MENU_TAGS, "action": "fast_tag", # Build the normal menu structure
"icon": "document-properties"}, is_fwd = self.slideshow_manager.is_forward()
is_rev = self.slideshow_manager.is_reverse()
slideshow_submenu = [
{"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd
else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
"icon": "media-playback-stop" if is_fwd else "media-playback-start"},
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
"action": "slideshow_reverse",
"icon": "media-playback-stop" if is_rev else "media-seek-backward"},
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
]
menu_structure = [
{"text": UITexts.CONTEXT_MENU_OPEN, "icon": "document-open",
"dynamic_submenu": self._populate_open_with_menu},
"separator", "separator",
{"text": UITexts.DETECT_FACES, "action": "detect_faces", {"text": UITexts.VIEWER_MENU_TAGS,
"icon": "edit-image-face-recognize"}, "action": "fast_tag", "icon": "document-properties"},
{"text": UITexts.VIEWER_MENU_DETECT_AREAS,
"icon": "edit-image-face-recognize", "submenu": [
{"text": UITexts.VIEWER_MENU_DETECT_FACES,
"action": "detect_faces"},
{"text": UITexts.VIEWER_MENU_DETECT_PETS, "action": "detect_pets"},
]},
"separator", "separator",
{"text": UITexts.DETECT_PETS, "action": "detect_pets", {"text": UITexts.VIEWER_MENU_MANIPULATE,
"icon": "edit-image-face-recognize"}, "icon": "transform", "submenu": [
"separator", {"text": UITexts.VIEWER_MENU_ROTATE,
{"text": UITexts.DETECT_BODIES, "action": "detect_bodies", "icon": "transform-rotate", "submenu": [
"icon": "edit-image-face-recognize"},
"separator",
{"text": UITexts.VIEWER_MENU_ROTATE, "icon": "transform-rotate",
"submenu": [
{"text": UITexts.VIEWER_MENU_ROTATE_LEFT, {"text": UITexts.VIEWER_MENU_ROTATE_LEFT,
"action": "rotate_left", "icon": "object-rotate-left"}, "action": "rotate_left", "icon": "object-rotate-left"},
{"text": UITexts.VIEWER_MENU_ROTATE_RIGHT, {"text": UITexts.VIEWER_MENU_ROTATE_RIGHT,
"action": "rotate_right", "icon": "object-rotate-right"} "action": "rotate_right", "icon": "object-rotate-right"}
]}, ]},
"separator", {"text": UITexts.VIEWER_MENU_FLIP,
{"text": UITexts.VIEWER_MENU_FLIP, "icon": "transform-flip", "submenu": [ "icon": "transform-flip", "submenu": [
{"text": UITexts.VIEWER_MENU_FLIP_H, {"text": UITexts.VIEWER_MENU_FLIP_H,
"action": "flip_horizontal", "action": "flip_horizontal", "icon": "object-flip-horizontal"},
"icon": "object-flip-horizontal"},
{"text": UITexts.VIEWER_MENU_FLIP_V, {"text": UITexts.VIEWER_MENU_FLIP_V,
"action": "flip_vertical", "icon": "object-flip-vertical"} "action": "flip_vertical", "icon": "object-flip-vertical"}
]}
]}, ]},
"separator", {"text": UITexts.VIEWER_MENU_ZOOM, "icon": "zoom", "submenu": [
{"text": UITexts.VIEWER_MENU_RENAME, "action": "rename", {"text": UITexts.VIEWER_MENU_ZOOM_IN,
"icon": "edit-rename"}, "action": "zoom_in", "icon": "zoom-in"},
{"text": UITexts.VIEWER_MENU_ZOOM_OUT,
"action": "zoom_out", "icon": "zoom-out"},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_FIT_SCREEN, {"text": UITexts.VIEWER_MENU_FIT_SCREEN,
"slot": self.zoom_manager.toggle_fit_to_screen, "slot": self.zoom_manager.toggle_fit_to_screen,
"icon": "zoom-fit-best"}, "icon": "zoom-fit-best"},
]},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop", {"text": UITexts.VIEWER_MENU_RENAME,
"icon": "transform-crop", "checkable": True}, # checked updated later "action": "rename", "icon": "edit-rename"},
{"text": UITexts.CONTEXT_MENU_CLIPBOARD,
"icon": "edit-copy", "submenu": [
{"text": UITexts.VIEWER_MENU_COPY_IMAGE,
"action": "copy_image", "icon": "image-x-generic"},
{"text": UITexts.VIEWER_MENU_COPY_PATH,
"action": "copy_path", "icon": "document-properties"},
{"text": UITexts.CONTEXT_MENU_COPY_DIR,
"action": "copy_dir_path", "icon": "folder"},
]},
{"text": UITexts.VIEWER_MENU_CROP,
"action": "toggle_crop", "icon": "transform-crop", "checkable": True,
"checked": self.crop_mode},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [ {"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [
{"text": UITexts.VIEWER_MENU_COMPARE_1, {"text": UITexts.VIEWER_MENU_COMPARE_1,
@@ -2668,104 +2862,43 @@ class ImageViewer(QWidget):
] ]
if self.movie: if self.movie:
is_paused = self.movie.state() == QMovie.Paused_ is_paused = self.movie.state() == QMovie.Paused
pause_text = (UITexts.VIEWER_MENU_RESUME_ANIMATION if is_paused pause_text = UITexts.VIEWER_MENU_RESUME_ANIMATION \
else UITexts.VIEWER_MENU_PAUSE_ANIMATION) if is_paused else UITexts.VIEWER_MENU_PAUSE_ANIMATION
pause_icon = ("media-playback-start" if is_paused pause_icon = "media-playback-start" \
else "media-playback-pause") if is_paused else "media-playback-pause"
menu_items.append({"text": pause_text, "action": "toggle_animation", menu_structure.append(
{"text": pause_text, "action": "toggle_animation",
"icon": pause_icon}) "icon": pause_icon})
is_fwd_slideshow = self.slideshow_manager.is_forward() menu_structure.extend([
is_rev_slideshow = self.slideshow_manager.is_reverse() {"text": UITexts.VIEWER_MENU_SLIDESHOW,
"icon": "view-presentation", "submenu": slideshow_submenu},
slideshow_submenu = [
{"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd_slideshow
else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
"icon": "media-playback-stop" if is_fwd_slideshow
else "media-playback-start"},
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev_slideshow
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
"action": "slideshow_reverse",
"icon": "media-playback-stop" if is_rev_slideshow
else "media-seek-backward"},
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
]
menu_items.extend([
{"text": UITexts.VIEWER_MENU_SLIDESHOW, "icon": "view-presentation",
"submenu": slideshow_submenu},
"separator", "separator",
{"text": UITexts.SHOW_FACES, "action": "toggle_faces", {"text": UITexts.SHOW_FACES,
"icon": "edit-image-face-show", "action": "toggle_faces", "icon": "edit-image-face-show",
"checkable": True, "checked": self.controller.show_faces}, "checkable": True,
"checked": self.controller.show_faces if self.controller else False},
{"text": UITexts.VIEWER_MENU_SHOW_FILMSTRIP, {"text": UITexts.VIEWER_MENU_SHOW_FILMSTRIP,
"action": "toggle_filmstrip", "icon": "view-filmstrip", "checkable": True, "action": "toggle_filmstrip", "icon": "view-filmstrip",
"checked": self.filmstrip.isVisible()}, "checkable": True, "checked": self.filmstrip.isVisible()},
{"text": UITexts.VIEWER_MENU_SHOW_STATUSBAR, {"text": UITexts.VIEWER_MENU_SHOW_STATUSBAR,
"action": "toggle_statusbar", "icon": "view-bottom-panel", "action": "toggle_statusbar", "icon": "view-bottom-panel",
"checkable": True, "checkable": True, "checked": self.status_bar_container.isVisible()},
"checked": self.status_bar_container.isVisible()},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_EXIT_FULLSCREEN {"text": UITexts.VIEWER_MENU_EXIT_FULLSCREEN
if self.isFullScreen() else UITexts.VIEWER_MENU_ENTER_FULLSCREEN, if self.isFullScreen() else UITexts.VIEWER_MENU_ENTER_FULLSCREEN,
"action": "fullscreen", "action": "fullscreen", "icon": "view-fullscreen"
"icon": "view-fullscreen" if not self.isFullScreen() else "view-restore"}, if not self.isFullScreen() else "view-restore"},
"separator", "separator",
{"text": "Show/hide main window", {"text": "Show/hide main window",
"action": "toggle_visibility", "action": "toggle_visibility", "icon": "view-restore"},
"icon": "view-restore"},
"separator", "separator",
{"text": UITexts.CONTEXT_MENU_PROPERTIES, "action": "properties", {"text": UITexts.CONTEXT_MENU_PROPERTIES,
"icon": "document-properties"} "action": "properties", "icon": "document-properties"}
]) ])
def build_actions(target_menu, items): self._build_menu_from_data(menu, menu_structure)
for item in items:
if item == "separator":
target_menu.addSeparator()
continue
action_name = item.get("action")
display_text = item["text"]
# Only add shortcut to final actions, not to submenus
if action_name and "submenu" not in item and \
action_name in self.action_to_shortcut:
key, mods = self.action_to_shortcut[action_name]
# Handle both Qt.KeyboardModifier (enum) and Qt.KeyboardModifiers
# (flags) by ensuring we have an integer value for the modifier
# before the bitwise OR.
try:
mod_val = int(mods)
except TypeError:
mod_val = mods.value
seq = QKeySequence(mod_val | key)
shortcut_str = seq.toString(QKeySequence.NativeText)
if shortcut_str:
display_text += f"\t{shortcut_str}"
icon = QIcon.fromTheme(item.get("icon", ""))
if "submenu" in item:
submenu = target_menu.addMenu(icon, item["text"])
build_actions(submenu, item["submenu"])
else:
action = target_menu.addAction(icon, display_text)
slot = item.get("slot")
if action_name:
action.triggered.connect(
lambda checked=False, name=action_name:
self._execute_action(name))
elif slot:
action.triggered.connect(slot)
if item.get("checkable"):
action.setCheckable(True)
action.setChecked(item.get("checked", False))
build_actions(menu, menu_items)
return menu return menu

View File

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

View File

@@ -27,6 +27,7 @@ from constants import (
FACES_MENU_MAX_ITEMS_DEFAULT, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL, FACES_MENU_MAX_ITEMS_DEFAULT, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL,
AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR, AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR,
MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL, MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
HAVE_BAGHEERASEARCH_LIB,
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, TAGS_MENU_MAX_ITEMS_DEFAULT, SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, TAGS_MENU_MAX_ITEMS_DEFAULT,
THUMBNAILS_FILENAME_LINES_DEFAULT, THUMBNAILS_FILENAME_LINES_DEFAULT,
THUMBNAILS_REFRESH_INTERVAL_DEFAULT, THUMBNAILS_BG_COLOR_DEFAULT, THUMBNAILS_REFRESH_INTERVAL_DEFAULT, THUMBNAILS_BG_COLOR_DEFAULT,
@@ -35,7 +36,7 @@ from constants import (
THUMBNAILS_TOOLTIP_FG_COLOR_DEFAULT, THUMBNAILS_FILENAME_FONT_SIZE_DEFAULT, THUMBNAILS_TOOLTIP_FG_COLOR_DEFAULT, THUMBNAILS_FILENAME_FONT_SIZE_DEFAULT,
THUMBNAILS_TAGS_LINES_DEFAULT, THUMBNAILS_TAGS_FONT_SIZE_DEFAULT, THUMBNAILS_TAGS_LINES_DEFAULT, THUMBNAILS_TAGS_FONT_SIZE_DEFAULT,
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_WHEEL_SPEED_DEFAULT, VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_WHEEL_SPEED_DEFAULT,
UITexts, save_app_config, HAVE_BAGHEERASEARCH_LIB UITexts, save_app_config
) )

View File

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

View File

@@ -398,7 +398,7 @@ class TagEditWidget(QWidget):
if not full_path: if not full_path:
return "" return ""
words = full_path.replace('/', ' ').split() words = full_path.replace('/', ' ').split()
search_terms = [f"tags:{word}" for word in words if word] search_terms = [f"tags:'{word}'" for word in words if word]
return " ".join(search_terms) return " ".join(search_terms)
def _get_current_query_text(self): def _get_current_query_text(self):