v0.9.14
This commit is contained in:
@@ -14,7 +14,7 @@ Classes:
|
||||
MainWindow: The main application window containing the thumbnail grid and docks.
|
||||
"""
|
||||
__appname__ = "BagheeraView"
|
||||
__version__ = "0.9.13"
|
||||
__version__ = "0.9.14"
|
||||
__author__ = "Ignacio Serantes"
|
||||
__email__ = "kde@aynoa.net"
|
||||
__license__ = "LGPL"
|
||||
@@ -77,7 +77,7 @@ from widgets import (
|
||||
TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget,
|
||||
FavoritesWidget
|
||||
)
|
||||
from metadatamanager import XattrManager
|
||||
from metadatamanager import load_common_metadata
|
||||
|
||||
|
||||
class ShortcutHelpDialog(QDialog):
|
||||
@@ -626,19 +626,20 @@ class ThumbnailDelegate(QStyledItemDelegate):
|
||||
|
||||
# Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap
|
||||
# conversion on every paint event.
|
||||
cache_key = f"thumb_{path}_{mtime}_{thumb_size}"
|
||||
actual_tier = self.main_win.cache.get_available_tier(path, thumb_size, mtime)
|
||||
cache_key = f"thumb_{path}_{mtime}_{thumb_size}_{actual_tier}"
|
||||
source_pixmap = QPixmapCache.find(cache_key)
|
||||
|
||||
if not source_pixmap or source_pixmap.isNull():
|
||||
# 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(
|
||||
res = self.main_win.cache.get_thumbnail(
|
||||
path, requested_size=thumb_size, curr_mtime=mtime,
|
||||
inode=inode, device_id=device_id, async_load=True)
|
||||
|
||||
if img and not img.isNull():
|
||||
source_pixmap = QPixmap.fromImage(img)
|
||||
if res.image and not res.image.isNull():
|
||||
source_pixmap = QPixmap.fromImage(res.image)
|
||||
QPixmapCache.insert(cache_key, source_pixmap)
|
||||
else:
|
||||
# Fallback: Check a separate cache key for the placeholder to avoid
|
||||
@@ -3458,26 +3459,27 @@ class MainWindow(QMainWindow):
|
||||
if not path:
|
||||
return
|
||||
|
||||
# Determine tags and rating based on provided metadata or disk read
|
||||
if metadata:
|
||||
tags = metadata.get('tags', [])
|
||||
rating = metadata.get('rating', 0)
|
||||
else:
|
||||
res = load_common_metadata(path)
|
||||
tags, rating = res.tags, res.rating
|
||||
|
||||
# Find the item in the source model and update its data
|
||||
for row in range(self.thumbnail_model.rowCount()):
|
||||
item = self.thumbnail_model.item(row)
|
||||
if item and item.data(PATH_ROLE) == path:
|
||||
# Reload metadata for this item from xattr
|
||||
try:
|
||||
if metadata and 'tags' in metadata:
|
||||
tags = metadata['tags']
|
||||
else: # Fallback to reading from disk if not provided
|
||||
raw = XattrManager.get_attribute(path, XATTR_NAME)
|
||||
tags = sorted(list(set(t.strip() for t in raw.split(',')
|
||||
if t.strip()))) if raw else []
|
||||
item.setData(tags, TAGS_ROLE)
|
||||
except Exception:
|
||||
item.setData([], TAGS_ROLE)
|
||||
try:
|
||||
item.setData(metadata.get('rating', 0)
|
||||
if metadata else 0, RATING_ROLE)
|
||||
except Exception:
|
||||
item.setData(0, RATING_ROLE) # Default to 0 if error
|
||||
item.setData(tags, TAGS_ROLE)
|
||||
item.setData(rating, RATING_ROLE)
|
||||
|
||||
tooltip_text = f"{os.path.basename(path)}\n{path}"
|
||||
if tags:
|
||||
display_tags = [t.split('/')[-1] for t in tags]
|
||||
tooltip_text += f"\n{UITexts.TAGS_TAB}: {', '.join(
|
||||
display_tags)}"
|
||||
item.setToolTip(tooltip_text)
|
||||
|
||||
# Notify the view that the data has changed
|
||||
source_idx = self.thumbnail_model.indexFromItem(item)
|
||||
@@ -3485,13 +3487,10 @@ class MainWindow(QMainWindow):
|
||||
source_idx, source_idx, [TAGS_ROLE, RATING_ROLE])
|
||||
|
||||
# Update internal data structure to prevent stale data on rebuild
|
||||
current_tags = item.data(TAGS_ROLE)
|
||||
current_rating = item.data(RATING_ROLE)
|
||||
self._update_internal_data(path, tags=current_tags,
|
||||
rating=current_rating)
|
||||
self._update_internal_data(path, tags=tags, rating=rating)
|
||||
|
||||
# Update proxy filter cache to prevent stale filtering
|
||||
self.proxy_model.add_to_cache(path, current_tags)
|
||||
self.proxy_model.add_to_cache(path, tags)
|
||||
break
|
||||
|
||||
if self.main_dock.isVisible():
|
||||
@@ -3594,8 +3593,13 @@ class MainWindow(QMainWindow):
|
||||
if not paths:
|
||||
return
|
||||
|
||||
# Prioritize visible paths
|
||||
visible = self.get_visible_image_paths()
|
||||
visible_set = set(visible)
|
||||
ordered_paths = visible + [p for p in paths if p not in visible_set]
|
||||
|
||||
self.thumbnail_generator = ThumbnailGenerator(
|
||||
self.cache, paths, size, self.thread_pool_manager)
|
||||
self.cache, ordered_paths, size, self.thread_pool_manager)
|
||||
self.thumbnail_generator.generation_complete.connect(
|
||||
self.on_high_res_generation_finished)
|
||||
self.thumbnail_generator.progress.connect(
|
||||
|
||||
@@ -62,6 +62,8 @@ Ahora que la carga es rápida, ¿cómo puedo implementar una precarga inteligent
|
||||
|
||||
¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista?
|
||||
|
||||
v0.9.14 -
|
||||
· Corregido el problema de resolución de los thumbnails
|
||||
|
||||
v0.9.13 -
|
||||
· Añadida la opción de favoritos.
|
||||
|
||||
@@ -29,7 +29,7 @@ if FORCE_X11:
|
||||
# --- CONFIGURATION ---
|
||||
PROG_NAME = "Bagheera Image Viewer"
|
||||
PROG_ID = "bagheeraview"
|
||||
PROG_VERSION = "0.9.13"
|
||||
PROG_VERSION = "0.9.14"
|
||||
PROG_AUTHOR = "Ignacio Serantes"
|
||||
|
||||
# --- CACHE SETTINGS ---
|
||||
|
||||
@@ -112,8 +112,8 @@ class ImagePreloader(QThread):
|
||||
img = reader.read()
|
||||
if not img.isNull():
|
||||
# Load tags and rating here to avoid re-reading in main thread
|
||||
tags, rating = load_common_metadata(path)
|
||||
self.image_ready.emit(idx, path, img, tags, rating)
|
||||
res = load_common_metadata(path)
|
||||
self.image_ready.emit(idx, path, img, res.tags, res.rating)
|
||||
except Exception as e:
|
||||
logger.warning(f"ImagePreloader failed to load {path}: {e}")
|
||||
|
||||
@@ -220,7 +220,8 @@ class ImageController(QObject):
|
||||
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)
|
||||
res = load_common_metadata(path)
|
||||
self._current_tags, self._current_rating = res.tags, res.rating
|
||||
self._current_metadata_path = path
|
||||
|
||||
self._trigger_preload()
|
||||
@@ -258,7 +259,8 @@ class ImageController(QObject):
|
||||
|
||||
# Load tags and rating if not already set for this path
|
||||
if self._current_metadata_path != path:
|
||||
self._current_tags, self._current_rating = load_common_metadata(path)
|
||||
res = load_common_metadata(path)
|
||||
self._current_tags, self._current_rating = res.tags, res.rating
|
||||
self._current_metadata_path = path
|
||||
|
||||
self._loaded_path = path
|
||||
@@ -778,7 +780,8 @@ class ImageController(QObject):
|
||||
# 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)
|
||||
res = load_common_metadata(path)
|
||||
self._current_tags, self._current_rating = res.tags, res.rating
|
||||
self._current_metadata_path = path
|
||||
else:
|
||||
self._current_tags = []
|
||||
@@ -810,7 +813,8 @@ class ImageController(QObject):
|
||||
# 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)
|
||||
res = load_common_metadata(path)
|
||||
self._current_tags, self._current_rating = res.tags, res.rating
|
||||
self._current_metadata_path = path
|
||||
else:
|
||||
self._current_tags = []
|
||||
|
||||
128
imagescanner.py
128
imagescanner.py
@@ -36,13 +36,13 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
|
||||
|
||||
from constants import (
|
||||
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES,
|
||||
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS, RATING_XATTR_NAME,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES, XATTR_NAME,
|
||||
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES,
|
||||
UITexts
|
||||
)
|
||||
|
||||
from imageviewer import ImageViewer
|
||||
from metadatamanager import XattrManager
|
||||
from metadatamanager import load_common_metadata
|
||||
|
||||
if HAVE_BAGHEERASEARCH_LIB:
|
||||
from bagheera_search_lib import BagheeraSearcher
|
||||
@@ -50,6 +50,10 @@ if HAVE_BAGHEERASEARCH_LIB:
|
||||
# Set up logging for better debugging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Result structure for thumbnail retrieval to ensure API stability
|
||||
ThumbnailResult = collections.namedtuple('ThumbnailResult', ['image', 'mtime', 'tier'])
|
||||
EMPTY_THUMBNAIL = ThumbnailResult(None, 0, 0)
|
||||
|
||||
|
||||
class ThreadPoolManager:
|
||||
"""Manages a global QThreadPool to dynamically adjust thread count."""
|
||||
@@ -151,11 +155,10 @@ class ScannerWorker(QRunnable):
|
||||
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:
|
||||
res = self.cache.get_thumbnail(
|
||||
self.path, size, curr_mtime=curr_mtime,
|
||||
inode=curr_inode, device_id=curr_dev)
|
||||
if not res.image or res.mtime != curr_mtime or res.tier != size:
|
||||
# Use generation lock to prevent multiple threads generating
|
||||
with self.cache.generation_lock(
|
||||
self.path, size, curr_mtime,
|
||||
@@ -177,19 +180,20 @@ class ScannerWorker(QRunnable):
|
||||
else:
|
||||
# Another thread generated it, re-fetch
|
||||
if size == min_size:
|
||||
re_thumb, _ = self.cache.get_thumbnail(
|
||||
re_res = 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
|
||||
smallest_thumb_for_signal = re_res.image
|
||||
elif size == min_size:
|
||||
# valid thumb exists, use it for signal
|
||||
smallest_thumb_for_signal = thumb
|
||||
smallest_thumb_for_signal = res.image
|
||||
|
||||
tags = []
|
||||
rating = 0
|
||||
if self.load_metadata_flag:
|
||||
tags, rating = self._load_metadata(fd)
|
||||
res_meta = load_common_metadata(fd)
|
||||
tags, rating = res_meta.tags, res_meta.rating
|
||||
self.result = (self.path, smallest_thumb_for_signal,
|
||||
curr_mtime, tags, rating, curr_inode, curr_dev)
|
||||
except Exception as e:
|
||||
@@ -206,21 +210,6 @@ class ScannerWorker(QRunnable):
|
||||
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."""
|
||||
@@ -475,12 +464,12 @@ class CacheLoader(QThread):
|
||||
|
||||
# Call synchronous get_thumbnail to fetch and decode
|
||||
# This puts the result into the RAM cache
|
||||
img, _ = self.cache.get_thumbnail(
|
||||
res = self.cache.get_thumbnail(
|
||||
path, size, curr_mtime=mtime, inode=inode,
|
||||
device_id=dev, async_load=False
|
||||
)
|
||||
|
||||
if img:
|
||||
if res.image:
|
||||
self.cache.thumbnail_loaded.emit(path, size)
|
||||
|
||||
self.cache._mark_load_complete(path, size)
|
||||
@@ -722,7 +711,7 @@ class ThumbnailCache(QObject):
|
||||
def _check_disk_cache(self, path, search_order, mtime, dev_id, inode_key):
|
||||
"""Helper to check LMDB synchronously."""
|
||||
if not self._lmdb_env or not inode_key or dev_id == 0:
|
||||
return None, 0
|
||||
return EMPTY_THUMBNAIL
|
||||
|
||||
txn = None
|
||||
try:
|
||||
@@ -754,35 +743,52 @@ class ThumbnailCache(QObject):
|
||||
self._thumbnail_cache[path][size] = (img, mtime)
|
||||
self._cache_bytes_size += img.sizeInBytes()
|
||||
self._path_to_inode[path] = (dev_id, inode_key)
|
||||
return img, mtime
|
||||
return ThumbnailResult(img, mtime, size)
|
||||
except Exception as e:
|
||||
logger.debug(f"Cache lookup error for {path}: {e}")
|
||||
finally:
|
||||
if txn:
|
||||
txn.abort()
|
||||
|
||||
return None, 0
|
||||
return EMPTY_THUMBNAIL
|
||||
|
||||
def get_available_tier(self, path, requested_size, mtime):
|
||||
"""Returns the best available tier in memory for the given path."""
|
||||
target_tier = self._get_tier_for_size(requested_size)
|
||||
with self._read_lock():
|
||||
cached_sizes = self._thumbnail_cache.get(path)
|
||||
if not cached_sizes:
|
||||
return 0
|
||||
if target_tier in cached_sizes:
|
||||
img, cached_mtime = cached_sizes[target_tier]
|
||||
if img and not img.isNull() and cached_mtime == mtime:
|
||||
return target_tier
|
||||
for size in THUMBNAIL_SIZES:
|
||||
if size in cached_sizes:
|
||||
img, cached_mtime = cached_sizes[size]
|
||||
if img and not img.isNull() and cached_mtime == mtime:
|
||||
return size
|
||||
return 0
|
||||
|
||||
def get_thumbnail(self, path, requested_size, curr_mtime=None,
|
||||
inode=None, device_id=None, async_load=False):
|
||||
"""
|
||||
Safely retrieve a thumbnail from cache, finding the best available size.
|
||||
Returns: tuple (QImage or None, mtime) or (None, 0) if not found.
|
||||
Returns: ThumbnailResult object.
|
||||
"""
|
||||
# 1. Determine the ideal tier and create a prioritized search order.
|
||||
target_tier = self._get_tier_for_size(requested_size)
|
||||
search_order = [target_tier] + \
|
||||
sorted([s for s in THUMBNAIL_SIZES if s > target_tier]) + \
|
||||
sorted([s for s in THUMBNAIL_SIZES if s < target_tier], reverse=True)
|
||||
|
||||
# 2. Resolve file identity (mtime, dev, inode)
|
||||
mtime, dev_id, inode_key = self._resolve_file_identity(
|
||||
path, curr_mtime, inode, device_id)
|
||||
|
||||
if mtime is None:
|
||||
return None, 0
|
||||
return EMPTY_THUMBNAIL
|
||||
|
||||
best_img, best_mtime, best_tier = None, 0, 0
|
||||
|
||||
# 3. Check memory cache (fastest)
|
||||
with self._read_lock():
|
||||
cached_sizes = self._thumbnail_cache.get(path)
|
||||
if cached_sizes:
|
||||
@@ -790,15 +796,22 @@ class ThumbnailCache(QObject):
|
||||
if size in cached_sizes:
|
||||
img, cached_mtime = cached_sizes[size]
|
||||
if img and not img.isNull() and cached_mtime == mtime:
|
||||
return img, mtime
|
||||
if size == target_tier:
|
||||
return ThumbnailResult(img, mtime, size)
|
||||
if not best_img:
|
||||
best_img, best_mtime, best_tier = img, mtime, size
|
||||
|
||||
# 4. Handle Async Request
|
||||
if async_load:
|
||||
self._queue_async_load(path, target_tier, mtime, dev_id, inode_key)
|
||||
return None, 0
|
||||
return ThumbnailResult(best_img, best_mtime, best_tier)
|
||||
|
||||
# 5. Check Disk Cache (Sync fallback)
|
||||
return self._check_disk_cache(path, search_order, mtime, dev_id, inode_key)
|
||||
res = self._check_disk_cache(path, [target_tier], mtime, dev_id, inode_key)
|
||||
if res.image:
|
||||
return res
|
||||
if best_img:
|
||||
return ThumbnailResult(best_img, best_mtime, best_tier)
|
||||
other_tiers = [s for s in search_order if s != target_tier]
|
||||
return self._check_disk_cache(path, other_tiers, mtime, dev_id, inode_key)
|
||||
|
||||
def _mark_load_complete(self, path, size):
|
||||
"""Remove item from pending loading set."""
|
||||
@@ -849,6 +862,7 @@ class ThumbnailCache(QObject):
|
||||
if self._cache_writer:
|
||||
self._cache_writer.enqueue(
|
||||
(dev_id, inode_key, img, mtime, size, path), block=block)
|
||||
self.thumbnail_loaded.emit(path, size)
|
||||
return True
|
||||
|
||||
def invalidate_path(self, path):
|
||||
@@ -1074,12 +1088,32 @@ class ThumbnailCache(QObject):
|
||||
|
||||
for size in THUMBNAIL_SIZES:
|
||||
try:
|
||||
db = self._get_device_db(device_id, size, write=True)
|
||||
if not db:
|
||||
continue
|
||||
|
||||
with env.begin(write=True) as txn:
|
||||
txn.delete(inode_key, db=db)
|
||||
# Get DB handle using the current transaction and don't force create
|
||||
db = self._get_device_db(device_id, size, write=False, txn=txn)
|
||||
if not db:
|
||||
continue
|
||||
|
||||
try:
|
||||
txn.delete(inode_key, db=db)
|
||||
except lmdb.Error as e:
|
||||
if "Invalid argument" in str(e):
|
||||
# Handle potential stale DB handles (EINVAL)
|
||||
db_name = f"dev_{device_id}_{size}".encode('utf-8')
|
||||
self._db_lock.lock()
|
||||
self._db_handles.pop(db_name, None)
|
||||
self._db_lock.unlock()
|
||||
# Retry with a fresh handle within the same transaction
|
||||
db = self._get_device_db(
|
||||
device_id, size, write=False, txn=txn)
|
||||
if db:
|
||||
txn.delete(inode_key, db=db)
|
||||
elif "not found" in str(e).lower():
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
except lmdb.NotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting from LMDB for size {size}: {e}")
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from constants import (
|
||||
ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH,
|
||||
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL,
|
||||
VIEWER_WHEEL_SPEED_DEFAULT, XATTR_NAME, ZOOM_DESKTOP_RATIO, UITexts,
|
||||
AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES
|
||||
)
|
||||
from imagecontroller import ImageController
|
||||
from widgets import FaceNameInputWidget
|
||||
@@ -156,9 +157,9 @@ class FilmstripLoader(QThread):
|
||||
# Small sleep to prevent UI freezing during heavy IO bursts
|
||||
self.msleep(1)
|
||||
try:
|
||||
img, _ = self.cache.get_thumbnail(path, self.icon_size)
|
||||
if img and not img.isNull():
|
||||
self.thumbnail_loaded.emit(index, img)
|
||||
res = self.cache.get_thumbnail(path, self.icon_size)
|
||||
if res.image and not res.image.isNull():
|
||||
self.thumbnail_loaded.emit(index, res.image)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -2535,6 +2536,13 @@ class ImageViewer(QWidget):
|
||||
action.setCheckable(True)
|
||||
action.setChecked(item.get("checked", False))
|
||||
|
||||
if "enabled" in item:
|
||||
action.setEnabled(item["enabled"])
|
||||
|
||||
if "tooltip" in item:
|
||||
action.setToolTip(item["tooltip"])
|
||||
action.setStatusTip(item["tooltip"])
|
||||
|
||||
def restore_scroll_for_pane(self, pane, config):
|
||||
"""
|
||||
Applies the saved scrollbar positions from a layout configuration.
|
||||
@@ -2802,8 +2810,13 @@ class ImageViewer(QWidget):
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_AREAS,
|
||||
"icon": "edit-image-face-recognize", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_FACES,
|
||||
"action": "detect_faces"},
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_PETS, "action": "detect_pets"},
|
||||
"action": "detect_faces",
|
||||
"enabled": bool(AVAILABLE_FACE_ENGINES),
|
||||
"tooltip": "" if AVAILABLE_FACE_ENGINES else UITexts.NO_FACE_LIBS},
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_PETS,
|
||||
"action": "detect_pets",
|
||||
"enabled": bool(AVAILABLE_PET_ENGINES),
|
||||
"tooltip": "" if AVAILABLE_PET_ENGINES else UITexts.NO_FACE_LIBS},
|
||||
]},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_MANIPULATE,
|
||||
|
||||
@@ -8,6 +8,7 @@ Classes:
|
||||
MetadataManager: A class with static methods to read metadata from files.
|
||||
"""
|
||||
import os
|
||||
import collections
|
||||
from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus
|
||||
try:
|
||||
import exiv2
|
||||
@@ -18,6 +19,9 @@ except ImportError:
|
||||
from utils import preserve_mtime
|
||||
from constants import RATING_XATTR_NAME, XATTR_NAME
|
||||
|
||||
MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating'])
|
||||
EMPTY_METADATA = MetadataResult([], 0)
|
||||
|
||||
|
||||
def notify_baloo(path):
|
||||
"""
|
||||
@@ -54,9 +58,9 @@ def load_common_metadata(path):
|
||||
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
|
||||
try:
|
||||
rating = int(raw_rating)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
rating = 0
|
||||
return tags, rating
|
||||
return MetadataResult(tags, rating)
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "bagheeraview"
|
||||
version = "0.9.13"
|
||||
version = "0.9.14"
|
||||
authors = [
|
||||
{ name = "Ignacio Serantes" }
|
||||
]
|
||||
|
||||
48
settings.py
48
settings.py
@@ -958,37 +958,41 @@ class SettingsDialog(QDialog):
|
||||
|
||||
def update_mediapipe_status(self):
|
||||
"""Checks for MediaPipe model file and updates UI accordingly."""
|
||||
if "mediapipe" not in AVAILABLE_FACE_ENGINES or not self.face_engine_combo:
|
||||
# --- Rostros ---
|
||||
if self.face_engine_combo and "mediapipe" in AVAILABLE_FACE_ENGINES:
|
||||
model_exists = os.path.exists(MEDIAPIPE_FACE_MODEL_PATH)
|
||||
mediapipe_index = self.face_engine_combo.findText("mediapipe")
|
||||
|
||||
if mediapipe_index != -1:
|
||||
item = self.face_engine_combo.model().item(mediapipe_index)
|
||||
if item:
|
||||
item.setEnabled(model_exists)
|
||||
|
||||
if self.download_model_btn:
|
||||
self.download_model_btn.hide()
|
||||
return
|
||||
self.download_model_btn.setVisible(not model_exists)
|
||||
|
||||
model_exists = os.path.exists(MEDIAPIPE_FACE_MODEL_PATH)
|
||||
mediapipe_index = self.face_engine_combo.findText("mediapipe")
|
||||
|
||||
if mediapipe_index != -1:
|
||||
item = self.face_engine_combo.model().item(mediapipe_index)
|
||||
if item:
|
||||
item.setEnabled(model_exists)
|
||||
|
||||
if self.download_model_btn:
|
||||
self.download_model_btn.setVisible(not model_exists)
|
||||
|
||||
if self.face_engine_combo.currentText() == "mediapipe" and not model_exists:
|
||||
if self.face_engine_combo.count() > 1:
|
||||
if self.face_engine_combo.currentText() == "mediapipe" and not model_exists:
|
||||
for i in range(self.face_engine_combo.count()):
|
||||
if self.face_engine_combo.model().item(i).isEnabled():
|
||||
self.face_engine_combo.setCurrentIndex(i)
|
||||
break
|
||||
elif self.download_model_btn:
|
||||
self.download_model_btn.hide()
|
||||
|
||||
if "mediapipe" not in AVAILABLE_PET_ENGINES or not self.pet_engine_combo:
|
||||
# --- Mascotas (Pets) ---
|
||||
if not AVAILABLE_PET_ENGINES:
|
||||
self.pet_engine_combo.setEnabled(False)
|
||||
self.pet_tags_edit.setEnabled(False)
|
||||
self.pet_history_spin.setEnabled(False)
|
||||
self.pet_color_btn.setEnabled(False)
|
||||
if self.download_pet_model_btn:
|
||||
self.download_pet_model_btn.hide()
|
||||
return
|
||||
|
||||
pet_model_exists = os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH)
|
||||
if self.download_pet_model_btn:
|
||||
self.download_pet_model_btn.setVisible(not pet_model_exists)
|
||||
elif "mediapipe" in AVAILABLE_PET_ENGINES:
|
||||
pet_model_exists = os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH)
|
||||
if self.download_pet_model_btn:
|
||||
self.download_pet_model_btn.setVisible(not pet_model_exists)
|
||||
elif self.download_pet_model_btn:
|
||||
self.download_pet_model_btn.hide()
|
||||
|
||||
def start_model_download(self):
|
||||
"""Starts the background thread to download the MediaPipe model."""
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="bagheeraview",
|
||||
version="0.9.13",
|
||||
version="0.9.14",
|
||||
author="Ignacio Serantes",
|
||||
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind",
|
||||
long_description="A fast image viewer built with PySide6, featuring search and "
|
||||
|
||||
Reference in New Issue
Block a user