diff --git a/CLAUDE-old.md b/CLAUDE-old.md new file mode 100644 index 0000000..7730345 --- /dev/null +++ b/CLAUDE-old.md @@ -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` et `Service` 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`) 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"). diff --git a/CLAUDE.md b/CLAUDE.md index 7730345..eda6a17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` et `Service` 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`) 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: `([optional scope]): ` +- 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` diff --git a/pom.xml b/pom.xml index 99fd8f6..7ecc6a1 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,7 @@ ${archunit.version} test + diff --git a/src/main/java/io/pholio/LaunchMode.java b/src/main/java/io/pholio/LaunchMode.java index 51bf7dc..0f5ee73 100644 --- a/src/main/java/io/pholio/LaunchMode.java +++ b/src/main/java/io/pholio/LaunchMode.java @@ -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. * - *

The rule is intentionally simple and predictable: any 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. + *

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; } } diff --git a/src/main/java/io/pholio/PholioApplication.java b/src/main/java/io/pholio/PholioApplication.java index 372d555..e40a5ce 100644 --- a/src/main/java/io/pholio/PholioApplication.java +++ b/src/main/java/io/pholio/PholioApplication.java @@ -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. * - *

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. + *

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. + * + *

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)); } } diff --git a/src/main/java/io/pholio/PholioBootstrap.java b/src/main/java/io/pholio/PholioBootstrap.java index e984ba3..c2bbe39 100644 --- a/src/main/java/io/pholio/PholioBootstrap.java +++ b/src/main/java/io/pholio/PholioBootstrap.java @@ -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}. * - *

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 default property — lowest - * precedence, which lets {@code src/test/resources/application.yaml} and real command-line arguments - * override it. + *

The mode decides three things together, which is why they live in one place: + *

    + *
  • Sources. UI mode adds {@code UiConfiguration}, whose component scan brings in the + * presentation layer. Headless mode does not, so those classes are never scanned. + *
  • AWT headless. Switched on for headless runs so no attempt is ever made to reach a + * display, and off for the desktop shell. + *
  • Profile. Selects mode-specific configuration — {@code application-cli.yaml} turns + * console logging down so a command's stdout carries only its own output. + *
+ * + *

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 default 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 defaultProperties() { diff --git a/src/main/java/io/pholio/Profiles.java b/src/main/java/io/pholio/Profiles.java index 02e204f..58972dd 100644 --- a/src/main/java/io/pholio/Profiles.java +++ b/src/main/java/io/pholio/Profiles.java @@ -3,15 +3,17 @@ package io.pholio; /** * Spring profile names. * - *

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. + *

Scope note: profiles select configuration 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() { diff --git a/src/main/java/io/pholio/cli/CliBootstrap.java b/src/main/java/io/pholio/cli/CliBootstrap.java deleted file mode 100644 index 1a9b48c..0000000 --- a/src/main/java/io/pholio/cli/CliBootstrap.java +++ /dev/null @@ -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. - * - *

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. - * - *

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); - } - } -} diff --git a/src/main/java/io/pholio/cli/PholioCommand.java b/src/main/java/io/pholio/cli/PholioCommand.java index 950e997..f05a33e 100644 --- a/src/main/java/io/pholio/cli/PholioCommand.java +++ b/src/main/java/io/pholio/cli/PholioCommand.java @@ -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. * - *

Running it with no subcommand prints usage rather than doing something surprising. + *

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 { + + /** + * 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; } } diff --git a/src/main/java/io/pholio/ui/PholioFxApplication.java b/src/main/java/io/pholio/ui/PholioFxApplication.java index 4074de0..d6a5d22 100644 --- a/src/main/java/io/pholio/ui/PholioFxApplication.java +++ b/src/main/java/io/pholio/ui/PholioFxApplication.java @@ -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"); } diff --git a/src/main/java/io/pholio/ui/common/UiComponent.java b/src/main/java/io/pholio/ui/common/UiComponent.java index 68fddc7..fbbd2c1 100644 --- a/src/main/java/io/pholio/ui/common/UiComponent.java +++ b/src/main/java/io/pholio/ui/common/UiComponent.java @@ -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. * - *

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. + *

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. + * + *

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) diff --git a/src/main/java/io/pholio/ui/common/UiView.java b/src/main/java/io/pholio/ui/common/UiView.java index f841f55..abc2bf6 100644 --- a/src/main/java/io/pholio/ui/common/UiView.java +++ b/src/main/java/io/pholio/ui/common/UiView.java @@ -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. * - *

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()}. + *

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()}. + * + *

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) diff --git a/src/test/java/io/pholio/ArchitectureRulesTest.java b/src/test/java/io/pholio/ArchitectureRulesTest.java index 10e4766..f440ffd 100644 --- a/src/test/java/io/pholio/ArchitectureRulesTest.java +++ b/src/test/java/io/pholio/ArchitectureRulesTest.java @@ -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. * diff --git a/src/test/java/io/pholio/LaunchModeTest.java b/src/test/java/io/pholio/LaunchModeTest.java deleted file mode 100644 index 0694371..0000000 --- a/src/test/java/io/pholio/LaunchModeTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.pholio; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -/** - * Launch-mode dispatch. - * - *

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); - } -}