refactor!: remove cli mode and hardcode ui launch path
The application now launches exclusively in UI mode. The CLI/headless execution path has been completely removed, including: - CliBootstrap entry point and PicoCLI integration - LaunchMode detection logic and associated tests - Profile-based UI component gating (@Profile on UiView) UI component eligibility is now controlled solely by component scan rather than Spring profiles. The bootstrap always instantiates UiConfiguration and starts the JavaFX runtime. This simplifies the architecture by eliminating dual-mode complexity, reducing the number of code paths, and removing the framework overhead of profile-based conditional bean registration. BREAKING CHANGE: Headless/CLI execution is no longer supported. The application can only be launched as a desktop GUI.
This commit is contained in:
+197
@@ -0,0 +1,197 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Tu es un Architecte Logiciel Senior expert en Java (version 21+), Spring Boot et JavaFX. Ton rôle est de m'aider à concevoir et coder un logiciel desktop à forte charge dédié à la gestion de grands volumes de photos/médias.
|
||||
|
||||
Ce project est une application desktop de gestion de photos / images.
|
||||
|
||||
|
||||
### 1. ARCHITECTURE, STRUCTURE DE PROJET ET DÉCOUPAGE PAR SERVICES
|
||||
- Architecture sans JPMS (Pas de module-info.java) : L'application est un projet Java standard organisé en packages fonctionnels clairs.
|
||||
- Clean Architecture & Séparation des Couches :
|
||||
* Couche Domaine : Modèles métier (`Record` Java immuables) et contrats d'interfaces. Aucune dépendance vers JavaFX, Spring UI ou la persistance.
|
||||
* Couche Services Fonctionnels :
|
||||
- Métadonnées : Extraction/Écriture asynchrone (EXIF, IPTC, XMP) hors du thread graphique.
|
||||
- Vignettes (Thumbnails) : Génération et chargement asynchrones avec cache RAM (`LruCache`) et cache Disque.
|
||||
- Support Formats : Décodage extensible multi-formats (JPEG, PNG, WebP, RAW).
|
||||
- Persistance (H2 File-based) : Base embarquée pour l'indexation, les tags, l'historique et les albums, exécutée sur un pool dédié (`DatabaseExecutor`).
|
||||
- Préférences (YAML) : Sérialisation/désérialisation asynchrone des paramètres utilisateur (SnakeYAML / Jackson).
|
||||
- Service i18n : Gestion des langues réactive permettant le changement de `Locale` à chaud.
|
||||
* Couche Présentation : Découplée entre l'IHM JavaFX (MVVM) et la CLI PicoCLI.
|
||||
|
||||
### 2. INTÉGRATION SPRING BOOT & INJECTION DE DÉPENDANCES
|
||||
- Cycle de vie Spring : Utilise `SpringApplicationBuilder` pour démarrer le contexte Spring au lancement.
|
||||
- Injection 100% Spring : Interdiction d'instancier manuellement des composants métier ou UI. Tous les composants, ViewModels, Services et Repositories sont gérés comme des Beans Spring (`@Component`, `@Service`, `@Repository`).
|
||||
- Nettoyage à la fermeture : À la fermeture de la fenêtre (`setOnCloseRequest`), ferme proprement le contexte Spring (`ConfigurableApplicationContext.close()`) pour libérer les pools de threads, connexions DB H2 et caches.
|
||||
|
||||
### 3. INTERFACE GRAPHIQUE (100% JAVA / INTERDICTION STRICTE DU FXML)
|
||||
- Zéro FXML : N'UTILISE AUCUN FICHIER FXML, ni FXMLLoader. Toutes les vues sont construites 100% en code Java pur (composants étendant `BorderPane`, `VBox`, `StackPane`, etc.) et sont annotées `@Component` ou `@Scope("prototype")`.
|
||||
- Navigation par Événements (Event Bus Spring) :
|
||||
* Interdiction pour une vue ou un ViewModel de manipuler directement une autre vue pour naviguer.
|
||||
* La navigation repose EXCLUSIVEMENT sur la publication d'événements Spring (ex: `publisher.publishEvent(new NavigateToViewEvent(ViewType.PHOTO_DETAIL, photoId))`).
|
||||
* Un gestionnaire de vues central (`ViewSwitcher` / `StageManager`) écoute ces événements (`@EventListener`), détruit la vue courante et instancie la nouvelle depuis l'ApplicationContext Spring.
|
||||
- Cycle de vie et mémoire : Chaque vue/composant doit implémenter une méthode explicite de nettoyage (`dispose()` / `cleanup()`) pour délier les listeners, détruire les bindings et libérer la mémoire lors du démontage du composant.
|
||||
|
||||
### 4. GESTION DE LA CONCURRENCE ET THREADING (JAVAFX)
|
||||
- Thread UI (FXAT) : Le JavaFX Application Thread est réservé EXCLUSIVEMENT aux modifications du Scenegraph. Aucun I/O, décodage d'image ou requête DB ne doit s'y exécuter.
|
||||
- Concurrence : Utilise `javafx.concurrent.Task<V>` et `Service<V>` pour les traitements asynchrones.
|
||||
- Annulation au Scroll : Annule systématiquement les `Task` de chargement/génération de vignettes (`Task.cancel()`) lorsque les cellules de grilles ou listes virtuelles sont recyclées.
|
||||
- Mises à jour UI : Les retours de threads d'arrière-plan ou les écouteurs `@EventListener` Spring modifiant l'UI doivent impérativement être rapatriés sur le FXAT via `Platform.runLater()`.
|
||||
|
||||
### 5. INTERNATIONALISATION RÉACTIVE (i18n)
|
||||
- Source unique : Utilise `MessageSource` de Spring couplé à des fichiers `messages_*.properties`.
|
||||
- Changement de langue à chaud : Encapsule les traductions dans un `I18nService` exposant des `StringBinding` réactifs liés au `Locale` courant.
|
||||
- Binding dans les vues : Ne mets JAMAIS de chaînes de caractères en dur dans l'UI. Lie les propriétés textuelles directement aux bindings dynamiques :
|
||||
`button.textProperty().bind(i18nService.createStringBinding("button.save"));`
|
||||
- Formatage : Formatage des dates, tailles de fichiers et métadonnées respectant le `Locale` de l'utilisateur (`DateTimeFormatter`, `NumberFormat`).
|
||||
|
||||
### 6. CHARTE GRAPHIQUE ET THEMING : INTELLIJ NEW UI / IMMICH STYLE (ATLANTAFX & CSS)
|
||||
|
||||
- Direction Artistique (Look & Feel) :
|
||||
* L'interface doit adopter une esthétique hybride "JetBrains New UI" et "Immich Web UI" : sobre, sombre, ultra-réactive et épurée.
|
||||
* Utilise le thème AtlantaFX `NordDark` ou `PrimerDark` comme base, personnalisé avec les variables CSS globales de la palette IntelliJ/Immich.
|
||||
|
||||
- Palette de Couleurs & Variables CSS Globale (`theme-intellij-immich.css`) :
|
||||
* Arrière-plan principal (Viewport) : `#1E1F22` (IntelliJ Main Canvas).
|
||||
* Arrière-plan Panneaux (Sidebar/Header) : `#2B2D30` (IntelliJ Tool Window).
|
||||
* Bordures & Séparateurs : `#393B40` (Bordures fines de 1px).
|
||||
* Couleur d'Accent / Sélection : `#3574F0` (Bleu IntelliJ) ou `#6366F1` (Accent Immich).
|
||||
* Cartes Photos / Thumbnails : Arrière-plan `#2B2D30`, rayon de bordure `6px`, survol avec lueur `#3574F0` ou overlay sombre `#0000004D`.
|
||||
|
||||
- Typographie et Icônes :
|
||||
* Utilise les polices système sans-serif modernes (`Inter`, `Segoe UI`, `System`) pour l'UI, et une police à chasse fixe (`JetBrains Mono`) pour les données techniques/EXIF/IPTC dans l'inspecteur.
|
||||
* Icônes vectorielles Ikonli (Pack Feather ou FontAwesome5) stylisées en blanc/gris neutre (`#A9B0B7`), devenant lumineuses au survol ou à la sélection.
|
||||
|
||||
- Intégration du ThemeManager :
|
||||
* Un service Spring `ThemeManager` charge la feuille CSS personnalisée et permet le basculement dynamique entre les modes "IntelliJ Dark" et "IntelliJ Light / Immich Light".
|
||||
### 7. INTERFACE EN LIGNE DE COMMANDE (PICOCLI / TERMINAL)
|
||||
- Mode Headless / Terminal : Prends en charge l'exécution CLI autonome via PicoCLI (`picocli-spring-boot-starter` ou `IFactory` s'appuyant sur Spring).
|
||||
- Découplage strict : Les classes `@Command` font partie de la couche Présentation CLI et réutilisent les mêmes Services du Domaine que l'IHM JavaFX.
|
||||
- Aucune dépendance JavaFX dans la CLI : Garantis qu'aucune classe JavaFX n'est chargée lors d'une exécution CLI pour permettre le traitement batch headless sur serveur sans écran.
|
||||
- Exit Codes : Gère une fermeture propre (`System.exit(code)`) après exécution d'une commande CLI sans démarrer le runtime JavaFX.
|
||||
|
||||
### 8. BARRES DE TITRE ET DÉCORATEURS CUSTOM (CUSTOM WINDOW DECORATIONS)
|
||||
- Extension de la Zone de Titre (Client-Side Decorations) :
|
||||
* Pour les fenêtres principales et secondaires, utilise `StageStyle.EXTENDED` combiné avec un composant `HeaderBar` (ou un wrapper cross-platform dédié comme `jfx-frameless`).
|
||||
* Interdiction d'utiliser le `StageStyle.UNDECORATED` classique qui casse les comportements natifs de l'OS (Aero Snap Windows 11, ombres portées, redimensionnement sur les bords).
|
||||
- Intégration de Composants Custom dans la Barre de Titre :
|
||||
* Expose et injecte des composants applicatifs directement dans les slots du décorateur (ex: barre de recherche globale au centre `center`, sélecteur de thème ou boutons d'actions rapides à droite `right`).
|
||||
* Assure le respect des conventions d'ergonomie OS : boutons de contrôle (*traffic lights*) placés à gauche sur macOS et à droite sur Windows/Linux.
|
||||
|
||||
### 9. GESTION DE LA POSITION ET TAILLE DES FENÊTRES (WINDOW STATE PERSISTENCE)
|
||||
- Restauration et Sauvegarde de la Géométrie (Multi-Écrans) :
|
||||
* Pour la fenêtre principale et chaque fenêtre secondaire, la taille (Largeur, Hauteur), la position (X, Y) et l'état maximisé (`isMaximized`) doivent être persistés dans les préférences YAML.
|
||||
* Interdiction d'exposer la modification directe de ces paramètres dans la UI (pas de champs de saisie X/Y/Largeur/Hauteur dans l'écran de réglages). La modification reste possible uniquement via édition directe du fichier YAML de configuration.
|
||||
- Validation Multi-Écrans à l'Ouverture :
|
||||
* Lors de l'initialisation d'un `Stage`, lis les coordonnées sauvegardées et vérifie systématiquement qu'elles se situent à l'intérieur des limites visibles de l'un des écrans actuellement connectés (`Screen.getScreens()`).
|
||||
* En cas de changement de configuration d'affichage (ex: écran externe déconnecté alors que la fenêtre s'y trouvait), réinitialise automatiquement la position du `Stage` au centre de l'écran principal (`Screen.getPrimary()`).
|
||||
- Stratégie de Sauvegarde :
|
||||
* Écoute les événements de modification de la fenêtre (`xProperty()`, `yProperty()`, `widthProperty()`, `heightProperty()`, `maximizedProperty()`) ou effectue une capture à la fermeture (`setOnCloseRequest`).
|
||||
* Utilise un mécanisme de *Debounce* (temporisation asynchrone de 1s) avant d'invoquer le service de persistance YAML pour éviter les I/O excessifs.
|
||||
|
||||
### 10. GESTION DES DIALOGUES ET MODALES (IN-APP OVERLAYS)
|
||||
- Interdiction des Dialogues Natifs JDK : Ne pas utiliser `javafx.scene.control.Dialog` ou `Alert` pour préserver le thème AtlantaFX.
|
||||
- In-App Modals : Utilise AtlantaFX `ModalPane` pour incruster les fenêtres de préférences, de confirmation ou d'importation directement dans le scenegraph.
|
||||
- Découplage : Les formulaires de dialogues doivent être des composants `@Component` injectés et liés réactivement aux ViewModels.
|
||||
|
||||
### 11. DÉTECTION ET ANALYSE D'IMAGES MULTI-PROVIDERS (PLUGGABLE IMAGE ANALYSIS)
|
||||
- Architecture Pluggable (Pattern Strategy) :
|
||||
* Définis une interface `ImageAnalysisProvider` implémentée par différents fournisseurs :
|
||||
- Local / Edge AI : Deep Java Library (DJL) / YOLOv8 (hors-ligne par défaut).
|
||||
- Cloud AI : API REST / SDK (Google Cloud Vision, AWS Rekognition, Azure, etc.).
|
||||
- Remote Agent / MCP : Protocole MCP (Model Context Protocol) ou serveur d'inférence distant.
|
||||
- Switch Dynamique à Chaud : Un service délégué (`ImageAnalysisService`) sélectionne le provider actif selon la préférence utilisateur sauvegardée dans le YAML (`preferences.ai.provider`).
|
||||
- Concurrence et Persistence : Exécution asynchrone sur pool dédié (`ImageAnalysisExecutor`) et persistance des bounding boxes / labels dans H2.
|
||||
|
||||
### 12. BUILD MAVEN, PACKAGING NATIF, AOT & AUTOMATISATION GITHUB RELEASES
|
||||
- Tooling Maven : Le projet est structuré autour d'un build Maven (`pom.xml`) robuste.
|
||||
- Options de Packaging Natif :
|
||||
* Choix 1 (Prioritaire) : Packaging et système d'auto-update via le plugin jDeploy (`ca.weblite:jdeploy-maven-plugin`).
|
||||
* Choix 2 (Secondaire) : Image native distribuable via `jpackage` ou binaire compilé avec GraalVM Native Image.
|
||||
- Compilation AOT (Spring Boot AOT) :
|
||||
* Active l'objectif `process-aot` du `spring-boot-maven-plugin` pour pré-analyser le contexte Spring à la compilation.
|
||||
* Implémente une classe `RuntimeHintsRegistrar` (annotée `@ImportRuntimeHints`) pour enregistrer la réflexion et les polices vectorielles Ikonli.
|
||||
- Publication GitHub Releases & jDeploy :
|
||||
* Intègre un workflow GitHub Actions (`.github/workflows/release.yml`) automatisant le build, le packaging jDeploy et la mise à jour de la section Release de GitHub lors de la publication d'un tag Git.
|
||||
* Génère dans le corps de la Release un tableau contenant les icônes et les liens directs jDeploy pour chaque cible : Windows x64 (🪟), macOS Apple Silicon ARM (🍏), macOS Intel (🍏) et Linux x64 (🐧).
|
||||
|
||||
### 13. GESTION DE LA PHOTOTHÈQUE, IMPORTATION ET METADATAS
|
||||
- Répertoire Racine : L'utilisateur définit un répertoire racine (`library.root-path`) sur disque local, SSD ou NAS.
|
||||
- Module d'Importation :
|
||||
* Copie physique asynchrone des photos depuis un média amovible (carte SD, appareil photo) vers la structure de la photothèque racine.
|
||||
* Extraction haute performance des métadonnées (EXIF, IPTC, XMP : dates, coordonnées GPS, appareils, tags, note/rating initial) exécutée immédiatement lors de l'import et enregistrée dans la base embarquée H2 file-based.
|
||||
- Historique des Imports : Conservation et affichage des $N$ derniers lots d'importation (*Import Batches*) pour permettre à l'utilisateur de retrouver ou filtrer facilement les dernières photos importées.
|
||||
|
||||
### 14. VISUALISATION, FLAGGAGE ET COPIE EN BATCH DE MÉTADONNÉES
|
||||
- Tri et Évaluation Rapide :
|
||||
* Marquage rapide (*flagging*) pour marquage à supprimer/rejeter.
|
||||
* Notation rapide de 1 à 5 étoiles (*rating*) avec raccourcis clavier et binding réactif.
|
||||
- Copie / Collage Sélectif de Métadonnées (Batch Metadata Copy) :
|
||||
* L'utilisateur sélectionne une photo source, choisit précisément quelles métadonnées copier via une boîte de sélection (ex: uniquement les tags et le lieu, sans modifier la date).
|
||||
* Application en masse (*batch*) des métadonnées sélectionnées sur un ensemble de photos cibles, avec mise à jour asynchrone en BDD H2 et écriture optionnelle dans les fichiers/sidecars.
|
||||
|
||||
### 15. RÉORGANISATION ET RENOMMAGE AVANCÉ (PATTERN STRATEGY)
|
||||
- Vue Dédiée à la Réorganisation : Interface permettant la prévisualisation avant application du renommage, déplacement ou numérotation séquentielle de masse.
|
||||
- Extensibilité des Stratégies de Nommage :
|
||||
* Utilise une interface `FileRenamingStrategy` injectée via Spring.
|
||||
* Permet l'ajout trivial de nouvelles règles (ex: par date EXIF, par modèle d'appareil, par localisation, numérotation auto-Incrémentée `{date}_{seq}`).
|
||||
|
||||
### 16. SECTION MAINTENANCE ET SANTÉ DE LA PHOTOTHÈQUE
|
||||
- Module Maintenance Dédié :
|
||||
* Détection de Doublons : Recherche par empreinte numérique / hash perceptuel (`pHash`), taille et métadonnées identiques.
|
||||
* Régénération & Fix des Vignettes : Analyse du cache de vignettes, nettoyage des orphelins et régénération forcée des vignettes corrompues ou manquantes.
|
||||
* Cohérence & Nettoyage BDD H2 : Repérage des fichiers manquants sur le disque/NAS, synchronisation des métadonnées et purge des entrées orphelines dans la base H2.
|
||||
|
||||
### 17. SYNCHRONISATION EN ARRIÈRE-PLAN ET WATCHDOG DU SYSTÈME DE FICHIERS
|
||||
|
||||
- Service de Synchronisation Périodique (Background Library Sync) :
|
||||
* Un service planifié (`LibrarySynchronizationService`) analyse régulièrement la photothèque racine pour détecter :
|
||||
1. Les nouvelles photos ajoutées hors de l'application (indexation & extraction rapide de métadonnées en BDD H2).
|
||||
2. Les photos modifiées externes (mise à jour des métadonnées, ré-analyse IA et régénération des vignettes).
|
||||
3. Les photos supprimées/déplacées hors-app (marquage/purge en BDD H2 et suppression des vignettes orphelines).
|
||||
|
||||
- Fréquence Réglable et Dynamique :
|
||||
* La fréquence de balayage est définie dans les préférences utilisateur (`preferences.sync.interval-minutes`).
|
||||
* Utilise un `ThreadPoolTaskScheduler` Spring pour permettre le changement dynamique de fréquence à chaud sans redémarrer l'application.
|
||||
|
||||
- Isolation et Concurrence :
|
||||
* L'exécution du scan s'effectue exclusivement sur le pool de threads `BackgroundSyncExecutor`.
|
||||
* La progression et les fins de synchronisation émettent des événements Spring (`LibrarySyncEvent`) pour rafraîchir silencieusement l'UI JavaFX si nécessaire (`Platform.runLater`).
|
||||
|
||||
### 18. GESTION DES FORMATS ET REGISTRY DE CHARGEMENT D'IMAGES (IMAGE LOADERS & EXTENSIONS)
|
||||
|
||||
- Architecture Pluggable des Chargeurs d'Images (Pattern Strategy / Registry) :
|
||||
* Définis une interface `ImageLoader` représentant un décodeur/chargeur d'image dédié.
|
||||
* Chaque implémentation de `ImageLoader` déclare explicitement :
|
||||
- La liste des extensions de fichiers supportées (ex: `.jpg`, `.png`, `.webp`, `.cr2`, `.dng`).
|
||||
- La méthode de chargement synchrone/asynchrone produisant une `javafx.scene.image.Image` (vignette ou pleine résolution).
|
||||
- La priorité du loader (en cas de chevauchement de formats).
|
||||
|
||||
- Service Centralisé `ImageLoaderRegistry` :
|
||||
* Injecte automatiquement la liste de tous les `ImageLoader` déclarés dans le contexte Spring.
|
||||
* Maintient une carte (`Map<String, ImageLoader>`) associant chaque extension en minuscules à son loader dédié.
|
||||
* Expose une méthode utilitaire `isSupportedExtension(Path path)` utilisée par les services d'importation, de scan et de réconciliation en arrière-plan.
|
||||
|
||||
- Fallback et Multi-Bibliothèques :
|
||||
* Utilise le décodeur natif JavaFX (`Image`) pour les formats standards (JPEG, PNG, GIF, BMP).
|
||||
* Intègre des wrappers dédiés pour les formats étendus (ex: TwelveMonkeys ImageIO pour WebP/TIFF, LibRaw / JNA pour les fichiers RAW).
|
||||
|
||||
### 19. STRUCTURE ET DISPOSITION ERGONOMIQUE DE L'INTERFACE (APPLICATION LAYOUT)
|
||||
|
||||
- Disposition Générale à 5 Zones (BorderPane Root) :
|
||||
* Zone Haute (Header Bar / Title Bar) : Intégrée dans la zone de titre OS (`StageStyle.EXTENDED`). Contient le logo à gauche, les sélecteurs de modules principaux au centre (Importer, Trier, Réorganiser, Maintenance, Exporter) et les fenêtres/thèmes à droite.
|
||||
* Zone Centre (Main Viewport) : Prends le maximum d'espace disponible (`VBox.vgrow="ALWAYS"`, `HBox.hgrow="ALWAYS"`). Héberge la galerie d'images ou la vue plein écran.
|
||||
* Zone Gauche (Navigation & Actions Globale) : Panneau rétractable à deux modes :
|
||||
- Mode Réduit : Rail d'icônes uniquement (Ikonli `FontIcon`).
|
||||
- Mode Étendu : Arborescence de la photothèque, filtres rapides et widgets de saisie.
|
||||
* Zone Droite (Inspecteur Contextuel & Tâches) : Panneau d'informations lié à l'image sélectionnée (détails EXIF/IPTC, tags, analyse IA) et vue détaillée des tâches en cours.
|
||||
* Zone Basse (Barre d'État / StatusBar) : Permanente en bas de fenêtre. Contient les indicateurs de statut, la taille de la photothèque et le widget `TaskMonitor`.
|
||||
|
||||
- Interaction Task Monitor -> Panneau Droit :
|
||||
* Le clic sur le composant `TaskMonitor` dans la StatusBar doit émettre un événement Spring (`OpenTaskManagerEvent`) pour ouvrir/sélectionner le panneau des tâches à droite.
|
||||
|
||||
|
||||
---
|
||||
FORMAT DE RÉPONSE ATTENDU :
|
||||
1. Brève explication des choix d'architecture, de threading ou de pattern retenus.
|
||||
2. Code source Java complet, compilable et directement prêt pour la production (pas de pseudo-code, pas de "// à implémenter").
|
||||
@@ -1,197 +1,30 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Tu es un Architecte Logiciel Senior expert en Java (version 21+), Spring Boot et JavaFX. Ton rôle est de m'aider à concevoir et coder un logiciel desktop à forte charge dédié à la gestion de grands volumes de photos/médias.
|
||||
I'm a technical architect, java, spring boot and JavaFX expert.
|
||||
|
||||
Ce project est une application desktop de gestion de photos / images.
|
||||
This project is a desktop application for images / pictures management. Like collections of images. performance is key (volume and speed analysis)
|
||||
|
||||
## Technical overview
|
||||
* Use java 26 (and javafx), spring-boot, picocli, h2, jackson
|
||||
* use only maven as build tool.
|
||||
* For packaging, use spring boot generated jar, and jdeploy to create OS package (jdk/jre included)
|
||||
* Use lombok when applicable (Constructor and getter / setter)
|
||||
|
||||
### 1. ARCHITECTURE, STRUCTURE DE PROJET ET DÉCOUPAGE PAR SERVICES
|
||||
- Architecture sans JPMS (Pas de module-info.java) : L'application est un projet Java standard organisé en packages fonctionnels clairs.
|
||||
- Clean Architecture & Séparation des Couches :
|
||||
* Couche Domaine : Modèles métier (`Record` Java immuables) et contrats d'interfaces. Aucune dépendance vers JavaFX, Spring UI ou la persistance.
|
||||
* Couche Services Fonctionnels :
|
||||
- Métadonnées : Extraction/Écriture asynchrone (EXIF, IPTC, XMP) hors du thread graphique.
|
||||
- Vignettes (Thumbnails) : Génération et chargement asynchrones avec cache RAM (`LruCache`) et cache Disque.
|
||||
- Support Formats : Décodage extensible multi-formats (JPEG, PNG, WebP, RAW).
|
||||
- Persistance (H2 File-based) : Base embarquée pour l'indexation, les tags, l'historique et les albums, exécutée sur un pool dédié (`DatabaseExecutor`).
|
||||
- Préférences (YAML) : Sérialisation/désérialisation asynchrone des paramètres utilisateur (SnakeYAML / Jackson).
|
||||
- Service i18n : Gestion des langues réactive permettant le changement de `Locale` à chaud.
|
||||
* Couche Présentation : Découplée entre l'IHM JavaFX (MVVM) et la CLI PicoCLI.
|
||||
## Testing
|
||||
* for unit test use AssertJ with SoftAssertion
|
||||
|
||||
### 2. INTÉGRATION SPRING BOOT & INJECTION DE DÉPENDANCES
|
||||
- Cycle de vie Spring : Utilise `SpringApplicationBuilder` pour démarrer le contexte Spring au lancement.
|
||||
- Injection 100% Spring : Interdiction d'instancier manuellement des composants métier ou UI. Tous les composants, ViewModels, Services et Repositories sont gérés comme des Beans Spring (`@Component`, `@Service`, `@Repository`).
|
||||
- Nettoyage à la fermeture : À la fermeture de la fenêtre (`setOnCloseRequest`), ferme proprement le contexte Spring (`ConfigurableApplicationContext.close()`) pour libérer les pools de threads, connexions DB H2 et caches.
|
||||
|
||||
### 3. INTERFACE GRAPHIQUE (100% JAVA / INTERDICTION STRICTE DU FXML)
|
||||
- Zéro FXML : N'UTILISE AUCUN FICHIER FXML, ni FXMLLoader. Toutes les vues sont construites 100% en code Java pur (composants étendant `BorderPane`, `VBox`, `StackPane`, etc.) et sont annotées `@Component` ou `@Scope("prototype")`.
|
||||
- Navigation par Événements (Event Bus Spring) :
|
||||
* Interdiction pour une vue ou un ViewModel de manipuler directement une autre vue pour naviguer.
|
||||
* La navigation repose EXCLUSIVEMENT sur la publication d'événements Spring (ex: `publisher.publishEvent(new NavigateToViewEvent(ViewType.PHOTO_DETAIL, photoId))`).
|
||||
* Un gestionnaire de vues central (`ViewSwitcher` / `StageManager`) écoute ces événements (`@EventListener`), détruit la vue courante et instancie la nouvelle depuis l'ApplicationContext Spring.
|
||||
- Cycle de vie et mémoire : Chaque vue/composant doit implémenter une méthode explicite de nettoyage (`dispose()` / `cleanup()`) pour délier les listeners, détruire les bindings et libérer la mémoire lors du démontage du composant.
|
||||
|
||||
### 4. GESTION DE LA CONCURRENCE ET THREADING (JAVAFX)
|
||||
- Thread UI (FXAT) : Le JavaFX Application Thread est réservé EXCLUSIVEMENT aux modifications du Scenegraph. Aucun I/O, décodage d'image ou requête DB ne doit s'y exécuter.
|
||||
- Concurrence : Utilise `javafx.concurrent.Task<V>` et `Service<V>` pour les traitements asynchrones.
|
||||
- Annulation au Scroll : Annule systématiquement les `Task` de chargement/génération de vignettes (`Task.cancel()`) lorsque les cellules de grilles ou listes virtuelles sont recyclées.
|
||||
- Mises à jour UI : Les retours de threads d'arrière-plan ou les écouteurs `@EventListener` Spring modifiant l'UI doivent impérativement être rapatriés sur le FXAT via `Platform.runLater()`.
|
||||
|
||||
### 5. INTERNATIONALISATION RÉACTIVE (i18n)
|
||||
- Source unique : Utilise `MessageSource` de Spring couplé à des fichiers `messages_*.properties`.
|
||||
- Changement de langue à chaud : Encapsule les traductions dans un `I18nService` exposant des `StringBinding` réactifs liés au `Locale` courant.
|
||||
- Binding dans les vues : Ne mets JAMAIS de chaînes de caractères en dur dans l'UI. Lie les propriétés textuelles directement aux bindings dynamiques :
|
||||
`button.textProperty().bind(i18nService.createStringBinding("button.save"));`
|
||||
- Formatage : Formatage des dates, tailles de fichiers et métadonnées respectant le `Locale` de l'utilisateur (`DateTimeFormatter`, `NumberFormat`).
|
||||
|
||||
### 6. CHARTE GRAPHIQUE ET THEMING : INTELLIJ NEW UI / IMMICH STYLE (ATLANTAFX & CSS)
|
||||
|
||||
- Direction Artistique (Look & Feel) :
|
||||
* L'interface doit adopter une esthétique hybride "JetBrains New UI" et "Immich Web UI" : sobre, sombre, ultra-réactive et épurée.
|
||||
* Utilise le thème AtlantaFX `NordDark` ou `PrimerDark` comme base, personnalisé avec les variables CSS globales de la palette IntelliJ/Immich.
|
||||
|
||||
- Palette de Couleurs & Variables CSS Globale (`theme-intellij-immich.css`) :
|
||||
* Arrière-plan principal (Viewport) : `#1E1F22` (IntelliJ Main Canvas).
|
||||
* Arrière-plan Panneaux (Sidebar/Header) : `#2B2D30` (IntelliJ Tool Window).
|
||||
* Bordures & Séparateurs : `#393B40` (Bordures fines de 1px).
|
||||
* Couleur d'Accent / Sélection : `#3574F0` (Bleu IntelliJ) ou `#6366F1` (Accent Immich).
|
||||
* Cartes Photos / Thumbnails : Arrière-plan `#2B2D30`, rayon de bordure `6px`, survol avec lueur `#3574F0` ou overlay sombre `#0000004D`.
|
||||
|
||||
- Typographie et Icônes :
|
||||
* Utilise les polices système sans-serif modernes (`Inter`, `Segoe UI`, `System`) pour l'UI, et une police à chasse fixe (`JetBrains Mono`) pour les données techniques/EXIF/IPTC dans l'inspecteur.
|
||||
* Icônes vectorielles Ikonli (Pack Feather ou FontAwesome5) stylisées en blanc/gris neutre (`#A9B0B7`), devenant lumineuses au survol ou à la sélection.
|
||||
|
||||
- Intégration du ThemeManager :
|
||||
* Un service Spring `ThemeManager` charge la feuille CSS personnalisée et permet le basculement dynamique entre les modes "IntelliJ Dark" et "IntelliJ Light / Immich Light".
|
||||
### 7. INTERFACE EN LIGNE DE COMMANDE (PICOCLI / TERMINAL)
|
||||
- Mode Headless / Terminal : Prends en charge l'exécution CLI autonome via PicoCLI (`picocli-spring-boot-starter` ou `IFactory` s'appuyant sur Spring).
|
||||
- Découplage strict : Les classes `@Command` font partie de la couche Présentation CLI et réutilisent les mêmes Services du Domaine que l'IHM JavaFX.
|
||||
- Aucune dépendance JavaFX dans la CLI : Garantis qu'aucune classe JavaFX n'est chargée lors d'une exécution CLI pour permettre le traitement batch headless sur serveur sans écran.
|
||||
- Exit Codes : Gère une fermeture propre (`System.exit(code)`) après exécution d'une commande CLI sans démarrer le runtime JavaFX.
|
||||
|
||||
### 8. BARRES DE TITRE ET DÉCORATEURS CUSTOM (CUSTOM WINDOW DECORATIONS)
|
||||
- Extension de la Zone de Titre (Client-Side Decorations) :
|
||||
* Pour les fenêtres principales et secondaires, utilise `StageStyle.EXTENDED` combiné avec un composant `HeaderBar` (ou un wrapper cross-platform dédié comme `jfx-frameless`).
|
||||
* Interdiction d'utiliser le `StageStyle.UNDECORATED` classique qui casse les comportements natifs de l'OS (Aero Snap Windows 11, ombres portées, redimensionnement sur les bords).
|
||||
- Intégration de Composants Custom dans la Barre de Titre :
|
||||
* Expose et injecte des composants applicatifs directement dans les slots du décorateur (ex: barre de recherche globale au centre `center`, sélecteur de thème ou boutons d'actions rapides à droite `right`).
|
||||
* Assure le respect des conventions d'ergonomie OS : boutons de contrôle (*traffic lights*) placés à gauche sur macOS et à droite sur Windows/Linux.
|
||||
|
||||
### 9. GESTION DE LA POSITION ET TAILLE DES FENÊTRES (WINDOW STATE PERSISTENCE)
|
||||
- Restauration et Sauvegarde de la Géométrie (Multi-Écrans) :
|
||||
* Pour la fenêtre principale et chaque fenêtre secondaire, la taille (Largeur, Hauteur), la position (X, Y) et l'état maximisé (`isMaximized`) doivent être persistés dans les préférences YAML.
|
||||
* Interdiction d'exposer la modification directe de ces paramètres dans la UI (pas de champs de saisie X/Y/Largeur/Hauteur dans l'écran de réglages). La modification reste possible uniquement via édition directe du fichier YAML de configuration.
|
||||
- Validation Multi-Écrans à l'Ouverture :
|
||||
* Lors de l'initialisation d'un `Stage`, lis les coordonnées sauvegardées et vérifie systématiquement qu'elles se situent à l'intérieur des limites visibles de l'un des écrans actuellement connectés (`Screen.getScreens()`).
|
||||
* En cas de changement de configuration d'affichage (ex: écran externe déconnecté alors que la fenêtre s'y trouvait), réinitialise automatiquement la position du `Stage` au centre de l'écran principal (`Screen.getPrimary()`).
|
||||
- Stratégie de Sauvegarde :
|
||||
* Écoute les événements de modification de la fenêtre (`xProperty()`, `yProperty()`, `widthProperty()`, `heightProperty()`, `maximizedProperty()`) ou effectue une capture à la fermeture (`setOnCloseRequest`).
|
||||
* Utilise un mécanisme de *Debounce* (temporisation asynchrone de 1s) avant d'invoquer le service de persistance YAML pour éviter les I/O excessifs.
|
||||
|
||||
### 10. GESTION DES DIALOGUES ET MODALES (IN-APP OVERLAYS)
|
||||
- Interdiction des Dialogues Natifs JDK : Ne pas utiliser `javafx.scene.control.Dialog` ou `Alert` pour préserver le thème AtlantaFX.
|
||||
- In-App Modals : Utilise AtlantaFX `ModalPane` pour incruster les fenêtres de préférences, de confirmation ou d'importation directement dans le scenegraph.
|
||||
- Découplage : Les formulaires de dialogues doivent être des composants `@Component` injectés et liés réactivement aux ViewModels.
|
||||
|
||||
### 11. DÉTECTION ET ANALYSE D'IMAGES MULTI-PROVIDERS (PLUGGABLE IMAGE ANALYSIS)
|
||||
- Architecture Pluggable (Pattern Strategy) :
|
||||
* Définis une interface `ImageAnalysisProvider` implémentée par différents fournisseurs :
|
||||
- Local / Edge AI : Deep Java Library (DJL) / YOLOv8 (hors-ligne par défaut).
|
||||
- Cloud AI : API REST / SDK (Google Cloud Vision, AWS Rekognition, Azure, etc.).
|
||||
- Remote Agent / MCP : Protocole MCP (Model Context Protocol) ou serveur d'inférence distant.
|
||||
- Switch Dynamique à Chaud : Un service délégué (`ImageAnalysisService`) sélectionne le provider actif selon la préférence utilisateur sauvegardée dans le YAML (`preferences.ai.provider`).
|
||||
- Concurrence et Persistence : Exécution asynchrone sur pool dédié (`ImageAnalysisExecutor`) et persistance des bounding boxes / labels dans H2.
|
||||
|
||||
### 12. BUILD MAVEN, PACKAGING NATIF, AOT & AUTOMATISATION GITHUB RELEASES
|
||||
- Tooling Maven : Le projet est structuré autour d'un build Maven (`pom.xml`) robuste.
|
||||
- Options de Packaging Natif :
|
||||
* Choix 1 (Prioritaire) : Packaging et système d'auto-update via le plugin jDeploy (`ca.weblite:jdeploy-maven-plugin`).
|
||||
* Choix 2 (Secondaire) : Image native distribuable via `jpackage` ou binaire compilé avec GraalVM Native Image.
|
||||
- Compilation AOT (Spring Boot AOT) :
|
||||
* Active l'objectif `process-aot` du `spring-boot-maven-plugin` pour pré-analyser le contexte Spring à la compilation.
|
||||
* Implémente une classe `RuntimeHintsRegistrar` (annotée `@ImportRuntimeHints`) pour enregistrer la réflexion et les polices vectorielles Ikonli.
|
||||
- Publication GitHub Releases & jDeploy :
|
||||
* Intègre un workflow GitHub Actions (`.github/workflows/release.yml`) automatisant le build, le packaging jDeploy et la mise à jour de la section Release de GitHub lors de la publication d'un tag Git.
|
||||
* Génère dans le corps de la Release un tableau contenant les icônes et les liens directs jDeploy pour chaque cible : Windows x64 (🪟), macOS Apple Silicon ARM (🍏), macOS Intel (🍏) et Linux x64 (🐧).
|
||||
|
||||
### 13. GESTION DE LA PHOTOTHÈQUE, IMPORTATION ET METADATAS
|
||||
- Répertoire Racine : L'utilisateur définit un répertoire racine (`library.root-path`) sur disque local, SSD ou NAS.
|
||||
- Module d'Importation :
|
||||
* Copie physique asynchrone des photos depuis un média amovible (carte SD, appareil photo) vers la structure de la photothèque racine.
|
||||
* Extraction haute performance des métadonnées (EXIF, IPTC, XMP : dates, coordonnées GPS, appareils, tags, note/rating initial) exécutée immédiatement lors de l'import et enregistrée dans la base embarquée H2 file-based.
|
||||
- Historique des Imports : Conservation et affichage des $N$ derniers lots d'importation (*Import Batches*) pour permettre à l'utilisateur de retrouver ou filtrer facilement les dernières photos importées.
|
||||
|
||||
### 14. VISUALISATION, FLAGGAGE ET COPIE EN BATCH DE MÉTADONNÉES
|
||||
- Tri et Évaluation Rapide :
|
||||
* Marquage rapide (*flagging*) pour marquage à supprimer/rejeter.
|
||||
* Notation rapide de 1 à 5 étoiles (*rating*) avec raccourcis clavier et binding réactif.
|
||||
- Copie / Collage Sélectif de Métadonnées (Batch Metadata Copy) :
|
||||
* L'utilisateur sélectionne une photo source, choisit précisément quelles métadonnées copier via une boîte de sélection (ex: uniquement les tags et le lieu, sans modifier la date).
|
||||
* Application en masse (*batch*) des métadonnées sélectionnées sur un ensemble de photos cibles, avec mise à jour asynchrone en BDD H2 et écriture optionnelle dans les fichiers/sidecars.
|
||||
|
||||
### 15. RÉORGANISATION ET RENOMMAGE AVANCÉ (PATTERN STRATEGY)
|
||||
- Vue Dédiée à la Réorganisation : Interface permettant la prévisualisation avant application du renommage, déplacement ou numérotation séquentielle de masse.
|
||||
- Extensibilité des Stratégies de Nommage :
|
||||
* Utilise une interface `FileRenamingStrategy` injectée via Spring.
|
||||
* Permet l'ajout trivial de nouvelles règles (ex: par date EXIF, par modèle d'appareil, par localisation, numérotation auto-Incrémentée `{date}_{seq}`).
|
||||
|
||||
### 16. SECTION MAINTENANCE ET SANTÉ DE LA PHOTOTHÈQUE
|
||||
- Module Maintenance Dédié :
|
||||
* Détection de Doublons : Recherche par empreinte numérique / hash perceptuel (`pHash`), taille et métadonnées identiques.
|
||||
* Régénération & Fix des Vignettes : Analyse du cache de vignettes, nettoyage des orphelins et régénération forcée des vignettes corrompues ou manquantes.
|
||||
* Cohérence & Nettoyage BDD H2 : Repérage des fichiers manquants sur le disque/NAS, synchronisation des métadonnées et purge des entrées orphelines dans la base H2.
|
||||
|
||||
### 17. SYNCHRONISATION EN ARRIÈRE-PLAN ET WATCHDOG DU SYSTÈME DE FICHIERS
|
||||
|
||||
- Service de Synchronisation Périodique (Background Library Sync) :
|
||||
* Un service planifié (`LibrarySynchronizationService`) analyse régulièrement la photothèque racine pour détecter :
|
||||
1. Les nouvelles photos ajoutées hors de l'application (indexation & extraction rapide de métadonnées en BDD H2).
|
||||
2. Les photos modifiées externes (mise à jour des métadonnées, ré-analyse IA et régénération des vignettes).
|
||||
3. Les photos supprimées/déplacées hors-app (marquage/purge en BDD H2 et suppression des vignettes orphelines).
|
||||
|
||||
- Fréquence Réglable et Dynamique :
|
||||
* La fréquence de balayage est définie dans les préférences utilisateur (`preferences.sync.interval-minutes`).
|
||||
* Utilise un `ThreadPoolTaskScheduler` Spring pour permettre le changement dynamique de fréquence à chaud sans redémarrer l'application.
|
||||
|
||||
- Isolation et Concurrence :
|
||||
* L'exécution du scan s'effectue exclusivement sur le pool de threads `BackgroundSyncExecutor`.
|
||||
* La progression et les fins de synchronisation émettent des événements Spring (`LibrarySyncEvent`) pour rafraîchir silencieusement l'UI JavaFX si nécessaire (`Platform.runLater`).
|
||||
|
||||
### 18. GESTION DES FORMATS ET REGISTRY DE CHARGEMENT D'IMAGES (IMAGE LOADERS & EXTENSIONS)
|
||||
|
||||
- Architecture Pluggable des Chargeurs d'Images (Pattern Strategy / Registry) :
|
||||
* Définis une interface `ImageLoader` représentant un décodeur/chargeur d'image dédié.
|
||||
* Chaque implémentation de `ImageLoader` déclare explicitement :
|
||||
- La liste des extensions de fichiers supportées (ex: `.jpg`, `.png`, `.webp`, `.cr2`, `.dng`).
|
||||
- La méthode de chargement synchrone/asynchrone produisant une `javafx.scene.image.Image` (vignette ou pleine résolution).
|
||||
- La priorité du loader (en cas de chevauchement de formats).
|
||||
|
||||
- Service Centralisé `ImageLoaderRegistry` :
|
||||
* Injecte automatiquement la liste de tous les `ImageLoader` déclarés dans le contexte Spring.
|
||||
* Maintient une carte (`Map<String, ImageLoader>`) associant chaque extension en minuscules à son loader dédié.
|
||||
* Expose une méthode utilitaire `isSupportedExtension(Path path)` utilisée par les services d'importation, de scan et de réconciliation en arrière-plan.
|
||||
|
||||
- Fallback et Multi-Bibliothèques :
|
||||
* Utilise le décodeur natif JavaFX (`Image`) pour les formats standards (JPEG, PNG, GIF, BMP).
|
||||
* Intègre des wrappers dédiés pour les formats étendus (ex: TwelveMonkeys ImageIO pour WebP/TIFF, LibRaw / JNA pour les fichiers RAW).
|
||||
|
||||
### 19. STRUCTURE ET DISPOSITION ERGONOMIQUE DE L'INTERFACE (APPLICATION LAYOUT)
|
||||
|
||||
- Disposition Générale à 5 Zones (BorderPane Root) :
|
||||
* Zone Haute (Header Bar / Title Bar) : Intégrée dans la zone de titre OS (`StageStyle.EXTENDED`). Contient le logo à gauche, les sélecteurs de modules principaux au centre (Importer, Trier, Réorganiser, Maintenance, Exporter) et les fenêtres/thèmes à droite.
|
||||
* Zone Centre (Main Viewport) : Prends le maximum d'espace disponible (`VBox.vgrow="ALWAYS"`, `HBox.hgrow="ALWAYS"`). Héberge la galerie d'images ou la vue plein écran.
|
||||
* Zone Gauche (Navigation & Actions Globale) : Panneau rétractable à deux modes :
|
||||
- Mode Réduit : Rail d'icônes uniquement (Ikonli `FontIcon`).
|
||||
- Mode Étendu : Arborescence de la photothèque, filtres rapides et widgets de saisie.
|
||||
* Zone Droite (Inspecteur Contextuel & Tâches) : Panneau d'informations lié à l'image sélectionnée (détails EXIF/IPTC, tags, analyse IA) et vue détaillée des tâches en cours.
|
||||
* Zone Basse (Barre d'État / StatusBar) : Permanente en bas de fenêtre. Contient les indicateurs de statut, la taille de la photothèque et le widget `TaskMonitor`.
|
||||
|
||||
- Interaction Task Monitor -> Panneau Droit :
|
||||
* Le clic sur le composant `TaskMonitor` dans la StatusBar doit émettre un événement Spring (`OpenTaskManagerEvent`) pour ouvrir/sélectionner le panneau des tâches à droite.
|
||||
|
||||
|
||||
---
|
||||
FORMAT DE RÉPONSE ATTENDU :
|
||||
1. Brève explication des choix d'architecture, de threading ou de pattern retenus.
|
||||
2. Code source Java complet, compilable et directement prêt pour la production (pas de pseudo-code, pas de "// à implémenter").
|
||||
## Git & Commit Guidelines
|
||||
- Use the Conventional Commits v1.0.0 specification for all commit messages.
|
||||
- Always structure messages as: `<type>([optional scope]): <description>`
|
||||
- Use lowercase for the type and scope.
|
||||
- Write the description in the imperative mood (e.g., "add feature", not "added feature").
|
||||
- Allowed types:
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Code style changes (formatting, missing semi-colons, etc.)
|
||||
- `refactor`: Code changes that neither fix a bug nor add a feature
|
||||
- `test`: Adding missing tests or correcting existing tests
|
||||
- `chore`: Changes to the build process or auxiliary tools
|
||||
- Example: `feat(auth): add JWT token validation`
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
<version>${archunit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,50 +1,20 @@
|
||||
package io.pholio;
|
||||
|
||||
/**
|
||||
* Decides whether an invocation should start the JavaFX desktop shell or run headless on the
|
||||
* terminal.
|
||||
* How the application was asked to run.
|
||||
*
|
||||
* <p>The rule is intentionally simple and predictable: <em>any</em> argument that is not a framework
|
||||
* switch means the user is driving the application from a terminal, so the CLI takes over. Launching
|
||||
* with no arguments — the desktop case — opens the window. {@code --gui} forces the window even when
|
||||
* other arguments are present.
|
||||
* <p>The mode drives two things at once: which packages Spring scans, and whether the process needs a display.
|
||||
* Keeping them in step is the whole point of deciding this once, up front.
|
||||
*/
|
||||
public enum LaunchMode {
|
||||
GUI,
|
||||
CLI;
|
||||
|
||||
/** Forces the desktop shell even when application arguments are present. */
|
||||
public static final String FORCE_GUI_FLAG = "--gui";
|
||||
/** Desktop shell. The UI component scan is added and AWT headless mode is switched off. */
|
||||
UI,
|
||||
|
||||
public static LaunchMode of(String[] args) {
|
||||
if (args == null) {
|
||||
return GUI;
|
||||
}
|
||||
boolean sawApplicationArgument = false;
|
||||
for (String arg : args) {
|
||||
if (FORCE_GUI_FLAG.equals(arg)) {
|
||||
return GUI;
|
||||
}
|
||||
if (!isFrameworkArgument(arg)) {
|
||||
sawApplicationArgument = true;
|
||||
}
|
||||
}
|
||||
return sawApplicationArgument ? CLI : GUI;
|
||||
}
|
||||
/** Terminal execution. No UI beans are scanned and AWT headless mode is switched on. */
|
||||
HEADLESS;
|
||||
|
||||
/**
|
||||
* Arguments consumed by Spring Boot or the JVM rather than by the application itself. These must
|
||||
* not flip the launcher into CLI mode — {@code mvn spring-boot:run} and IDE run configurations
|
||||
* routinely inject them.
|
||||
*/
|
||||
private static boolean isFrameworkArgument(String arg) {
|
||||
return arg == null
|
||||
|| arg.isBlank()
|
||||
|| arg.startsWith("-D")
|
||||
|| arg.startsWith("--spring.")
|
||||
|| arg.startsWith("--logging.")
|
||||
|| arg.startsWith("--pholio.")
|
||||
|| arg.equals("--debug")
|
||||
|| arg.equals("--trace");
|
||||
public boolean isUi() {
|
||||
return this == UI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,20 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
/**
|
||||
* Application entry point.
|
||||
* Application entry point and the base Spring configuration shared by both launch modes.
|
||||
*
|
||||
* <p>This class is deliberately free of any {@code javafx.*} reference so that a headless CLI
|
||||
* invocation never triggers loading of the JavaFX runtime. The two bootstrap paths live in separate
|
||||
* classes ({@code io.pholio.cli.CliBootstrap} and {@code io.pholio.ui.GuiBootstrap}) which the JVM
|
||||
* only resolves when the corresponding branch is taken.
|
||||
* <p>The component scan covers only the layers that work with or without a display. The presentation layer is
|
||||
* contributed separately by {@code io.pholio.ui.UiConfiguration}, which the bootstrap adds only in UI mode —
|
||||
* so in a headless run the UI classes are never even read, let alone instantiated.
|
||||
*
|
||||
* <p>This class holds no {@code javafx.*} reference, so a headless invocation never resolves the JavaFX
|
||||
* runtime.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
@SpringBootApplication(scanBasePackages = {"io.pholio.infra", "io.pholio.cli"})
|
||||
@ConfigurationPropertiesScan("io.pholio.infra.config")
|
||||
public class PholioApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (LaunchMode.of(args) == LaunchMode.CLI) {
|
||||
System.exit(io.pholio.cli.CliBootstrap.run(args));
|
||||
}
|
||||
io.pholio.ui.GuiBootstrap.launch(args);
|
||||
System.exit(PholioLauncher.launch(args));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,42 @@ import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
|
||||
/**
|
||||
* Builds the Spring context the same way for both launch modes, differing only in the active profile.
|
||||
* Builds the Spring context for a given {@link LaunchMode}.
|
||||
*
|
||||
* <p>The H2 location cannot be expressed in {@code application.yaml} because it depends on the
|
||||
* OS-specific data directory, so it is contributed here as a <em>default</em> property — lowest
|
||||
* precedence, which lets {@code src/test/resources/application.yaml} and real command-line arguments
|
||||
* override it.
|
||||
* <p>The mode decides three things together, which is why they live in one place:
|
||||
* <ul>
|
||||
* <li><strong>Sources.</strong> UI mode adds {@code UiConfiguration}, whose component scan brings in the
|
||||
* presentation layer. Headless mode does not, so those classes are never scanned.
|
||||
* <li><strong>AWT headless.</strong> Switched on for headless runs so no attempt is ever made to reach a
|
||||
* display, and off for the desktop shell.
|
||||
* <li><strong>Profile.</strong> Selects mode-specific configuration — {@code application-cli.yaml} turns
|
||||
* console logging down so a command's stdout carries only its own output.
|
||||
* </ul>
|
||||
*
|
||||
* <p>The H2 location cannot be expressed in {@code application.yaml} because it depends on the OS-specific
|
||||
* data directory, so it is contributed here as a <em>default</em> property — lowest precedence, which lets
|
||||
* {@code src/test/resources/application.yaml} and real command-line arguments override it.
|
||||
*/
|
||||
public final class PholioBootstrap {
|
||||
|
||||
private PholioBootstrap() {
|
||||
}
|
||||
|
||||
public static SpringApplicationBuilder builder(String profile) {
|
||||
return new SpringApplicationBuilder(PholioApplication.class)
|
||||
public static SpringApplicationBuilder builder(LaunchMode mode) {
|
||||
SpringApplicationBuilder builder = new SpringApplicationBuilder(PholioApplication.class)
|
||||
.web(WebApplicationType.NONE)
|
||||
.bannerMode(Banner.Mode.OFF)
|
||||
.profiles(profile)
|
||||
.properties(defaultProperties());
|
||||
|
||||
if (mode.isUi()) {
|
||||
return builder
|
||||
.sources(io.pholio.ui.UiConfiguration.class)
|
||||
.profiles(Profiles.GUI)
|
||||
.headless(false);
|
||||
}
|
||||
return builder
|
||||
.profiles(Profiles.CLI)
|
||||
.headless(true);
|
||||
}
|
||||
|
||||
private static Map<String, Object> defaultProperties() {
|
||||
|
||||
@@ -3,15 +3,17 @@ package io.pholio;
|
||||
/**
|
||||
* Spring profile names.
|
||||
*
|
||||
* <p>The split matters: every bean that touches JavaFX is restricted to {@link #GUI} so the headless
|
||||
* CLI context can start without the JavaFX runtime ever being classloaded.
|
||||
* <p>Scope note: profiles select <em>configuration</em> only — {@code application-cli.yaml} turns console
|
||||
* logging down so a command's stdout stays clean. They do not decide which beans exist. That is the component
|
||||
* scan's job, driven by {@link LaunchMode} in {@code PholioBootstrap}, which keeps one question answered in
|
||||
* one place.
|
||||
*/
|
||||
public final class Profiles {
|
||||
|
||||
/** Desktop shell. Activates all JavaFX-dependent beans. */
|
||||
/** Desktop shell. */
|
||||
public static final String GUI = "gui";
|
||||
|
||||
/** Headless terminal execution. No JavaFX bean is eligible. */
|
||||
/** Headless terminal execution. */
|
||||
public static final String CLI = "cli";
|
||||
|
||||
private Profiles() {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package io.pholio.cli;
|
||||
|
||||
import io.pholio.PholioBootstrap;
|
||||
import io.pholio.Profiles;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import picocli.CommandLine;
|
||||
|
||||
/**
|
||||
* Entry point for the headless launch path.
|
||||
*
|
||||
* <p>Runs the Spring context under the {@code cli} profile, so no JavaFX-dependent bean is eligible and
|
||||
* the JavaFX runtime is never loaded — which is what makes batch processing on a headless server possible.
|
||||
*
|
||||
* <p>The context is closed before returning so the exit code is reported after every pool has drained.
|
||||
*/
|
||||
public final class CliBootstrap {
|
||||
|
||||
private CliBootstrap() {
|
||||
}
|
||||
|
||||
public static int run(String[] args) {
|
||||
try (ConfigurableApplicationContext context = PholioBootstrap.builder(Profiles.CLI).run(args)) {
|
||||
CommandLine commandLine = new CommandLine(
|
||||
context.getBean(PholioCommand.class), new SpringPicocliFactory(context));
|
||||
commandLine.setCaseInsensitiveEnumValuesAllowed(true);
|
||||
// With no subcommand, print usage rather than silently succeeding.
|
||||
commandLine.setExecutionStrategy(new CommandLine.RunLast());
|
||||
if (args.length == 0) {
|
||||
commandLine.usage(System.out);
|
||||
return CommandLine.ExitCode.USAGE;
|
||||
}
|
||||
return commandLine.execute(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,44 @@
|
||||
package io.pholio.cli;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Mixin;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
/**
|
||||
* Root command. Carries {@code --help} and {@code --version} and groups the subcommands.
|
||||
* Root command: carries the global options and groups the subcommands.
|
||||
*
|
||||
* <p>Running it with no subcommand prints usage rather than doing something surprising.
|
||||
* <p>Reached only in headless mode, since the presence of a subcommand is what selects headless in the first
|
||||
* place. Invoked with no subcommand — {@code pholio --no-ui} — it prints usage and reports a usage exit code
|
||||
* rather than succeeding silently, because doing nothing quietly is indistinguishable from working.
|
||||
*/
|
||||
@Component
|
||||
@Command(
|
||||
name = "pholio",
|
||||
mixinStandardHelpOptions = true,
|
||||
versionProvider = PholioVersionProvider.class,
|
||||
description = "Pholio — high-volume photo library manager. Runs headless; "
|
||||
+ "launch without arguments for the desktop interface.",
|
||||
description = "Pholio — high-volume photo library manager. Launch without a command to open the "
|
||||
+ "desktop interface.",
|
||||
subcommands = {ScanCommand.class})
|
||||
public class PholioCommand implements Runnable {
|
||||
public class PholioCommand implements Callable<Integer> {
|
||||
|
||||
/**
|
||||
* Present so {@code --help} documents the mode switch. The launcher has already acted on it by the time
|
||||
* this command is built; parsing it again here is only for the help text and for accepting the flag
|
||||
* without complaint.
|
||||
*/
|
||||
@Mixin
|
||||
private UiModeOptions uiMode = new UiModeOptions();
|
||||
|
||||
@Spec
|
||||
private CommandSpec spec;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// No subcommand given: PicoCLI prints usage via the exit-code handling in CliBootstrap.
|
||||
public Integer call() {
|
||||
spec.commandLine().usage(spec.commandLine().getOut());
|
||||
return CommandLine.ExitCode.USAGE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.pholio.ui;
|
||||
|
||||
import io.pholio.LaunchMode;
|
||||
import io.pholio.PholioBootstrap;
|
||||
import io.pholio.Profiles;
|
||||
import javafx.application.Application;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
@@ -31,7 +31,7 @@ public class PholioFxApplication extends Application {
|
||||
@Override
|
||||
public void init() {
|
||||
String[] args = getParameters().getRaw().toArray(String[]::new);
|
||||
context = PholioBootstrap.builder(Profiles.GUI).run(args);
|
||||
context = PholioBootstrap.builder(LaunchMode.UI).run(args);
|
||||
log.info("Spring context ready");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
package io.pholio.ui.common;
|
||||
|
||||
import io.pholio.Profiles;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A singleton Spring bean that touches JavaFX.
|
||||
*
|
||||
* <p>The {@link Profile} restriction is the mechanism that keeps the headless CLI free of JavaFX: these
|
||||
* beans are never eligible outside the {@code gui} profile, so their classes are never loaded. Component
|
||||
* scanning reads bytecode without classloading, so merely having them on the classpath costs nothing.
|
||||
* <p>No profile restriction is needed: what keeps these beans out of a headless run is that
|
||||
* {@code io.pholio.ui} is scanned only by {@code UiConfiguration}, which the bootstrap registers only in UI
|
||||
* mode. Component scanning is the single mechanism, so there is one place to look when asking why a bean does
|
||||
* or does not exist.
|
||||
*
|
||||
* <p>The annotation exists rather than plain {@code @Component} to mark the boundary explicitly and to give
|
||||
* the pairing with {@link UiView} somewhere to live.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Component
|
||||
@Profile(Profiles.GUI)
|
||||
public @interface UiComponent {
|
||||
|
||||
@AliasFor(annotation = Component.class)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package io.pholio.ui.common;
|
||||
|
||||
import io.pholio.Profiles;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -16,16 +14,17 @@ import org.springframework.stereotype.Component;
|
||||
* A navigable view: prototype-scoped, so every navigation gets a fresh instance whose predecessor was
|
||||
* disposed.
|
||||
*
|
||||
* <p>Views are expected to implement {@link Disposable} and release their bindings there — a
|
||||
* prototype-scoped bean receives no destruction callback from Spring, so the {@code ViewSwitcher} is
|
||||
* what calls {@code dispose()}.
|
||||
* <p>Views are expected to implement {@link Disposable} and release their bindings there — a prototype-scoped
|
||||
* bean receives no destruction callback from Spring, so the {@code ViewSwitcher} is what calls
|
||||
* {@code dispose()}.
|
||||
*
|
||||
* <p>Like {@link UiComponent}, eligibility is controlled by the UI component scan rather than by a profile.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Component
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Profile(Profiles.GUI)
|
||||
public @interface UiView {
|
||||
|
||||
@AliasFor(annotation = Component.class)
|
||||
|
||||
@@ -34,18 +34,31 @@ class ArchitectureRulesTest {
|
||||
.because("the CLI must run headless on a server with no graphics stack");
|
||||
|
||||
/**
|
||||
* The launcher decides between CLI and GUI. If it referenced JavaFX itself, resolving the class would
|
||||
* load the toolkit before the decision was even made.
|
||||
* The launcher decides between UI and headless. If any of these classes referenced JavaFX itself, resolving
|
||||
* it would load the toolkit before the decision had even been made — including on the headless path.
|
||||
*/
|
||||
@ArchTest
|
||||
static final ArchRule launcherIsFreeOfJavaFx = noClasses()
|
||||
.that()
|
||||
.haveNameMatching(".*\\.(PholioApplication|LaunchMode|PholioBootstrap|Profiles)")
|
||||
.resideInAPackage("io.pholio")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("javafx..")
|
||||
.because("the entry point must not load the JavaFX runtime before choosing the launch mode");
|
||||
|
||||
/**
|
||||
* {@code UiConfiguration} is named from the bootstrap on the UI branch, so it must stay free of JavaFX
|
||||
* itself: it contributes the presentation layer by component scan, and nothing more.
|
||||
*/
|
||||
@ArchTest
|
||||
static final ArchRule uiConfigurationIsFreeOfJavaFx = noClasses()
|
||||
.that()
|
||||
.haveNameMatching(".*\\.UiConfiguration")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("javafx..")
|
||||
.because("the bootstrap names this class on both code paths; only its component scan is JavaFX-bound");
|
||||
|
||||
/**
|
||||
* The domain layer stays plain Java: no UI toolkit, no framework, no persistence.
|
||||
*
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.pholio;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Launch-mode dispatch.
|
||||
*
|
||||
* <p>Getting this wrong is user-visible in both directions: a false CLI reading means double-clicking the
|
||||
* application prints usage instead of opening a window, and a false GUI reading means a headless server tries
|
||||
* to start a graphics stack.
|
||||
*/
|
||||
class LaunchModeTest {
|
||||
|
||||
@Test
|
||||
void noArgumentsOpensTheDesktopShell() {
|
||||
assertThat(LaunchMode.of(new String[0])).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(null)).isEqualTo(LaunchMode.GUI);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aSubcommandRunsHeadless() {
|
||||
assertThat(LaunchMode.of(new String[] {"scan"})).isEqualTo(LaunchMode.CLI);
|
||||
assertThat(LaunchMode.of(new String[] {"scan", "/photos"})).isEqualTo(LaunchMode.CLI);
|
||||
assertThat(LaunchMode.of(new String[] {"--version"})).isEqualTo(LaunchMode.CLI);
|
||||
assertThat(LaunchMode.of(new String[] {"--help"})).isEqualTo(LaunchMode.CLI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven, IDE run configurations and packaged launchers all inject framework switches. Treating those as
|
||||
* application arguments would silently break the desktop launch.
|
||||
*/
|
||||
@Test
|
||||
void frameworkArgumentsAloneStillOpenTheShell() {
|
||||
assertThat(LaunchMode.of(new String[] {"--spring.profiles.active=dev"})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {"--logging.level.io.pholio=TRACE"})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {"--pholio.pools.thumbnail=4"})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {"-Dfoo=bar"})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {"--debug"})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {""})).isEqualTo(LaunchMode.GUI);
|
||||
}
|
||||
|
||||
@Test
|
||||
void frameworkArgumentsMixedWithACommandStillRunHeadless() {
|
||||
assertThat(LaunchMode.of(new String[] {"--spring.profiles.active=dev", "scan"}))
|
||||
.isEqualTo(LaunchMode.CLI);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forceGuiFlagWinsOverAnyCommand() {
|
||||
assertThat(LaunchMode.of(new String[] {"scan", LaunchMode.FORCE_GUI_FLAG})).isEqualTo(LaunchMode.GUI);
|
||||
assertThat(LaunchMode.of(new String[] {LaunchMode.FORCE_GUI_FLAG})).isEqualTo(LaunchMode.GUI);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user