Skip to content

Commit 92403f4

Browse files
Feat: Splashscreen & Implement lazy loading for heavy dependencies (#39)
* Refactor: Implement lazy loading for heavy dependencies in ImagePipeline and MainWindow * New splashscreen, refactor some classes to be imported in the future * Add app icon PNG to build artifacts for Windows and macOS * Refactor splash screen handling and improve loading message display * Remove unused splash message handling and related code * Refactor splash screen handling to initialize QApplication and display splash earlier in the main function * Update src/main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ruff format --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ef6211e commit 92403f4

File tree

4 files changed

+71
-48
lines changed

4 files changed

+71
-48
lines changed

.github/workflows/release-build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ jobs:
142142
--hidden-import core.build_info `
143143
--add-data "src/ui/dark_theme.qss;." `
144144
--add-data "assets/app_icon.ico;." `
145+
--add-data "assets/app_icon.png;." `
145146
--hidden-import pyexiv2 `
146147
--hidden-import PyQt6.QtCore `
147148
--hidden-import PyQt6.QtGui `
@@ -181,6 +182,7 @@ jobs:
181182
--hidden-import core.build_info \
182183
--add-data src/ui/dark_theme.qss:. \
183184
--add-data assets/app_icon.ico:. \
185+
--add-data assets/app_icon.png:. \
184186
--hidden-import PyQt6.QtCore \
185187
--hidden-import PyQt6.QtGui \
186188
--hidden-import PyQt6.QtWidgets \

src/core/similarity_engine.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
from PyQt6.QtCore import QObject, pyqtSignal
77
import numpy as np # Import numpy for array manipulation
88
from sklearn.cluster import DBSCAN
9-
from sklearn.metrics.pairwise import (
10-
cosine_similarity,
11-
) # Import for similarity calculation
129

1310
from core.image_pipeline import ImagePipeline
1411
from .app_settings import (
@@ -385,8 +382,17 @@ def cluster_embeddings(self, embeddings: Dict[str, List[float]]):
385382
[embeddings[path] for path in paths_in_group], dtype=np.float32
386383
)
387384

388-
# Calculate pairwise cosine similarities
389-
pairwise_similarities = cosine_similarity(group_embeds)
385+
# Lazy import for cosine_similarity to defer sklearn.metrics loading
386+
try:
387+
from sklearn.metrics.pairwise import cosine_similarity
388+
except ImportError as e:
389+
logger.error(f"Failed to import cosine_similarity: {e}")
390+
pairwise_similarities = np.array(
391+
[[1.0]]
392+
) # Fallback for single item or error
393+
else:
394+
# Calculate pairwise cosine similarities
395+
pairwise_similarities = cosine_similarity(group_embeds)
390396

391397
# We want the average of the upper triangle (excluding diagonal which is 1.0)
392398
upper_triangle_indices = np.triu_indices(

src/main.py

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import sys
22
import os
3+
import time
34

45
# Ensure the 'src' directory is on sys.path when executing as a script
56
SRC_DIR = os.path.dirname(__file__)
6-
77
if SRC_DIR and SRC_DIR not in sys.path:
88
sys.path.insert(0, SRC_DIR)
99

@@ -16,19 +16,13 @@
1616
# If we can't initialize pyexiv2, log the error but don't prevent app startup
1717
print(f"Warning: Failed to initialize pyexiv2: {e}")
1818

19-
import logging # noqa: E402 # Must be after pyexiv2 initialization
20-
import time # noqa: E402
19+
import logging # noqa: E402
2120
import argparse # noqa: E402
2221
import traceback # noqa: E402 # For global exception handler
23-
from PyQt6.QtWidgets import ( # noqa: E402
24-
QApplication,
25-
QMessageBox,
26-
) # QMessageBox for global exception handler
27-
from PyQt6.QtGui import QIcon # noqa: E402
2822

29-
from ui.main_window import MainWindow # noqa: E402
30-
from ui.app_controller import AppController # noqa: E402
31-
from pillow_heif import register_heif_opener # noqa: E402
23+
from PyQt6.QtCore import Qt, QTimer # noqa: E402
24+
from PyQt6.QtWidgets import QApplication, QMessageBox, QSplashScreen # noqa: E402
25+
from PyQt6.QtGui import QIcon, QPixmap # noqa: E402
3226

3327

3428
def load_stylesheet(filename: str = "src/ui/dark_theme.qss") -> str:
@@ -53,13 +47,9 @@ def load_stylesheet(filename: str = "src/ui/dark_theme.qss") -> str:
5347
candidates = [
5448
os.path.join(
5549
base_dir, "dark_theme.qss"
56-
), # we bundle at top-level in frozen builds
57-
os.path.join(
58-
base_dir, filename
59-
), # e.g., src/ui/dark_theme.qss inside frozen or source
60-
os.path.abspath(
61-
filename
62-
), # direct path from CWD when running from repo root
50+
), # bundled at top-level in frozen builds
51+
os.path.join(base_dir, filename), # e.g., src/ui/dark_theme.qss
52+
os.path.abspath(filename), # direct path from CWD
6353
]
6454

6555
for path in candidates:
@@ -119,9 +109,7 @@ def global_exception_handler(exc_type, exc_value, exc_traceback):
119109
error_box.setIcon(QMessageBox.Icon.Critical)
120110
error_box.setWindowTitle("Application Error")
121111
error_box.setText(main_error_text)
122-
error_box.setDetailedText(
123-
error_message_details
124-
) # Full traceback for expert users/reporting
112+
error_box.setDetailedText(error_message_details) # Full traceback
125113
error_box.setStandardButtons(QMessageBox.StandardButton.Ok)
126114
error_box.exec()
127115
except Exception as e_msgbox:
@@ -196,6 +184,31 @@ def apply_app_identity(app: QApplication, main_window=None) -> None:
196184
def main():
197185
"""Main application entry point."""
198186

187+
# Create QApplication early for splash screen
188+
app = QApplication(sys.argv)
189+
190+
# --- Splash: show immediately (no text), then set message after it’s visible ---
191+
splash_total_start = time.perf_counter()
192+
193+
splash_path = os.path.join(
194+
os.path.dirname(os.path.dirname(__file__)), "assets", "app_icon.png"
195+
)
196+
splash_pix = QPixmap(splash_path).scaled(
197+
400,
198+
300,
199+
Qt.AspectRatioMode.KeepAspectRatio,
200+
Qt.TransformationMode.FastTransformation,
201+
)
202+
splash = QSplashScreen(splash_pix)
203+
204+
# Show the splash immediately (no text yet)
205+
splash.show()
206+
app.processEvents() # should be fast; no text/layout yet
207+
208+
from pillow_heif import register_heif_opener # noqa: E402
209+
210+
register_heif_opener()
211+
199212
# --- Enable Faulthandler for crash analysis ---
200213
import faulthandler
201214

@@ -220,8 +233,6 @@ def main():
220233
except Exception:
221234
pass
222235

223-
register_heif_opener()
224-
225236
# Parse command-line arguments
226237
parser = argparse.ArgumentParser(description="PhotoSort")
227238
parser.add_argument("--folder", type=str, help="Open specified folder at startup")
@@ -283,11 +294,10 @@ def main():
283294
"File logging disabled. To enable, set PHOTOSORT_ENABLE_FILE_LOGGING=true."
284295
)
285296

286-
# --- Suppress verbose third-party loggers ---
297+
# --- Suppress verbose third-party loggers ---
287298
logging.getLogger("PIL").setLevel(logging.INFO)
288299
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.INFO)
289300
logging.getLogger("PIL.TiffImagePlugin").setLevel(logging.INFO)
290-
# You might also want to set it for the more general Image module if logs still appear
291301
logging.getLogger("PIL.Image").setLevel(logging.INFO)
292302
# --- End Suppress verbose third-party loggers ---
293303

@@ -299,6 +309,9 @@ def main():
299309
main_start_time = time.perf_counter()
300310
logging.info("Application starting...")
301311

312+
from ui.main_window import MainWindow
313+
from ui.app_controller import AppController
314+
302315
# Handle clear-cache argument
303316
if args.clear_cache:
304317
clear_application_caches_start_time = time.perf_counter()
@@ -307,23 +320,6 @@ def main():
307320
f"Caches cleared via command line in {time.perf_counter() - clear_application_caches_start_time:.4f}s"
308321
)
309322

310-
app_instantiation_start_time = time.perf_counter()
311-
app = QApplication(sys.argv)
312-
logging.debug(
313-
f"QApplication instantiated in {time.perf_counter() - app_instantiation_start_time:.4f}s"
314-
)
315-
316-
# Load and apply the stylesheet
317-
stylesheet_load_start_time = time.perf_counter()
318-
stylesheet = load_stylesheet()
319-
if stylesheet:
320-
app.setStyleSheet(stylesheet)
321-
logging.debug(
322-
f"Stylesheet loaded and applied in {time.perf_counter() - stylesheet_load_start_time:.4f}s"
323-
)
324-
else:
325-
logging.warning("Stylesheet not found. Using default style.")
326-
327323
mainwindow_instantiation_start_time = time.perf_counter()
328324
window = MainWindow(initial_folder=args.folder)
329325
apply_app_identity(app, window)
@@ -336,6 +332,26 @@ def main():
336332
logging.debug(
337333
f"MainWindow shown in {time.perf_counter() - window_show_start_time:.4f}s"
338334
)
335+
splash.finish(window)
336+
logging.debug(
337+
f"Splashscreen finished in {time.perf_counter() - splash_total_start:.4f}s"
338+
)
339+
340+
# Defer stylesheet loading to after splash finish to avoid blocking startup
341+
stylesheet_load_start = time.perf_counter()
342+
343+
def apply_stylesheet():
344+
stylesheet = load_stylesheet()
345+
if stylesheet:
346+
app.setStyleSheet(stylesheet)
347+
logging.debug(
348+
f"Stylesheet applied in {time.perf_counter() - stylesheet_load_start:.4f}s"
349+
)
350+
351+
QTimer.singleShot(
352+
0,
353+
apply_stylesheet,
354+
)
339355

340356
logging.info(
341357
f"Application setup complete in {time.perf_counter() - main_start_time:.4f}s. Entering event loop."

src/ui/main_window.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
from ui.helpers.statusbar_utils import build_status_bar_info
7373
from ui.helpers.index_lookup_utils import find_proxy_index_for_path
7474

75-
# build_presentation now used only inside DeletionMarkController
7675
from ui.controllers.deletion_mark_controller import DeletionMarkController
7776
from ui.controllers.file_deletion_controller import FileDeletionController
7877
from ui.controllers.rotation_controller import RotationController

0 commit comments

Comments
 (0)