A bunch of changes

This commit is contained in:
Ignacio Serantes
2026-03-23 21:53:19 +01:00
parent a402828d1a
commit 547bfbf760
9 changed files with 544 additions and 150 deletions

View File

@@ -579,8 +579,6 @@ class ThumbnailDelegate(QStyledItemDelegate):
thumb_size = self.main_win.current_thumb_size thumb_size = self.main_win.current_thumb_size
path = index.data(PATH_ROLE) path = index.data(PATH_ROLE)
mtime = index.data(MTIME_ROLE) mtime = index.data(MTIME_ROLE)
inode = index.data(INODE_ROLE)
device_id = index.data(DEVICE_ROLE)
# Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap # Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap
# conversion on every paint event. # conversion on every paint event.
@@ -589,6 +587,8 @@ class ThumbnailDelegate(QStyledItemDelegate):
if not source_pixmap or source_pixmap.isNull(): if not source_pixmap or source_pixmap.isNull():
# Not in UI cache, try to get from main thumbnail cache (Memory/LMDB) # Not in UI cache, try to get from main thumbnail cache (Memory/LMDB)
inode = index.data(INODE_ROLE)
device_id = index.data(DEVICE_ROLE)
img, _ = self.main_win.cache.get_thumbnail( img, _ = self.main_win.cache.get_thumbnail(
path, requested_size=thumb_size, curr_mtime=mtime, path, requested_size=thumb_size, curr_mtime=mtime,
inode=inode, device_id=device_id, async_load=True) inode=inode, device_id=device_id, async_load=True)
@@ -863,21 +863,35 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
def lessThan(self, left, right): def lessThan(self, left, right):
"""Custom sorting logic for name and date.""" """Custom sorting logic for name and date."""
sort_role = self.sortRole() sort_role = self.sortRole()
left_data = self.sourceModel().data(left, sort_role)
right_data = self.sourceModel().data(right, sort_role)
if sort_role == MTIME_ROLE: if sort_role == MTIME_ROLE:
left = left_data if left_data is not None else 0 left_data = self.sourceModel().data(left, sort_role)
right = right_data if right_data is not None else 0 right_data = self.sourceModel().data(right, sort_role)
return left < right # Treat None as 0 for safe comparison
left_val = left_data if left_data is not None else 0
right_val = right_data if right_data is not None else 0
return left_val < right_val
# Default (DisplayRole) is case-insensitive name sorting # Default (DisplayRole) is name sorting.
# Handle None values safely # Optimization: Use the pre-calculated lowercase name from the cache
l_str = str(left_data) if left_data is not None else "" # to avoid repeated string operations during sorting.
r_str = str(right_data) if right_data is not None else "" left_path = self.sourceModel().data(left, PATH_ROLE)
right_path = self.sourceModel().data(right, PATH_ROLE)
# Fallback for non-thumbnail items (like headers) or if cache is missing
if not left_path or not right_path or not self._data_cache:
l_str = str(self.sourceModel().data(left, Qt.DisplayRole) or "")
r_str = str(self.sourceModel().data(right, Qt.DisplayRole) or "")
return l_str.lower() < r_str.lower() return l_str.lower() < r_str.lower()
# Get from cache, with a fallback just in case
_, left_name_lower = self._data_cache.get(
left_path, (None, os.path.basename(left_path).lower()))
_, right_name_lower = self._data_cache.get(
right_path, (None, os.path.basename(right_path).lower()))
return left_name_lower < right_name_lower
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
""" """
@@ -908,6 +922,7 @@ class MainWindow(QMainWindow):
self.current_thumb_size = THUMBNAILS_DEFAULT_SIZE self.current_thumb_size = THUMBNAILS_DEFAULT_SIZE
self.face_names_history = [] self.face_names_history = []
self.pet_names_history = [] self.pet_names_history = []
self.body_names_history = []
self.object_names_history = [] self.object_names_history = []
self.landmark_names_history = [] self.landmark_names_history = []
self.mru_tags = deque(maxlen=APP_CONFIG.get( self.mru_tags = deque(maxlen=APP_CONFIG.get(
@@ -1466,6 +1481,10 @@ class MainWindow(QMainWindow):
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")
select_paths = [selected_path] if selected_path else None
if "window_state" in mw_data: if "window_state" in mw_data:
self.restoreState( self.restoreState(
QByteArray.fromBase64(mw_data["window_state"].encode())) QByteArray.fromBase64(mw_data["window_state"].encode()))
@@ -1521,7 +1540,7 @@ class MainWindow(QMainWindow):
paths.append(d) paths.append(d)
self.start_scan([p.strip() for p in paths if p.strip() self.start_scan([p.strip() for p in paths if p.strip()
and os.path.exists(os.path.expanduser(p.strip()))], and os.path.exists(os.path.expanduser(p.strip()))],
select_path=mw_data.get("selected_path")) select_paths=select_paths)
if search_text: if search_text:
self.search_input.setEditText(search_text) self.search_input.setEditText(search_text)
@@ -1643,6 +1662,11 @@ class MainWindow(QMainWindow):
if len(self.face_names_history) > new_max_faces: if len(self.face_names_history) > new_max_faces:
self.face_names_history = self.face_names_history[:new_max_faces] self.face_names_history = self.face_names_history[:new_max_faces]
new_max_bodies = APP_CONFIG.get("body_menu_max_items",
constants.FACES_MENU_MAX_ITEMS_DEFAULT)
if len(self.body_names_history) > new_max_bodies:
self.body_names_history = self.body_names_history[:new_max_bodies]
new_bg_color = APP_CONFIG.get("thumbnails_bg_color", new_bg_color = APP_CONFIG.get("thumbnails_bg_color",
constants.THUMBNAILS_BG_COLOR_DEFAULT) constants.THUMBNAILS_BG_COLOR_DEFAULT)
self.thumbnail_view.setStyleSheet(f"background-color: {new_bg_color};") self.thumbnail_view.setStyleSheet(f"background-color: {new_bg_color};")
@@ -1974,6 +1998,44 @@ class MainWindow(QMainWindow):
return False return False
def get_selected_paths(self):
"""Returns a list of all selected file paths."""
paths = []
seen = set()
for idx in self.thumbnail_view.selectedIndexes():
path = self.proxy_model.data(idx, PATH_ROLE)
if path and path not in seen:
paths.append(path)
seen.add(path)
return paths
def restore_selection(self, paths):
"""Restores selection for a list of paths."""
if not paths:
return
selection_model = self.thumbnail_view.selectionModel()
selection = QItemSelection()
first_valid_index = QModelIndex()
for path in paths:
if path in self._path_to_model_index:
persistent_index = self._path_to_model_index[path]
if persistent_index.isValid():
source_index = QModelIndex(persistent_index)
proxy_index = self.proxy_model.mapFromSource(source_index)
if proxy_index.isValid():
selection.select(proxy_index, proxy_index)
if not first_valid_index.isValid():
first_valid_index = proxy_index
if not selection.isEmpty():
selection_model.select(selection, QItemSelectionModel.ClearAndSelect)
if first_valid_index.isValid():
self.thumbnail_view.setCurrentIndex(first_valid_index)
self.thumbnail_view.scrollTo(
first_valid_index, QAbstractItemView.EnsureVisible)
def toggle_visibility(self): def toggle_visibility(self):
"""Toggles the visibility of the main window, opening a viewer if needed.""" """Toggles the visibility of the main window, opening a viewer if needed."""
if self.isVisible(): if self.isVisible():
@@ -2247,7 +2309,7 @@ class MainWindow(QMainWindow):
w.load_and_fit_image() w.load_and_fit_image()
def start_scan(self, paths, sync_viewer=False, active_viewer=None, def start_scan(self, paths, sync_viewer=False, active_viewer=None,
select_path=None): select_paths=None):
""" """
Starts a new background scan for images. Starts a new background scan for images.
@@ -2255,7 +2317,7 @@ class MainWindow(QMainWindow):
paths (list): A list of file paths or directories to scan. paths (list): A list of file paths or directories to scan.
sync_viewer (bool): If True, avoids clearing the grid. sync_viewer (bool): If True, avoids clearing the grid.
active_viewer (ImageViewer): A viewer to sync with the scan results. active_viewer (ImageViewer): A viewer to sync with the scan results.
select_path (str): A path to select automatically after the scan finishes. select_paths (list): A list of paths to select automatically.
""" """
self.is_cleaning = True self.is_cleaning = True
self._suppress_updates = True self._suppress_updates = True
@@ -2299,11 +2361,11 @@ class MainWindow(QMainWindow):
self.scanner.progress_msg.connect(self.status_lbl.setText) self.scanner.progress_msg.connect(self.status_lbl.setText)
self.scanner.more_files_available.connect(self.more_files_available) self.scanner.more_files_available.connect(self.more_files_available)
self.scanner.finished_scan.connect( self.scanner.finished_scan.connect(
lambda n: self._on_scan_finished(n, select_path)) lambda n: self._on_scan_finished(n, select_paths))
self.scanner.start() self.scanner.start()
self._scan_all = False self._scan_all = False
def _on_scan_finished(self, n, select_path=None): def _on_scan_finished(self, n, select_paths=None):
"""Slot for when the image scanner has finished.""" """Slot for when the image scanner has finished."""
self._suppress_updates = False self._suppress_updates = False
self._scanner_last_index = self._scanner_total_files self._scanner_last_index = self._scanner_total_files
@@ -2331,8 +2393,8 @@ class MainWindow(QMainWindow):
self.update_tag_edit_widget() self.update_tag_edit_widget()
# Select a specific path if requested (e.g., after layout restore) # Select a specific path if requested (e.g., after layout restore)
if select_path: if select_paths:
self.find_and_select_path(select_path) self.restore_selection(select_paths)
# Final rebuild to ensure all items are correctly placed # Final rebuild to ensure all items are correctly placed
if self.rebuild_timer.isActive(): if self.rebuild_timer.isActive():
@@ -2573,7 +2635,7 @@ class MainWindow(QMainWindow):
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None)) self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
# Preserve selection # Preserve selection
selected_path = self.get_current_selected_path() selected_paths = self.get_selected_paths()
mode = self.sort_combo.currentText() mode = self.sort_combo.currentText()
rev = "" in mode rev = "" in mode
@@ -2628,7 +2690,7 @@ class MainWindow(QMainWindow):
self._suppress_updates = False self._suppress_updates = False
self.apply_filters() self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True) self.thumbnail_view.setUpdatesEnabled(True)
self.find_and_select_path(selected_path) self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \ if self.main_dock.isVisible() and \
self.tags_tabs.currentWidget() == self.filter_widget: self.tags_tabs.currentWidget() == self.filter_widget:
@@ -2782,7 +2844,7 @@ class MainWindow(QMainWindow):
self._suppress_updates = False self._suppress_updates = False
self.apply_filters() self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True) self.thumbnail_view.setUpdatesEnabled(True)
self.find_and_select_path(selected_path) self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \ if self.main_dock.isVisible() and \
self.tags_tabs.currentWidget() == self.filter_widget: self.tags_tabs.currentWidget() == self.filter_widget:
@@ -3064,7 +3126,7 @@ class MainWindow(QMainWindow):
return return
# Preserve selection # Preserve selection
selected_path = self.get_current_selected_path() selected_paths = self.get_selected_paths()
# Gather filter criteria from the UI # Gather filter criteria from the UI
include_tags = set() include_tags = set()
@@ -3112,8 +3174,8 @@ class MainWindow(QMainWindow):
self.filtered_count_lbl.setText(UITexts.FILTERED_ZERO) self.filtered_count_lbl.setText(UITexts.FILTERED_ZERO)
# Restore selection if it's still visible # Restore selection if it's still visible
if selected_path: if selected_paths:
self.find_and_select_path(selected_path) self.restore_selection(selected_paths)
# Sync open viewers with the new list of visible paths # Sync open viewers with the new list of visible paths
visible_paths = self.get_visible_image_paths() visible_paths = self.get_visible_image_paths()
@@ -3163,13 +3225,18 @@ class MainWindow(QMainWindow):
target_list.append(current_path) target_list.append(current_path)
new_index = len(target_list) - 1 new_index = len(target_list) - 1
w.controller.update_list( # Check if we are preserving the image to pass correct metadata
target_list, new_index if new_index != -1 else None) tags_to_pass = None
rating_to_pass = 0
if new_index != -1 and new_index < len(target_list):
if target_list[new_index] == current_path_in_viewer:
tags_to_pass = viewer_tags
rating_to_pass = viewer_rating
# Pass current image's tags and rating to the controller
w.controller.update_list( w.controller.update_list(
target_list, new_index if new_index != -1 else None, target_list, new_index if new_index != -1 else None,
viewer_tags, viewer_rating) tags_to_pass, rating_to_pass)
if not w._is_persistent and not w.controller.image_list: if not w._is_persistent and not w.controller.image_list:
w.close() w.close()
continue continue
@@ -3468,16 +3535,16 @@ class MainWindow(QMainWindow):
if not self.history: if not self.history:
return return
current_selection = self.get_current_selected_path() current_selection = self.get_selected_paths()
term = self.history[0] term = self.history[0]
if term.startswith("file:/"): if term.startswith("file:/"):
path = term[6:] path = term[6:]
if os.path.isfile(path): if os.path.isfile(path):
self.start_scan([os.path.dirname(path)], select_path=current_selection) self.start_scan([os.path.dirname(path)], select_paths=current_selection)
return return
self.process_term(term, select_path=current_selection) self.process_term(term, select_paths=current_selection)
def process_term(self, term, select_path=None): def process_term(self, term, select_paths=None):
"""Processes a search term, file path, or layout directive.""" """Processes a search term, file path, or layout directive."""
self.add_to_history(term) self.add_to_history(term)
self.update_search_input() self.update_search_input()
@@ -3529,7 +3596,7 @@ class MainWindow(QMainWindow):
else: else:
# If a directory or search term, start a scan # If a directory or search term, start a scan
self.start_scan([path], select_path=select_path) self.start_scan([path], select_paths=select_paths)
def update_search_input(self): def update_search_input(self):
"""Updates the search input combo box with history items and icons.""" """Updates the search input combo box with history items and icons."""
@@ -3607,6 +3674,7 @@ class MainWindow(QMainWindow):
self.tags_tabs.setCurrentIndex(d["active_dock_tab"]) self.tags_tabs.setCurrentIndex(d["active_dock_tab"])
self.face_names_history = d.get("face_names_history", []) self.face_names_history = d.get("face_names_history", [])
self.pet_names_history = d.get("pet_names_history", []) self.pet_names_history = d.get("pet_names_history", [])
self.body_names_history = d.get("body_names_history", [])
self.object_names_history = d.get("object_names_history", []) self.object_names_history = d.get("object_names_history", [])
self.landmark_names_history = d.get("landmark_names_history", []) self.landmark_names_history = d.get("landmark_names_history", [])
@@ -3674,6 +3742,7 @@ class MainWindow(QMainWindow):
APP_CONFIG["active_dock_tab"] = self.tags_tabs.currentIndex() APP_CONFIG["active_dock_tab"] = self.tags_tabs.currentIndex()
APP_CONFIG["face_names_history"] = self.face_names_history APP_CONFIG["face_names_history"] = self.face_names_history
APP_CONFIG["pet_names_history"] = self.pet_names_history APP_CONFIG["pet_names_history"] = self.pet_names_history
APP_CONFIG["body_names_history"] = self.body_names_history
APP_CONFIG["object_names_history"] = self.object_names_history APP_CONFIG["object_names_history"] = self.object_names_history
APP_CONFIG["landmark_names_history"] = self.landmark_names_history APP_CONFIG["landmark_names_history"] = self.landmark_names_history
APP_CONFIG["mru_tags"] = list(self.mru_tags) APP_CONFIG["mru_tags"] = list(self.mru_tags)

