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
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:
parent
3092388226
commit
5763fa629e
@ -40,4 +40,5 @@ RUN apt-get update && apt-get install -y \
|
|||||||
# Misc tools used in workflows
|
# Misc tools used in workflows
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
|
nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@ -321,6 +321,7 @@ class FrameManager:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
print(f"Error loading SVG {svg_path}: {e}")
|
print(f"Error loading SVG {svg_path}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
@ -404,6 +405,7 @@ class FrameManager:
|
|||||||
return img
|
return img
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
print(f"Error getting corner image for {frame.name}: {e}")
|
print(f"Error getting corner image for {frame.name}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
@ -780,7 +782,9 @@ class FrameManager:
|
|||||||
# Try SVG rendering for PDF
|
# Try SVG rendering for PDF
|
||||||
if frame.asset_path and frame.frame_type == FrameType.CORNERS:
|
if frame.asset_path and frame.frame_type == FrameType.CORNERS:
|
||||||
corner_size_pt = frame_thickness * 2
|
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()
|
canvas.restoreState()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@ class GLWidget(
|
|||||||
|
|
||||||
# Set up OpenGL surface format with explicit double buffering
|
# Set up OpenGL surface format with explicit double buffering
|
||||||
from PyQt6.QtGui import QSurfaceFormat
|
from PyQt6.QtGui import QSurfaceFormat
|
||||||
|
|
||||||
fmt = QSurfaceFormat()
|
fmt = QSurfaceFormat()
|
||||||
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
|
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
|
||||||
fmt.setSwapInterval(1) # Enable vsync
|
fmt.setSwapInterval(1) # Enable vsync
|
||||||
@ -89,7 +90,7 @@ class GLWidget(
|
|||||||
This fixes the Qt widget hierarchy issue where window() returns None
|
This fixes the Qt widget hierarchy issue where window() returns None
|
||||||
because the GL widget is nested in container widgets.
|
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):
|
def update(self):
|
||||||
"""Override update to force immediate repaint"""
|
"""Override update to force immediate repaint"""
|
||||||
@ -276,10 +277,18 @@ class GLWidget(
|
|||||||
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
||||||
|
|
||||||
# If dragging, adjust drag_start_pos to account for pan_offset change
|
# 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_x = self.pan_offset[0] - old_pan_x
|
||||||
pan_delta_y = self.pan_offset[1] - old_pan_y
|
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
|
# Clamp pan offset to content bounds
|
||||||
if hasattr(self, "clamp_pan_offset"):
|
if hasattr(self, "clamp_pan_offset"):
|
||||||
@ -321,7 +330,12 @@ class GLWidget(
|
|||||||
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
||||||
|
|
||||||
# If dragging, adjust drag_start_pos to account for pan_offset change
|
# 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_x = self.pan_offset[0] - old_pan_x
|
||||||
pan_delta_y = self.pan_offset[1] - old_pan_y
|
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)
|
||||||
|
|||||||
@ -8,7 +8,6 @@ across models.py, pdf_exporter.py, and async_backend.py.
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Image Processing Utilities
|
# Image Processing Utilities
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -210,9 +209,7 @@ def apply_rounded_corners(
|
|||||||
|
|
||||||
mask_large = Image.new("L", (ss_width, ss_height), 0)
|
mask_large = Image.new("L", (ss_width, ss_height), 0)
|
||||||
draw = ImageDraw.Draw(mask_large)
|
draw = ImageDraw.Draw(mask_large)
|
||||||
draw.rounded_rectangle(
|
draw.rounded_rectangle([0, 0, ss_width - 1, ss_height - 1], radius=ss_radius, fill=255)
|
||||||
[0, 0, ss_width - 1, ss_height - 1], radius=ss_radius, fill=255
|
|
||||||
)
|
|
||||||
|
|
||||||
# Downscale with LANCZOS for smooth antialiased edges
|
# Downscale with LANCZOS for smooth antialiased edges
|
||||||
mask = mask_large.resize((width, height), Image.Resampling.LANCZOS)
|
mask = mask_large.resize((width, height), Image.Resampling.LANCZOS)
|
||||||
|
|||||||
@ -29,8 +29,7 @@ class LoadingWidget(QWidget):
|
|||||||
self.setFixedSize(280, 80)
|
self.setFixedSize(280, 80)
|
||||||
|
|
||||||
# Styling
|
# Styling
|
||||||
self.setStyleSheet(
|
self.setStyleSheet("""
|
||||||
"""
|
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: rgba(50, 50, 50, 230);
|
background-color: rgba(50, 50, 50, 230);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -55,8 +54,7 @@ class LoadingWidget(QWidget):
|
|||||||
stop:1 rgba(100, 160, 210, 220));
|
stop:1 rgba(100, 160, 210, 220));
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class AsyncLoadingMixin:
|
|||||||
def window(self) -> "QMainWindow":
|
def window(self) -> "QMainWindow":
|
||||||
"""Expected from QWidget"""
|
"""Expected from QWidget"""
|
||||||
...
|
...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Mixin to add async loading capabilities to GLWidget.
|
Mixin to add async loading capabilities to GLWidget.
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class ElementSelectionMixin:
|
|||||||
def window(self) -> "QMainWindow":
|
def window(self) -> "QMainWindow":
|
||||||
"""Expected from QWidget"""
|
"""Expected from QWidget"""
|
||||||
...
|
...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Mixin providing element selection and hit detection functionality.
|
Mixin providing element selection and hit detection functionality.
|
||||||
|
|
||||||
|
|||||||
@ -180,9 +180,7 @@ class MouseInteractionMixin:
|
|||||||
source_page = self.selected_element._parent_page
|
source_page = self.selected_element._parent_page
|
||||||
|
|
||||||
if current_page is not source_page:
|
if current_page is not source_page:
|
||||||
self._transfer_element_to_page(
|
self._transfer_element_to_page(self.selected_element, source_page, current_page, x, y, current_renderer)
|
||||||
self.selected_element, source_page, current_page, x, y, current_renderer
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._move_element_within_page(x, y, source_page)
|
self._move_element_within_page(x, y, source_page)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class _SaveBridge(QObject):
|
|||||||
Signals can be safely emitted from any thread; connected slots run on
|
Signals can be safely emitted from any thread; connected slots run on
|
||||||
the main (GUI) thread via Qt's queued connection.
|
the main (GUI) thread via Qt's queued connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
progress = pyqtSignal(int, str)
|
progress = pyqtSignal(int, str)
|
||||||
finished = pyqtSignal(bool, str)
|
finished = pyqtSignal(bool, str)
|
||||||
|
|
||||||
@ -690,7 +691,9 @@ class FileOperationsMixin:
|
|||||||
else:
|
else:
|
||||||
self.show_status("PDF export failed to start", 3000)
|
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):
|
def clean_assets(self):
|
||||||
"""Find and remove duplicate and unused asset files to save space"""
|
"""Find and remove duplicate and unused asset files to save space"""
|
||||||
from PyQt6.QtWidgets import QProgressDialog, QCheckBox
|
from PyQt6.QtWidgets import QProgressDialog, QCheckBox
|
||||||
@ -731,9 +734,7 @@ class FileOperationsMixin:
|
|||||||
# Check if there's anything to clean
|
# Check if there's anything to clean
|
||||||
if dup_files == 0 and unused_files == 0:
|
if dup_files == 0 and unused_files == 0:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self, "Assets Clean", "No duplicate or unused files were found in your project assets."
|
||||||
"Assets Clean",
|
|
||||||
"No duplicate or unused files were found in your project assets."
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -752,8 +753,7 @@ class FileOperationsMixin:
|
|||||||
dup_checkbox = None
|
dup_checkbox = None
|
||||||
if dup_files > 0:
|
if dup_files > 0:
|
||||||
dup_checkbox = QCheckBox(
|
dup_checkbox = QCheckBox(
|
||||||
f"Remove {dup_files} duplicate file(s) in {dup_groups} group(s) "
|
f"Remove {dup_files} duplicate file(s) in {dup_groups} group(s) " f"(saves {format_bytes(dup_bytes)})"
|
||||||
f"(saves {format_bytes(dup_bytes)})"
|
|
||||||
)
|
)
|
||||||
dup_checkbox.setChecked(True)
|
dup_checkbox.setChecked(True)
|
||||||
dup_checkbox.setToolTip(
|
dup_checkbox.setToolTip(
|
||||||
@ -765,9 +765,7 @@ class FileOperationsMixin:
|
|||||||
# Unused checkbox
|
# Unused checkbox
|
||||||
unused_checkbox = None
|
unused_checkbox = None
|
||||||
if unused_files > 0:
|
if unused_files > 0:
|
||||||
unused_checkbox = QCheckBox(
|
unused_checkbox = QCheckBox(f"Remove {unused_files} unused file(s) (saves {format_bytes(unused_bytes)})")
|
||||||
f"Remove {unused_files} unused file(s) (saves {format_bytes(unused_bytes)})"
|
|
||||||
)
|
|
||||||
unused_checkbox.setChecked(True)
|
unused_checkbox.setChecked(True)
|
||||||
unused_checkbox.setToolTip(
|
unused_checkbox.setToolTip(
|
||||||
"Unused files exist in the assets folder but are not referenced\n"
|
"Unused files exist in the assets folder but are not referenced\n"
|
||||||
@ -806,6 +804,7 @@ class FileOperationsMixin:
|
|||||||
|
|
||||||
# Remove duplicates if selected
|
# Remove duplicates if selected
|
||||||
if dup_checkbox and dup_checkbox.isChecked():
|
if dup_checkbox and dup_checkbox.isChecked():
|
||||||
|
|
||||||
def update_image_references(old_path: str, new_path: str):
|
def update_image_references(old_path: str, new_path: str):
|
||||||
"""Update all ImageData elements that reference the old path"""
|
"""Update all ImageData elements that reference the old path"""
|
||||||
from pyPhotoAlbum.models import ImageData
|
from pyPhotoAlbum.models import ImageData
|
||||||
@ -842,10 +841,12 @@ class FileOperationsMixin:
|
|||||||
"Cleanup Complete",
|
"Cleanup Complete",
|
||||||
f"Removed {total_removed} file(s).\n\n"
|
f"Removed {total_removed} file(s).\n\n"
|
||||||
f"Saved {format_bytes(total_saved)} of disk space.\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:
|
else:
|
||||||
self.show_status("No files were removed")
|
self.show_status("No files were removed")
|
||||||
|
|
||||||
|
|||||||
@ -148,7 +148,6 @@ class PageOperationsMixin:
|
|||||||
self.project.working_dpi = values["working_dpi"]
|
self.project.working_dpi = values["working_dpi"]
|
||||||
self.project.export_dpi = values["export_dpi"]
|
self.project.export_dpi = values["export_dpi"]
|
||||||
|
|
||||||
|
|
||||||
# Apply to other pages based on scope
|
# Apply to other pages based on scope
|
||||||
# 0 = page only, 1 = non-manual pages, 2 = all pages
|
# 0 = page only, 1 = non-manual pages, 2 = all pages
|
||||||
apply_scope = values.get("apply_scope", 0)
|
apply_scope = values.get("apply_scope", 0)
|
||||||
|
|||||||
@ -56,6 +56,7 @@ class StyleOperationsMixin:
|
|||||||
# Delete texture if it exists (will be recreated on next render)
|
# Delete texture if it exists (will be recreated on next render)
|
||||||
if hasattr(img, "_texture_id") and img._texture_id:
|
if hasattr(img, "_texture_id") and img._texture_id:
|
||||||
from pyPhotoAlbum.gl_imports import glDeleteTextures
|
from pyPhotoAlbum.gl_imports import glDeleteTextures
|
||||||
|
|
||||||
try:
|
try:
|
||||||
glDeleteTextures([img._texture_id])
|
glDeleteTextures([img._texture_id])
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -119,7 +119,9 @@ class ViewOperationsMixin:
|
|||||||
self.show_status(f"Grid {status}", 2000)
|
self.show_status(f"Grid {status}", 2000)
|
||||||
print(f"Grid {status}")
|
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):
|
def open_print_settings(self):
|
||||||
"""Open the print settings dialog (bleed and safe area)"""
|
"""Open the print settings dialog (bleed and safe area)"""
|
||||||
if not self.project:
|
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
|
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):
|
def toggle_print_guides(self):
|
||||||
"""Toggle print guide lines (bleed/cut/safe area)"""
|
"""Toggle print guide lines (bleed/cut/safe area)"""
|
||||||
if not self.project:
|
if not self.project:
|
||||||
@ -209,11 +213,11 @@ class ViewOperationsMixin:
|
|||||||
tooltip="Show/hide the image browser panel",
|
tooltip="Show/hide the image browser panel",
|
||||||
tab="View",
|
tab="View",
|
||||||
group="Panels",
|
group="Panels",
|
||||||
shortcut="Ctrl+B"
|
shortcut="Ctrl+B",
|
||||||
)
|
)
|
||||||
def toggle_image_browser(self):
|
def toggle_image_browser(self):
|
||||||
"""Toggle the thumbnail browser visibility"""
|
"""Toggle the thumbnail browser visibility"""
|
||||||
if hasattr(self, '_thumbnail_browser'):
|
if hasattr(self, "_thumbnail_browser"):
|
||||||
if self._thumbnail_browser.isVisible():
|
if self._thumbnail_browser.isVisible():
|
||||||
self._thumbnail_browser.hide()
|
self._thumbnail_browser.hide()
|
||||||
self.show_status("Image browser hidden", 2000)
|
self.show_status("Image browser hidden", 2000)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class PageNavigationMixin:
|
|||||||
def window(self) -> "QMainWindow":
|
def window(self) -> "QMainWindow":
|
||||||
"""Expected from QWidget"""
|
"""Expected from QWidget"""
|
||||||
...
|
...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Mixin providing page navigation and ghost page functionality.
|
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)
|
List of tuples (page_type, page_or_ghost_data, y_offset)
|
||||||
"""
|
"""
|
||||||
# Use stored reference to main window
|
# 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:
|
if main_window is None:
|
||||||
main_window = self.window()
|
main_window = self.window()
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class RenderingMixin:
|
|||||||
glLoadIdentity()
|
glLoadIdentity()
|
||||||
|
|
||||||
# Use stored reference to main window
|
# 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:
|
if main_window is None:
|
||||||
# Fallback to window() if _main_window not set
|
# Fallback to window() if _main_window not set
|
||||||
main_window = self.window()
|
main_window = self.window()
|
||||||
@ -51,7 +51,7 @@ class RenderingMixin:
|
|||||||
self.initial_zoom_set = True
|
self.initial_zoom_set = True
|
||||||
|
|
||||||
# Update scrollbars now that we have content bounds
|
# 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()
|
self._main_window.update_scrollbars()
|
||||||
|
|
||||||
dpi = project.working_dpi
|
dpi = project.working_dpi
|
||||||
@ -385,7 +385,9 @@ class RenderingMixin:
|
|||||||
sy - bleed_screen,
|
sy - bleed_screen,
|
||||||
sw + 2 * bleed_screen,
|
sw + 2 * bleed_screen,
|
||||||
sh + 2 * bleed_screen,
|
sh + 2 * bleed_screen,
|
||||||
0.0, 0.67, 0.0,
|
0.0,
|
||||||
|
0.67,
|
||||||
|
0.0,
|
||||||
dashed=True,
|
dashed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -400,7 +402,9 @@ class RenderingMixin:
|
|||||||
sy + safe_screen,
|
sy + safe_screen,
|
||||||
sw - 2 * safe_screen,
|
sw - 2 * safe_screen,
|
||||||
sh - 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
|
glColor3f(1.0, 1.0, 1.0) # Reset colour
|
||||||
|
|||||||
@ -130,12 +130,7 @@ class ImageStyle:
|
|||||||
|
|
||||||
def has_styling(self) -> bool:
|
def has_styling(self) -> bool:
|
||||||
"""Check if any styling is applied (non-default values)."""
|
"""Check if any styling is applied (non-default values)."""
|
||||||
return (
|
return self.corner_radius > 0 or self.border_width > 0 or self.shadow_enabled or self.frame_style is not None
|
||||||
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]:
|
def serialize(self) -> Dict[str, Any]:
|
||||||
"""Serialize style to dictionary."""
|
"""Serialize style to dictionary."""
|
||||||
@ -422,7 +417,9 @@ class ImageData(BaseLayoutElement):
|
|||||||
img_width, img_height = int(w), int(h)
|
img_width, img_height = int(w), int(h)
|
||||||
|
|
||||||
# Calculate texture coordinates for center crop with element's crop_info
|
# 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)
|
# Enable blending for transparency (rounded corners)
|
||||||
glEnable(GL_BLEND)
|
glEnable(GL_BLEND)
|
||||||
|
|||||||
@ -71,6 +71,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
|
|||||||
img = Image.open(task.image_path)
|
img = Image.open(task.image_path)
|
||||||
except Exception as open_err:
|
except Exception as open_err:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
return (task.task_id, None, f"Image.open failed: {open_err}\n{traceback.format_exc()}")
|
return (task.task_id, None, f"Image.open failed: {open_err}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
# Now import the rest
|
# Now import the rest
|
||||||
@ -84,6 +85,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
|
|||||||
)
|
)
|
||||||
except Exception as import_err:
|
except Exception as import_err:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
return (task.task_id, None, f"Import image_utils failed: {import_err}\n{traceback.format_exc()}")
|
return (task.task_id, None, f"Import image_utils failed: {import_err}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
# Convert to RGBA
|
# Convert to RGBA
|
||||||
@ -136,6 +138,7 @@ def _process_image_task(task: ImageTask) -> Tuple[str, Optional[bytes], Optional
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
return (task.task_id, None, f"{str(e)}\n{traceback.format_exc()}")
|
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:
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||||
future_to_idx = {
|
future_to_idx = {
|
||||||
executor.submit(
|
executor.submit(self._render_item_to_bytes, item, page_width_pt, page_height_pt, bleed_pt): i
|
||||||
self._render_item_to_bytes, item, page_width_pt, page_height_pt, bleed_pt
|
|
||||||
): i
|
|
||||||
for i, item in enumerate(page_sequence)
|
for i, item in enumerate(page_sequence)
|
||||||
}
|
}
|
||||||
completed = 0
|
completed = 0
|
||||||
@ -348,9 +349,7 @@ class PDFExporter:
|
|||||||
|
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
def _make_blank_page_bytes(
|
def _make_blank_page_bytes(self, page_width_pt: float, page_height_pt: float, bleed_pt: float) -> 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."""
|
"""Return a minimal single-blank-page PDF for use as an error placeholder."""
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
c = canvas.Canvas(buf, pagesize=(page_width_pt + 2 * bleed_pt, page_height_pt + 2 * bleed_pt))
|
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):
|
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")
|
self._render_element(c, element, 0, cover_width_pt, cover_height_pt, "Cover")
|
||||||
|
|
||||||
|
|
||||||
c.showPage() # Finish cover page
|
c.showPage() # Finish cover page
|
||||||
|
|
||||||
def _export_single_page(
|
def _export_single_page(
|
||||||
@ -809,7 +807,12 @@ class PDFExporter:
|
|||||||
|
|
||||||
# Convert to points (bleed_pt shifts content inside the expanded PDF page)
|
# Convert to points (bleed_pt shifts content inside the expanded PDF page)
|
||||||
x_pt = adjusted_x_mm * self.MM_TO_POINTS + bleed_pt
|
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
|
width_pt = crop_width_mm * self.MM_TO_POINTS
|
||||||
height_pt = element_height_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
|
ctx: RenderContext containing all rendering parameters
|
||||||
"""
|
"""
|
||||||
# Check for pre-processed image in cache
|
# Check for pre-processed image in cache
|
||||||
task_id = self._make_task_id(
|
task_id = self._make_task_id(ctx.image_element, ctx.crop_left, ctx.crop_right, ctx.width_pt, ctx.height_pt)
|
||||||
ctx.image_element, ctx.crop_left, ctx.crop_right, ctx.width_pt, ctx.height_pt
|
|
||||||
)
|
|
||||||
cropped_img = self._processed_images.get(task_id)
|
cropped_img = self._processed_images.get(task_id)
|
||||||
|
|
||||||
if cropped_img is None:
|
if cropped_img is None:
|
||||||
|
|||||||
@ -18,7 +18,6 @@ from pyPhotoAlbum.version_manager import (
|
|||||||
DataMigration,
|
DataMigration,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Legacy constant for backward compatibility
|
# Legacy constant for backward compatibility
|
||||||
SERIALIZATION_VERSION = CURRENT_DATA_VERSION
|
SERIALIZATION_VERSION = CURRENT_DATA_VERSION
|
||||||
|
|
||||||
@ -257,10 +256,7 @@ def save_to_zip_async(
|
|||||||
if idx % 10 == 0 or idx == len(asset_files) - 1:
|
if idx % 10 == 0 or idx == len(asset_files) - 1:
|
||||||
progress = 25 + int((idx + 1) / len(asset_files) * progress_range)
|
progress = 25 + int((idx + 1) / len(asset_files) * progress_range)
|
||||||
if on_progress:
|
if on_progress:
|
||||||
on_progress(
|
on_progress(progress, f"Adding assets... ({idx + 1}/{len(asset_files)})")
|
||||||
progress,
|
|
||||||
f"Adding assets... ({idx + 1}/{len(asset_files)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
if on_progress:
|
if on_progress:
|
||||||
on_progress(95, "Finalizing save...")
|
on_progress(95, "Finalizing save...")
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Thumbnail Browser Widget - displays thumbnails from a folder for drag-and-drop into album.
|
Thumbnail Browser Widget - displays thumbnails from a folder for drag-and-drop into album.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QDockWidget, QScrollBar
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
|
||||||
QLabel, QFileDialog, QDockWidget, QScrollBar
|
|
||||||
)
|
|
||||||
from PyQt6.QtCore import Qt, QSize, QMimeData, QUrl, QPoint
|
from PyQt6.QtCore import Qt, QSize, QMimeData, QUrl, QPoint
|
||||||
from PyQt6.QtGui import QDrag, QCursor, QPainter, QFont, QColor
|
from PyQt6.QtGui import QDrag, QCursor, QPainter, QFont, QColor
|
||||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||||
@ -16,7 +14,6 @@ from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|||||||
from pyPhotoAlbum.gl_imports import *
|
from pyPhotoAlbum.gl_imports import *
|
||||||
from pyPhotoAlbum.mixins.viewport import ViewportMixin
|
from pyPhotoAlbum.mixins.viewport import ViewportMixin
|
||||||
|
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"]
|
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"]
|
||||||
|
|
||||||
|
|
||||||
@ -56,8 +53,7 @@ class ThumbnailItem:
|
|||||||
|
|
||||||
def contains_point(self, x: float, y: float) -> bool:
|
def contains_point(self, x: float, y: float) -> bool:
|
||||||
"""Check if point is inside this thumbnail."""
|
"""Check if point is inside this thumbnail."""
|
||||||
return (self.x <= x <= self.x + self.thumbnail_size and
|
return self.x <= x <= self.x + self.thumbnail_size and self.y <= y <= self.y + self.thumbnail_size
|
||||||
self.y <= y <= self.y + self.thumbnail_size)
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailGLWidget(QOpenGLWidget):
|
class ThumbnailGLWidget(QOpenGLWidget):
|
||||||
@ -121,7 +117,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
glMatrixMode(GL_MODELVIEW)
|
glMatrixMode(GL_MODELVIEW)
|
||||||
|
|
||||||
# Rearrange thumbnails to fit new width
|
# 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()
|
self._arrange_thumbnails()
|
||||||
else:
|
else:
|
||||||
# Still update scrollbar even if no thumbnails
|
# Still update scrollbar even if no thumbnails
|
||||||
@ -186,7 +182,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
# Render based on state: texture, loading placeholder, or empty placeholder
|
# Render based on state: texture, loading placeholder, or empty placeholder
|
||||||
if thumb._texture_id:
|
if thumb._texture_id:
|
||||||
# Calculate aspect-ratio-corrected dimensions
|
# 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
|
img_aspect = thumb._img_width / thumb._img_height
|
||||||
thumb_aspect = w / h
|
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_MIN_FILTER, GL_LINEAR)
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||||
glTexImage2D(
|
glTexImage2D(
|
||||||
GL_TEXTURE_2D, 0, GL_RGBA,
|
GL_TEXTURE_2D, 0, GL_RGBA, pil_image.width, pil_image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data
|
||||||
pil_image.width, pil_image.height,
|
|
||||||
0, GL_RGBA, GL_UNSIGNED_BYTE, img_data
|
|
||||||
)
|
)
|
||||||
|
|
||||||
thumb._texture_id = texture_id
|
thumb._texture_id = texture_id
|
||||||
@ -340,7 +334,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
|
|
||||||
def _arrange_thumbnails(self):
|
def _arrange_thumbnails(self):
|
||||||
"""Arrange thumbnails in a grid based on widget width and zoom level."""
|
"""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()
|
self.thumbnails.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -381,6 +375,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
# Check if we need a date header (only in date sort mode)
|
# Check if we need a date header (only in date sort mode)
|
||||||
if self.sort_mode == "date" and self._get_image_date_func:
|
if self.sort_mode == "date" and self._get_image_date_func:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
timestamp = self._get_image_date_func(image_file)
|
timestamp = self._get_image_date_func(image_file)
|
||||||
date_obj = datetime.fromtimestamp(timestamp)
|
date_obj = datetime.fromtimestamp(timestamp)
|
||||||
date_str = date_obj.strftime("%B %d, %Y") # e.g., "December 13, 2025"
|
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."""
|
"""Update which thumbnails are already used in the project."""
|
||||||
# Get reference to main window's project
|
# Get reference to main window's project
|
||||||
main_window = self.window()
|
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
|
return
|
||||||
|
|
||||||
project = main_window.project
|
project = main_window.project
|
||||||
@ -512,6 +507,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
used_paths = set()
|
used_paths = set()
|
||||||
for page in project.pages:
|
for page in project.pages:
|
||||||
from pyPhotoAlbum.models import ImageData
|
from pyPhotoAlbum.models import ImageData
|
||||||
|
|
||||||
for element in page.layout.elements:
|
for element in page.layout.elements:
|
||||||
if isinstance(element, ImageData) and element.image_path:
|
if isinstance(element, ImageData) and element.image_path:
|
||||||
# Resolve to absolute path for comparison
|
# Resolve to absolute path for comparison
|
||||||
@ -531,11 +527,11 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
|
|
||||||
# Get main window's async loader
|
# Get main window's async loader
|
||||||
main_window = self.window()
|
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
|
return
|
||||||
|
|
||||||
gl_widget = main_window._gl_widget
|
gl_widget = main_window._gl_widget
|
||||||
if not hasattr(gl_widget, 'async_image_loader'):
|
if not hasattr(gl_widget, "async_image_loader"):
|
||||||
return
|
return
|
||||||
|
|
||||||
from pyPhotoAlbum.async_backend import LoadPriority
|
from pyPhotoAlbum.async_backend import LoadPriority
|
||||||
@ -550,7 +546,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
Path(thumb.image_path),
|
Path(thumb.image_path),
|
||||||
priority=LoadPriority.LOW,
|
priority=LoadPriority.LOW,
|
||||||
target_size=(200, 200), # Small thumbnails
|
target_size=(200, 200), # Small thumbnails
|
||||||
user_data=thumb
|
user_data=thumb,
|
||||||
)
|
)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
thumb._async_loading = False # Reset on error
|
thumb._async_loading = False # Reset on error
|
||||||
@ -623,10 +619,7 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
# Pan the view (right-click or middle-click drag)
|
# Pan the view (right-click or middle-click drag)
|
||||||
# Only allow vertical panning - grid is always horizontally centered
|
# Only allow vertical panning - grid is always horizontally centered
|
||||||
delta = event.pos() - self.drag_start_pos
|
delta = event.pos() - self.drag_start_pos
|
||||||
self.pan_offset = (
|
self.pan_offset = (0, self.pan_offset[1] + delta.y()) # No horizontal pan - grid is centered
|
||||||
0, # No horizontal pan - grid is centered
|
|
||||||
self.pan_offset[1] + delta.y()
|
|
||||||
)
|
|
||||||
self.drag_start_pos = event.pos()
|
self.drag_start_pos = event.pos()
|
||||||
self._update_scrollbar_position()
|
self._update_scrollbar_position()
|
||||||
self.update()
|
self.update()
|
||||||
@ -664,15 +657,12 @@ class ThumbnailGLWidget(QOpenGLWidget):
|
|||||||
# Keep horizontal pan at 0 (grid is always horizontally centered)
|
# Keep horizontal pan at 0 (grid is always horizontally centered)
|
||||||
self.pan_offset = (
|
self.pan_offset = (
|
||||||
0, # No horizontal pan - grid is centered in _arrange_thumbnails
|
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:
|
else:
|
||||||
# Scroll mode - scroll vertically only
|
# Scroll mode - scroll vertically only
|
||||||
scroll_amount = delta * 0.5 # Adjust sensitivity
|
scroll_amount = delta * 0.5 # Adjust sensitivity
|
||||||
self.pan_offset = (
|
self.pan_offset = (0, self.pan_offset[1] + scroll_amount) # No horizontal pan
|
||||||
0, # No horizontal pan
|
|
||||||
self.pan_offset[1] + scroll_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
self._update_scrollbar_position()
|
self._update_scrollbar_position()
|
||||||
self.update()
|
self.update()
|
||||||
@ -757,11 +747,12 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
self.setWidget(main_widget)
|
self.setWidget(main_widget)
|
||||||
|
|
||||||
# Dock settings
|
# Dock settings
|
||||||
self.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea |
|
self.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
|
||||||
Qt.DockWidgetArea.RightDockWidgetArea)
|
self.setFeatures(
|
||||||
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable |
|
QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||||
QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
| QDockWidget.DockWidgetFeature.DockWidgetMovable
|
||||||
QDockWidget.DockWidgetFeature.DockWidgetFloatable)
|
| QDockWidget.DockWidgetFeature.DockWidgetFloatable
|
||||||
|
)
|
||||||
|
|
||||||
# Connect to main window's async loader when shown
|
# Connect to main window's async loader when shown
|
||||||
self._connect_async_loader()
|
self._connect_async_loader()
|
||||||
@ -769,15 +760,15 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
def _connect_async_loader(self):
|
def _connect_async_loader(self):
|
||||||
"""Connect to main window's async image loader."""
|
"""Connect to main window's async image loader."""
|
||||||
main_window = self.window()
|
main_window = self.window()
|
||||||
if not hasattr(main_window, '_gl_widget'):
|
if not hasattr(main_window, "_gl_widget"):
|
||||||
return
|
return
|
||||||
|
|
||||||
gl_widget = main_window._gl_widget
|
gl_widget = main_window._gl_widget
|
||||||
if not hasattr(gl_widget, 'async_image_loader'):
|
if not hasattr(gl_widget, "async_image_loader"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Avoid duplicate connections
|
# Avoid duplicate connections
|
||||||
if hasattr(self, '_async_connected') and self._async_connected:
|
if hasattr(self, "_async_connected") and self._async_connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -800,7 +791,7 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
self,
|
self,
|
||||||
"Select Image Folder",
|
"Select Image Folder",
|
||||||
str(self.gl_widget.current_folder) if self.gl_widget.current_folder else str(Path.home()),
|
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:
|
if folder_path:
|
||||||
@ -826,7 +817,7 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
self.current_sort = sort_mode
|
self.current_sort = sort_mode
|
||||||
|
|
||||||
# Re-sort the image files in the GL widget
|
# 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()
|
self._apply_sort()
|
||||||
# Re-arrange thumbnails with new order
|
# Re-arrange thumbnails with new order
|
||||||
self.gl_widget._arrange_thumbnails()
|
self.gl_widget._arrange_thumbnails()
|
||||||
@ -835,7 +826,7 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
|
|
||||||
def _apply_sort(self):
|
def _apply_sort(self):
|
||||||
"""Apply current sort mode to image files."""
|
"""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
|
return
|
||||||
if self.current_sort == "name":
|
if self.current_sort == "name":
|
||||||
# Sort by filename only (not full path)
|
# Sort by filename only (not full path)
|
||||||
@ -875,6 +866,7 @@ class ThumbnailBrowserDock(QDockWidget):
|
|||||||
if tag == "DateTimeOriginal":
|
if tag == "DateTimeOriginal":
|
||||||
# Convert EXIF date format "2023:12:13 14:30:00" to timestamp
|
# Convert EXIF date format "2023:12:13 14:30:00" to timestamp
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dt = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
|
dt = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
|
||||||
return dt.timestamp()
|
return dt.timestamp()
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import uuid
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, Any, Optional, Callable, List
|
from typing import Dict, Any, Optional, Callable, List
|
||||||
|
|
||||||
|
|
||||||
# Current data version - increment when making breaking changes to data format
|
# Current data version - increment when making breaking changes to data format
|
||||||
CURRENT_DATA_VERSION = "3.0"
|
CURRENT_DATA_VERSION = "3.0"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user