Compare commits
6 Commits
bacd3a7f56
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0349155fd2 | ||
|
|
b87e34a1b8 | ||
|
|
20e5318a53 | ||
|
|
144ad665e4 | ||
|
|
291f2f9e47 | ||
|
|
547bfbf760 |
155
bagheeraview.py
155
bagheeraview.py
@@ -68,7 +68,8 @@ from constants import (
|
|||||||
)
|
)
|
||||||
import constants
|
import constants
|
||||||
from settings import SettingsDialog
|
from settings import SettingsDialog
|
||||||
from imagescanner import CacheCleaner, ImageScanner, ThumbnailCache, ThumbnailGenerator
|
from imagescanner import (CacheCleaner, ImageScanner, ThumbnailCache,
|
||||||
|
ThumbnailGenerator, ThreadPoolManager)
|
||||||
from imageviewer import ImageViewer
|
from imageviewer import ImageViewer
|
||||||
from propertiesdialog import PropertiesDialog
|
from propertiesdialog import PropertiesDialog
|
||||||
from widgets import (
|
from widgets import (
|
||||||
@@ -579,8 +580,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 +588,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 +864,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):
|
||||||
"""
|
"""
|
||||||
@@ -889,13 +904,14 @@ class MainWindow(QMainWindow):
|
|||||||
scanners and individual image viewer windows.
|
scanners and individual image viewer windows.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cache, args):
|
def __init__(self, cache, args, thread_pool_manager):
|
||||||
"""
|
"""
|
||||||
Initializes the MainWindow.
|
Initializes the MainWindow.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cache (ThumbnailCache): The shared thumbnail cache instance.
|
cache (ThumbnailCache): The shared thumbnail cache instance.
|
||||||
args (list): Command-line arguments passed to the application.
|
args (list): Command-line arguments passed to the application.
|
||||||
|
thread_pool_manager (ThreadPoolManager): The shared thread pool manager.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
@@ -903,11 +919,13 @@ class MainWindow(QMainWindow):
|
|||||||
self.set_app_icon()
|
self.set_app_icon()
|
||||||
|
|
||||||
self.viewer_shortcuts = {}
|
self.viewer_shortcuts = {}
|
||||||
|
self.thread_pool_manager = thread_pool_manager
|
||||||
self.full_history = []
|
self.full_history = []
|
||||||
self.history = []
|
self.history = []
|
||||||
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(
|
||||||
@@ -1305,12 +1323,14 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_scroll_interaction(self, value):
|
def _on_scroll_interaction(self, value):
|
||||||
"""Pauses scanning during scroll to keep UI fluid."""
|
"""Pauses scanning during scroll to keep UI fluid."""
|
||||||
if self.scanner and self.scanner.isRunning():
|
if self.scanner and self.scanner.isRunning():
|
||||||
|
self.thread_pool_manager.set_user_active(True)
|
||||||
self.scanner.set_paused(True)
|
self.scanner.set_paused(True)
|
||||||
self.resume_scan_timer.start()
|
self.resume_scan_timer.start()
|
||||||
|
|
||||||
def _resume_scanning(self):
|
def _resume_scanning(self):
|
||||||
"""Resumes scanning after interaction pause."""
|
"""Resumes scanning after interaction pause."""
|
||||||
if self.scanner:
|
if self.scanner:
|
||||||
|
self.thread_pool_manager.set_user_active(False)
|
||||||
# Prioritize currently visible images
|
# Prioritize currently visible images
|
||||||
visible_paths = self.get_visible_image_paths()
|
visible_paths = self.get_visible_image_paths()
|
||||||
self.scanner.prioritize(visible_paths)
|
self.scanner.prioritize(visible_paths)
|
||||||
@@ -1466,6 +1486,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 +1545,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 +1667,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};")
|
||||||
@@ -1652,6 +1681,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Trigger a repaint to apply other color changes like filename color
|
# Trigger a repaint to apply other color changes like filename color
|
||||||
self._apply_global_stylesheet()
|
self._apply_global_stylesheet()
|
||||||
|
self.thread_pool_manager.update_default_thread_count()
|
||||||
self.thumbnail_view.updateGeometries()
|
self.thumbnail_view.updateGeometries()
|
||||||
self.thumbnail_view.viewport().update()
|
self.thumbnail_view.viewport().update()
|
||||||
|
|
||||||
@@ -1974,6 +2004,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 +2315,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 +2323,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
|
||||||
@@ -2290,6 +2358,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.is_cleaning = False
|
self.is_cleaning = False
|
||||||
self.scanner = ImageScanner(self.cache, paths, is_file_list=self._scan_all,
|
self.scanner = ImageScanner(self.cache, paths, is_file_list=self._scan_all,
|
||||||
|
thread_pool_manager=self.thread_pool_manager,
|
||||||
viewers=self.viewers)
|
viewers=self.viewers)
|
||||||
if self._is_loading_all:
|
if self._is_loading_all:
|
||||||
self.scanner.set_auto_load(True)
|
self.scanner.set_auto_load(True)
|
||||||
@@ -2299,11 +2368,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 +2400,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 +2642,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 +2697,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 +2851,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 +3133,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 +3181,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 +3232,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
|
||||||
@@ -3449,7 +3523,8 @@ class MainWindow(QMainWindow):
|
|||||||
if not paths:
|
if not paths:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.thumbnail_generator = ThumbnailGenerator(self.cache, paths, size)
|
self.thumbnail_generator = ThumbnailGenerator(
|
||||||
|
self.cache, paths, size, self.thread_pool_manager)
|
||||||
self.thumbnail_generator.generation_complete.connect(
|
self.thumbnail_generator.generation_complete.connect(
|
||||||
self.on_high_res_generation_finished)
|
self.on_high_res_generation_finished)
|
||||||
self.thumbnail_generator.progress.connect(
|
self.thumbnail_generator.progress.connect(
|
||||||
@@ -3468,16 +3543,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 +3604,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 +3682,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 +3750,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)
|
||||||
@@ -3914,7 +3991,8 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Create a ThumbnailGenerator to regenerate the thumbnail
|
# Create a ThumbnailGenerator to regenerate the thumbnail
|
||||||
size = self._get_tier_for_size(self.current_thumb_size)
|
size = self._get_tier_for_size(self.current_thumb_size)
|
||||||
self.thumbnail_generator = ThumbnailGenerator(self.cache, [path], size)
|
self.thumbnail_generator = ThumbnailGenerator(
|
||||||
|
self.cache, [path], size, self.thread_pool_manager)
|
||||||
self.thumbnail_generator.generation_complete.connect(
|
self.thumbnail_generator.generation_complete.connect(
|
||||||
self.on_high_res_generation_finished)
|
self.on_high_res_generation_finished)
|
||||||
self.thumbnail_generator.progress.connect(
|
self.thumbnail_generator.progress.connect(
|
||||||
@@ -4293,6 +4371,7 @@ def main():
|
|||||||
# Increase QPixmapCache limit (default is usually small, ~10MB) to ~100MB
|
# Increase QPixmapCache limit (default is usually small, ~10MB) to ~100MB
|
||||||
QPixmapCache.setCacheLimit(102400)
|
QPixmapCache.setCacheLimit(102400)
|
||||||
|
|
||||||
|
thread_pool_manager = ThreadPoolManager()
|
||||||
cache = ThumbnailCache()
|
cache = ThumbnailCache()
|
||||||
|
|
||||||
args = [a for a in sys.argv[1:] if a != "--x11"]
|
args = [a for a in sys.argv[1:] if a != "--x11"]
|
||||||
@@ -4301,7 +4380,7 @@ def main():
|
|||||||
if path.startswith("file:/"):
|
if path.startswith("file:/"):
|
||||||
path = path[6:]
|
path = path[6:]
|
||||||
|
|
||||||
win = MainWindow(cache, args)
|
win = MainWindow(cache, args, thread_pool_manager)
|
||||||
shortcut_controller = AppShortcutController(win)
|
shortcut_controller = AppShortcutController(win)
|
||||||
win.shortcut_controller = shortcut_controller
|
win.shortcut_controller = shortcut_controller
|
||||||
app.installEventFilter(shortcut_controller)
|
app.installEventFilter(shortcut_controller)
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
v0.9.11 -
|
v0.9.11 -
|
||||||
· Hacer que el image viewer standalone admita múltiles sort
|
· 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.
|
||||||
|
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`.
|
||||||
|
Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity.
|
||||||
|
|
||||||
|
Implement a mechanism to monitor system CPU load and adjust the thread pool size accordingly.
|
||||||
|
Refactor the `ThreadPoolManager` to be a QObject and emit signals when the thread count changes.
|
||||||
|
|
||||||
|
Implement a "Comparison" mode to view 2 or 4 images side-by-side in the viewer.
|
||||||
|
|
||||||
|
· 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 +30,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.
|
||||||
|
|||||||
127
constants.py
127
constants.py
@@ -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-dev"
|
PROG_VERSION = "0.9.11"
|
||||||
PROG_AUTHOR = "Ignacio Serantes"
|
PROG_AUTHOR = "Ignacio Serantes"
|
||||||
|
|
||||||
# --- CACHE SETTINGS ---
|
# --- CACHE SETTINGS ---
|
||||||
@@ -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 ---
|
||||||
@@ -167,6 +167,8 @@ 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 = (
|
||||||
@@ -193,6 +195,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 +211,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 +280,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"),
|
||||||
@@ -283,6 +291,10 @@ 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"),
|
||||||
|
"compare_1": ("Single View", "View"),
|
||||||
|
"compare_2": ("Compare 2 Images", "View"),
|
||||||
|
"compare_4": ("Compare 4 Images", "View"),
|
||||||
|
"link_panes": ("Link Panes", "View"),
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_VIEWER_SHORTCUTS = {
|
DEFAULT_VIEWER_SHORTCUTS = {
|
||||||
@@ -299,6 +311,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),
|
||||||
@@ -310,6 +323,10 @@ DEFAULT_VIEWER_SHORTCUTS = {
|
|||||||
"toggle_visibility": (Qt.Key_H, Qt.ControlModifier),
|
"toggle_visibility": (Qt.Key_H, Qt.ControlModifier),
|
||||||
"toggle_crop": (Qt.Key_C, Qt.NoModifier),
|
"toggle_crop": (Qt.Key_C, Qt.NoModifier),
|
||||||
"save_crop": (Qt.Key_S, Qt.ControlModifier),
|
"save_crop": (Qt.Key_S, Qt.ControlModifier),
|
||||||
|
"compare_1": (Qt.Key_1, Qt.AltModifier),
|
||||||
|
"compare_2": (Qt.Key_2, Qt.AltModifier),
|
||||||
|
"compare_4": (Qt.Key_4, Qt.AltModifier),
|
||||||
|
"link_panes": (Qt.Key_L, Qt.AltModifier),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -395,13 +412,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 +428,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 +439,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 +463,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 +482,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 +526,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 +560,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 "
|
||||||
@@ -707,6 +743,11 @@ _UI_TEXTS = {
|
|||||||
"VIEWER_MENU_CROP": "Crop Mode",
|
"VIEWER_MENU_CROP": "Crop Mode",
|
||||||
"VIEWER_MENU_SAVE_CROP": "Save Selection...",
|
"VIEWER_MENU_SAVE_CROP": "Save Selection...",
|
||||||
"SAVE_CROP_TITLE": "Save Cropped Image",
|
"SAVE_CROP_TITLE": "Save Cropped Image",
|
||||||
|
"VIEWER_MENU_COMPARE": "Comparison Mode",
|
||||||
|
"VIEWER_MENU_COMPARE_1": "Single View",
|
||||||
|
"VIEWER_MENU_COMPARE_2": "2 Images",
|
||||||
|
"VIEWER_MENU_COMPARE_4": "4 Images",
|
||||||
|
"VIEWER_MENU_LINK_PANES": "Link Panes",
|
||||||
"SAVE_CROP_FILTER": "Images (*.jpg *.jpeg *.png *.bmp *.webp)",
|
"SAVE_CROP_FILTER": "Images (*.jpg *.jpeg *.png *.bmp *.webp)",
|
||||||
"SLIDESHOW_INTERVAL_TITLE": "Slideshow Interval",
|
"SLIDESHOW_INTERVAL_TITLE": "Slideshow Interval",
|
||||||
"SLIDESHOW_INTERVAL_TEXT": "Seconds:",
|
"SLIDESHOW_INTERVAL_TEXT": "Seconds:",
|
||||||
@@ -801,19 +842,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 +868,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 +892,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 +913,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 +965,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.",
|
||||||
@@ -1118,6 +1177,11 @@ _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_COMPARE": "Modo Comparación",
|
||||||
|
"VIEWER_MENU_COMPARE_1": "Vista Única",
|
||||||
|
"VIEWER_MENU_COMPARE_2": "2 Imágenes",
|
||||||
|
"VIEWER_MENU_COMPARE_4": "4 Imágenes",
|
||||||
|
"VIEWER_MENU_LINK_PANES": "Vincular Paneles",
|
||||||
"SAVE_CROP_TITLE": "Guardar Imagen Recortada",
|
"SAVE_CROP_TITLE": "Guardar Imagen Recortada",
|
||||||
"SAVE_CROP_FILTER": "Imágenes (*.jpg *.jpeg *.png *.bmp *.webp)",
|
"SAVE_CROP_FILTER": "Imágenes (*.jpg *.jpeg *.png *.bmp *.webp)",
|
||||||
"SLIDESHOW_INTERVAL_TITLE": "Intervalo de Presentación",
|
"SLIDESHOW_INTERVAL_TITLE": "Intervalo de Presentación",
|
||||||
@@ -1213,19 +1277,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 +1303,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 +1328,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 +1349,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 +1404,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 +1436,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.",
|
||||||
@@ -1533,6 +1615,11 @@ _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_COMPARE": "Modo Comparación",
|
||||||
|
"VIEWER_MENU_COMPARE_1": "Vista Única",
|
||||||
|
"VIEWER_MENU_COMPARE_2": "2 Imaxes",
|
||||||
|
"VIEWER_MENU_COMPARE_4": "4 Imaxes",
|
||||||
|
"VIEWER_MENU_LINK_PANES": "Vincular Paneis",
|
||||||
"SAVE_CROP_TITLE": "Gardar Imaxe Recortada",
|
"SAVE_CROP_TITLE": "Gardar Imaxe Recortada",
|
||||||
"SAVE_CROP_FILTER": "Imaxes (*.jpg *.jpeg *.png *.bmp *.webp)",
|
"SAVE_CROP_FILTER": "Imaxes (*.jpg *.jpeg *.png *.bmp *.webp)",
|
||||||
"SLIDESHOW_INTERVAL_TITLE": "Intervalo da Presentación",
|
"SLIDESHOW_INTERVAL_TITLE": "Intervalo da Presentación",
|
||||||
|
|||||||
@@ -11,16 +11,19 @@ Classes:
|
|||||||
interacts with the ImagePreloader.
|
interacts with the ImagePreloader.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
from PySide6.QtCore import QThread, Signal, QMutex, QWaitCondition, QObject, Qt
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ImagePreloader(QThread):
|
class ImagePreloader(QThread):
|
||||||
@@ -78,21 +81,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,10 +112,10 @@ 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 as e:
|
||||||
pass
|
logger.warning(f"ImagePreloader failed to load {path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
class ImageController(QObject):
|
class ImageController(QObject):
|
||||||
@@ -157,6 +145,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
|
||||||
@@ -169,6 +159,12 @@ class ImageController(QObject):
|
|||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Stops the background preloader thread."""
|
"""Stops the background preloader thread."""
|
||||||
self.preloader.stop()
|
self.preloader.stop()
|
||||||
|
self._current_metadata_path = None
|
||||||
|
self._loaded_path = None
|
||||||
|
self._current_tags = []
|
||||||
|
self._current_rating = 0
|
||||||
|
self._cached_next_image = None
|
||||||
|
self._cached_next_index = -1
|
||||||
|
|
||||||
def _trigger_preload(self):
|
def _trigger_preload(self):
|
||||||
"""Identifies the next image in the list and asks the preloader to load it."""
|
"""Identifies the next image in the list and asks the preloader to load it."""
|
||||||
@@ -219,16 +215,26 @@ 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()
|
||||||
|
|
||||||
|
# Optimization: Check if image is already loaded
|
||||||
|
if path and self._loaded_path == path and not self.pixmap_original.isNull():
|
||||||
|
# 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._trigger_preload()
|
||||||
|
return True, False
|
||||||
|
|
||||||
self.pixmap_original = QPixmap()
|
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 = []
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False, False
|
||||||
|
|
||||||
# Check cache
|
# Check cache
|
||||||
if self.index == self._cached_next_index and self._cached_next_image:
|
if self.index == self._cached_next_index and self._cached_next_image:
|
||||||
@@ -236,6 +242,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
|
||||||
@@ -246,15 +253,18 @@ class ImageController(QObject):
|
|||||||
image = reader.read()
|
image = reader.read()
|
||||||
if image.isNull():
|
if image.isNull():
|
||||||
self._trigger_preload()
|
self._trigger_preload()
|
||||||
return False
|
return False, 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, True
|
||||||
|
|
||||||
def load_faces(self):
|
def load_faces(self):
|
||||||
"""
|
"""
|
||||||
@@ -422,6 +432,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 +475,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 +523,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 +534,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 +562,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 +584,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 +660,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 +769,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 +806,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()
|
||||||
|
|||||||
501
imagescanner.py
501
imagescanner.py
@@ -28,34 +28,220 @@ import collections
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import lmdb
|
import lmdb
|
||||||
from PySide6.QtCore import (QObject, QThread, Signal, QMutex, QReadWriteLock, QSize,
|
from PySide6.QtCore import (
|
||||||
QWaitCondition, QByteArray, QBuffer, QIODevice, Qt, QTimer,
|
QObject, QThread, Signal, QMutex, QReadWriteLock, QSize, QSemaphore, QWaitCondition,
|
||||||
QRunnable, QThreadPool)
|
QByteArray, QBuffer, QIODevice, Qt, QTimer, QRunnable, QThreadPool, QFile
|
||||||
|
)
|
||||||
from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
|
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,
|
IMAGE_EXTENSIONS, SEARCH_CMD, THUMBNAIL_SIZES, RATING_XATTR_NAME, XATTR_NAME,
|
||||||
UITexts, SCANNER_SETTINGS_DEFAULTS
|
UITexts, SCANNER_SETTINGS_DEFAULTS, HAVE_BAGHEERASEARCH_LIB
|
||||||
)
|
)
|
||||||
|
|
||||||
from imageviewer import ImageViewer
|
from imageviewer import ImageViewer
|
||||||
from metadatamanager import XattrManager
|
from metadatamanager import XattrManager
|
||||||
|
|
||||||
|
if HAVE_BAGHEERASEARCH_LIB:
|
||||||
try:
|
try:
|
||||||
# Attempt to import bagheerasearch for direct integration
|
|
||||||
from bagheera_search_lib import BagheeraSearcher
|
from bagheera_search_lib import BagheeraSearcher
|
||||||
HAVE_BAGHEERASEARCH_LIB = True
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_BAGHEERASEARCH_LIB = False
|
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__)
|
||||||
|
|
||||||
|
|
||||||
def generate_thumbnail(path, size):
|
class ThreadPoolManager:
|
||||||
|
"""Manages a global QThreadPool to dynamically adjust thread count."""
|
||||||
|
def __init__(self):
|
||||||
|
self.pool = QThreadPool()
|
||||||
|
self.default_thread_count = APP_CONFIG.get(
|
||||||
|
"generation_threads",
|
||||||
|
SCANNER_SETTINGS_DEFAULTS.get("generation_threads", 4)
|
||||||
|
)
|
||||||
|
self.pool.setMaxThreadCount(self.default_thread_count)
|
||||||
|
self.is_user_active = False
|
||||||
|
logger.info(f"ThreadPoolManager initialized with "
|
||||||
|
f"{self.default_thread_count} threads.")
|
||||||
|
|
||||||
|
def get_pool(self):
|
||||||
|
"""Returns the managed QThreadPool instance."""
|
||||||
|
return self.pool
|
||||||
|
|
||||||
|
def set_user_active(self, active):
|
||||||
|
"""
|
||||||
|
Adjusts thread count based on user activity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
active (bool): True if the user is interacting with the UI.
|
||||||
|
"""
|
||||||
|
if active == self.is_user_active:
|
||||||
|
return
|
||||||
|
self.is_user_active = active
|
||||||
|
if active:
|
||||||
|
# User is active, reduce threads to 1 to prioritize UI responsiveness.
|
||||||
|
self.pool.setMaxThreadCount(1)
|
||||||
|
logger.debug("User is active, reducing thread pool to 1.")
|
||||||
|
else:
|
||||||
|
# User is idle, restore to default thread count.
|
||||||
|
self.pool.setMaxThreadCount(self.default_thread_count)
|
||||||
|
logger.debug(f"User is idle, restoring thread pool to "
|
||||||
|
f"{self.default_thread_count}.")
|
||||||
|
|
||||||
|
def update_default_thread_count(self):
|
||||||
|
"""Updates the default thread count from application settings."""
|
||||||
|
self.default_thread_count = APP_CONFIG.get(
|
||||||
|
"generation_threads",
|
||||||
|
SCANNER_SETTINGS_DEFAULTS.get("generation_threads", 4)
|
||||||
|
)
|
||||||
|
# Only apply if not in a user-active (low-thread) state.
|
||||||
|
if not self.is_user_active:
|
||||||
|
self.pool.setMaxThreadCount(self.default_thread_count)
|
||||||
|
logger.info(f"Default thread count updated to {self.default_thread_count}.")
|
||||||
|
|
||||||
|
|
||||||
|
class ScannerWorker(QRunnable):
|
||||||
|
"""
|
||||||
|
Worker to process a single image in a thread pool.
|
||||||
|
Handles thumbnail retrieval/generation and metadata loading.
|
||||||
|
"""
|
||||||
|
def __init__(self, cache, path, target_sizes=None, load_metadata=True,
|
||||||
|
signal_emitter=None, semaphore=None):
|
||||||
|
super().__init__()
|
||||||
|
self.cache = cache
|
||||||
|
self.path = path
|
||||||
|
self.target_sizes = target_sizes
|
||||||
|
self.load_metadata_flag = load_metadata
|
||||||
|
self.emitter = signal_emitter
|
||||||
|
self.semaphore = semaphore
|
||||||
|
self._is_cancelled = False
|
||||||
|
# Result will be (path, thumb, mtime, tags, rating, inode, dev) or None
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Marks the worker as cancelled."""
|
||||||
|
self._is_cancelled = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from constants import SCANNER_GENERATE_SIZES
|
||||||
|
|
||||||
|
sizes_to_check = self.target_sizes if self.target_sizes is not None \
|
||||||
|
else SCANNER_GENERATE_SIZES
|
||||||
|
|
||||||
|
if self._is_cancelled:
|
||||||
|
if self.semaphore:
|
||||||
|
self.semaphore.release()
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = None
|
||||||
|
try:
|
||||||
|
# Optimize: Open file once to reuse FD for stat and xattrs
|
||||||
|
fd = os.open(self.path, os.O_RDONLY)
|
||||||
|
stat_res = os.fstat(fd)
|
||||||
|
curr_mtime = stat_res.st_mtime
|
||||||
|
curr_inode = stat_res.st_ino
|
||||||
|
curr_dev = stat_res.st_dev
|
||||||
|
|
||||||
|
smallest_thumb_for_signal = None
|
||||||
|
min_size = min(sizes_to_check) if sizes_to_check else 0
|
||||||
|
|
||||||
|
# Ensure required thumbnails exist
|
||||||
|
for size in sizes_to_check:
|
||||||
|
if self._is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if a valid thumbnail for this size exists
|
||||||
|
thumb, mtime = self.cache.get_thumbnail(self.path, size,
|
||||||
|
curr_mtime=curr_mtime,
|
||||||
|
inode=curr_inode,
|
||||||
|
device_id=curr_dev)
|
||||||
|
if not thumb or mtime != curr_mtime:
|
||||||
|
# Use generation lock to prevent multiple threads generating
|
||||||
|
with self.cache.generation_lock(
|
||||||
|
self.path, size, curr_mtime,
|
||||||
|
curr_inode, curr_dev) as should_gen:
|
||||||
|
if self._is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if should_gen:
|
||||||
|
# I am the owner, I generate the thumbnail
|
||||||
|
new_thumb = generate_thumbnail(self.path, size, fd=fd)
|
||||||
|
if self._is_cancelled:
|
||||||
|
return
|
||||||
|
if new_thumb and not new_thumb.isNull():
|
||||||
|
self.cache.set_thumbnail(
|
||||||
|
self.path, new_thumb, curr_mtime, size,
|
||||||
|
inode=curr_inode, device_id=curr_dev, block=True)
|
||||||
|
if size == min_size:
|
||||||
|
smallest_thumb_for_signal = new_thumb
|
||||||
|
else:
|
||||||
|
# Another thread generated it, re-fetch
|
||||||
|
if size == min_size:
|
||||||
|
re_thumb, _ = self.cache.get_thumbnail(
|
||||||
|
self.path, size, curr_mtime=curr_mtime,
|
||||||
|
inode=curr_inode, device_id=curr_dev,
|
||||||
|
async_load=False)
|
||||||
|
smallest_thumb_for_signal = re_thumb
|
||||||
|
elif size == min_size:
|
||||||
|
# valid thumb exists, use it for signal
|
||||||
|
smallest_thumb_for_signal = thumb
|
||||||
|
|
||||||
|
tags = []
|
||||||
|
rating = 0
|
||||||
|
if self.load_metadata_flag:
|
||||||
|
tags, rating = self._load_metadata(fd)
|
||||||
|
self.result = (self.path, smallest_thumb_for_signal,
|
||||||
|
curr_mtime, tags, rating, curr_inode, curr_dev)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing image {self.path}: {e}")
|
||||||
|
self.result = None
|
||||||
|
finally:
|
||||||
|
if fd is not None:
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if self.emitter:
|
||||||
|
self.emitter.emit_progress()
|
||||||
|
if self.semaphore:
|
||||||
|
self.semaphore.release()
|
||||||
|
|
||||||
|
def _load_metadata(self, path_or_fd):
|
||||||
|
"""Loads tag and rating data for a path or file descriptor."""
|
||||||
|
tags = []
|
||||||
|
raw_tags = XattrManager.get_attribute(path_or_fd, 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_or_fd, RATING_XATTR_NAME, "0")
|
||||||
|
try:
|
||||||
|
rating = int(raw_rating)
|
||||||
|
except ValueError:
|
||||||
|
rating = 0
|
||||||
|
return tags, rating
|
||||||
|
|
||||||
|
|
||||||
|
def generate_thumbnail(path, size, fd=None):
|
||||||
"""Generates a QImage thumbnail for a given path and size."""
|
"""Generates a QImage thumbnail for a given path and size."""
|
||||||
try:
|
try:
|
||||||
|
qfile = None
|
||||||
|
if fd is not None:
|
||||||
|
try:
|
||||||
|
# Ensure we are at the beginning of the file
|
||||||
|
os.lseek(fd, 0, os.SEEK_SET)
|
||||||
|
qfile = QFile()
|
||||||
|
if qfile.open(fd, QIODevice.ReadOnly, QFile.DontCloseHandle):
|
||||||
|
reader = QImageReader(qfile)
|
||||||
|
else:
|
||||||
|
qfile = None
|
||||||
|
reader = QImageReader(path)
|
||||||
|
except OSError:
|
||||||
|
reader = QImageReader(path)
|
||||||
|
else:
|
||||||
reader = QImageReader(path)
|
reader = QImageReader(path)
|
||||||
|
|
||||||
# Optimization: Instruct the image decoder to scale while reading.
|
# Optimization: Instruct the image decoder to scale while reading.
|
||||||
@@ -130,6 +316,10 @@ class CacheWriter(QThread):
|
|||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Ensure we don't accept new items if stopping, especially when block=False
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
|
|
||||||
# --- Soft Cleaning: Deduplication ---
|
# --- Soft Cleaning: Deduplication ---
|
||||||
# Remove redundant pending updates for the same image/size (e.g.
|
# Remove redundant pending updates for the same image/size (e.g.
|
||||||
# rapid rotations)
|
# rapid rotations)
|
||||||
@@ -154,7 +344,7 @@ class CacheWriter(QThread):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self._mutex.lock()
|
self._mutex.lock()
|
||||||
self._running = False
|
self._running = False
|
||||||
self._queue.clear()
|
# Do not clear the queue here; let the run loop drain it to prevent data loss.
|
||||||
self._condition_new_data.wakeAll()
|
self._condition_new_data.wakeAll()
|
||||||
self._condition_space_available.wakeAll()
|
self._condition_space_available.wakeAll()
|
||||||
self._mutex.unlock()
|
self._mutex.unlock()
|
||||||
@@ -187,10 +377,7 @@ class CacheWriter(QThread):
|
|||||||
# Gather a batch of items
|
# Gather a batch of items
|
||||||
# Adaptive batch size: if queue is backing up, increase transaction size
|
# Adaptive batch size: if queue is backing up, increase transaction size
|
||||||
# to improve throughput.
|
# to improve throughput.
|
||||||
if not self._running:
|
# Respect max size even during shutdown to avoid OOM or huge transactions
|
||||||
# Flush everything if stopping
|
|
||||||
batch_limit = len(self._queue)
|
|
||||||
else:
|
|
||||||
batch_limit = self._max_size
|
batch_limit = self._max_size
|
||||||
|
|
||||||
batch = []
|
batch = []
|
||||||
@@ -1046,45 +1233,6 @@ class CacheCleaner(QThread):
|
|||||||
self.finished_clean.emit(removed_count)
|
self.finished_clean.emit(removed_count)
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailRunnable(QRunnable):
|
|
||||||
"""Runnable task to generate a single thumbnail."""
|
|
||||||
def __init__(self, cache, path, size, signal_emitter):
|
|
||||||
super().__init__()
|
|
||||||
self.cache = cache
|
|
||||||
self.path = path
|
|
||||||
self.size = size
|
|
||||||
self.emitter = signal_emitter
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
# Optimization: Single stat call per file
|
|
||||||
stat_res = os.stat(self.path)
|
|
||||||
curr_mtime = stat_res.st_mtime
|
|
||||||
inode = stat_res.st_ino
|
|
||||||
dev = stat_res.st_dev
|
|
||||||
|
|
||||||
# Check cache first to avoid expensive generation
|
|
||||||
thumb, mtime = self.cache.get_thumbnail(
|
|
||||||
self.path, self.size, curr_mtime=curr_mtime,
|
|
||||||
inode=inode, device_id=dev, async_load=False)
|
|
||||||
|
|
||||||
if not thumb or mtime != curr_mtime:
|
|
||||||
# Use the generation lock to coordinate
|
|
||||||
with self.cache.generation_lock(
|
|
||||||
self.path, self.size, curr_mtime, inode, dev) as should_gen:
|
|
||||||
if should_gen:
|
|
||||||
# I am the owner, I generate the thumbnail
|
|
||||||
new_thumb = generate_thumbnail(self.path, self.size)
|
|
||||||
if new_thumb and not new_thumb.isNull():
|
|
||||||
self.cache.set_thumbnail(
|
|
||||||
self.path, new_thumb, curr_mtime, self.size,
|
|
||||||
inode=inode, device_id=dev, block=True)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error generating thumbnail for {self.path}: {e}")
|
|
||||||
finally:
|
|
||||||
self.emitter.emit_progress()
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailGenerator(QThread):
|
class ThumbnailGenerator(QThread):
|
||||||
"""
|
"""
|
||||||
Background thread to generate thumbnails for a specific size for a list of
|
Background thread to generate thumbnails for a specific size for a list of
|
||||||
@@ -1100,31 +1248,35 @@ class ThumbnailGenerator(QThread):
|
|||||||
def emit_progress(self):
|
def emit_progress(self):
|
||||||
self.progress_tick.emit()
|
self.progress_tick.emit()
|
||||||
|
|
||||||
def __init__(self, cache, paths, size):
|
def __init__(self, cache, paths, size, thread_pool_manager):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.size = size
|
self.size = size
|
||||||
self._abort = False
|
self._abort = False
|
||||||
|
self.thread_pool_manager = thread_pool_manager
|
||||||
|
self._workers = []
|
||||||
|
self._workers_mutex = QMutex()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stops the worker thread gracefully."""
|
"""Stops the worker thread gracefully."""
|
||||||
self._abort = True
|
self._abort = True
|
||||||
|
self._workers_mutex.lock()
|
||||||
|
for worker in self._workers:
|
||||||
|
worker.shutdown()
|
||||||
|
self._workers_mutex.unlock()
|
||||||
self.wait()
|
self.wait()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Main execution loop. Uses a thread pool to process paths in parallel.
|
Main execution loop. Uses a thread pool to process paths in parallel.
|
||||||
"""
|
"""
|
||||||
pool = QThreadPool()
|
pool = self.thread_pool_manager.get_pool()
|
||||||
max_threads = APP_CONFIG.get(
|
|
||||||
"generation_threads",
|
|
||||||
SCANNER_SETTINGS_DEFAULTS.get("generation_threads", 4))
|
|
||||||
pool.setMaxThreadCount(max_threads)
|
|
||||||
|
|
||||||
emitter = self.SignalEmitter()
|
emitter = self.SignalEmitter()
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
total = len(self.paths)
|
total = len(self.paths)
|
||||||
|
sem = QSemaphore(0)
|
||||||
|
|
||||||
def on_tick():
|
def on_tick():
|
||||||
nonlocal processed_count
|
nonlocal processed_count
|
||||||
@@ -1138,13 +1290,33 @@ class ThumbnailGenerator(QThread):
|
|||||||
# The signal/slot mechanism handles thread safety automatically.
|
# The signal/slot mechanism handles thread safety automatically.
|
||||||
emitter.progress_tick.connect(on_tick, Qt.QueuedConnection)
|
emitter.progress_tick.connect(on_tick, Qt.QueuedConnection)
|
||||||
|
|
||||||
|
started_count = 0
|
||||||
for path in self.paths:
|
for path in self.paths:
|
||||||
if self._abort:
|
if self._abort:
|
||||||
break
|
break
|
||||||
runnable = ThumbnailRunnable(self.cache, path, self.size, emitter)
|
runnable = ScannerWorker(self.cache, path, target_sizes=[self.size],
|
||||||
pool.start(runnable)
|
load_metadata=False, signal_emitter=emitter,
|
||||||
|
semaphore=sem)
|
||||||
|
runnable.setAutoDelete(False)
|
||||||
|
|
||||||
pool.waitForDone()
|
self._workers_mutex.lock()
|
||||||
|
if self._abort:
|
||||||
|
self._workers_mutex.unlock()
|
||||||
|
break
|
||||||
|
self._workers.append(runnable)
|
||||||
|
self._workers_mutex.unlock()
|
||||||
|
|
||||||
|
pool.start(runnable)
|
||||||
|
started_count += 1
|
||||||
|
|
||||||
|
if started_count > 0:
|
||||||
|
sem.acquire(started_count)
|
||||||
|
|
||||||
|
self._workers_mutex.lock()
|
||||||
|
self._workers.clear()
|
||||||
|
self._workers_mutex.unlock()
|
||||||
|
|
||||||
|
if not self._abort:
|
||||||
self.generation_complete.emit()
|
self.generation_complete.emit()
|
||||||
|
|
||||||
|
|
||||||
@@ -1160,7 +1332,8 @@ class ImageScanner(QThread):
|
|||||||
finished_scan = Signal(int) # Total images found
|
finished_scan = Signal(int) # Total images found
|
||||||
more_files_available = Signal(int, int) # Last loaded index, remainder
|
more_files_available = Signal(int, int) # Last loaded index, remainder
|
||||||
|
|
||||||
def __init__(self, cache, paths, is_file_list=False, viewers=None):
|
def __init__(self, cache, paths, is_file_list=False, viewers=None,
|
||||||
|
thread_pool_manager=None):
|
||||||
# is_file_list is not used
|
# is_file_list is not used
|
||||||
if not paths or not isinstance(paths, (list, tuple)):
|
if not paths or not isinstance(paths, (list, tuple)):
|
||||||
logger.warning("ImageScanner initialized with empty or invalid paths")
|
logger.warning("ImageScanner initialized with empty or invalid paths")
|
||||||
@@ -1168,6 +1341,7 @@ class ImageScanner(QThread):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.all_files = []
|
self.all_files = []
|
||||||
|
self.thread_pool_manager = thread_pool_manager
|
||||||
self._viewers = viewers
|
self._viewers = viewers
|
||||||
self._seen_files = set()
|
self._seen_files = set()
|
||||||
self._is_file_list = is_file_list
|
self._is_file_list = is_file_list
|
||||||
@@ -1196,12 +1370,23 @@ class ImageScanner(QThread):
|
|||||||
self.pending_tasks = []
|
self.pending_tasks = []
|
||||||
self._priority_queue = collections.deque()
|
self._priority_queue = collections.deque()
|
||||||
self._processed_paths = set()
|
self._processed_paths = set()
|
||||||
|
self._current_workers = []
|
||||||
|
self._current_workers_mutex = QMutex()
|
||||||
|
|
||||||
# Initial load
|
# Initial load
|
||||||
self.pending_tasks.append((0, APP_CONFIG.get(
|
self.pending_tasks.append((0, APP_CONFIG.get(
|
||||||
"scan_batch_size", SCANNER_SETTINGS_DEFAULTS["scan_batch_size"])))
|
"scan_batch_size", SCANNER_SETTINGS_DEFAULTS["scan_batch_size"])))
|
||||||
self._last_update_time = 0
|
self._last_update_time = 0
|
||||||
|
|
||||||
|
if self.thread_pool_manager:
|
||||||
|
self.pool = self.thread_pool_manager.get_pool()
|
||||||
|
else:
|
||||||
|
self.pool = QThreadPool()
|
||||||
|
max_threads = APP_CONFIG.get(
|
||||||
|
"generation_threads",
|
||||||
|
SCANNER_SETTINGS_DEFAULTS.get("generation_threads", 4))
|
||||||
|
self.pool.setMaxThreadCount(max_threads)
|
||||||
|
|
||||||
logger.info(f"ImageScanner initialized with {len(paths)} paths")
|
logger.info(f"ImageScanner initialized with {len(paths)} paths")
|
||||||
|
|
||||||
def set_auto_load(self, enabled):
|
def set_auto_load(self, enabled):
|
||||||
@@ -1404,8 +1589,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()
|
||||||
@@ -1455,65 +1640,110 @@ class ImageScanner(QThread):
|
|||||||
self.finished_scan.emit(self.count)
|
self.finished_scan.emit(self.count)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.thread_pool_manager:
|
||||||
|
max_threads = self.thread_pool_manager.default_thread_count
|
||||||
|
else:
|
||||||
|
max_threads = APP_CONFIG.get(
|
||||||
|
"generation_threads",
|
||||||
|
SCANNER_SETTINGS_DEFAULTS.get("generation_threads", 4))
|
||||||
|
self.pool.setMaxThreadCount(max_threads)
|
||||||
|
|
||||||
images_loaded = 0
|
images_loaded = 0
|
||||||
batch = []
|
batch = []
|
||||||
while i < len(self.all_files):
|
while i < len(self.all_files):
|
||||||
|
|
||||||
if not self._is_running:
|
if not self._is_running:
|
||||||
return
|
return
|
||||||
self.msleep(1) # Force yield to UI thread per item
|
|
||||||
|
|
||||||
while self._paused and self._is_running:
|
while self._paused and self._is_running:
|
||||||
self.msleep(100)
|
self.msleep(100)
|
||||||
|
|
||||||
# 1. Check priority queue first
|
# Collect paths for this chunk to process in parallel
|
||||||
priority_path = None
|
chunk_size = max_threads * 2
|
||||||
|
tasks = [] # List of (path, is_from_priority_queue)
|
||||||
|
|
||||||
|
# 1. Drain priority queue up to chunk size
|
||||||
self.mutex.lock()
|
self.mutex.lock()
|
||||||
while self._priority_queue:
|
while len(tasks) < chunk_size and self._priority_queue:
|
||||||
p = self._priority_queue.popleft()
|
p = self._priority_queue.popleft()
|
||||||
if p not in self._processed_paths and p in self._seen_files:
|
if p not in self._processed_paths and p in self._seen_files:
|
||||||
priority_path = p
|
tasks.append((p, True))
|
||||||
break
|
|
||||||
self.mutex.unlock()
|
self.mutex.unlock()
|
||||||
|
|
||||||
# 2. Determine file to process
|
# 2. Fill remaining chunk space with sequential files
|
||||||
if priority_path:
|
temp_i = i
|
||||||
f_path = priority_path
|
while len(tasks) < chunk_size and temp_i < len(self.all_files):
|
||||||
# Don't increment 'i' yet, we are processing out of order
|
p = self.all_files[temp_i]
|
||||||
else:
|
# Skip if already processed (e.g. via priority earlier)
|
||||||
f_path = self.all_files[i]
|
if p not in self._processed_paths \
|
||||||
i += 1 # Only advance sequential index if processing sequentially
|
and Path(p).suffix.lower() in IMAGE_EXTENSIONS:
|
||||||
|
tasks.append((p, False))
|
||||||
|
temp_i += 1
|
||||||
|
|
||||||
if f_path not in self._processed_paths \
|
if not tasks:
|
||||||
and Path(f_path).suffix.lower() in IMAGE_EXTENSIONS:
|
# If no tasks found but still have files (e.g. all skipped extensions),
|
||||||
# Pass the batch list to store result instead of emitting immediately
|
# update index and continue loop
|
||||||
was_loaded = self._process_single_image(f_path, batch)
|
i = temp_i
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Submit tasks to thread pool
|
||||||
|
sem = QSemaphore(0)
|
||||||
|
runnables = []
|
||||||
|
|
||||||
|
self._current_workers_mutex.lock()
|
||||||
|
if not self._is_running:
|
||||||
|
self._current_workers_mutex.unlock()
|
||||||
|
return
|
||||||
|
|
||||||
|
for f_path, _ in tasks:
|
||||||
|
r = ScannerWorker(self.cache, f_path, semaphore=sem)
|
||||||
|
r.setAutoDelete(False)
|
||||||
|
runnables.append(r)
|
||||||
|
self._current_workers.append(r)
|
||||||
|
self.pool.start(r)
|
||||||
|
self._current_workers_mutex.unlock()
|
||||||
|
|
||||||
|
# Wait only for this chunk to finish using semaphore
|
||||||
|
sem.acquire(len(runnables))
|
||||||
|
|
||||||
|
self._current_workers_mutex.lock()
|
||||||
|
self._current_workers.clear()
|
||||||
|
self._current_workers_mutex.unlock()
|
||||||
|
|
||||||
|
if not self._is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process results
|
||||||
|
for r in runnables:
|
||||||
|
if r.result:
|
||||||
|
self._processed_paths.add(r.path)
|
||||||
|
batch.append(r.result)
|
||||||
|
self.count += 1
|
||||||
|
images_loaded += 1
|
||||||
|
|
||||||
|
# Clean up runnables
|
||||||
|
runnables.clear()
|
||||||
|
|
||||||
|
# Advance sequential index
|
||||||
|
i = temp_i
|
||||||
|
|
||||||
# Emit batch if size is enough (responsiveness optimization)
|
# Emit batch if size is enough (responsiveness optimization)
|
||||||
# Dynamic batching: Start small for instant feedback.
|
|
||||||
# Keep batches small enough to prevent UI starvation during rapid cache
|
|
||||||
# reads.
|
|
||||||
if self.count <= 100:
|
if self.count <= 100:
|
||||||
target_batch_size = 20
|
target_batch_size = 20
|
||||||
else:
|
else:
|
||||||
target_batch_size = 200
|
target_batch_size = 200
|
||||||
|
|
||||||
if len(batch) >= target_batch_size:
|
if len(batch) >= target_batch_size:
|
||||||
|
|
||||||
self.images_found.emit(batch)
|
self.images_found.emit(batch)
|
||||||
batch = []
|
batch = []
|
||||||
# Yield briefly to let the main thread process the emitted batch
|
self.msleep(10) # Yield to UI
|
||||||
# (update UI), preventing UI freeze during fast cache reading.
|
|
||||||
self.msleep(10)
|
|
||||||
|
|
||||||
if was_loaded:
|
# Check if loading limit reached
|
||||||
self._processed_paths.add(f_path)
|
|
||||||
images_loaded += 1
|
|
||||||
if images_loaded >= to_load and to_load > 0:
|
if images_loaded >= to_load and to_load > 0:
|
||||||
if batch: # Emit remaining items
|
if batch: # Emit remaining items
|
||||||
self.images_found.emit(batch)
|
self.images_found.emit(batch)
|
||||||
|
|
||||||
next_index = i + 1
|
next_index = i
|
||||||
total_files = len(self.all_files)
|
total_files = len(self.all_files)
|
||||||
self.index = next_index
|
self.index = next_index
|
||||||
self.progress_msg.emit(UITexts.LOADED_PARTIAL.format(
|
self.progress_msg.emit(UITexts.LOADED_PARTIAL.format(
|
||||||
@@ -1547,88 +1777,17 @@ class ImageScanner(QThread):
|
|||||||
self.progress_percent.emit(100)
|
self.progress_percent.emit(100)
|
||||||
self.finished_scan.emit(self.count)
|
self.finished_scan.emit(self.count)
|
||||||
|
|
||||||
def _load_metadata(self, path_or_fd):
|
|
||||||
"""Loads tag and rating data for a path or file descriptor."""
|
|
||||||
tags = []
|
|
||||||
|
|
||||||
raw_tags = XattrManager.get_attribute(path_or_fd, 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_or_fd, RATING_XATTR_NAME, "0")
|
|
||||||
try:
|
|
||||||
rating = int(raw_rating)
|
|
||||||
except ValueError:
|
|
||||||
rating = 0
|
|
||||||
return tags, rating
|
|
||||||
|
|
||||||
def _process_single_image(self, f_path, batch_list):
|
|
||||||
from constants import SCANNER_GENERATE_SIZES
|
|
||||||
|
|
||||||
fd = None
|
|
||||||
try:
|
|
||||||
# Optimize: Open file once to reuse FD for stat and xattrs
|
|
||||||
fd = os.open(f_path, os.O_RDONLY)
|
|
||||||
stat_res = os.fstat(fd)
|
|
||||||
curr_mtime = stat_res.st_mtime
|
|
||||||
curr_inode = stat_res.st_ino
|
|
||||||
curr_dev = stat_res.st_dev
|
|
||||||
|
|
||||||
smallest_thumb_for_signal = None
|
|
||||||
|
|
||||||
# Ensure required thumbnails exist
|
|
||||||
for size in SCANNER_GENERATE_SIZES:
|
|
||||||
# Check if a valid thumbnail for this size exists
|
|
||||||
thumb, mtime = self.cache.get_thumbnail(f_path, size,
|
|
||||||
curr_mtime=curr_mtime,
|
|
||||||
inode=curr_inode,
|
|
||||||
device_id=curr_dev)
|
|
||||||
if not thumb or mtime != curr_mtime:
|
|
||||||
# Use generation lock to prevent multiple threads generating the
|
|
||||||
# same thumb
|
|
||||||
with self.cache.generation_lock(
|
|
||||||
f_path, size, curr_mtime,
|
|
||||||
curr_inode, curr_dev) as should_gen:
|
|
||||||
if should_gen:
|
|
||||||
# I am the owner, I generate the thumbnail
|
|
||||||
new_thumb = generate_thumbnail(f_path, size)
|
|
||||||
if new_thumb and not new_thumb.isNull():
|
|
||||||
self.cache.set_thumbnail(
|
|
||||||
f_path, new_thumb, curr_mtime, size,
|
|
||||||
inode=curr_inode, device_id=curr_dev, block=True)
|
|
||||||
if size == min(SCANNER_GENERATE_SIZES):
|
|
||||||
smallest_thumb_for_signal = new_thumb
|
|
||||||
else:
|
|
||||||
# Another thread generated it, re-fetch to use it for the
|
|
||||||
# signal
|
|
||||||
if size == min(SCANNER_GENERATE_SIZES):
|
|
||||||
re_thumb, _ = self.cache.get_thumbnail(
|
|
||||||
f_path, size, curr_mtime=curr_mtime,
|
|
||||||
inode=curr_inode, device_id=curr_dev,
|
|
||||||
async_load=False)
|
|
||||||
smallest_thumb_for_signal = re_thumb
|
|
||||||
elif size == min(SCANNER_GENERATE_SIZES):
|
|
||||||
# valid thumb exists, use it for signal
|
|
||||||
smallest_thumb_for_signal = thumb
|
|
||||||
|
|
||||||
tags, rating = self._load_metadata(fd)
|
|
||||||
batch_list.append((f_path, smallest_thumb_for_signal,
|
|
||||||
curr_mtime, tags, rating, curr_inode, curr_dev))
|
|
||||||
self.count += 1
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing image {f_path}: {e}")
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
if fd is not None:
|
|
||||||
try:
|
|
||||||
os.close(fd)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
logger.info("ImageScanner stop requested")
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
|
|
||||||
|
# Cancel currently running workers in the active batch
|
||||||
|
self._current_workers_mutex.lock()
|
||||||
|
for worker in self._current_workers:
|
||||||
|
worker.shutdown()
|
||||||
|
self._current_workers_mutex.unlock()
|
||||||
|
|
||||||
|
# Wake up the condition variable
|
||||||
self.mutex.lock()
|
self.mutex.lock()
|
||||||
self.condition.wakeAll()
|
self.condition.wakeAll()
|
||||||
self.mutex.unlock()
|
self.mutex.unlock()
|
||||||
|
|||||||
1048
imageviewer.py
1048
imageviewer.py
File diff suppressed because it is too large
Load Diff
@@ -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."""
|
||||||
|
|
||||||
@@ -136,3 +155,33 @@ class XattrManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IOError(f"Could not save xattr '{attr_name}' "
|
raise IOError(f"Could not save xattr '{attr_name}' "
|
||||||
"for {file_path}: {e}") from e
|
"for {file_path}: {e}") from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_attributes(path):
|
||||||
|
"""
|
||||||
|
Gets all extended attributes for a file as a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The path to the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary mapping attribute names to values.
|
||||||
|
"""
|
||||||
|
attributes = {}
|
||||||
|
if not path:
|
||||||
|
return attributes
|
||||||
|
try:
|
||||||
|
keys = os.listxattr(path)
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
val = os.getxattr(path, key)
|
||||||
|
try:
|
||||||
|
val_str = val.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
val_str = str(val)
|
||||||
|
attributes[key] = val_str
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
pass
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
pass
|
||||||
|
return attributes
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Classes:
|
|||||||
PropertiesDialog: A QDialog that presents file properties in a tabbed
|
PropertiesDialog: A QDialog that presents file properties in a tabbed
|
||||||
interface.
|
interface.
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QLabel, QVBoxLayout, QMessageBox, QMenu, QInputDialog,
|
QWidget, QLabel, QVBoxLayout, QMessageBox, QMenu, QInputDialog,
|
||||||
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget,
|
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget,
|
||||||
@@ -18,14 +17,40 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QImageReader, QIcon, QColor
|
QImageReader, QIcon, QColor
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (QThread, Signal, Qt, QFileInfo, QLocale)
|
||||||
Qt, QFileInfo, QLocale
|
|
||||||
)
|
|
||||||
from constants import (
|
from constants import (
|
||||||
RATING_XATTR_NAME, XATTR_NAME, UITexts
|
RATING_XATTR_NAME, XATTR_NAME, UITexts
|
||||||
)
|
)
|
||||||
from metadatamanager import MetadataManager, HAVE_EXIV2, notify_baloo
|
from metadatamanager import MetadataManager, HAVE_EXIV2, XattrManager
|
||||||
from utils import preserve_mtime
|
|
||||||
|
|
||||||
|
class PropertiesLoader(QThread):
|
||||||
|
"""Background thread to load metadata (xattrs and EXIF) asynchronously."""
|
||||||
|
loaded = Signal(dict, dict)
|
||||||
|
|
||||||
|
def __init__(self, path, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.path = path
|
||||||
|
self._abort = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Signals the thread to stop and waits for it."""
|
||||||
|
self._abort = True
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Xattrs
|
||||||
|
if self._abort:
|
||||||
|
return
|
||||||
|
xattrs = XattrManager.get_all_attributes(self.path)
|
||||||
|
|
||||||
|
if self._abort:
|
||||||
|
return
|
||||||
|
|
||||||
|
# EXIF
|
||||||
|
exif_data = MetadataManager.read_all_metadata(self.path)
|
||||||
|
if not self._abort:
|
||||||
|
self.loaded.emit(xattrs, exif_data)
|
||||||
|
|
||||||
|
|
||||||
class PropertiesDialog(QDialog):
|
class PropertiesDialog(QDialog):
|
||||||
@@ -51,6 +76,7 @@ class PropertiesDialog(QDialog):
|
|||||||
self.setWindowTitle(UITexts.PROPERTIES_TITLE)
|
self.setWindowTitle(UITexts.PROPERTIES_TITLE)
|
||||||
self._initial_tags = initial_tags if initial_tags is not None else []
|
self._initial_tags = initial_tags if initial_tags is not None else []
|
||||||
self._initial_rating = initial_rating
|
self._initial_rating = initial_rating
|
||||||
|
self.loader = None
|
||||||
self.resize(400, 500)
|
self.resize(400, 500)
|
||||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||||
|
|
||||||
@@ -128,7 +154,8 @@ class PropertiesDialog(QDialog):
|
|||||||
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.table.customContextMenuRequested.connect(self.show_context_menu)
|
self.table.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
|
|
||||||
self.load_metadata()
|
# Initial partial load (synchronous, just passed args)
|
||||||
|
self.update_metadata_table({}, initial_only=True)
|
||||||
meta_layout.addWidget(self.table)
|
meta_layout.addWidget(self.table)
|
||||||
tabs.addTab(meta_widget, QIcon.fromTheme("document-properties"),
|
tabs.addTab(meta_widget, QIcon.fromTheme("document-properties"),
|
||||||
UITexts.PROPERTIES_METADATA_TAB)
|
UITexts.PROPERTIES_METADATA_TAB)
|
||||||
@@ -159,7 +186,8 @@ class PropertiesDialog(QDialog):
|
|||||||
# This is a disk read.
|
# This is a disk read.
|
||||||
self.exif_table.customContextMenuRequested.connect(self.show_exif_context_menu)
|
self.exif_table.customContextMenuRequested.connect(self.show_exif_context_menu)
|
||||||
|
|
||||||
self.load_exif_data()
|
# Placeholder for EXIF
|
||||||
|
self.update_exif_table(None)
|
||||||
|
|
||||||
exif_layout.addWidget(self.exif_table)
|
exif_layout.addWidget(self.exif_table)
|
||||||
tabs.addTab(exif_widget, QIcon.fromTheme("view-details"),
|
tabs.addTab(exif_widget, QIcon.fromTheme("view-details"),
|
||||||
@@ -173,10 +201,18 @@ class PropertiesDialog(QDialog):
|
|||||||
btn_box.rejected.connect(self.close)
|
btn_box.rejected.connect(self.close)
|
||||||
layout.addWidget(btn_box)
|
layout.addWidget(btn_box)
|
||||||
|
|
||||||
def load_metadata(self):
|
# Start background loading
|
||||||
|
self.reload_metadata()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.loader and self.loader.isRunning():
|
||||||
|
self.loader.stop()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def update_metadata_table(self, disk_xattrs, initial_only=False):
|
||||||
"""
|
"""
|
||||||
Loads metadata from the file's text keys (via QImageReader) and
|
Updates the metadata table with extended attributes.
|
||||||
extended attributes (xattrs) into the metadata table.
|
Merges initial tags/rating with loaded xattrs.
|
||||||
"""
|
"""
|
||||||
self.table.blockSignals(True)
|
self.table.blockSignals(True)
|
||||||
self.table.setRowCount(0)
|
self.table.setRowCount(0)
|
||||||
@@ -188,26 +224,11 @@ class PropertiesDialog(QDialog):
|
|||||||
if self._initial_rating > 0:
|
if self._initial_rating > 0:
|
||||||
preloaded_xattrs[RATING_XATTR_NAME] = str(self._initial_rating)
|
preloaded_xattrs[RATING_XATTR_NAME] = str(self._initial_rating)
|
||||||
|
|
||||||
# Read other xattrs from disk
|
|
||||||
xattrs = {}
|
|
||||||
try:
|
|
||||||
for xkey in os.listxattr(self.path):
|
|
||||||
# Avoid re-reading already known attributes
|
|
||||||
if xkey not in preloaded_xattrs:
|
|
||||||
try:
|
|
||||||
val = os.getxattr(self.path, xkey) # This is a disk read
|
|
||||||
try:
|
|
||||||
val_str = val.decode('utf-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
val_str = str(val)
|
|
||||||
xattrs[xkey] = val_str
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Combine preloaded and newly read xattrs
|
# Combine preloaded and newly read xattrs
|
||||||
all_xattrs = {**preloaded_xattrs, **xattrs}
|
all_xattrs = preloaded_xattrs.copy()
|
||||||
|
if not initial_only and disk_xattrs:
|
||||||
|
# Disk data takes precedence or adds to it
|
||||||
|
all_xattrs.update(disk_xattrs)
|
||||||
|
|
||||||
self.table.setRowCount(len(all_xattrs))
|
self.table.setRowCount(len(all_xattrs))
|
||||||
|
|
||||||
@@ -224,11 +245,34 @@ class PropertiesDialog(QDialog):
|
|||||||
row += 1
|
row += 1
|
||||||
self.table.blockSignals(False)
|
self.table.blockSignals(False)
|
||||||
|
|
||||||
def load_exif_data(self):
|
def reload_metadata(self):
|
||||||
"""Loads EXIF, XMP, and IPTC metadata using the MetadataManager."""
|
"""Starts the background thread to load metadata."""
|
||||||
|
if self.loader and self.loader.isRunning():
|
||||||
|
# Already running
|
||||||
|
return
|
||||||
|
self.loader = PropertiesLoader(self.path, self)
|
||||||
|
self.loader.loaded.connect(self.on_data_loaded)
|
||||||
|
self.loader.start()
|
||||||
|
|
||||||
|
def on_data_loaded(self, xattrs, exif_data):
|
||||||
|
"""Slot called when metadata is loaded from the thread."""
|
||||||
|
self.update_metadata_table(xattrs, initial_only=False)
|
||||||
|
self.update_exif_table(exif_data)
|
||||||
|
|
||||||
|
def update_exif_table(self, exif_data):
|
||||||
|
"""Updates the EXIF table with loaded data."""
|
||||||
self.exif_table.blockSignals(True)
|
self.exif_table.blockSignals(True)
|
||||||
self.exif_table.setRowCount(0)
|
self.exif_table.setRowCount(0)
|
||||||
|
|
||||||
|
if exif_data is None:
|
||||||
|
# Loading state
|
||||||
|
self.exif_table.setRowCount(1)
|
||||||
|
item = QTableWidgetItem("Loading data...")
|
||||||
|
item.setFlags(Qt.ItemIsEnabled)
|
||||||
|
self.exif_table.setItem(0, 0, item)
|
||||||
|
self.exif_table.blockSignals(False)
|
||||||
|
return
|
||||||
|
|
||||||
if not HAVE_EXIV2:
|
if not HAVE_EXIV2:
|
||||||
self.exif_table.setRowCount(1)
|
self.exif_table.setRowCount(1)
|
||||||
error_color = QColor("red")
|
error_color = QColor("red")
|
||||||
@@ -243,8 +287,6 @@ class PropertiesDialog(QDialog):
|
|||||||
self.exif_table.blockSignals(False)
|
self.exif_table.blockSignals(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
exif_data = MetadataManager.read_all_metadata(self.path)
|
|
||||||
|
|
||||||
if not exif_data:
|
if not exif_data:
|
||||||
self.exif_table.setRowCount(1)
|
self.exif_table.setRowCount(1)
|
||||||
item = QTableWidgetItem(UITexts.INFO)
|
item = QTableWidgetItem(UITexts.INFO)
|
||||||
@@ -291,16 +333,11 @@ class PropertiesDialog(QDialog):
|
|||||||
if item.column() == 1:
|
if item.column() == 1:
|
||||||
key = self.table.item(item.row(), 0).text()
|
key = self.table.item(item.row(), 0).text()
|
||||||
val = item.text()
|
val = item.text()
|
||||||
|
# Treat empty or whitespace-only values as removal to match previous
|
||||||
|
# behavior
|
||||||
|
val_to_set = val if val.strip() else None
|
||||||
try:
|
try:
|
||||||
with preserve_mtime(self.path):
|
XattrManager.set_attribute(self.path, key, val_to_set)
|
||||||
if not val.strip():
|
|
||||||
try:
|
|
||||||
os.removexattr(self.path, key)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
os.setxattr(self.path, key, val.encode('utf-8'))
|
|
||||||
notify_baloo(self.path)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, UITexts.ERROR,
|
QMessageBox.warning(self, UITexts.ERROR,
|
||||||
UITexts.PROPERTIES_ERROR_SET_ATTR.format(e))
|
UITexts.PROPERTIES_ERROR_SET_ATTR.format(e))
|
||||||
@@ -361,10 +398,8 @@ class PropertiesDialog(QDialog):
|
|||||||
key))
|
key))
|
||||||
if ok2:
|
if ok2:
|
||||||
try:
|
try:
|
||||||
with preserve_mtime(self.path):
|
XattrManager.set_attribute(self.path, key, val)
|
||||||
os.setxattr(self.path, key, val.encode('utf-8'))
|
self.reload_metadata()
|
||||||
notify_baloo(self.path)
|
|
||||||
self.load_metadata()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, UITexts.ERROR,
|
QMessageBox.warning(self, UITexts.ERROR,
|
||||||
UITexts.PROPERTIES_ERROR_ADD_ATTR.format(e))
|
UITexts.PROPERTIES_ERROR_ADD_ATTR.format(e))
|
||||||
@@ -378,9 +413,7 @@ class PropertiesDialog(QDialog):
|
|||||||
"""
|
"""
|
||||||
key = self.table.item(row, 0).text()
|
key = self.table.item(row, 0).text()
|
||||||
try:
|
try:
|
||||||
with preserve_mtime(self.path):
|
XattrManager.set_attribute(self.path, key, None)
|
||||||
os.removexattr(self.path, key)
|
|
||||||
notify_baloo(self.path)
|
|
||||||
self.table.removeRow(row)
|
self.table.removeRow(row)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, UITexts.ERROR,
|
QMessageBox.warning(self, UITexts.ERROR,
|
||||||
|
|||||||
105
settings.py
105
settings.py
@@ -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,
|
||||||
@@ -34,7 +35,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,
|
UITexts, save_app_config, HAVE_BAGHEERASEARCH_LIB
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -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(
|
||||||
@@ -687,19 +741,36 @@ class SettingsDialog(QDialog):
|
|||||||
self.threads_spin.setValue(scan_threads)
|
self.threads_spin.setValue(scan_threads)
|
||||||
|
|
||||||
# Set search engine
|
# Set search engine
|
||||||
index = self.search_engine_combo.findData(search_engine)
|
if HAVE_BAGHEERASEARCH_LIB:
|
||||||
|
self.search_engine_combo.setEnabled(True)
|
||||||
|
if search_engine != "Baloo":
|
||||||
|
index = self.search_engine_combo.findData("Bagheera")
|
||||||
if index != -1:
|
if index != -1:
|
||||||
self.search_engine_combo.setCurrentIndex(index)
|
self.search_engine_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
index = self.search_engine_combo.findData("Baloo")
|
||||||
|
if index != -1:
|
||||||
|
self.search_engine_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
self.search_engine_combo.setEnabled(False)
|
||||||
|
if SEARCH_CMD:
|
||||||
|
index = self.search_engine_combo.findData("Baloo")
|
||||||
|
if index != -1:
|
||||||
|
self.search_engine_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
self.search_engine_combo.setCurrentIndex(-1)
|
||||||
|
|
||||||
self.scan_full_on_start_checkbox.setChecked(scan_full_on_start)
|
self.scan_full_on_start_checkbox.setChecked(scan_full_on_start)
|
||||||
|
|
||||||
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 +780,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 +790,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 +845,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(
|
||||||
@@ -938,19 +1024,23 @@ class SettingsDialog(QDialog):
|
|||||||
APP_CONFIG["scan_max_level"] = self.scan_max_level_spin.value()
|
APP_CONFIG["scan_max_level"] = self.scan_max_level_spin.value()
|
||||||
APP_CONFIG["generation_threads"] = self.threads_spin.value()
|
APP_CONFIG["generation_threads"] = self.threads_spin.value()
|
||||||
APP_CONFIG["scan_batch_size"] = self.scan_batch_size_spin.value()
|
APP_CONFIG["scan_batch_size"] = self.scan_batch_size_spin.value()
|
||||||
|
if HAVE_BAGHEERASEARCH_LIB:
|
||||||
APP_CONFIG["search_engine"] = self.search_engine_combo.currentData()
|
APP_CONFIG["search_engine"] = self.search_engine_combo.currentData()
|
||||||
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 +1065,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()
|
||||||
|
|||||||
13
widgets.py
13
widgets.py
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user