View File

@@ -1,5 +1,16 @@
v0.9.11 - v0.9.11 -
· Hacer que el image viewer standalone admita múltiles sort · Filmstrip fixed
HAVE_BAGHEERASEARCH_LIB
Refactor `load_image` to check if `pixmap_original` is already valid before reloading to optimize performance.
Check if the `ImagePreloader` handles file deletion correctly if the file is deleted while being preloaded.
Me gustaría implementar un modo de "Comparación" para ver 2 o 4 imágenes lado a lado en el visor. ¿Cómo podría abordarlo?
· La instalación no debe usar Bagheera como motor a no ser que esté instalado.
· Hacer que el image viewer standalone admita múltiples sort
· Comprobar hotkeys y funcionamiento en general. · Comprobar hotkeys y funcionamiento en general.
· Inhibir el salvapantallas con el slideshow y añadir opción de menú para inhibirlo durante un tiempo determinado · Inhibir el salvapantallas con el slideshow y añadir opción de menú para inhibirlo durante un tiempo determinado
· Mejorar el menú Open, con nombres correctos e iconos adecuados · Mejorar el menú Open, con nombres correctos e iconos adecuados
@@ -12,12 +23,8 @@ v0.9.11 -
· 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.
Analiza si la estrategia LIFO (Last-In, First-Out) en `CacheLoader` es la ideal para una galería de imágenes o si debería ser mixta.
¿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? ¿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?
Verifica si el uso de `QPixmapCache` en `ThumbnailDelegate.paint_thumbnail` está optimizado para evitar la conversión repetitiva de QImage a QPixmap, lo que podría causar ralentizaciones al hacer scroll rápido.
Check if the `_suppress_updates` flag correctly prevents potential race conditions in `update_tag_list` when switching view modes rapidly. 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.

View File

