More fixes for CI
Some checks failed
Lint / lint (push) Successful in 20s
Python CI / test (push) Failing after 1m42s
Tests / test (3.12) (push) Failing after 6s
Tests / test (3.13) (push) Failing after 6s
Tests / test (3.14) (push) Failing after 8s
Tests / test (3.11) (push) Failing after 37s

This commit is contained in:
Duncan Tourolle 2026-04-09 22:39:15 +02:00
parent 3092388226
commit 5763fa629e
19 changed files with 107 additions and 98 deletions

View File

@ -40,4 +40,5 @@ RUN apt-get update && apt-get install -y \
# Misc tools used in workflows
curl \
git \
nodejs \
&& rm -rf /var/lib/apt/lists/*

View File

@ -321,6 +321,7 @@ class FrameManager:
except Exception as e:
import traceback
print(f"Error loading SVG {svg_path}: {e}")
traceback.print_exc()
return None
@ -404,6 +405,7 @@ class FrameManager:
return img
except Exception as e:
import traceback
print(f"Error getting corner image for {frame.name}: {e}")
traceback.print_exc()
return None
@ -780,7 +782,9 @@ class FrameManager:
# Try SVG rendering for PDF
if frame.asset_path and frame.frame_type == FrameType.CORNERS:
corner_size_pt = frame_thickness * 2
if self._render_svg_corners_pdf(canvas, frame, x_pt, y_pt, width_pt, height_pt, corner_size_pt, color, corners):
if self._render_svg_corners_pdf(
canvas, frame, x_pt, y_pt, width_pt, height_pt, corner_size_pt, color, corners
):
canvas.restoreState()
return

View File

@ -61,6 +61,7 @@ class GLWidget(
# Set up OpenGL surface format with explicit double buffering
from PyQt6.QtGui import QSurfaceFormat
fmt = QSurfaceFormat()
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
fmt.setSwapInterval(1) # Enable vsync
@ -89,7 +90,7 @@ class GLWidget(
This fixes the Qt widget hierarchy issue where window() returns None
because the GL widget is nested in container widgets.
"""
return self._main_window if hasattr(self, '_main_window') else super().window()
return self._main_window if hasattr(self, "_main_window") else super().window()
def update(self):
"""Override update to force immediate repaint"""
@ -276,10 +277,18 @@ class GLWidget(
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
# If dragging, adjust drag_start_pos to account for pan_offset change
if hasattr(self, 'is_dragging') and self.is_dragging and hasattr(self, 'drag_start_pos') and self.drag_start_pos:
if (
hasattr(self, "is_dragging")
and self.is_dragging
and hasattr(self, "drag_start_pos")
and self.drag_start_pos
):
pan_delta_x = self.pan_offset[0] - old_pan_x
pan_delta_y = self.pan_offset[1] - old_pan_y
self.drag_start_pos = (self.drag_start_pos[0] + pan_delta_x, self.drag_start_pos[1] + pan_delta_y)
self.drag_start_pos = (
self.drag_start_pos[0] + pan_delta_x,
self.drag_start_pos[1] + pan_delta_y,
)
# Clamp pan offset to content bounds
if hasattr(self, "clamp_pan_offset"):
@ -321,7 +330,12 @@ class GLWidget(
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
# If dragging, adjust drag_start_pos to account for pan_offset change
if hasattr(self, 'is_dragging') and self.is_dragging and hasattr(self, 'drag_start_pos') and self.drag_start_pos:
if (
hasattr(self, "is_dragging")
and self.is_dragging
and hasattr(self, "drag_start_pos")
and self.drag_start_pos
):
pan_delta_x = self.pan_offset[0] - old_pan_x
pan_delta_y = self.pan_offset[1] - old_pan_y
self.drag_start_pos = (self.drag_start_pos[0] + pan_delta_x, self.drag_start_pos[1] + pan_delta_y)

View File

@ -8,7 +8,6 @@ across models.py, pdf_exporter.py, and async_backend.py.
from typing import Tuple
from PIL import Image
# =============================================================================
# Image Processing Utilities
# =============================================================================
@ -210,9 +209,7 @@ def apply_rounded_corners(
mask_large = Image.new("L", (ss_width, ss_height), 0)
draw = ImageDraw.Draw(mask_large)
draw.rounded_rectangle(
[0, 0, ss_width - 1, ss_height - 1], radius=ss_radius, fill=255
)
draw.rounded_rectangle([0, 0, ss_width - 1, ss_height - 1], radius=ss_radius, fill=255)
# Downscale with LANCZOS for smooth antialiased edges
mask = mask_large.resize((width, height), Image.Resampling.LANCZOS)

View File

@ -29,8 +29,7 @@ class LoadingWidget(QWidget):
self.setFixedSize(280, 80)
# Styling
self.setStyleSheet(
"""
self.setStyleSheet("""
QWidget {
background-color: rgba(50, 50, 50, 230);
border-radius: 8px;
@ -55,8 +54,7 @@ class LoadingWidget(QWidget):
stop:1 rgba(100, 160, 210, 220));
border-radius: 3px;
}
"""
)
""")
# Layout
layout = QVBoxLayout()

View File

@ -29,6 +29,7 @@ class AsyncLoadingMixin:
def window(self) -> "QMainWindow":
"""Expected from QWidget"""
...
"""
Mixin to add async loading capabilities to GLWidget.

View File

@ -17,6 +17,7 @@ class ElementSelectionMixin:
def window(self) -> "QMainWindow":
"""Expected from QWidget"""
...
"""
Mixin providing element selection and hit detection functionality.

View File

@ -180,9 +180,7 @@ class MouseInteractionMixin:
source_page = self.selected_element._parent_page
if current_page is not source_page:
self._transfer_element_to_page(
self.selected_element, source_page, current_page, x, y, current_renderer
)
self._transfer_element_to_page(self.selected_element, source_page, current_page, x, y, current_renderer)
else:
self._move_element_within_page(x, y, source_page)
else:

View File

@ -39,6 +39,7 @@ class _SaveBridge(QObject):
Signals can be safely emitted from any thread; connected slots run on
the main (GUI) thread via Qt's queued connection.
"""
progress = pyqtSignal(int, str)
finished = pyqtSignal(bool, str)
@ -690,7 +691,9 @@ class FileOperationsMixin:
else:
self.show_status("PDF export failed to start", 3000)
@ribbon_action(label="Clean Assets", tooltip="Find and remove duplicate or unused image files", tab="Home", group="File")
@ribbon_action(
label="Clean Assets", tooltip="Find and remove duplicate or unused image files", tab="Home", group="File"
)
def clean_assets(self):
"""Find and remove duplicate and unused asset files to save space"""
from PyQt6.QtWidgets import QProgressDialog, QCheckBox
@ -731,9 +734,7 @@ class FileOperationsMixin:
# Check if there's anything to clean
if dup_files == 0 and unused_files == 0:
QMessageBox.information(
self,
"Assets Clean",
"No duplicate or unused files were found in your project assets."
self, "Assets Clean", "No duplicate or unused files were found in your project assets."
)
return
@ -752,8 +753,7 @@ class FileOperationsMixin:
dup_checkbox = None
if dup_files > 0:
dup_checkbox = QCheckBox(
f"Remove {dup_files} duplicate file(s) in {dup_groups} group(s) "
f"(saves {format_bytes(dup_bytes)})"
f"Remove {dup_files} duplicate file(s) in {dup_groups} group(s) " f"(saves {format_bytes(dup_bytes)})"
)
dup_checkbox.setChecked(True)
dup_checkbox.setToolTip(
@ -765,9 +765,7 @@ class FileOperationsMixin:
# Unused checkbox
unused_checkbox = None
if unused_files > 0:
unused_checkbox = QCheckBox(
f"Remove {unused_files} unused file(s) (saves {format_bytes(unused_bytes)})"
)
unused_checkbox = QCheckBox(f"Remove {unused_files} unused file(s) (saves {format_bytes(unused_bytes)})")
unused_checkbox.setChecked(True)
unused_checkbox.setToolTip(
"Unused files exist in the assets folder but are not referenced\n"
@ -806,6 +804,7 @@ class FileOperationsMixin:
# Remove duplicates if selected
if dup_checkbox and dup_checkbox.isChecked():
def update_image_references(old_path: str, new_path: str):
"""Update all ImageData elements that reference the old path"""
from pyPhotoAlbum.models import ImageData
@ -842,10 +841,12 @@ class FileOperationsMixin:
"Cleanup Complete",
f"Removed {total_removed} file(s).\n\n"
f"Saved {format_bytes(total_saved)} of disk space.\n\n"
f"Remember to save your project to preserve these changes."
f"Remember to save your project to preserve these changes.",
)
self.show_status(f"Asset cleanup complete: removed {total_removed} files, saved {format_bytes(total_saved)}")
self.show_status(
f"Asset cleanup complete: removed {total_removed} files, saved {format_bytes(total_saved)}"
)
else:
self.show_status("No files were removed")

View File

@ -148,7 +148,6 @@ class PageOperationsMixin:
self.project.working_dpi = values["working_dpi"]
self.project.export_dpi = values["export_dpi"]
# Apply to other pages based on scope
# 0 = page only, 1 = non-manual pages, 2 = all pages
apply_scope = values.get("apply_scope", 0)

View File

@ -56,6 +56,7 @@ class StyleOperationsMixin:
# Delete texture if it exists (will be recreated on next render)
if hasattr(img, "_texture_id") and img._texture_id:
from pyPhotoAlbum.gl_imports import glDeleteTextures
try:
glDeleteTextures([img._texture_id])
except Exception:

View File

@ -119,7 +119,9 @@ class ViewOperationsMixin:
self.show_status(f"Grid {status}", 2000)
print(f"Grid {status}")
@ribbon_action(label="Print Settings...", tooltip="Configure bleed and safe area for all pages", tab="View", group="Guides")
@ribbon_action(
label="Print Settings...", tooltip="Configure bleed and safe area for all pages", tab="View", group="Guides"
)
def open_print_settings(self):
"""Open the print settings dialog (bleed and safe area)"""
if not self.project:
@ -135,7 +137,9 @@ class ViewOperationsMixin:
f"Bleed: {values['page_bleed_mm']:.1f}mm, Safe area: {values['page_safe_area_mm']:.1f}mm", 2000
)
@ribbon_action(label="Print Guides", tooltip="Toggle bleed/cut/safe-area guide lines in the editor", tab="View", group="Guides")
@ribbon_action(
label="Print Guides", tooltip="Toggle bleed/cut/safe-area guide lines in the editor", tab="View", group="Guides"
)
def toggle_print_guides(self):
"""Toggle print guide lines (bleed/cut/safe area)"""
if not self.project:
@ -209,11 +213,11 @@ class ViewOperationsMixin:
tooltip="Show/hide the image browser panel",
tab="View",
group="Panels",
shortcut="Ctrl+B"
shortcut="Ctrl+B",
)
def toggle_image_browser(self):
"""Toggle the thumbnail browser visibility"""
if hasattr(self, '_thumbnail_browser'):
if hasattr(self, "_thumbnail_browser"):
if self._thumbnail_browser.isVisible():
self._thumbnail_browser.hide()
self.show_status("Image browser hidden", 2000)

View File

@ -20,6 +20,7 @@ class PageNavigationMixin:
def window(self) -> "QMainWindow":
"""Expected from QWidget"""
...
"""
Mixin providing page navigation and ghost page functionality.
@ -71,7 +72,7 @@ class PageNavigationMixin:
List of tuples (page_type, page_or_ghost_data, y_offset)
"""
# Use stored reference to main window
main_window = getattr(self, '_main_window', None)
main_window = getattr(self, "_main_window", None)
if main_window is None:
main_window = self.window()

View File

@ -26,7 +26,7 @@ class RenderingMixin:
glLoadIdentity()
# Use stored reference to main window
main_window = getattr(self, '_main_window', None)
main_window = getattr(self, "_main_window", None)
if main_window is None:
# Fallback to window() if _main_window not set
main_window = self.window()
@ -51,7 +51,7 @@ class RenderingMixin:
self.initial_zoom_set = True
# Update scrollbars now that we have content bounds
if hasattr(self, '_main_window') and hasattr(self._main_window, "update_scrollbars"):
if hasattr(self, "_main_window") and hasattr(self._main_window, "update_scrollbars"):
self._main_window.update_scrollbars()
dpi = project.working_dpi
@ -385,7 +385,9 @@ class RenderingMixin:
sy - bleed_screen,
sw + 2 * bleed_screen,
sh + 2 * bleed_screen,
0.0, 0.67, 0.0,
0.0,
0.67,
0.0,
dashed=True,
)
@ -400,7 +402,9 @@ class RenderingMixin:
sy + safe_screen,
sw - 2 * safe_screen,
sh - 2 * safe_screen,
0.8, 0.0, 0.0,
0.8,
0.0,
0.0,
)
glColor3f(1.0, 1.0, 1.0) # Reset colour

View File

@ -130,12 +130,7 @@ class ImageStyle:
def has_styling(self) -> bool:
"""Check if any styling is applied (non-default values)."""
return (
self.corner_radius > 0
or self.border_width > 0
or self.shadow_enabled
or self.frame_style is not None
)
return self.corner_radius > 0 or self.border_width > 0 or self.shadow_enabled or self.frame_style is not None
def serialize(self) -> Dict[str, Any]:
"""Serialize style to dictionary."""
@ -422,7 +417,9 @@ class ImageData(BaseLayoutElement):
img_width, img_height = int(w), int(h)
# Calculate texture coordinates for center crop with element's crop_info
tx_min, ty_min, tx_max, ty_max = calculate_center_crop_coords(img_width, img_height, w, h, self.crop_info)
tx_min, ty_min, tx_max, ty_max = calculate_center_crop_coords(
img_width, img_height, w, h, self.crop_info
)
# Enable blending for transparency (rounded corners)
glEnable(GL_BLEND)

View File

@ -71,6 +71,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
img = Image.open(task.image_path)
except Exception as open_err:
import traceback
return (task.task_id, None, f"Image.open failed: {open_err}\n{traceback.format_exc()}")
# Now import the rest
@ -84,6 +85,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
)
except Exception as import_err:
import traceback
return (task.task_id, None, f"Import image_utils failed: {import_err}\n{traceback.format_exc()}")
# Convert to RGBA
@ -136,6 +138,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
except Exception as e:
import traceback
return (task.task_id, None, f"{str(e)}\n{traceback.format_exc()}")
@ -246,9 +249,7 @@ class PDFExporter:
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
future_to_idx = {
executor.submit(
self._render_item_to_bytes, item, page_width_pt, page_height_pt, bleed_pt
): i
executor.submit(self._render_item_to_bytes, item, page_width_pt, page_height_pt, bleed_pt): i
for i, item in enumerate(page_sequence)
}
completed = 0
@ -348,9 +349,7 @@ class PDFExporter:
return buf.getvalue()
def _make_blank_page_bytes(
self, page_width_pt: float, page_height_pt: float, bleed_pt: float
) -> bytes:
def _make_blank_page_bytes(self, page_width_pt: float, page_height_pt: float, bleed_pt: float) -> bytes:
"""Return a minimal single-blank-page PDF for use as an error placeholder."""
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=(page_width_pt + 2 * bleed_pt, page_height_pt + 2 * bleed_pt))
@ -615,7 +614,6 @@ class PDFExporter:
for element in sorted(page.layout.elements, key=lambda x: x.z_index):
self._render_element(c, element, 0, cover_width_pt, cover_height_pt, "Cover")
c.showPage() # Finish cover page
def _export_single_page(
@ -809,7 +807,12 @@ class PDFExporter:
# Convert to points (bleed_pt shifts content inside the expanded PDF page)
x_pt = adjusted_x_mm * self.MM_TO_POINTS + bleed_pt
y_pt = params.page_height_pt + bleed_pt - (element_y_mm * self.MM_TO_POINTS) - (element_height_mm * self.MM_TO_POINTS)
y_pt = (
params.page_height_pt
+ bleed_pt
- (element_y_mm * self.MM_TO_POINTS)
- (element_height_mm * self.MM_TO_POINTS)
)
width_pt = crop_width_mm * self.MM_TO_POINTS
height_pt = element_height_mm * self.MM_TO_POINTS
@ -861,9 +864,7 @@ class PDFExporter:
ctx: RenderContext containing all rendering parameters
"""
# Check for pre-processed image in cache
task_id = self._make_task_id(
ctx.image_element, ctx.crop_left, ctx.crop_right, ctx.width_pt, ctx.height_pt
)
task_id = self._make_task_id(ctx.image_element, ctx.crop_left, ctx.crop_right, ctx.width_pt, ctx.height_pt)
cropped_img = self._processed_images.get(task_id)
if cropped_img is None:

View File

@ -18,7 +18,6 @@ from pyPhotoAlbum.version_manager import (
DataMigration,
)
# Legacy constant for backward compatibility
SERIALIZATION_VERSION = CURRENT_DATA_VERSION
@ -257,10 +256,7 @@ def save_to_zip_async(
if idx % 10 == 0 or idx == len(asset_files) - 1:
progress = 25 + int((idx + 1) / len(asset_files) * progress_range)
if on_progress:
on_progress(
progress,
f"Adding assets... ({idx + 1}/{len(asset_files)})"
)
on_progress(progress, f"Adding assets... ({idx + 1}/{len(asset_files)})")
if on_progress:
on_progress(95, "Finalizing save...")

View File

@ -1,14 +1,12 @@
"""
Thumbnail Browser Widget - displays thumbnails from a folder for drag-and-drop into album.
"""
import os
from pathlib import Path
from typing import Optional, List, Tuple
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QFileDialog, QDockWidget, QScrollBar
)
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QDockWidget, QScrollBar
from PyQt6.QtCore import Qt, QSize, QMimeData, QUrl, QPoint
from PyQt6.QtGui import QDrag, QCursor, QPainter, QFont, QColor
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
@ -16,7 +14,6 @@ from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from pyPhotoAlbum.gl_imports import *
from pyPhotoAlbum.mixins.viewport import ViewportMixin
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"]
@ -56,8 +53,7 @@ class ThumbnailItem:
def contains_point(self, x: float, y: float) -> bool:
"""Check if point is inside this thumbnail."""
return (self.x <= x <= self.x + self.thumbnail_size and
self.y <= y <= self.y + self.thumbnail_size)
return self.x <= x <= self.x + self.thumbnail_size and self.y <= y <= self.y + self.thumbnail_size
class ThumbnailGLWidget(QOpenGLWidget):
@ -121,7 +117,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
glMatrixMode(GL_MODELVIEW)
# Rearrange thumbnails to fit new width
if hasattr(self, 'image_files') and self.image_files:
if hasattr(self, "image_files") and self.image_files:
self._arrange_thumbnails()
else:
# Still update scrollbar even if no thumbnails
@ -186,7 +182,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
# Render based on state: texture, loading placeholder, or empty placeholder
if thumb._texture_id:
# Calculate aspect-ratio-corrected dimensions
if hasattr(thumb, '_img_width') and hasattr(thumb, '_img_height'):
if hasattr(thumb, "_img_width") and hasattr(thumb, "_img_height"):
img_aspect = thumb._img_width / thumb._img_height
thumb_aspect = w / h
@ -301,9 +297,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGBA,
pil_image.width, pil_image.height,
0, GL_RGBA, GL_UNSIGNED_BYTE, img_data
GL_TEXTURE_2D, 0, GL_RGBA, pil_image.width, pil_image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data
)
thumb._texture_id = texture_id
@ -340,7 +334,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
def _arrange_thumbnails(self):
"""Arrange thumbnails in a grid based on widget width and zoom level."""
if not hasattr(self, 'image_files') or not self.image_files:
if not hasattr(self, "image_files") or not self.image_files:
self.thumbnails.clear()
return
@ -381,6 +375,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
# Check if we need a date header (only in date sort mode)
if self.sort_mode == "date" and self._get_image_date_func:
from datetime import datetime
timestamp = self._get_image_date_func(image_file)
date_obj = datetime.fromtimestamp(timestamp)
date_str = date_obj.strftime("%B %d, %Y") # e.g., "December 13, 2025"
@ -503,7 +498,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
"""Update which thumbnails are already used in the project."""
# Get reference to main window's project
main_window = self.window()
if not hasattr(main_window, 'project') or not main_window.project:
if not hasattr(main_window, "project") or not main_window.project:
return
project = main_window.project
@ -512,6 +507,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
used_paths = set()
for page in project.pages:
from pyPhotoAlbum.models import ImageData
for element in page.layout.elements:
if isinstance(element, ImageData) and element.image_path:
# Resolve to absolute path for comparison
@ -531,11 +527,11 @@ class ThumbnailGLWidget(QOpenGLWidget):
# Get main window's async loader
main_window = self.window()
if not main_window or not hasattr(main_window, '_gl_widget'):
if not main_window or not hasattr(main_window, "_gl_widget"):
return
gl_widget = main_window._gl_widget
if not hasattr(gl_widget, 'async_image_loader'):
if not hasattr(gl_widget, "async_image_loader"):
return
from pyPhotoAlbum.async_backend import LoadPriority
@ -550,7 +546,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
Path(thumb.image_path),
priority=LoadPriority.LOW,
target_size=(200, 200), # Small thumbnails
user_data=thumb
user_data=thumb,
)
except RuntimeError:
thumb._async_loading = False # Reset on error
@ -623,10 +619,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
# Pan the view (right-click or middle-click drag)
# Only allow vertical panning - grid is always horizontally centered
delta = event.pos() - self.drag_start_pos
self.pan_offset = (
0, # No horizontal pan - grid is centered
self.pan_offset[1] + delta.y()
)
self.pan_offset = (0, self.pan_offset[1] + delta.y()) # No horizontal pan - grid is centered
self.drag_start_pos = event.pos()
self._update_scrollbar_position()
self.update()
@ -664,15 +657,12 @@ class ThumbnailGLWidget(QOpenGLWidget):
# Keep horizontal pan at 0 (grid is always horizontally centered)
self.pan_offset = (
0, # No horizontal pan - grid is centered in _arrange_thumbnails
mouse_y - world_y * self.zoom_level
mouse_y - world_y * self.zoom_level,
)
else:
# Scroll mode - scroll vertically only
scroll_amount = delta * 0.5 # Adjust sensitivity
self.pan_offset = (
0, # No horizontal pan
self.pan_offset[1] + scroll_amount
)
self.pan_offset = (0, self.pan_offset[1] + scroll_amount) # No horizontal pan
self._update_scrollbar_position()
self.update()
@ -757,11 +747,12 @@ class ThumbnailBrowserDock(QDockWidget):
self.setWidget(main_widget)
# Dock settings
self.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea)
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable |
QDockWidget.DockWidgetFeature.DockWidgetMovable |
QDockWidget.DockWidgetFeature.DockWidgetFloatable)
self.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
self.setFeatures(
QDockWidget.DockWidgetFeature.DockWidgetClosable
| QDockWidget.DockWidgetFeature.DockWidgetMovable
| QDockWidget.DockWidgetFeature.DockWidgetFloatable
)
# Connect to main window's async loader when shown
self._connect_async_loader()
@ -769,15 +760,15 @@ class ThumbnailBrowserDock(QDockWidget):
def _connect_async_loader(self):
"""Connect to main window's async image loader."""
main_window = self.window()
if not hasattr(main_window, '_gl_widget'):
if not hasattr(main_window, "_gl_widget"):
return
gl_widget = main_window._gl_widget
if not hasattr(gl_widget, 'async_image_loader'):
if not hasattr(gl_widget, "async_image_loader"):
return
# Avoid duplicate connections
if hasattr(self, '_async_connected') and self._async_connected:
if hasattr(self, "_async_connected") and self._async_connected:
return
try:
@ -800,7 +791,7 @@ class ThumbnailBrowserDock(QDockWidget):
self,
"Select Image Folder",
str(self.gl_widget.current_folder) if self.gl_widget.current_folder else str(Path.home()),
QFileDialog.Option.ShowDirsOnly
QFileDialog.Option.ShowDirsOnly,
)
if folder_path:
@ -826,7 +817,7 @@ class ThumbnailBrowserDock(QDockWidget):
self.current_sort = sort_mode
# Re-sort the image files in the GL widget
if hasattr(self.gl_widget, 'image_files') and self.gl_widget.image_files:
if hasattr(self.gl_widget, "image_files") and self.gl_widget.image_files:
self._apply_sort()
# Re-arrange thumbnails with new order
self.gl_widget._arrange_thumbnails()
@ -835,7 +826,7 @@ class ThumbnailBrowserDock(QDockWidget):
def _apply_sort(self):
"""Apply current sort mode to image files."""
if not hasattr(self.gl_widget, 'image_files') or not self.gl_widget.image_files:
if not hasattr(self.gl_widget, "image_files") or not self.gl_widget.image_files:
return
if self.current_sort == "name":
# Sort by filename only (not full path)
@ -875,6 +866,7 @@ class ThumbnailBrowserDock(QDockWidget):
if tag == "DateTimeOriginal":
# Convert EXIF date format "2023:12:13 14:30:00" to timestamp
from datetime import datetime
try:
dt = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
return dt.timestamp()

View File

@ -7,7 +7,6 @@ import uuid
from datetime import datetime, timezone
from typing import Dict, Any, Optional, Callable, List
# Current data version - increment when making breaking changes to data format
CURRENT_DATA_VERSION = "3.0"