@@ -110,7 +110,7 @@ SCANNER_SETTINGS_DEFAULTS = {
"scan_full_on_start": True, "scan_full_on_start": True,
"person_tags": "", "person_tags": "",
"generation_threads": 4, "generation_threads": 4,
"search_engine": "Native" "search_engine": ""
} }
# --- IMAGE VIEWER DEFAULTS --- # --- IMAGE VIEWER DEFAULTS ---
@@ -193,6 +193,10 @@ AVAILABLE_PET_ENGINES = []
if HAVE_MEDIAPIPE: if HAVE_MEDIAPIPE:
AVAILABLE_PET_ENGINES.append("mediapipe") AVAILABLE_PET_ENGINES.append("mediapipe")
AVAILABLE_BODY_ENGINES = []
if HAVE_MEDIAPIPE:
AVAILABLE_BODY_ENGINES.append("mediapipe")
# Determine the default engine. This can be overridden by user config. # Determine the default engine. This can be overridden by user config.
DEFAULT_FACE_ENGINE = AVAILABLE_FACE_ENGINES[0] if AVAILABLE_FACE_ENGINES else None DEFAULT_FACE_ENGINE = AVAILABLE_FACE_ENGINES[0] if AVAILABLE_FACE_ENGINES else None
DEFAULT_PET_ENGINE = AVAILABLE_PET_ENGINES[0] if AVAILABLE_PET_ENGINES else None DEFAULT_PET_ENGINE = AVAILABLE_PET_ENGINES[0] if AVAILABLE_PET_ENGINES else None
@@ -205,6 +209,7 @@ PET_DETECTION_ENGINE = APP_CONFIG.get("pet_detection_engine",
DEFAULT_PET_ENGINE) DEFAULT_PET_ENGINE)
DEFAULT_PET_BOX_COLOR = "#98FB98" # PaleGreen DEFAULT_PET_BOX_COLOR = "#98FB98" # PaleGreen
DEFAULT_BODY_BOX_COLOR = "#FF4500" # OrangeRed
DEFAULT_OBJECT_BOX_COLOR = "#FFD700" # Gold DEFAULT_OBJECT_BOX_COLOR = "#FFD700" # Gold
DEFAULT_LANDMARK_BOX_COLOR = "#00BFFF" # DeepSkyBlue DEFAULT_LANDMARK_BOX_COLOR = "#00BFFF" # DeepSkyBlue
# --- SHORTCUTS --- # --- SHORTCUTS ---
@@ -273,6 +278,7 @@ VIEWER_ACTIONS = {
"detect_faces": ("Detect Faces", "Actions"), "detect_faces": ("Detect Faces", "Actions"),
"detect_pets": ("Detect Pets", "Actions"), "detect_pets": ("Detect Pets", "Actions"),
"fast_tag": ("Quick Tags", "Actions"), "fast_tag": ("Quick Tags", "Actions"),
"detect_bodies": ("Detect Bodies", "Actions"),
"rotate_right": ("Rotate Right", "Transform"), "rotate_right": ("Rotate Right", "Transform"),
"rotate_left": ("Rotate Left", "Transform"), "rotate_left": ("Rotate Left", "Transform"),
"zoom_in": ("Zoom In", "Transform"), "zoom_in": ("Zoom In", "Transform"),
@@ -299,6 +305,7 @@ DEFAULT_VIEWER_SHORTCUTS = {
"fullscreen": (Qt.Key_F11, Qt.NoModifier), "fullscreen": (Qt.Key_F11, Qt.NoModifier),
"detect_faces": (Qt.Key_F, Qt.NoModifier), "detect_faces": (Qt.Key_F, Qt.NoModifier),
"detect_pets": (Qt.Key_P, Qt.NoModifier), "detect_pets": (Qt.Key_P, Qt.NoModifier),
"detect_bodies": (Qt.Key_B, Qt.NoModifier),
"fast_tag": (Qt.Key_T, Qt.NoModifier), "fast_tag": (Qt.Key_T, Qt.NoModifier),
"rotate_right": (Qt.Key_Plus, Qt.ControlModifier), "rotate_right": (Qt.Key_Plus, Qt.ControlModifier),
"rotate_left": (Qt.Key_Minus, Qt.ControlModifier), "rotate_left": (Qt.Key_Minus, Qt.ControlModifier),
@@ -395,13 +402,15 @@ _UI_TEXTS = {
"RENAME_VIEWER_ERROR_TEXT": "Could not rename file: {}", "RENAME_VIEWER_ERROR_TEXT": "Could not rename file: {}",
"ADD_FACE_TITLE": "Add Face", "ADD_FACE_TITLE": "Add Face",
"ADD_PET_TITLE": "Add Pet", "ADD_PET_TITLE": "Add Pet",
"ADD_BODY_TITLE": "Add Body",
"ADD_OBJECT_TITLE": "Add Object", "ADD_OBJECT_TITLE": "Add Object",
"ADD_LANDMARK_TITLE": "Add Landmark", "ADD_LANDMARK_TITLE": "Add Landmark",
"ADD_FACE_LABEL": "Name:", "ADD_FACE_LABEL": "Name:",
"ADD_PET_LABEL": "Name:", "ADD_PET_LABEL": "Name:",
"ADD_BODY_LABEL": "Name:",
"ADD_OBJECT_LABEL": "Name:", "ADD_OBJECT_LABEL": "Name:",
"ADD_LANDMARK_LABEL": "Name:", "ADD_LANDMARK_LABEL": "Name:",
"DELETE_FACE": "Delete Face or area", "DELETE_AREA_TITLE": "Delete area",
"CREATE_TAG_TITLE": "Create Tag", "CREATE_TAG_TITLE": "Create Tag",
"CREATE_TAG_TEXT": "The tag for '{}' does not exist. Do you want to create a " "CREATE_TAG_TEXT": "The tag for '{}' does not exist. Do you want to create a "
"new one?", "new one?",
@@ -409,6 +418,8 @@ _UI_TEXTS = {
"NEW_PERSON_TAG_TEXT": "Enter the full path for the tag:", "NEW_PERSON_TAG_TEXT": "Enter the full path for the tag:",
"NEW_PET_TAG_TITLE": "New Pet Tag", "NEW_PET_TAG_TITLE": "New Pet Tag",
"NEW_PET_TAG_TEXT": "Enter the full path for the tag:", "NEW_PET_TAG_TEXT": "Enter the full path for the tag:",
"NEW_BODY_TAG_TITLE": "New Body Tag",
"NEW_BODY_TAG_TEXT": "Enter the full path for the tag:",
"NEW_OBJECT_TAG_TITLE": "New Object Tag", "NEW_OBJECT_TAG_TITLE": "New Object Tag",
"NEW_OBJECT_TAG_TEXT": "Enter the full path for the tag:", "NEW_OBJECT_TAG_TEXT": "Enter the full path for the tag:",
"NEW_LANDMARK_TAG_TITLE": "New Landmark Tag", "NEW_LANDMARK_TAG_TITLE": "New Landmark Tag",
@@ -418,10 +429,11 @@ _UI_TEXTS = {
"one:", "one:",
"FACE_NAME_TOOLTIP": "Type a name or select from history.", "FACE_NAME_TOOLTIP": "Type a name or select from history.",
"CLEAR_TEXT_TOOLTIP": "Clear text field", "CLEAR_TEXT_TOOLTIP": "Clear text field",
"RENAME_FACE_TITLE": "Rename Face or area", "RENAME_AREA_TITLE": "Rename area",
"SHOW_FACES": "Show Faces && other areas", "SHOW_FACES": "Show Faces && other areas",
"DETECT_FACES": "Detect Face", "DETECT_FACES": "Detect Face",
"DETECT_PETS": "Detect Pets", "DETECT_PETS": "Detect Pets",
"DETECT_BODIES": "Detect Bodies",
"NO_FACE_LIBS": "No face detection libraries found. Install 'mediapipe' or " "NO_FACE_LIBS": "No face detection libraries found. Install 'mediapipe' or "
"'face_recognition'.", "'face_recognition'.",
"THUMBNAIL_NO_NAME": "No name", "THUMBNAIL_NO_NAME": "No name",
@@ -441,7 +453,7 @@ _UI_TEXTS = {
"MENU_SHOW_HISTORY": "Show History", "MENU_SHOW_HISTORY": "Show History",
"MENU_SETTINGS": "Settings", "MENU_SETTINGS": "Settings",
"SETTINGS_GROUP_SCANNER": "Scanner", "SETTINGS_GROUP_SCANNER": "Scanner",
"SETTINGS_GROUP_FACES": "Faces && areas", "SETTINGS_GROUP_AREAS": "Areas",
"SETTINGS_GROUP_THUMBNAILS": "Thumbnails", "SETTINGS_GROUP_THUMBNAILS": "Thumbnails",
"SETTINGS_GROUP_VIEWER": "Image Viewer", "SETTINGS_GROUP_VIEWER": "Image Viewer",
"SETTINGS_PERSON_TAGS_LABEL": "Person tags:", "SETTINGS_PERSON_TAGS_LABEL": "Person tags:",
@@ -460,8 +472,19 @@ _UI_TEXTS = {
"to remember.", "to remember.",
"TYPE_FACE": "Face", "TYPE_FACE": "Face",
"TYPE_PET": "Pet", "TYPE_PET": "Pet",
"TYPE_BODY": "Body",
"TYPE_OBJECT": "Object", "TYPE_OBJECT": "Object",
"TYPE_LANDMARK": "Landmark", "TYPE_LANDMARK": "Landmark",
"SETTINGS_BODY_TAGS_LABEL": "Body tags:",
"SETTINGS_BODY_ENGINE_LABEL": "Body Detection Engine:",
"SETTINGS_BODY_COLOR_LABEL": "Body box color:",
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Max body history:",
"SETTINGS_BODY_TAGS_TOOLTIP": "Default tags for bodies, separated by commas.",
"SETTINGS_BODY_ENGINE_TOOLTIP": "Library used for body detection.",
"SETTINGS_BODY_COLOR_TOOLTIP": "Color of the bounding box drawn around "
"detected bodies.",
"SETTINGS_BODY_HISTORY_TOOLTIP": "Maximum number of recently used body names "
"to remember.",
"SETTINGS_OBJECT_TAGS_LABEL": "Object tags:", "SETTINGS_OBJECT_TAGS_LABEL": "Object tags:",
"SETTINGS_OBJECT_ENGINE_LABEL": "Object Detection Engine:", "SETTINGS_OBJECT_ENGINE_LABEL": "Object Detection Engine:",
"SETTINGS_OBJECT_COLOR_LABEL": "Object box color:", "SETTINGS_OBJECT_COLOR_LABEL": "Object box color:",
@@ -493,12 +516,15 @@ _UI_TEXTS = {
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Thumbnails rating color:", "SETTINGS_THUMBS_RATING_COLOR_LABEL": "Thumbnails rating color:",
"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_TOOLTIP": "Maximum number of simultaneous threads to"
"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:",
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Scan Full On Start:", "SETTINGS_SCAN_FULL_ON_START_LABEL": "Scan Full On Start:",
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "File search engine:", "SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "File search engine:",
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Engine to use for finding files. " "SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Engine to use for finding files. "
"'Native' uses BagheeraSearch library. 'baloosearch' uses KDE Baloo command.", "'Bagheera' uses BagheeraSearch library. 'Baloo' uses 'baloosearch' command.",
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Maximum directory depth to scan " "SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Maximum directory depth to scan "
"recursively.", "recursively.",
"SETTINGS_SCAN_BATCH_SIZE_TOOLTIP": "Number of images to load in each batch.", "SETTINGS_SCAN_BATCH_SIZE_TOOLTIP": "Number of images to load in each batch.",
@@ -524,8 +550,8 @@ _UI_TEXTS = {
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_TOOLTIP": "Font size for filenames in " "SETTINGS_THUMBS_FILENAME_FONT_SIZE_TOOLTIP": "Font size for filenames in "
"thumbnails.", "thumbnails.",
"SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Font size for tags in thumbnails.", "SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Font size for tags in thumbnails.",
"SEARCH_ENGINE_NATIVE": "Native", "SEARCH_ENGINE_NATIVE": "Bagheera",
"SEARCH_ENGINE_BALOO": "baloosearch", "SEARCH_ENGINE_BALOO": "Baloo",
"SETTINGS_VIEWER_WHEEL_SPEED_LABEL": "Viewer mouse wheel speed:", "SETTINGS_VIEWER_WHEEL_SPEED_LABEL": "Viewer mouse wheel speed:",
"SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Filename lines:", "SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Filename lines:",
"SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Number of lines for the filename " "SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Number of lines for the filename "
@@ -801,19 +827,23 @@ _UI_TEXTS = {
"RENAME_VIEWER_ERROR_TEXT": "No se pudo renombrar el archivo: {}", "RENAME_VIEWER_ERROR_TEXT": "No se pudo renombrar el archivo: {}",
"ADD_FACE_TITLE": "Añadir Rostro", "ADD_FACE_TITLE": "Añadir Rostro",
"ADD_PET_TITLE": "Añadir Mascota", "ADD_PET_TITLE": "Añadir Mascota",
"ADD_BODY_TITLE": "Añadir Cuerpo",
"ADD_OBJECT_TITLE": "Añadir Objeto", "ADD_OBJECT_TITLE": "Añadir Objeto",
"ADD_LANDMARK_TITLE": "Añadir Lugar", "ADD_LANDMARK_TITLE": "Añadir Lugar",
"ADD_FACE_LABEL": "Nombre:", "ADD_FACE_LABEL": "Nombre:",
"ADD_PET_LABEL": "Nombre:", "ADD_PET_LABEL": "Nombre:",
"ADD_BODY_LABEL": "Nombre:",
"ADD_OBJECT_LABEL": "Nombre:", "ADD_OBJECT_LABEL": "Nombre:",
"ADD_LANDMARK_LABEL": "Nombre:", "ADD_LANDMARK_LABEL": "Nombre:",
"DELETE_FACE": "Eliminar Rostro o área", "DELETE_AREA_TITLE": "Eliminar área",
"CREATE_TAG_TITLE": "Crear Etiqueta", "CREATE_TAG_TITLE": "Crear Etiqueta",
"CREATE_TAG_TEXT": "La etiqueta para '{}' no existe. ¿Deseas crear una nueva?", "CREATE_TAG_TEXT": "La etiqueta para '{}' no existe. ¿Deseas crear una nueva?",
"NEW_PERSON_TAG_TITLE": "Nueva Etiqueta de Persona", "NEW_PERSON_TAG_TITLE": "Nueva Etiqueta de Persona",
"NEW_PERSON_TAG_TEXT": "Introduce la ruta completa de la etiqueta:", "NEW_PERSON_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
"NEW_PET_TAG_TITLE": "Nueva Etiqueta de Mascota", "NEW_PET_TAG_TITLE": "Nueva Etiqueta de Mascota",
"NEW_PET_TAG_TEXT": "Introduce la ruta completa de la etiqueta:", "NEW_PET_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
"NEW_BODY_TAG_TITLE": "Nueva Etiqueta de Cuerpo",
"NEW_BODY_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
"NEW_OBJECT_TAG_TITLE": "Nueva Etiqueta de Objeto", "NEW_OBJECT_TAG_TITLE": "Nueva Etiqueta de Objeto",
"NEW_OBJECT_TAG_TEXT": "Introduce la ruta completa de la etiqueta:", "NEW_OBJECT_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
"NEW_LANDMARK_TAG_TITLE": "Nueva Etiqueta de Lugar", "NEW_LANDMARK_TAG_TITLE": "Nueva Etiqueta de Lugar",
@@ -823,10 +853,11 @@ _UI_TEXTS = {
"selecciona la correcta:", "selecciona la correcta:",
"FACE_NAME_TOOLTIP": "Escribe un nombre o selecciónalo del historial.", "FACE_NAME_TOOLTIP": "Escribe un nombre o selecciónalo del historial.",
"CLEAR_TEXT_TOOLTIP": "Limpiar el campo de texto", "CLEAR_TEXT_TOOLTIP": "Limpiar el campo de texto",
"RENAME_FACE_TITLE": "Renombrar Rostro o área", "RENAME_AREA_TITLE": "Renombrar área",
"SHOW_FACES": "Mostrar Rostros y otras áreas", "SHOW_FACES": "Mostrar Rostros y otras áreas",
"DETECT_FACES": "Detectar Rostros", "DETECT_FACES": "Detectar Rostros",
"DETECT_PETS": "Detectar Mascotas", "DETECT_PETS": "Detectar Mascotas",
"DETECT_BODIES": "Detectar Cuerpos",
"NO_FACE_LIBS": "No se encontraron librerías de detección de rostros. Instale " "NO_FACE_LIBS": "No se encontraron librerías de detección de rostros. Instale "
"'mediapipe' o 'face_recognition'.", "'mediapipe' o 'face_recognition'.",
"THUMBNAIL_NO_NAME": "Sin nombre", "THUMBNAIL_NO_NAME": "Sin nombre",
@@ -846,7 +877,7 @@ _UI_TEXTS = {
"MENU_SHOW_HISTORY": "Mostrar Historial", "MENU_SHOW_HISTORY": "Mostrar Historial",
"MENU_SETTINGS": "Opciones", "MENU_SETTINGS": "Opciones",
"SETTINGS_GROUP_SCANNER": "Escáner", "SETTINGS_GROUP_SCANNER": "Escáner",
"SETTINGS_GROUP_FACES": "Rostros y áreas", "SETTINGS_GROUP_AREAS": "Áreas",
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas", "SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
"SETTINGS_GROUP_VIEWER": "Visor de Imágenes", "SETTINGS_GROUP_VIEWER": "Visor de Imágenes",
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:", "SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:",
@@ -867,8 +898,21 @@ _UI_TEXTS = {
"usados recientemente para recordar.", "usados recientemente para recordar.",
"TYPE_FACE": "Cara", "TYPE_FACE": "Cara",
"TYPE_PET": "Mascota", "TYPE_PET": "Mascota",
"TYPE_BODY": "Cuerpo",
"TYPE_OBJECT": "Objeto", "TYPE_OBJECT": "Objeto",
"TYPE_LANDMARK": "Lugar", "TYPE_LANDMARK": "Lugar",
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de cuerpo:",
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de cuerpos:",
"SETTINGS_BODY_COLOR_LABEL": "Color del recuadro de cuerpo:",
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial cuerpos:",
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para cuerpos, "
"separadas por comas.",
"SETTINGS_BODY_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
"cuerpos.",
"SETTINGS_BODY_COLOR_TOOLTIP": "Color del cuadro delimitador dibujado "
"alrededor de los cuerpos detectados.",
"SETTINGS_BODY_HISTORY_TOOLTIP": "Número máximo de nombres de cuerpos "
"usados recientemente para recordar.",
"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:",
@@ -906,8 +950,8 @@ _UI_TEXTS = {
"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. "
"'Nativo' usa la librería de BagheeraSearch. 'baloosearch' usa el commando de" "'Bagheera' usa la librería de BagheeraSearch. 'Baloo0 usa el commando "
"KDE Baloo.", "'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 "
"escanear recursivamente.", "escanear recursivamente.",
@@ -1213,19 +1257,23 @@ _UI_TEXTS = {
"RENAME_VIEWER_ERROR_TEXT": "Non se puido renomear o ficheiro: {}", "RENAME_VIEWER_ERROR_TEXT": "Non se puido renomear o ficheiro: {}",
"ADD_FACE_TITLE": "Engadir Rostro", "ADD_FACE_TITLE": "Engadir Rostro",
"ADD_PET_TITLE": "Engadir Mascota", "ADD_PET_TITLE": "Engadir Mascota",
"ADD_BODY_TITLE": "Engadir Corpo",
"ADD_OBJECT_TITLE": "Engadir Obxecto", "ADD_OBJECT_TITLE": "Engadir Obxecto",
"ADD_LANDMARK_TITLE": "Engadir Lugar", "ADD_LANDMARK_TITLE": "Engadir Lugar",
"ADD_FACE_LABEL": "Nome:", "ADD_FACE_LABEL": "Nome:",
"ADD_PET_LABEL": "Nome:", "ADD_PET_LABEL": "Nome:",
"ADD_BODY_LABEL": "Nome:",
"ADD_OBJECT_LABEL": "Nome:", "ADD_OBJECT_LABEL": "Nome:",
"ADD_LANDMARK_LABEL": "Nome:", "ADD_LANDMARK_LABEL": "Nome:",
"DELETE_FACE": "Eliminar Rostro ou área", "DELETE_AREA_TITLE": "Eliminar área",
"CREATE_TAG_TITLE": "Crear Etiqueta", "CREATE_TAG_TITLE": "Crear Etiqueta",
"CREATE_TAG_TEXT": "A etiqueta para '{}' non existe. Desexas crear unha nova?", "CREATE_TAG_TEXT": "A etiqueta para '{}' non existe. Desexas crear unha nova?",
"NEW_PERSON_TAG_TITLE": "Nova Etiqueta de Persoa", "NEW_PERSON_TAG_TITLE": "Nova Etiqueta de Persoa",
"NEW_PERSON_TAG_TEXT": "Introduce a ruta completa da etiqueta:", "NEW_PERSON_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
"NEW_PET_TAG_TITLE": "Nova Etiqueta de Mascota", "NEW_PET_TAG_TITLE": "Nova Etiqueta de Mascota",
"NEW_PET_TAG_TEXT": "Introduce a ruta completa da etiqueta:", "NEW_PET_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
"NEW_BODY_TAG_TITLE": "Nova Etiqueta de Corpo",
"NEW_BODY_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
"NEW_OBJECT_TAG_TITLE": "Nova Etiqueta de Obxecto", "NEW_OBJECT_TAG_TITLE": "Nova Etiqueta de Obxecto",
"NEW_OBJECT_TAG_TEXT": "Introduce a ruta completa da etiqueta:", "NEW_OBJECT_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
"NEW_LANDMARK_TAG_TITLE": "Nova Etiqueta de Lugar", "NEW_LANDMARK_TAG_TITLE": "Nova Etiqueta de Lugar",
@@ -1235,10 +1283,11 @@ _UI_TEXTS = {
"selecciona a correcta:", "selecciona a correcta:",
"FACE_NAME_TOOLTIP": "Escribe un nome ou selecciónao do historial.", "FACE_NAME_TOOLTIP": "Escribe un nome ou selecciónao do historial.",
"CLEAR_TEXT_TOOLTIP": "Limpar o campo de texto", "CLEAR_TEXT_TOOLTIP": "Limpar o campo de texto",
"RENAME_FACE_TITLE": "Renomear Rostro ou área", "RENAME_AREA_TITLE": "Renomear área",
"SHOW_FACES": "Amosar Rostros e outras áreas", "SHOW_FACES": "Amosar Rostros e outras áreas",
"DETECT_FACES": "Detectar Rostros", "DETECT_FACES": "Detectar Rostros",
"DETECT_PETS": "Detectar Mascotas", "DETECT_PETS": "Detectar Mascotas",
"DETECT_BODIES": "Detectar Corpos",
"NO_FACE_LIBS": "Non se atoparon librarías de detección de rostros. Instale " "NO_FACE_LIBS": "Non se atoparon librarías de detección de rostros. Instale "
"'mediapipe' ou 'face_recognition'.", "'mediapipe' ou 'face_recognition'.",
"THUMBNAIL_NO_NAME": "Sen nome", "THUMBNAIL_NO_NAME": "Sen nome",
@@ -1259,7 +1308,7 @@ _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_FACES": "Rostros e á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:",
@@ -1280,8 +1329,21 @@ _UI_TEXTS = {
"recentemente para lembrar.", "recentemente para lembrar.",
"TYPE_FACE": "Cara", "TYPE_FACE": "Cara",
"TYPE_PET": "Mascota", "TYPE_PET": "Mascota",
"TYPE_BODY": "Corpo",
"TYPE_OBJECT": "Obxecto", "TYPE_OBJECT": "Obxecto",
"TYPE_LANDMARK": "Lugar", "TYPE_LANDMARK": "Lugar",
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de corpo:",
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de corpos:",
"SETTINGS_BODY_COLOR_LABEL": "Cor do cadro de corpo:",
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial corpos:",
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para corpos, "
"separadas por comas.",
"SETTINGS_BODY_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
"corpos.",
"SETTINGS_BODY_COLOR_TOOLTIP": "Cor do cadro delimitador debuxado arredor "
"dos corpos detectados.",
"SETTINGS_BODY_HISTORY_TOOLTIP": "Número máximo de nomes de corpos usados "
"recentemente para lembrar.",
"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:",
@@ -1322,8 +1384,8 @@ _UI_TEXTS = {
"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:",
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar ficheiros. " "SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar ficheiros. "
"'Nativo' usa la librería de BagheeraSearch. 'baloosearch' usa o comando de " "'Bagheera' usa a libraría de BagheeraSearch. 'Baloo' usa o comando de "
"KDE Baloo.", "'baloosearch'.",
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo ao Inicio:", "SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo ao Inicio:",
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidade máxima de directorio para " "SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidade máxima de directorio para "
"escanear recursivamente.", "escanear recursivamente.",
@@ -1354,8 +1416,8 @@ _UI_TEXTS = {
"ficheiro en miniaturas.", "ficheiro en miniaturas.",
"SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Tamaño de fonte para etiquetas en " "SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Tamaño de fonte para etiquetas en "
"miniaturas.", "miniaturas.",
"SEARCH_ENGINE_NATIVE": "Nativo", "SEARCH_ENGINE_NATIVE": "Bagheera",
"SEARCH_ENGINE_BALOO": "baloosearch", "SEARCH_ENGINE_BALOO": "Baloo",
"SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Liñas para nome de ficheiro:", "SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Liñas para nome de ficheiro:",
"SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Número de liñas para o nome do " "SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Número de liñas para o nome do "
"ficheiro debaixo da miniatura.", "ficheiro debaixo da miniatura.",

View File

@@ -16,11 +16,11 @@ from PySide6.QtCore import QThread, Signal, QMutex, QWaitCondition, QObject, Qt
from PySide6.QtGui import QImage, QImageReader, QPixmap, QTransform from PySide6.QtGui import QImage, QImageReader, QPixmap, QTransform
from xmpmanager import XmpManager from xmpmanager import XmpManager
from constants import ( from constants import (
APP_CONFIG, AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES, APP_CONFIG, AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES, AVAILABLE_BODY_ENGINES,
MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL, MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL, MEDIAPIPE_OBJECT_MODEL_PATH,
MEDIAPIPE_OBJECT_MODEL_URL, RATING_XATTR_NAME, XATTR_NAME, UITexts MEDIAPIPE_OBJECT_MODEL_URL, RATING_XATTR_NAME, XATTR_NAME, UITexts
) )
from metadatamanager import XattrManager from metadatamanager import XattrManager, load_common_metadata
class ImagePreloader(QThread): class ImagePreloader(QThread):
@@ -78,21 +78,6 @@ class ImagePreloader(QThread):
self.mutex.unlock() self.mutex.unlock()
self.wait() self.wait()
def _load_metadata(self, path):
"""Loads tag and rating data for a path."""
tags = []
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
if raw_tags:
tags = sorted(list(set(t.strip()
for t in raw_tags.split(',') if t.strip())))
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
try:
rating = int(raw_rating)
except ValueError:
rating = 0
return tags, rating
def run(self): def run(self):
""" """
The main execution loop for the thread. The main execution loop for the thread.
@@ -124,7 +109,7 @@ class ImagePreloader(QThread):
img = reader.read() img = reader.read()
if not img.isNull(): if not img.isNull():
# Load tags and rating here to avoid re-reading in main thread # Load tags and rating here to avoid re-reading in main thread
tags, rating = self._load_metadata(path) tags, rating = load_common_metadata(path)
self.image_ready.emit(idx, path, img, tags, rating) self.image_ready.emit(idx, path, img, tags, rating)
except Exception: except Exception:
pass pass
@@ -157,6 +142,8 @@ class ImageController(QObject):
self.faces = [] self.faces = []
self._current_tags = initial_tags if initial_tags is not None else [] self._current_tags = initial_tags if initial_tags is not None else []
self._current_rating = initial_rating self._current_rating = initial_rating
self._current_metadata_path = None
self._loaded_path = None
self.show_faces = False self.show_faces = False
# Preloading # Preloading
@@ -219,11 +206,27 @@ class ImageController(QObject):
Loads the current image into the controller's main pixmap. Loads the current image into the controller's main pixmap.
""" """
path = self.get_current_path() path = self.get_current_path()
self.pixmap_original = QPixmap()
# Optimization: Check if image is already loaded
if path and self._loaded_path == path and not self.pixmap_original.isNull():
self.rotation = 0
self.flip_h = False
self.flip_v = False
self.faces = []
# Ensure metadata is consistent with current path
if self._current_metadata_path != path:
self._current_tags, self._current_rating = load_common_metadata(path)
self._current_metadata_path = path
self.load_faces()
self._trigger_preload()
return True
self.pixmap_original = QPixmap()
self._loaded_path = None
self.rotation = 0 self.rotation = 0
self.flip_h = False self.flip_h = False
self._current_tags = []
self._current_rating = 0
self.flip_v = False self.flip_v = False
self.faces = [] self.faces = []
@@ -236,6 +239,7 @@ class ImageController(QObject):
# Clear cache to free memory as we have consumed the image # Clear cache to free memory as we have consumed the image
self._current_tags = self._cached_next_tags self._current_tags = self._cached_next_tags
self._current_rating = self._cached_next_rating self._current_rating = self._cached_next_rating
self._current_metadata_path = path
self._cached_next_image = None self._cached_next_image = None
self._cached_next_index = -1 self._cached_next_index = -1
self._cached_next_tags = None self._cached_next_tags = None
@@ -249,9 +253,12 @@ class ImageController(QObject):
return False return False
self.pixmap_original = QPixmap.fromImage(image) self.pixmap_original = QPixmap.fromImage(image)
# Load tags and rating if not from cache # Load tags and rating if not already set for this path
self._current_tags, self._current_rating = self._load_metadata(path) if self._current_metadata_path != path:
self._current_tags, self._current_rating = load_common_metadata(path)
self._current_metadata_path = path
self._loaded_path = path
self.load_faces() self.load_faces()
self._trigger_preload() self._trigger_preload()
return True return True
@@ -422,6 +429,38 @@ class ImageController(QObject):
face_data['h'] = h face_data['h'] = h
return face_data return face_data
def _create_region_from_pixels(self, x, y, w, h, img_w, img_h, region_type):
"""
Creates a normalized region dictionary from pixel coordinates.
Args:
x (float): Top-left x coordinate in pixels.
y (float): Top-left y coordinate in pixels.
w (float): Width in pixels.
h (float): Height in pixels.
img_w (int): Image width in pixels.
img_h (int): Image height in pixels.
region_type (str): The type of region (Face, Pet, Body).
Returns:
dict: Validated normalized region or None.
"""
if img_w <= 0 or img_h <= 0:
return None
if w <= 0 or h <= 0:
return None
new_region = {
'name': '',
'x': (x + w / 2) / img_w,
'y': (y + h / 2) / img_h,
'w': w / img_w,
'h': h / img_h,
'type': region_type
}
return self._clamp_and_validate_face(new_region)
def _detect_faces_face_recognition(self, path): def _detect_faces_face_recognition(self, path):
"""Detects faces using the 'face_recognition' library.""" """Detects faces using the 'face_recognition' library."""
import face_recognition import face_recognition
@@ -433,12 +472,9 @@ class ImageController(QObject):
for (top, right, bottom, left) in face_locations: for (top, right, bottom, left) in face_locations:
box_w = right - left box_w = right - left
box_h = bottom - top box_h = bottom - top
new_face = { validated_face = self._create_region_from_pixels(
'name': '', left, top, box_w, box_h, w, h, 'Face'
'x': (left + box_w / 2) / w, 'y': (top + box_h / 2) / h, )
'w': box_w / w, 'h': box_h / h, 'type': 'Face'
}
validated_face = self._clamp_and_validate_face(new_face)
if validated_face: if validated_face:
new_faces.append(validated_face) new_faces.append(validated_face)
except Exception as e: except Exception as e:
@@ -484,15 +520,10 @@ class ImageController(QObject):
img_h, img_w = mp_image.height, mp_image.width img_h, img_w = mp_image.height, mp_image.width
for detection in detection_result.detections: for detection in detection_result.detections:
bbox = detection.bounding_box # This is in pixels bbox = detection.bounding_box # This is in pixels
new_face = { validated_face = self._create_region_from_pixels(
'name': '', bbox.origin_x, bbox.origin_y, bbox.width, bbox.height,
'x': (bbox.origin_x + bbox.width / 2) / img_w, img_w, img_h, 'Face'
'y': (bbox.origin_y + bbox.height / 2) / img_h, )
'w': bbox.width / img_w,
'h': bbox.height / img_h,
'type': 'Face'
}
validated_face = self._clamp_and_validate_face(new_face)
if validated_face: if validated_face:
new_faces.append(validated_face) new_faces.append(validated_face)
@@ -500,19 +531,27 @@ class ImageController(QObject):
print(f"Error during MediaPipe detection: {e}") print(f"Error during MediaPipe detection: {e}")
return new_faces return new_faces
def _detect_pets_mediapipe(self, path): def _detect_objects_mediapipe(self, path, allowlist, max_results, region_type):
"""Detects pets using the 'mediapipe' library object detection.""" """
Generic method to detect objects using MediaPipe ObjectDetector.
Args:
path (str): Path to image file.
allowlist (list): List of category names to detect.
max_results (int): Maximum number of results to return.
region_type (str): The 'type' label for the detected regions.
"""
import mediapipe as mp import mediapipe as mp
from mediapipe.tasks import python from mediapipe.tasks import python
from mediapipe.tasks.python import vision from mediapipe.tasks.python import vision
new_pets = [] new_regions = []
if not os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH): if not os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH):
print(f"MediaPipe model not found at: {MEDIAPIPE_OBJECT_MODEL_PATH}") print(f"MediaPipe model not found at: {MEDIAPIPE_OBJECT_MODEL_PATH}")
print("Please download 'efficientdet_lite0.tflite' and place it there.") print("Please download 'efficientdet_lite0.tflite' and place it there.")
print(f"URL: {MEDIAPIPE_OBJECT_MODEL_URL}") print(f"URL: {MEDIAPIPE_OBJECT_MODEL_URL}")
return new_pets return new_regions
try: try:
base_options = python.BaseOptions( base_options = python.BaseOptions(
@@ -520,8 +559,8 @@ class ImageController(QObject):
options = vision.ObjectDetectorOptions( options = vision.ObjectDetectorOptions(
base_options=base_options, base_options=base_options,
score_threshold=0.5, score_threshold=0.5,
max_results=5, max_results=max_results,
category_allowlist=["cat", "dog"]) # Detect cats and dogs category_allowlist=allowlist)
# Silence MediaPipe warnings (stderr) during initialization # Silence MediaPipe warnings (stderr) during initialization
stderr_fd = 2 stderr_fd = 2
@@ -542,21 +581,24 @@ class ImageController(QObject):
img_h, img_w = mp_image.height, mp_image.width img_h, img_w = mp_image.height, mp_image.width
for detection in detection_result.detections: for detection in detection_result.detections:
bbox = detection.bounding_box bbox = detection.bounding_box
new_pet = { validated_region = self._create_region_from_pixels(
'name': '', bbox.origin_x, bbox.origin_y, bbox.width, bbox.height,
'x': (bbox.origin_x + bbox.width / 2) / img_w, img_w, img_h, region_type
'y': (bbox.origin_y + bbox.height / 2) / img_h, )
'w': bbox.width / img_w, if validated_region:
'h': bbox.height / img_h, new_regions.append(validated_region)
'type': 'Pet'
}
validated_pet = self._clamp_and_validate_face(new_pet)
if validated_pet:
new_pets.append(validated_pet)
except Exception as e: except Exception as e:
print(f"Error during MediaPipe pet detection: {e}") print(f"Error during MediaPipe {region_type} detection: {e}")
return new_pets return new_regions
def _detect_pets_mediapipe(self, path):
"""Detects pets using the 'mediapipe' library object detection."""
return self._detect_objects_mediapipe(path, ["cat", "dog"], 5, "Pet")
def _detect_bodies_mediapipe(self, path):
"""Detects bodies using the 'mediapipe' library object detection."""
return self._detect_objects_mediapipe(path, ["person"], 10, "Body")
def detect_faces(self): def detect_faces(self):
""" """
@@ -615,6 +657,21 @@ class ImageController(QObject):
return [] return []
def detect_bodies(self):
"""
Detects bodies using a configured or available detection engine.
"""
path = self.get_current_path()
if not path:
return []
engine = APP_CONFIG.get("body_detection_engine", "mediapipe")
if engine == "mediapipe" and "mediapipe" in AVAILABLE_BODY_ENGINES:
return self._detect_bodies_mediapipe(path)
return []
def get_display_pixmap(self): def get_display_pixmap(self):
""" """
Applies current transformations (rotation, zoom, flip) to the original Applies current transformations (rotation, zoom, flip) to the original
@@ -709,30 +766,27 @@ class ImageController(QObject):
elif self.index < 0: elif self.index < 0:
self.index = 0 self.index = 0
# Update current image metadata if provided # Update current image metadata
self._current_tags = current_image_tags \ if current_image_tags is not None:
if current_image_tags is not None else [] self._current_tags = current_image_tags
self._current_rating = current_image_rating self._current_rating = current_image_rating
self._current_metadata_path = self.get_current_path()
else:
# Reload from disk if not provided to ensure consistency
path = self.get_current_path()
if path:
self._current_tags, self._current_rating = load_common_metadata(path)
self._current_metadata_path = path
else:
self._current_tags = []
self._current_rating = 0
self._current_metadata_path = None
self._cached_next_image = None self._cached_next_image = None
self._cached_next_index = -1 self._cached_next_index = -1
self._trigger_preload() self._trigger_preload()
self.list_updated.emit(self.index) self.list_updated.emit(self.index)
def _load_metadata(self, path):
"""Loads tag and rating data for a path."""
tags = []
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
if raw_tags:
tags = sorted(list(set(t.strip()
for t in raw_tags.split(',') if t.strip())))
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
try:
rating = int(raw_rating)
except ValueError:
rating = 0
return tags, rating
def update_list_on_exists(self, new_list, new_index=None): def update_list_on_exists(self, new_list, new_index=None):
""" """
Updates the list only if the old list is a subset of the new one. Updates the list only if the old list is a subset of the new one.
@@ -749,8 +803,17 @@ class ImageController(QObject):
self.index = new_index self.index = new_index
if self.index >= len(self.image_list): if self.index >= len(self.image_list):
self.index = max(0, len(self.image_list) - 1) self.index = max(0, len(self.image_list) - 1)
self._current_tags = [] # Clear current tags/rating, will be reloaded
# Reload metadata for the current image to avoid stale/empty state
path = self.get_current_path()
if path:
self._current_tags, self._current_rating = load_common_metadata(path)
self._current_metadata_path = path
else:
self._current_tags = []
self._current_rating = 0 self._current_rating = 0
self._current_metadata_path = None
self._cached_next_image = None self._cached_next_image = None
self._cached_next_index = -1 self._cached_next_index = -1
self._trigger_preload() self._trigger_preload()

View File

@@ -1404,8 +1404,8 @@ class ImageScanner(QThread):
return None, [] return None, []
def _search(self, query): def _search(self, query):
engine = APP_CONFIG.get("search_engine", "Native") engine = APP_CONFIG.get("search_engine", "Bagheera")
if HAVE_BAGHEERASEARCH_LIB and (engine == "Native" or not SEARCH_CMD): if HAVE_BAGHEERASEARCH_LIB and (engine == "Bagheera" or not SEARCH_CMD):
query_text, main_options, other_options = self._parse_query(query) query_text, main_options, other_options = self._parse_query(query)
try: try:
searcher = BagheeraSearcher() searcher = BagheeraSearcher()

View File

@@ -29,6 +29,7 @@ from PySide6.QtCore import (
from constants import ( from constants import (
APP_CONFIG, DEFAULT_FACE_BOX_COLOR, DEFAULT_PET_BOX_COLOR, DEFAULT_VIEWER_SHORTCUTS, APP_CONFIG, DEFAULT_FACE_BOX_COLOR, DEFAULT_PET_BOX_COLOR, DEFAULT_VIEWER_SHORTCUTS,
DEFAULT_BODY_BOX_COLOR,
DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR, FORCE_X11, ICON_THEME_VIEWER, DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR, FORCE_X11, ICON_THEME_VIEWER,
ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH, ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH,
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL, VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL,
@@ -47,6 +48,9 @@ class FaceNameDialog(QDialog):
if region_type == "Pet": if region_type == "Pet":
self.setWindowTitle(UITexts.ADD_PET_TITLE) self.setWindowTitle(UITexts.ADD_PET_TITLE)
layout_label = UITexts.ADD_PET_LABEL layout_label = UITexts.ADD_PET_LABEL
elif region_type == "Body":
self.setWindowTitle(UITexts.ADD_BODY_TITLE)
layout_label = UITexts.ADD_BODY_LABEL
elif region_type == "Object": elif region_type == "Object":
self.setWindowTitle(UITexts.ADD_OBJECT_TITLE) self.setWindowTitle(UITexts.ADD_OBJECT_TITLE)
layout_label = UITexts.ADD_OBJECT_LABEL layout_label = UITexts.ADD_OBJECT_LABEL
@@ -441,6 +445,8 @@ class FaceCanvas(QLabel):
face_color = QColor(face_color_str) face_color = QColor(face_color_str)
pet_color_str = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR) pet_color_str = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR)
pet_color = QColor(pet_color_str) pet_color = QColor(pet_color_str)
body_color_str = APP_CONFIG.get("body_box_color", DEFAULT_BODY_BOX_COLOR)
body_color = QColor(body_color_str)
object_color_str = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR) object_color_str = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR)
object_color = QColor(object_color_str) object_color = QColor(object_color_str)
landmark_color_str = APP_CONFIG.get("landmark_box_color", landmark_color_str = APP_CONFIG.get("landmark_box_color",
@@ -452,11 +458,14 @@ class FaceCanvas(QLabel):
rect = self.map_from_source(face) rect = self.map_from_source(face)
is_pet = face.get('type') == 'Pet' is_pet = face.get('type') == 'Pet'
is_body = face.get('type') == 'Body'
is_object = face.get('type') == 'Object' is_object = face.get('type') == 'Object'
is_landmark = face.get('type') == 'Landmark' is_landmark = face.get('type') == 'Landmark'
if is_pet: if is_pet:
color = pet_color color = pet_color
elif is_body:
color = body_color
elif is_object: elif is_object:
color = object_color color = object_color
elif is_landmark: elif is_landmark:
@@ -677,8 +686,10 @@ class FaceCanvas(QLabel):
elif event.button() == Qt.LeftButton: elif event.button() == Qt.LeftButton:
self.dragging = True self.dragging = True
self.drag_start_pos = event.globalPosition().toPoint() self.drag_start_pos = event.globalPosition().toPoint()
self.drag_start_scroll_x = self.viewer.scroll_area.horizontalScrollBar().value() self.drag_start_scroll_x = \
self.drag_start_scroll_y = self.viewer.scroll_area.verticalScrollBar().value() self.viewer.scroll_area.horizontalScrollBar().value()
self.drag_start_scroll_y = \
self.viewer.scroll_area.verticalScrollBar().value()
self.setCursor(Qt.ClosedHandCursor) self.setCursor(Qt.ClosedHandCursor)
event.accept() event.accept()
else: else:
@@ -863,12 +874,15 @@ class FaceCanvas(QLabel):
menu = QMenu(self) menu = QMenu(self)
action_face = menu.addAction(UITexts.TYPE_FACE) action_face = menu.addAction(UITexts.TYPE_FACE)
action_pet = menu.addAction(UITexts.TYPE_PET) action_pet = menu.addAction(UITexts.TYPE_PET)
action_body = menu.addAction(UITexts.TYPE_BODY)
action_object = menu.addAction(UITexts.TYPE_OBJECT) action_object = menu.addAction(UITexts.TYPE_OBJECT)
action_landmark = menu.addAction(UITexts.TYPE_LANDMARK) action_landmark = menu.addAction(UITexts.TYPE_LANDMARK)
# Show menu at mouse release position # Show menu at mouse release position
res = menu.exec(event.globalPosition().toPoint()) res = menu.exec(event.globalPosition().toPoint())
if res == action_pet: if res == action_pet:
region_type = "Pet" region_type = "Pet"
elif res == action_body:
region_type = "Body"
elif res == action_object: elif res == action_object:
region_type = "Object" region_type = "Object"
elif res == action_landmark: elif res == action_landmark:
@@ -885,6 +899,8 @@ class FaceCanvas(QLabel):
if self.viewer.main_win: if self.viewer.main_win:
if region_type == "Pet": if region_type == "Pet":
history_list = self.viewer.main_win.pet_names_history history_list = self.viewer.main_win.pet_names_history
elif region_type == "Body":
history_list = self.viewer.main_win.body_names_history
elif region_type == "Object": elif region_type == "Object":
history_list = self.viewer.main_win.object_names_history history_list = self.viewer.main_win.object_names_history
elif region_type == "Landmark": elif region_type == "Landmark":
@@ -903,6 +919,8 @@ class FaceCanvas(QLabel):
if self.viewer.main_win: if self.viewer.main_win:
if region_type == "Pet": if region_type == "Pet":
self.viewer.main_win.pet_names_history = updated_history self.viewer.main_win.pet_names_history = updated_history
elif region_type == "Body":
self.viewer.main_win.body_names_history = updated_history
elif region_type == "Object": elif region_type == "Object":
self.viewer.main_win.object_names_history = updated_history self.viewer.main_win.object_names_history = updated_history
elif region_type == "Landmark": elif region_type == "Landmark":
@@ -1192,6 +1210,7 @@ class ImageViewer(QWidget):
"flip_vertical": self.toggle_flip_vertical, "flip_vertical": self.toggle_flip_vertical,
"detect_faces": self.run_face_detection, "detect_faces": self.run_face_detection,
"detect_pets": self.run_pet_detection, "detect_pets": self.run_pet_detection,
"detect_bodies": self.run_body_detection,
"fast_tag": self.show_fast_tag_menu, "fast_tag": self.show_fast_tag_menu,
"rotate_right": lambda: self.apply_rotation(90, True), "rotate_right": lambda: self.apply_rotation(90, True),
"rotate_left": lambda: self.apply_rotation(-90, True), "rotate_left": lambda: self.apply_rotation(-90, True),
@@ -1217,7 +1236,7 @@ class ImageViewer(QWidget):
Optimized to update the existing list if possible, rather than Optimized to update the existing list if possible, rather than
rebuilding it entirely. rebuilding it entirely.
""" """
if not self.filmstrip.isVisible(): if self.filmstrip.isHidden():
return return
# --- OPTIMIZATION --- # --- OPTIMIZATION ---
@@ -1887,9 +1906,14 @@ class ImageViewer(QWidget):
zoom = int(self.controller.zoom_factor * 100) zoom = int(self.controller.zoom_factor * 100)
self.sb_info_label.setText(f"{w} x {h} px | {zoom}%") self.sb_info_label.setText(f"{w} x {h} px | {zoom}%")
# Use tags from controller's internal state # Use tags from metadata if provided (priority to avoid race conditions),
# otherwise fallback to controller's internal state.
tags_source = self.controller._current_tags
if metadata and 'tags' in metadata:
tags_source = metadata['tags']
display_tags = [t.strip().split('/')[-1] display_tags = [t.strip().split('/')[-1]
for t in self.controller._current_tags if t.strip()] for t in tags_source if t.strip()]
self.sb_tags_label.setText(", ".join(display_tags)) self.sb_tags_label.setText(", ".join(display_tags))
@Slot(str, dict) @Slot(str, dict)
@@ -2080,8 +2104,8 @@ class ImageViewer(QWidget):
return False return False
menu = QMenu(self) menu = QMenu(self)
action_del = menu.addAction(UITexts.DELETE_FACE) action_del = menu.addAction(UITexts.DELETE_AREA_TITLE)
action_ren = menu.addAction(UITexts.RENAME_FACE_TITLE) action_ren = menu.addAction(UITexts.RENAME_AREA_TITLE)
res = menu.exec(event.globalPos()) res = menu.exec(event.globalPos())
if res == action_del: if res == action_del:
@@ -2107,6 +2131,8 @@ class ImageViewer(QWidget):
if self.main_win: if self.main_win:
if region_type == "Pet": if region_type == "Pet":
history_list = self.main_win.pet_names_history history_list = self.main_win.pet_names_history
elif region_type == "Body":
history_list = self.main_win.body_names_history
elif region_type == "Object": elif region_type == "Object":
history_list = self.main_win.object_names_history history_list = self.main_win.object_names_history
elif region_type == "Landmark": elif region_type == "Landmark":
@@ -2135,6 +2161,8 @@ class ImageViewer(QWidget):
if self.main_win: if self.main_win:
if region_type == "Pet": if region_type == "Pet":
self.main_win.pet_names_history = updated_history self.main_win.pet_names_history = updated_history
elif region_type == "Body":
self.main_win.body_names_history = updated_history
elif region_type == "Object": elif region_type == "Object":
self.main_win.object_names_history = updated_history self.main_win.object_names_history = updated_history
elif region_type == "Landmark": elif region_type == "Landmark":
@@ -2185,6 +2213,9 @@ class ImageViewer(QWidget):
{"text": UITexts.DETECT_PETS, "action": "detect_pets", {"text": UITexts.DETECT_PETS, "action": "detect_pets",
"icon": "edit-image-face-recognize"}, "icon": "edit-image-face-recognize"},
"separator", "separator",
{"text": UITexts.DETECT_BODIES, "action": "detect_bodies",
"icon": "edit-image-face-recognize"},
"separator",
{"text": UITexts.VIEWER_MENU_ROTATE, "icon": "transform-rotate", {"text": UITexts.VIEWER_MENU_ROTATE, "icon": "transform-rotate",
"submenu": [ "submenu": [
{"text": UITexts.VIEWER_MENU_ROTATE_LEFT, {"text": UITexts.VIEWER_MENU_ROTATE_LEFT,
@@ -2491,6 +2522,61 @@ class ImageViewer(QWidget):
if added_count > 0: if added_count > 0:
self.controller.save_faces() self.controller.save_faces()
def run_body_detection(self):
"""Runs body detection on the current image."""
QApplication.setOverrideCursor(Qt.WaitCursor)
try:
new_bodies = self.controller.detect_bodies()
finally:
QApplication.restoreOverrideCursor()
if not new_bodies:
return
IOU_THRESHOLD = 0.7
added_count = 0
for new_body in new_bodies:
is_duplicate = False
for existing_face in self.controller.faces:
iou = self._calculate_iou(new_body, existing_face)
if iou > IOU_THRESHOLD:
is_duplicate = True
break
if is_duplicate:
continue
if not self.controller.show_faces:
self.toggle_faces()
self.controller.faces.append(new_body)
self.canvas.update()
w = self.canvas.width()
h = self.canvas.height()
self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w),
int(new_body.get('y', 0) * h), 50, 50)
QApplication.processEvents()
# For bodies, we typically don't ask for a name immediately unless desired
# Or we can treat it like pets/faces and ask. Let's ask.
history = self.main_win.body_names_history if self.main_win else []
full_tag, updated_history, ok = FaceNameDialog.get_name(
self, history, main_win=self.main_win, region_type="Body")
if ok and full_tag:
new_body['name'] = full_tag
self.controller.toggle_tag(full_tag, True)
if self.main_win:
self.main_win.body_names_history = updated_history
added_count += 1
else:
self.controller.faces.pop()
self.canvas.update()
if added_count > 0:
self.controller.save_faces()
def toggle_filmstrip(self): def toggle_filmstrip(self):
"""Shows or hides the filmstrip widget.""" """Shows or hides the filmstrip widget."""
visible = not self.filmstrip.isVisible() visible = not self.filmstrip.isVisible()

View File

@@ -16,6 +16,7 @@ except ImportError:
exiv2 = None exiv2 = None
HAVE_EXIV2 = False HAVE_EXIV2 = False
from utils import preserve_mtime from utils import preserve_mtime
from constants import RATING_XATTR_NAME, XATTR_NAME
def notify_baloo(path): def notify_baloo(path):
@@ -40,6 +41,24 @@ def notify_baloo(path):
QDBusConnection.sessionBus().call(msg, QDBus.NoBlock) QDBusConnection.sessionBus().call(msg, QDBus.NoBlock)
def load_common_metadata(path):
"""
Loads tag and rating data for a path using extended attributes.
"""
tags = []
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
if raw_tags:
tags = sorted(list(set(t.strip()
for t in raw_tags.split(',') if t.strip())))
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
try:
rating = int(raw_rating)
except ValueError:
rating = 0
return tags, rating
class MetadataManager: class MetadataManager:
"""Manages reading EXIF, IPTC, and XMP metadata.""" """Manages reading EXIF, IPTC, and XMP metadata."""

View File

@@ -25,7 +25,8 @@ from constants import (
APP_CONFIG, AVAILABLE_FACE_ENGINES, DEFAULT_FACE_BOX_COLOR, APP_CONFIG, AVAILABLE_FACE_ENGINES, DEFAULT_FACE_BOX_COLOR,
DEFAULT_PET_BOX_COLOR, DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR, DEFAULT_PET_BOX_COLOR, DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR,
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, MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL, AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR,
MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
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,
@@ -81,6 +82,7 @@ class SettingsDialog(QDialog):
self.current_face_color = DEFAULT_FACE_BOX_COLOR self.current_face_color = DEFAULT_FACE_BOX_COLOR
self.current_pet_color = DEFAULT_PET_BOX_COLOR self.current_pet_color = DEFAULT_PET_BOX_COLOR
self.current_body_color = DEFAULT_BODY_BOX_COLOR
self.current_object_color = DEFAULT_OBJECT_BOX_COLOR self.current_object_color = DEFAULT_OBJECT_BOX_COLOR
self.current_landmark_color = DEFAULT_LANDMARK_BOX_COLOR self.current_landmark_color = DEFAULT_LANDMARK_BOX_COLOR
self.current_thumbs_bg_color = THUMBNAILS_BG_COLOR_DEFAULT self.current_thumbs_bg_color = THUMBNAILS_BG_COLOR_DEFAULT
@@ -293,9 +295,9 @@ class SettingsDialog(QDialog):
search_engine_layout = QHBoxLayout() search_engine_layout = QHBoxLayout()
search_engine_label = QLabel(UITexts.SETTINGS_SCANNER_SEARCH_ENGINE_LABEL) search_engine_label = QLabel(UITexts.SETTINGS_SCANNER_SEARCH_ENGINE_LABEL)
self.search_engine_combo = QComboBox() self.search_engine_combo = QComboBox()
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_NATIVE, "Native") self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_NATIVE, "Bagheera")
if SEARCH_CMD: if SEARCH_CMD:
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_BALOO, "baloosearch") self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_BALOO, "Baloo")
search_engine_layout.addWidget(search_engine_label) search_engine_layout.addWidget(search_engine_label)
search_engine_layout.addWidget(self.search_engine_combo) search_engine_layout.addWidget(self.search_engine_combo)
@@ -462,6 +464,53 @@ class SettingsDialog(QDialog):
self.pet_history_spin.setToolTip(UITexts.SETTINGS_PET_HISTORY_TOOLTIP) self.pet_history_spin.setToolTip(UITexts.SETTINGS_PET_HISTORY_TOOLTIP)
faces_layout.addLayout(pet_history_layout) faces_layout.addLayout(pet_history_layout)
# --- Body Section ---
faces_layout.addSpacing(10)
body_header = QLabel("Body")
body_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(body_header)
body_tags_layout = QHBoxLayout()
body_tags_label = QLabel(UITexts.SETTINGS_BODY_TAGS_LABEL)
self.body_tags_edit = QLineEdit()
self.body_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.body_tags_edit.setClearButtonEnabled(True)
body_tags_layout.addWidget(body_tags_label)
body_tags_layout.addWidget(self.body_tags_edit)
body_tags_label.setToolTip(UITexts.SETTINGS_BODY_TAGS_TOOLTIP)
self.body_tags_edit.setToolTip(UITexts.SETTINGS_BODY_TAGS_TOOLTIP)
faces_layout.addLayout(body_tags_layout)
# body_engine_layout = QHBoxLayout()
# body_engine_label = QLabel(UITexts.SETTINGS_BODY_ENGINE_LABEL)
# self.body_engine_combo = QComboBox()
# self.body_engine_combo.addItems(AVAILABLE_BODY_ENGINES)
# body_engine_layout.addWidget(body_engine_label)
# body_engine_layout.addWidget(self.body_engine_combo, 1)
# body_engine_label.setToolTip(UITexts.SETTINGS_BODY_ENGINE_TOOLTIP)
# self.body_engine_combo.setToolTip(UITexts.SETTINGS_BODY_ENGINE_TOOLTIP)
# faces_layout.addLayout(body_engine_layout)
body_color_layout = QHBoxLayout()
body_color_label = QLabel(UITexts.SETTINGS_BODY_COLOR_LABEL)
self.body_color_btn = QPushButton()
self.body_color_btn.clicked.connect(self.choose_body_color)
body_color_layout.addWidget(body_color_label)
body_color_layout.addWidget(self.body_color_btn)
body_color_label.setToolTip(UITexts.SETTINGS_BODY_COLOR_TOOLTIP)
self.body_color_btn.setToolTip(UITexts.SETTINGS_BODY_COLOR_TOOLTIP)
faces_layout.addLayout(body_color_layout)
body_history_layout = QHBoxLayout()
self.body_history_spin = QSpinBox()
self.body_history_spin.setRange(5, 100)
body_hist_label = QLabel(UITexts.SETTINGS_BODY_HISTORY_COUNT_LABEL)
body_history_layout.addWidget(body_hist_label)
body_history_layout.addWidget(self.body_history_spin)
body_hist_label.setToolTip(UITexts.SETTINGS_BODY_HISTORY_TOOLTIP)
self.body_history_spin.setToolTip(UITexts.SETTINGS_BODY_HISTORY_TOOLTIP)
faces_layout.addLayout(body_history_layout)
# --- Object Section --- # --- Object Section ---
faces_layout.addSpacing(10) faces_layout.addSpacing(10)
object_header = QLabel("Object") object_header = QLabel("Object")
@@ -593,7 +642,7 @@ class SettingsDialog(QDialog):
# Add tabs in the new order # Add tabs in the new order
tabs.addTab(thumbs_tab, UITexts.SETTINGS_GROUP_THUMBNAILS) tabs.addTab(thumbs_tab, UITexts.SETTINGS_GROUP_THUMBNAILS)
tabs.addTab(viewer_tab, UITexts.SETTINGS_GROUP_VIEWER) tabs.addTab(viewer_tab, UITexts.SETTINGS_GROUP_VIEWER)
tabs.addTab(faces_tab, UITexts.SETTINGS_GROUP_FACES) tabs.addTab(faces_tab, UITexts.SETTINGS_GROUP_AREAS)
tabs.addTab(scanner_tab, UITexts.SETTINGS_GROUP_SCANNER) tabs.addTab(scanner_tab, UITexts.SETTINGS_GROUP_SCANNER)
# --- Button Box --- # --- Button Box ---
@@ -625,16 +674,19 @@ class SettingsDialog(QDialog):
person_tags = APP_CONFIG.get( person_tags = APP_CONFIG.get(
"person_tags", SCANNER_SETTINGS_DEFAULTS["person_tags"]) "person_tags", SCANNER_SETTINGS_DEFAULTS["person_tags"])
pet_tags = APP_CONFIG.get("pet_tags", "") pet_tags = APP_CONFIG.get("pet_tags", "")
body_tags = APP_CONFIG.get("body_tags", "")
object_tags = APP_CONFIG.get("object_tags", "") object_tags = APP_CONFIG.get("object_tags", "")
landmark_tags = APP_CONFIG.get("landmark_tags", "") landmark_tags = APP_CONFIG.get("landmark_tags", "")
face_detection_engine = APP_CONFIG.get("face_detection_engine") face_detection_engine = APP_CONFIG.get("face_detection_engine")
pet_detection_engine = APP_CONFIG.get("pet_detection_engine") pet_detection_engine = APP_CONFIG.get("pet_detection_engine")
body_detection_engine = APP_CONFIG.get("body_detection_engine")
object_detection_engine = APP_CONFIG.get("object_detection_engine") object_detection_engine = APP_CONFIG.get("object_detection_engine")
landmark_detection_engine = APP_CONFIG.get("landmark_detection_engine") landmark_detection_engine = APP_CONFIG.get("landmark_detection_engine")
face_color = APP_CONFIG.get("face_box_color", DEFAULT_FACE_BOX_COLOR) face_color = APP_CONFIG.get("face_box_color", DEFAULT_FACE_BOX_COLOR)
pet_color = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR) pet_color = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR)
body_color = APP_CONFIG.get("body_box_color", DEFAULT_BODY_BOX_COLOR)
object_color = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR) object_color = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR)
landmark_color = APP_CONFIG.get("landmark_box_color", landmark_color = APP_CONFIG.get("landmark_box_color",
DEFAULT_LANDMARK_BOX_COLOR) DEFAULT_LANDMARK_BOX_COLOR)
@@ -645,6 +697,8 @@ class SettingsDialog(QDialog):
"faces_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT) "faces_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
pet_history_count = APP_CONFIG.get( pet_history_count = APP_CONFIG.get(
"pets_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT) "pets_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
body_history_count = APP_CONFIG.get(
"body_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
object_history_count = APP_CONFIG.get( object_history_count = APP_CONFIG.get(
"object_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT) "object_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
landmark_history_count = APP_CONFIG.get( landmark_history_count = APP_CONFIG.get(
@@ -695,11 +749,13 @@ class SettingsDialog(QDialog):
self.person_tags_edit.setText(person_tags) self.person_tags_edit.setText(person_tags)
self.pet_tags_edit.setText(pet_tags) self.pet_tags_edit.setText(pet_tags)
self.body_tags_edit.setText(body_tags)
self.object_tags_edit.setText(object_tags) self.object_tags_edit.setText(object_tags)
self.landmark_tags_edit.setText(landmark_tags) self.landmark_tags_edit.setText(landmark_tags)
self.set_button_color(face_color) self.set_button_color(face_color)
self.set_pet_button_color(pet_color) self.set_pet_button_color(pet_color)
self.set_body_button_color(body_color)
self.set_object_button_color(object_color) self.set_object_button_color(object_color)
self.set_landmark_button_color(landmark_color) self.set_landmark_button_color(landmark_color)
@@ -709,6 +765,8 @@ class SettingsDialog(QDialog):
if self.pet_engine_combo and pet_detection_engine in AVAILABLE_PET_ENGINES: if self.pet_engine_combo and pet_detection_engine in AVAILABLE_PET_ENGINES:
self.pet_engine_combo.setCurrentText(pet_detection_engine) self.pet_engine_combo.setCurrentText(pet_detection_engine)
if body_detection_engine and hasattr(self, "body_detection_engine_combo"):
self.body_engine_combo.setCurrentText(body_detection_engine)
if object_detection_engine and hasattr(self, "object_engine_combo"): if object_detection_engine and hasattr(self, "object_engine_combo"):
self.object_engine_combo.setCurrentText(object_detection_engine) self.object_engine_combo.setCurrentText(object_detection_engine)
if landmark_detection_engine and hasattr(self, "landmark_engine_combo"): if landmark_detection_engine and hasattr(self, "landmark_engine_combo"):
@@ -717,6 +775,7 @@ class SettingsDialog(QDialog):
self.mru_tags_spin.setValue(mru_tags_count) self.mru_tags_spin.setValue(mru_tags_count)
self.face_history_spin.setValue(face_history_count) self.face_history_spin.setValue(face_history_count)
self.pet_history_spin.setValue(pet_history_count) self.pet_history_spin.setValue(pet_history_count)
self.body_history_spin.setValue(body_history_count)
self.object_history_spin.setValue(object_history_count) self.object_history_spin.setValue(object_history_count)
self.landmark_history_spin.setValue(landmark_history_count) self.landmark_history_spin.setValue(landmark_history_count)
@@ -771,6 +830,18 @@ class SettingsDialog(QDialog):
if color.isValid(): if color.isValid():
self.set_pet_button_color(color.name()) self.set_pet_button_color(color.name())
def set_body_button_color(self, color_str):
"""Sets the background color of the body button and stores the value."""
self.body_color_btn.setStyleSheet(
f"background-color: {color_str}; border: 1px solid gray;")
self.current_body_color = color_str
def choose_body_color(self):
"""Opens a color picker dialog for body box."""
color = QColorDialog.getColor(QColor(self.current_body_color), self)
if color.isValid():
self.set_body_button_color(color.name())
def set_object_button_color(self, color_str): def set_object_button_color(self, color_str):
"""Sets the background color of the object button.""" """Sets the background color of the object button."""
self.object_color_btn.setStyleSheet( self.object_color_btn.setStyleSheet(
@@ -942,15 +1013,18 @@ class SettingsDialog(QDialog):
APP_CONFIG["scan_full_on_start"] = self.scan_full_on_start_checkbox.isChecked() APP_CONFIG["scan_full_on_start"] = self.scan_full_on_start_checkbox.isChecked()
APP_CONFIG["person_tags"] = self.person_tags_edit.text() APP_CONFIG["person_tags"] = self.person_tags_edit.text()
APP_CONFIG["pet_tags"] = self.pet_tags_edit.text() APP_CONFIG["pet_tags"] = self.pet_tags_edit.text()
APP_CONFIG["body_tags"] = self.body_tags_edit.text()
APP_CONFIG["object_tags"] = self.object_tags_edit.text() APP_CONFIG["object_tags"] = self.object_tags_edit.text()
APP_CONFIG["landmark_tags"] = self.landmark_tags_edit.text() APP_CONFIG["landmark_tags"] = self.landmark_tags_edit.text()
APP_CONFIG["face_box_color"] = self.current_face_color APP_CONFIG["face_box_color"] = self.current_face_color
APP_CONFIG["pet_box_color"] = self.current_pet_color APP_CONFIG["pet_box_color"] = self.current_pet_color
APP_CONFIG["body_box_color"] = self.current_body_color
APP_CONFIG["object_box_color"] = self.current_object_color APP_CONFIG["object_box_color"] = self.current_object_color
APP_CONFIG["landmark_box_color"] = self.current_landmark_color APP_CONFIG["landmark_box_color"] = self.current_landmark_color
APP_CONFIG["tags_menu_max_items"] = self.mru_tags_spin.value() APP_CONFIG["tags_menu_max_items"] = self.mru_tags_spin.value()
APP_CONFIG["faces_menu_max_items"] = self.face_history_spin.value() APP_CONFIG["faces_menu_max_items"] = self.face_history_spin.value()
APP_CONFIG["pets_menu_max_items"] = self.pet_history_spin.value() APP_CONFIG["pets_menu_max_items"] = self.pet_history_spin.value()
APP_CONFIG["body_menu_max_items"] = self.body_history_spin.value()
APP_CONFIG["object_menu_max_items"] = self.object_history_spin.value() APP_CONFIG["object_menu_max_items"] = self.object_history_spin.value()
APP_CONFIG["landmark_menu_max_items"] = self.landmark_history_spin.value() APP_CONFIG["landmark_menu_max_items"] = self.landmark_history_spin.value()
@@ -975,9 +1049,10 @@ class SettingsDialog(QDialog):
APP_CONFIG["viewer_wheel_speed"] = self.viewer_wheel_spin.value() APP_CONFIG["viewer_wheel_speed"] = self.viewer_wheel_spin.value()
APP_CONFIG["viewer_auto_resize_window"] = \ APP_CONFIG["viewer_auto_resize_window"] = \
self.viewer_auto_resize_check.isChecked() self.viewer_auto_resize_check.isChecked()
if self.face_engine_combo:
APP_CONFIG["face_detection_engine"] = self.face_engine_combo.currentText() APP_CONFIG["face_detection_engine"] = self.face_engine_combo.currentText()
APP_CONFIG["pet_detection_engine"] = self.pet_engine_combo.currentText() APP_CONFIG["pet_detection_engine"] = self.pet_engine_combo.currentText()
if hasattr(self, "object_engine_combo"):
APP_CONFIG["body_detection_engine"] = self.body_engine_combo.currentText()
if hasattr(self, "object_engine_combo"): if hasattr(self, "object_engine_combo"):
APP_CONFIG["object_detection_engine"] = \ APP_CONFIG["object_detection_engine"] = \
self.object_engine_combo.currentText() self.object_engine_combo.currentText()

View File

@@ -1121,6 +1121,9 @@ class FaceNameInputWidget(QWidget):
if self.region_type == "Pet": if self.region_type == "Pet":
max_items = APP_CONFIG.get("pets_menu_max_items", max_items = APP_CONFIG.get("pets_menu_max_items",
FACES_MENU_MAX_ITEMS_DEFAULT) FACES_MENU_MAX_ITEMS_DEFAULT)
elif self.region_type == "Body":
max_items = APP_CONFIG.get("body_menu_max_items",
FACES_MENU_MAX_ITEMS_DEFAULT)
elif self.region_type == "Object": elif self.region_type == "Object":
max_items = APP_CONFIG.get("object_menu_max_items", max_items = APP_CONFIG.get("object_menu_max_items",
FACES_MENU_MAX_ITEMS_DEFAULT) FACES_MENU_MAX_ITEMS_DEFAULT)
@@ -1188,6 +1191,12 @@ class FaceNameInputWidget(QWidget):
parent_tags_str = "Pet" parent_tags_str = "Pet"
dialog_title = UITexts.NEW_PET_TAG_TITLE dialog_title = UITexts.NEW_PET_TAG_TITLE
dialog_text = UITexts.NEW_PET_TAG_TEXT dialog_text = UITexts.NEW_PET_TAG_TEXT
elif self.region_type == "Body":
parent_tags_str = APP_CONFIG.get("body_tags", "Body")
if not parent_tags_str or not parent_tags_str.strip():
parent_tags_str = "Body"
dialog_title = UITexts.NEW_BODY_TAG_TITLE
dialog_text = UITexts.NEW_BODY_TAG_TEXT
elif self.region_type == "Object": elif self.region_type == "Object":
parent_tags_str = APP_CONFIG.get("object_tags", "Object") parent_tags_str = APP_CONFIG.get("object_tags", "Object")
if not parent_tags_str or not parent_tags_str.strip(): if not parent_tags_str or not parent_tags_str.strip():
@@ -1273,6 +1282,10 @@ class FaceNameInputWidget(QWidget):
parent_tags_str = APP_CONFIG.get("pet_tags", "Pet") parent_tags_str = APP_CONFIG.get("pet_tags", "Pet")
if not parent_tags_str or not parent_tags_str.strip(): if not parent_tags_str or not parent_tags_str.strip():
parent_tags_str = "Pet" parent_tags_str = "Pet"
elif self.region_type == "Body":
parent_tags_str = APP_CONFIG.get("body_tags", "Body")
if not parent_tags_str or not parent_tags_str.strip():
parent_tags_str = "Body"
elif self.region_type == "Object": elif self.region_type == "Object":
parent_tags_str = APP_CONFIG.get("object_tags", "Object") parent_tags_str = APP_CONFIG.get("object_tags", "Object")
if not parent_tags_str or not parent_tags_str.strip(): if not parent_tags_str or not parent_tags_str.strip():