From 71449bee1b1e1cf40c36a176845d7e267976e59e Mon Sep 17 00:00:00 2001 From: "IETM_FIXE\\ietm6" Date: Tue, 21 Apr 2026 16:56:27 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20de=20l'UI=20:=20meilleur=20?= =?UTF-8?q?affichage=20des=20images=20que=20ce=20soit=20dans=20la=20partie?= =?UTF-8?q?=20lore=20ou=20la=20partie=20campagne=20(partie=20campagne=20:?= =?UTF-8?q?=20visualisation=20scrapbooking).=20Possibilit=C3=A9=20de=20r?= =?UTF-8?q?=C3=A9ordonner=20les=20champs=20dans=20les=20templates...=20Pas?= =?UTF-8?q?sage=20v0.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +- .../loremind/domain/campaigncontext/Arc.java | 8 +- .../domain/campaigncontext/Chapter.java | 8 +- .../domain/campaigncontext/Scene.java | 10 +- .../domain/lorecontext/ImageLayout.java | 18 + .../domain/lorecontext/TemplateField.java | 25 +- .../TemplateFieldListJsonConverter.java | 15 +- .../persistence/entity/ArcJpaEntity.java | 6 + .../persistence/entity/ChapterJpaEntity.java | 5 + .../persistence/entity/SceneJpaEntity.java | 5 + .../postgres/PostgresArcRepository.java | 6 + .../postgres/PostgresChapterRepository.java | 6 + .../postgres/PostgresSceneRepository.java | 6 + .../web/controller/ImageController.java | 4 + .../web/dto/campaigncontext/ArcDTO.java | 5 +- .../web/dto/campaigncontext/ChapterDTO.java | 5 +- .../web/dto/campaigncontext/SceneDTO.java | 5 +- .../web/dto/lorecontext/TemplateFieldDTO.java | 9 + .../infrastructure/web/mapper/ArcMapper.java | 2 + .../web/mapper/ChapterMapper.java | 2 + .../web/mapper/SceneMapper.java | 6 + .../web/mapper/TemplateFieldMapper.java | 22 +- docker-compose.yml | 6 +- .../arc-edit/arc-edit.component.html | 17 +- .../campaigns/arc-edit/arc-edit.component.ts | 6 +- .../arc-view/arc-view.component.html | 10 +- .../chapter-edit/chapter-edit.component.html | 17 +- .../chapter-edit/chapter-edit.component.ts | 5 +- .../chapter-view/chapter-view.component.html | 10 +- .../scene-edit/scene-edit.component.html | 17 +- .../scene-edit/scene-edit.component.ts | 3 + .../scene-view/scene-view.component.html | 10 +- .../lore/page-edit/page-edit.component.html | 1 + .../lore/page-view/page-view.component.html | 5 +- .../template-create.component.html | 27 +- .../template-create.component.scss | 38 +- .../template-create.component.ts | 34 +- .../template-edit.component.html | 27 +- .../template-edit.component.scss | 38 +- .../template-edit/template-edit.component.ts | 44 +- web/src/app/services/campaign.model.ts | 10 +- web/src/app/services/template.model.ts | 12 + .../image-gallery.component.html | 200 ++++++++-- .../image-gallery.component.scss | 375 +++++++++++++++++- .../image-gallery/image-gallery.component.ts | 38 +- 45 files changed, 1045 insertions(+), 90 deletions(-) create mode 100644 core/src/main/java/com/loremind/domain/lorecontext/ImageLayout.java diff --git a/README.md b/README.md index 1463469..3d60b72 100644 --- a/README.md +++ b/README.md @@ -67,4 +67,9 @@ docker compose up -d --build ## License -[À définir] +LoreMind est distribué sous licence **[GNU AGPL v3](LICENSE)**. + +En pratique : +- Tu peux l'utiliser gratuitement, l'héberger où tu veux, le modifier, le redistribuer. +- Si tu modifies le code et que tu exposes l'application modifiée sur un réseau (même en SaaS privé), tu dois rendre tes modifications publiques sous la même licence. +- Les univers (Lore) et campagnes que tu crées avec LoreMind **t'appartiennent entièrement** — la licence ne couvre que le code de l'application. diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java b/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java index 0a91af3..3c49055 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java @@ -38,12 +38,18 @@ public class Arc { private List relatedPageIds = new ArrayList<>(); /** - * IDs des images (Shared Kernel) servant d'illustrations a cet arc. + * IDs des images (Shared Kernel) servant d'illustrations a cet arc (ambiance). * Galerie ordonnee : la 1ere image est l'illustration principale. */ @Builder.Default private List illustrationImageIds = new ArrayList<>(); + /** + * IDs des images utilisees comme cartes / plans (outil de table). + */ + @Builder.Default + private List mapImageIds = new ArrayList<>(); + private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java b/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java index ad5f712..2fe92b6 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java @@ -34,11 +34,17 @@ public class Chapter { private List relatedPageIds = new ArrayList<>(); /** - * IDs des images (Shared Kernel) illustrant ce chapitre. + * IDs des images (Shared Kernel) illustrant ce chapitre (ambiance). */ @Builder.Default private List illustrationImageIds = new ArrayList<>(); + /** + * IDs des images utilisees comme cartes / plans pour ce chapitre (outil de table). + */ + @Builder.Default + private List mapImageIds = new ArrayList<>(); + private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java b/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java index df38659..4f65310 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java @@ -48,11 +48,19 @@ public class Scene { /** * IDs des images (Shared Kernel) illustrant cette scene. - * Utile pour carte du lieu, portraits des PNJ principaux, ambiance. + * Vocation "ambiance" : portraits, decors, moodboard. Rendu facon editorial. */ @Builder.Default private List illustrationImageIds = new ArrayList<>(); + /** + * IDs des images utilisees comme cartes / plans. + * Vocation "outil de table" : plan de donjon, carte du lieu, schema tactique. + * Rendu different des illustrations : vignettes plus grandes, ratio natif preserve. + */ + @Builder.Default + private List mapImageIds = new ArrayList<>(); + /** * Sorties narratives possibles depuis cette scène (graphe intra-chapitre). * Chaque branche décrit un choix des joueurs et la scène de destination. diff --git a/core/src/main/java/com/loremind/domain/lorecontext/ImageLayout.java b/core/src/main/java/com/loremind/domain/lorecontext/ImageLayout.java new file mode 100644 index 0000000..768954b --- /dev/null +++ b/core/src/main/java/com/loremind/domain/lorecontext/ImageLayout.java @@ -0,0 +1,18 @@ +package com.loremind.domain.lorecontext; + +/** + * Variante de rendu pour un champ de type IMAGE. + *

+ * - GALLERY : grille de vignettes (defaut, comportement historique) + * - HERO : premiere image en banniere pleine largeur, suivantes en petit + * - MASONRY : mosaique hauteurs variables facon Pinterest + * - CAROUSEL : defilement horizontal + *

+ * Uniquement significatif quand {@link FieldType} = IMAGE. Ignore pour TEXT. + */ +public enum ImageLayout { + GALLERY, + HERO, + MASONRY, + CAROUSEL +} diff --git a/core/src/main/java/com/loremind/domain/lorecontext/TemplateField.java b/core/src/main/java/com/loremind/domain/lorecontext/TemplateField.java index 17525d9..b03a494 100644 --- a/core/src/main/java/com/loremind/domain/lorecontext/TemplateField.java +++ b/core/src/main/java/com/loremind/domain/lorecontext/TemplateField.java @@ -12,10 +12,9 @@ import lombok.NoArgsConstructor; * Le type pilote le rendu cote front (textarea vs galerie d'images) ET * la logique metier (seuls les champs TEXT sont envoyes a l'IA pour generation). *

- * Evolution de `List fields` vers `List fields` : - * refactor propre (DDD Value Object polymorphism) permettant d'ajouter - * facilement d'autres types de champs (DATE, NUMBER, RICH_TEXT...) sans - * casser le contrat. + * Pour les champs IMAGE, {@link #layout} precise la variante de rendu + * (gallery/hero/masonry/carousel). Nullable : l'absence equivaut a GALLERY. + * Ignore pour les champs TEXT. */ @Data @Builder @@ -26,14 +25,26 @@ public class TemplateField { private String name; /** Type du champ, pilote le rendu et la generation IA. */ private FieldType type; + /** Variante de rendu pour les champs IMAGE. Null = GALLERY. */ + private ImageLayout layout; + + /** Constructeur de retrocompat : type seul, layout=null. */ + public TemplateField(String name, FieldType type) { + this(name, type, null); + } /** Raccourci : construit un champ de type TEXT (cas le plus courant). */ public static TemplateField text(String name) { - return new TemplateField(name, FieldType.TEXT); + return new TemplateField(name, FieldType.TEXT, null); } - /** Raccourci : construit un champ de type IMAGE. */ + /** Raccourci : construit un champ de type IMAGE avec layout GALLERY. */ public static TemplateField image(String name) { - return new TemplateField(name, FieldType.IMAGE); + return new TemplateField(name, FieldType.IMAGE, ImageLayout.GALLERY); + } + + /** Raccourci : construit un champ IMAGE avec un layout specifique. */ + public static TemplateField image(String name, ImageLayout layout) { + return new TemplateField(name, FieldType.IMAGE, layout); } } diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/converter/TemplateFieldListJsonConverter.java b/core/src/main/java/com/loremind/infrastructure/persistence/converter/TemplateFieldListJsonConverter.java index 17def3c..99c7833 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/converter/TemplateFieldListJsonConverter.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/converter/TemplateFieldListJsonConverter.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.loremind.domain.lorecontext.FieldType; +import com.loremind.domain.lorecontext.ImageLayout; import com.loremind.domain.lorecontext.TemplateField; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; @@ -72,8 +73,20 @@ public class TemplateFieldListJsonConverter // Type inconnu (ajoute par une version future) : fallback TEXT. type = FieldType.TEXT; } + ImageLayout layout = null; + if (type == FieldType.IMAGE) { + String layoutStr = item.path("layout").asText(null); + if (layoutStr != null && !layoutStr.isBlank()) { + try { + layout = ImageLayout.valueOf(layoutStr); + } catch (IllegalArgumentException ex) { + // Layout inconnu : on laisse null → rendu GALLERY par defaut cote UI. + layout = null; + } + } + } if (name != null && !name.isBlank()) { - result.add(new TemplateField(name, type)); + result.add(new TemplateField(name, type, layout)); } } // Autres types de noeuds (nombre, booleen...) : ignores silencieusement. diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java index 4300775..832c90c 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java @@ -68,6 +68,12 @@ public class ArcJpaEntity { @Builder.Default private List illustrationImageIds = new ArrayList<>(); + /** IDs des images "cartes / plans". */ + @Column(name = "map_image_ids", columnDefinition = "TEXT") + @Convert(converter = StringListJsonConverter.class) + @Builder.Default + private List mapImageIds = new ArrayList<>(); + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java index 2216f1b..83dfca5 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java @@ -57,6 +57,11 @@ public class ChapterJpaEntity { @Builder.Default private List illustrationImageIds = new ArrayList<>(); + @Column(name = "map_image_ids", columnDefinition = "TEXT") + @Convert(converter = StringListJsonConverter.class) + @Builder.Default + private List mapImageIds = new ArrayList<>(); + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java index af0c146..bf1220f 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java @@ -80,6 +80,11 @@ public class SceneJpaEntity { @Builder.Default private List illustrationImageIds = new ArrayList<>(); + @Column(name = "map_image_ids", columnDefinition = "TEXT") + @Convert(converter = StringListJsonConverter.class) + @Builder.Default + private List mapImageIds = new ArrayList<>(); + // Graphe narratif intra-chapitre : sorties possibles vers d'autres scènes. // Persisté en TEXT JSON via converter (pattern homogène avec les autres listes). @Column(name = "branches", columnDefinition = "TEXT") diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java index cfefd73..1395d81 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java @@ -83,6 +83,9 @@ public class PostgresArcRepository implements ArcRepository { .illustrationImageIds(jpaEntity.getIllustrationImageIds() != null ? new ArrayList<>(jpaEntity.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(jpaEntity.getMapImageIds() != null + ? new ArrayList<>(jpaEntity.getMapImageIds()) + : new ArrayList<>()) .createdAt(jpaEntity.getCreatedAt()) .updatedAt(jpaEntity.getUpdatedAt()) .build(); @@ -107,6 +110,9 @@ public class PostgresArcRepository implements ArcRepository { .illustrationImageIds(arc.getIllustrationImageIds() != null ? new ArrayList<>(arc.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(arc.getMapImageIds() != null + ? new ArrayList<>(arc.getMapImageIds()) + : new ArrayList<>()) .createdAt(arc.getCreatedAt()) .updatedAt(arc.getUpdatedAt()) .build(); diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java index e875f56..8fc7491 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java @@ -80,6 +80,9 @@ public class PostgresChapterRepository implements ChapterRepository { .illustrationImageIds(jpaEntity.getIllustrationImageIds() != null ? new ArrayList<>(jpaEntity.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(jpaEntity.getMapImageIds() != null + ? new ArrayList<>(jpaEntity.getMapImageIds()) + : new ArrayList<>()) .createdAt(jpaEntity.getCreatedAt()) .updatedAt(jpaEntity.getUpdatedAt()) .build(); @@ -102,6 +105,9 @@ public class PostgresChapterRepository implements ChapterRepository { .illustrationImageIds(chapter.getIllustrationImageIds() != null ? new ArrayList<>(chapter.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(chapter.getMapImageIds() != null + ? new ArrayList<>(chapter.getMapImageIds()) + : new ArrayList<>()) .createdAt(chapter.getCreatedAt()) .updatedAt(chapter.getUpdatedAt()) .build(); diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java index 875a9c2..4dd162e 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java @@ -85,6 +85,9 @@ public class PostgresSceneRepository implements SceneRepository { .illustrationImageIds(jpaEntity.getIllustrationImageIds() != null ? new ArrayList<>(jpaEntity.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(jpaEntity.getMapImageIds() != null + ? new ArrayList<>(jpaEntity.getMapImageIds()) + : new ArrayList<>()) .branches(jpaEntity.getBranches() != null ? new ArrayList<>(jpaEntity.getBranches()) : new ArrayList<>()) @@ -115,6 +118,9 @@ public class PostgresSceneRepository implements SceneRepository { .illustrationImageIds(scene.getIllustrationImageIds() != null ? new ArrayList<>(scene.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(scene.getMapImageIds() != null + ? new ArrayList<>(scene.getMapImageIds()) + : new ArrayList<>()) .branches(scene.getBranches() != null ? new ArrayList<>(scene.getBranches()) : new ArrayList<>()) diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/ImageController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/ImageController.java index f5137dd..b986b88 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/ImageController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/ImageController.java @@ -79,6 +79,10 @@ public class ImageController { .contentType(MediaType.parseMediaType(img.getContentType())) .contentLength(img.getSizeBytes()) .header(HttpHeaders.CACHE_CONTROL, "public, max-age=31536000, immutable") + // Autorise explicitement l'utilisation cross-origin du binaire dans une . + // Sans ce header, Firefox 109+ applique ORB (Opaque Response Blocking) et + // bloque l'image quand le front (localhost:4200) la charge depuis l'API (localhost:8080). + .header("Cross-Origin-Resource-Policy", "cross-origin") .body(new InputStreamResource(stream)); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java index bd11eaa..76fe8ee 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java @@ -27,6 +27,9 @@ public class ArcDTO { /** IDs des pages du Lore liées à cet arc (weak cross-context references). */ private List relatedPageIds = new ArrayList<>(); - /** IDs des images (Shared Kernel) illustrant cet arc. */ + /** IDs des images (Shared Kernel) illustrant cet arc (ambiance). */ private List illustrationImageIds = new ArrayList<>(); + + /** IDs des images utilisees comme cartes / plans. */ + private List mapImageIds = new ArrayList<>(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java index 9586cf0..4c11713 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java @@ -25,6 +25,9 @@ public class ChapterDTO { /** IDs des pages du Lore liées (weak cross-context references). */ private List relatedPageIds = new ArrayList<>(); - /** IDs des images (Shared Kernel) illustrant ce chapitre. */ + /** IDs des images (Shared Kernel) illustrant ce chapitre (ambiance). */ private List illustrationImageIds = new ArrayList<>(); + + /** IDs des images utilisees comme cartes / plans. */ + private List mapImageIds = new ArrayList<>(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java index c3cec12..ead1693 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java @@ -30,9 +30,12 @@ public class SceneDTO { /** IDs des pages du Lore liées (weak cross-context references). */ private List relatedPageIds = new ArrayList<>(); - /** IDs des images (Shared Kernel) illustrant cette scene. */ + /** IDs des images (Shared Kernel) illustrant cette scene (ambiance). */ private List illustrationImageIds = new ArrayList<>(); + /** IDs des images utilisees comme cartes / plans (outil de table). */ + private List mapImageIds = new ArrayList<>(); + /** Branches narratives : sorties possibles vers d'autres scènes du même chapitre. */ private List branches = new ArrayList<>(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/lorecontext/TemplateFieldDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/lorecontext/TemplateFieldDTO.java index a4c452b..d8f7083 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/lorecontext/TemplateFieldDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/lorecontext/TemplateFieldDTO.java @@ -9,6 +9,8 @@ import lombok.NoArgsConstructor; *

* Miroir wire-friendly de {@link com.loremind.domain.lorecontext.TemplateField}. * Le type est serialise en string (TEXT/IMAGE) pour interop facile avec Angular. + * Le layout (null pour TEXT, ou GALLERY/HERO/MASONRY/CAROUSEL pour IMAGE) pilote + * le rendu visuel des champs image cote front. */ @Data @NoArgsConstructor @@ -17,4 +19,11 @@ public class TemplateFieldDTO { private String name; /** "TEXT" ou "IMAGE" (string pour serialisation JSON transparente). */ private String type; + /** "GALLERY" | "HERO" | "MASONRY" | "CAROUSEL", null si type=TEXT. */ + private String layout; + + /** Retrocompat : constructeur sans layout. */ + public TemplateFieldDTO(String name, String type) { + this(name, type, null); + } } diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java index f673f2f..16e0617 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java @@ -31,6 +31,7 @@ public class ArcMapper { dto.setResolution(arc.getResolution()); dto.setRelatedPageIds(copyList(arc.getRelatedPageIds())); dto.setIllustrationImageIds(copyList(arc.getIllustrationImageIds())); + dto.setMapImageIds(copyList(arc.getMapImageIds())); return dto; } @@ -52,6 +53,7 @@ public class ArcMapper { .resolution(dto.getResolution()) .relatedPageIds(copyList(dto.getRelatedPageIds())) .illustrationImageIds(copyList(dto.getIllustrationImageIds())) + .mapImageIds(copyList(dto.getMapImageIds())) .build(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java index 1b58134..767c29e 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java @@ -29,6 +29,7 @@ public class ChapterMapper { dto.setNarrativeStakes(chapter.getNarrativeStakes()); dto.setRelatedPageIds(copyList(chapter.getRelatedPageIds())); dto.setIllustrationImageIds(copyList(chapter.getIllustrationImageIds())); + dto.setMapImageIds(copyList(chapter.getMapImageIds())); return dto; } @@ -48,6 +49,7 @@ public class ChapterMapper { .narrativeStakes(dto.getNarrativeStakes()) .relatedPageIds(copyList(dto.getRelatedPageIds())) .illustrationImageIds(copyList(dto.getIllustrationImageIds())) + .mapImageIds(copyList(dto.getMapImageIds())) .build(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java index 6bd0501..d66b4fe 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java @@ -41,6 +41,9 @@ public class SceneMapper { dto.setIllustrationImageIds(scene.getIllustrationImageIds() != null ? new ArrayList<>(scene.getIllustrationImageIds()) : new ArrayList<>()); + dto.setMapImageIds(scene.getMapImageIds() != null + ? new ArrayList<>(scene.getMapImageIds()) + : new ArrayList<>()); dto.setBranches(toBranchDTOs(scene.getBranches())); return dto; } @@ -70,6 +73,9 @@ public class SceneMapper { .illustrationImageIds(dto.getIllustrationImageIds() != null ? new ArrayList<>(dto.getIllustrationImageIds()) : new ArrayList<>()) + .mapImageIds(dto.getMapImageIds() != null + ? new ArrayList<>(dto.getMapImageIds()) + : new ArrayList<>()) .branches(toBranchDomain(dto.getBranches())) .build(); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/TemplateFieldMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/TemplateFieldMapper.java index ab0f6b0..3f7017c 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/TemplateFieldMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/TemplateFieldMapper.java @@ -1,6 +1,7 @@ package com.loremind.infrastructure.web.mapper; import com.loremind.domain.lorecontext.FieldType; +import com.loremind.domain.lorecontext.ImageLayout; import com.loremind.domain.lorecontext.TemplateField; import com.loremind.infrastructure.web.dto.lorecontext.TemplateFieldDTO; import org.springframework.stereotype.Component; @@ -11,6 +12,8 @@ import org.springframework.stereotype.Component; *

* Tolerance : un type inconnu recu du client est interprete comme TEXT * (plus safe que de rejeter la requete et d'interrompre la sauvegarde). + * Un layout inconnu ou absent sur un champ IMAGE est interprete comme GALLERY. + * Le layout est force a null pour les champs TEXT. */ @Component public class TemplateFieldMapper { @@ -18,7 +21,12 @@ public class TemplateFieldMapper { public TemplateFieldDTO toDTO(TemplateField field) { if (field == null) return null; String typeStr = field.getType() != null ? field.getType().name() : FieldType.TEXT.name(); - return new TemplateFieldDTO(field.getName(), typeStr); + String layoutStr = null; + if (field.getType() == FieldType.IMAGE) { + ImageLayout layout = field.getLayout() != null ? field.getLayout() : ImageLayout.GALLERY; + layoutStr = layout.name(); + } + return new TemplateFieldDTO(field.getName(), typeStr, layoutStr); } public TemplateField toDomain(TemplateFieldDTO dto) { @@ -29,6 +37,16 @@ public class TemplateFieldMapper { } catch (IllegalArgumentException ex) { type = FieldType.TEXT; } - return new TemplateField(dto.getName(), type); + ImageLayout layout = null; + if (type == FieldType.IMAGE) { + try { + layout = dto.getLayout() != null + ? ImageLayout.valueOf(dto.getLayout()) + : ImageLayout.GALLERY; + } catch (IllegalArgumentException ex) { + layout = ImageLayout.GALLERY; + } + } + return new TemplateField(dto.getName(), type, layout); } } diff --git a/docker-compose.yml b/docker-compose.yml index ae138cd..a2b7a16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,7 +55,7 @@ services: " core: - image: ${REGISTRY:-gitea.example.com}/ietm64/core:${TAG:-latest} + image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/core:${TAG:-latest} container_name: loremind-core depends_on: postgres: @@ -77,7 +77,7 @@ services: restart: unless-stopped brain: - image: ${REGISTRY:-gitea.example.com}/ietm64/brain:${TAG:-latest} + image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/brain:${TAG:-latest} container_name: loremind-brain environment: LLM_PROVIDER: ${LLM_PROVIDER:-ollama} @@ -95,7 +95,7 @@ services: restart: unless-stopped web: - image: ${REGISTRY:-gitea.example.com}/ietm64/web:${TAG:-latest} + image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/web:${TAG:-latest} container_name: loremind-web depends_on: - core diff --git a/web/src/app/campaigns/arc-edit/arc-edit.component.html b/web/src/app/campaigns/arc-edit/arc-edit.component.html index 53be4e7..9428928 100644 --- a/web/src/app/campaigns/arc-edit/arc-edit.component.html +++ b/web/src/app/campaigns/arc-edit/arc-edit.component.html @@ -18,15 +18,28 @@

- +
- Glisse-depose ou clique sur "+ Ajouter" pour uploader. JPEG, PNG, WebP ou GIF, 10 Mo max. + Ambiances, portraits, visuels evocateurs de l'arc. JPEG, PNG, WebP ou GIF, 10 Mo max. +
+ + +
+ + + + Cartes regionales et plans utiles aux joueurs pour situer l'action.
diff --git a/web/src/app/campaigns/arc-edit/arc-edit.component.ts b/web/src/app/campaigns/arc-edit/arc-edit.component.ts index c6f05c9..8123f4e 100644 --- a/web/src/app/campaigns/arc-edit/arc-edit.component.ts +++ b/web/src/app/campaigns/arc-edit/arc-edit.component.ts @@ -60,6 +60,8 @@ export class ArcEditComponent implements OnInit, OnDestroy { /** IDs des images illustrant cet arc (bind sur app-image-gallery editable). */ illustrationImageIds: string[] = []; + /** IDs des images utilisees comme cartes / plans (outil de table). */ + mapImageIds: string[] = []; constructor( private fb: FormBuilder, @@ -119,6 +121,7 @@ export class ArcEditComponent implements OnInit, OnDestroy { this.availablePages = pages; this.relatedPageIds = [...(arc.relatedPageIds ?? [])]; this.illustrationImageIds = [...(arc.illustrationImageIds ?? [])]; + this.mapImageIds = [...(arc.mapImageIds ?? [])]; this.pageTitleService.set(arc.name); this.form.patchValue({ name: arc.name, @@ -161,7 +164,8 @@ export class ArcEditComponent implements OnInit, OnDestroy { rewards: this.form.value.rewards, resolution: this.form.value.resolution, relatedPageIds: this.relatedPageIds, - illustrationImageIds: this.illustrationImageIds + illustrationImageIds: this.illustrationImageIds, + mapImageIds: this.mapImageIds }).subscribe({ next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]), error: () => console.error('Erreur lors de la sauvegarde') diff --git a/web/src/app/campaigns/arc-view/arc-view.component.html b/web/src/app/campaigns/arc-view/arc-view.component.html index 57eabd4..e6d025c 100644 --- a/web/src/app/campaigns/arc-view/arc-view.component.html +++ b/web/src/app/campaigns/arc-view/arc-view.component.html @@ -13,9 +13,15 @@
- +
- + +
+ + +
+

🗺️ Cartes & plans

+
diff --git a/web/src/app/campaigns/chapter-edit/chapter-edit.component.html b/web/src/app/campaigns/chapter-edit/chapter-edit.component.html index 05f1125..d5add2b 100644 --- a/web/src/app/campaigns/chapter-edit/chapter-edit.component.html +++ b/web/src/app/campaigns/chapter-edit/chapter-edit.component.html @@ -18,15 +18,28 @@ - +
- Ajoute des cartes, portraits ou ambiances pour illustrer ce chapitre. + Portraits, ambiances, scenes marquantes du chapitre. +
+ + +
+ + + + Cartes regionales, plans de donjon, schemas utiles a la table.
diff --git a/web/src/app/campaigns/chapter-edit/chapter-edit.component.ts b/web/src/app/campaigns/chapter-edit/chapter-edit.component.ts index 06b41e9..fc9145d 100644 --- a/web/src/app/campaigns/chapter-edit/chapter-edit.component.ts +++ b/web/src/app/campaigns/chapter-edit/chapter-edit.component.ts @@ -54,6 +54,7 @@ export class ChapterEditComponent implements OnInit, OnDestroy { loreId: string | null = null; relatedPageIds: string[] = []; illustrationImageIds: string[] = []; + mapImageIds: string[] = []; constructor( private fb: FormBuilder, @@ -111,6 +112,7 @@ export class ChapterEditComponent implements OnInit, OnDestroy { this.availablePages = pages; this.relatedPageIds = [...(chapter.relatedPageIds ?? [])]; this.illustrationImageIds = [...(chapter.illustrationImageIds ?? [])]; + this.mapImageIds = [...(chapter.mapImageIds ?? [])]; this.form.patchValue({ name: chapter.name, description: chapter.description ?? '', @@ -148,7 +150,8 @@ export class ChapterEditComponent implements OnInit, OnDestroy { playerObjectives: this.form.value.playerObjectives, narrativeStakes: this.form.value.narrativeStakes, relatedPageIds: this.relatedPageIds, - illustrationImageIds: this.illustrationImageIds + illustrationImageIds: this.illustrationImageIds, + mapImageIds: this.mapImageIds }).subscribe({ next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId]), error: () => console.error('Erreur lors de la sauvegarde') diff --git a/web/src/app/campaigns/chapter-view/chapter-view.component.html b/web/src/app/campaigns/chapter-view/chapter-view.component.html index 0c1929a..edbe576 100644 --- a/web/src/app/campaigns/chapter-view/chapter-view.component.html +++ b/web/src/app/campaigns/chapter-view/chapter-view.component.html @@ -18,9 +18,15 @@
- +
- + +
+ + +
+

🗺️ Cartes & plans

+
diff --git a/web/src/app/campaigns/scene-edit/scene-edit.component.html b/web/src/app/campaigns/scene-edit/scene-edit.component.html index 28ae0cf..2447dfe 100644 --- a/web/src/app/campaigns/scene-edit/scene-edit.component.html +++ b/web/src/app/campaigns/scene-edit/scene-edit.component.html @@ -18,15 +18,28 @@ - +
- Carte du lieu, portrait des PNJ presents, ambiance visuelle... + Portraits des PNJ, ambiance visuelle, scenes evocatrices... +
+ + +
+ + + + Plans du lieu, cartes tactiques, schemas utilisables a la table.
diff --git a/web/src/app/campaigns/scene-edit/scene-edit.component.ts b/web/src/app/campaigns/scene-edit/scene-edit.component.ts index 786c50a..f0e9d3a 100644 --- a/web/src/app/campaigns/scene-edit/scene-edit.component.ts +++ b/web/src/app/campaigns/scene-edit/scene-edit.component.ts @@ -53,6 +53,7 @@ export class SceneEditComponent implements OnInit, OnDestroy { loreId: string | null = null; relatedPageIds: string[] = []; illustrationImageIds: string[] = []; + mapImageIds: string[] = []; /** Scènes du chapitre courant (hors scène éditée) — alimente le dropdown des cibles. */ siblingScenes: Scene[] = []; @@ -129,6 +130,7 @@ export class SceneEditComponent implements OnInit, OnDestroy { this.availablePages = pages; this.relatedPageIds = [...(scene.relatedPageIds ?? [])]; this.illustrationImageIds = [...(scene.illustrationImageIds ?? [])]; + this.mapImageIds = [...(scene.mapImageIds ?? [])]; this.siblingScenes = chapterScenes.filter(s => s.id !== this.sceneId); this.branches = (scene.branches ?? []).map(b => ({ ...b })); this.form.patchValue({ @@ -179,6 +181,7 @@ export class SceneEditComponent implements OnInit, OnDestroy { enemies: this.form.value.enemies, relatedPageIds: this.relatedPageIds, illustrationImageIds: this.illustrationImageIds, + mapImageIds: this.mapImageIds, branches: this.branches }).subscribe({ next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId, 'scenes', this.sceneId]), diff --git a/web/src/app/campaigns/scene-view/scene-view.component.html b/web/src/app/campaigns/scene-view/scene-view.component.html index b319539..e69e74c 100644 --- a/web/src/app/campaigns/scene-view/scene-view.component.html +++ b/web/src/app/campaigns/scene-view/scene-view.component.html @@ -13,9 +13,15 @@
- +
- + +
+ + +
+

🗺️ Cartes & plans

+
diff --git a/web/src/app/lore/page-edit/page-edit.component.html b/web/src/app/lore/page-edit/page-edit.component.html index ace9149..ee34b49 100644 --- a/web/src/app/lore/page-edit/page-edit.component.html +++ b/web/src/app/lore/page-edit/page-edit.component.html @@ -65,6 +65,7 @@ diff --git a/web/src/app/lore/page-view/page-view.component.html b/web/src/app/lore/page-view/page-view.component.html index 40458b7..1d112e8 100644 --- a/web/src/app/lore/page-view/page-view.component.html +++ b/web/src/app/lore/page-view/page-view.component.html @@ -28,7 +28,10 @@

{{ field.name }}

- + +
diff --git a/web/src/app/lore/template-create/template-create.component.html b/web/src/app/lore/template-create/template-create.component.html index 8b28a1c..a8d62c8 100644 --- a/web/src/app/lore/template-create/template-create.component.html +++ b/web/src/app/lore/template-create/template-create.component.html @@ -37,7 +37,21 @@
    -
  • +
  • +
    + + +
    {{ f.name }} @@ -49,6 +63,17 @@ [title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'"> {{ f.type === 'TEXT' ? 'Texte' : 'Image' }} + diff --git a/web/src/app/lore/template-create/template-create.component.scss b/web/src/app/lore/template-create/template-create.component.scss index 4392674..d2f910a 100644 --- a/web/src/app/lore/template-create/template-create.component.scss +++ b/web/src/app/lore/template-create/template-create.component.scss @@ -124,7 +124,8 @@ &:hover { background: #363650; color: white; } } - .type-select { + .type-select, + .layout-select { background: #1a1a2e; border: 1px solid #2a2a3d; color: white; @@ -137,6 +138,12 @@ &:focus { outline: none; border-color: #6c63ff; } } + .layout-select { + height: 28px; + font-size: 0.72rem; + padding: 0 0.45rem; + } + input { flex: 1; background: #1a1a2e; @@ -153,6 +160,35 @@ } &.add-row { margin-top: 0.5rem; } + + .reorder-stack { + display: flex; + flex-direction: column; + gap: 2px; + + .btn-reorder { + width: 22px; + height: 16px; + background: transparent; + color: #6b7280; + border: 1px solid #2a2a3d; + border-radius: 3px; + padding: 0; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.12s, color 0.12s, border-color 0.12s; + + &:hover:not(:disabled) { + background: #2a2a3d; + color: white; + border-color: #6c63ff; + } + + &:disabled { opacity: 0.3; cursor: not-allowed; } + } + } } .btn-icon { diff --git a/web/src/app/lore/template-create/template-create.component.ts b/web/src/app/lore/template-create/template-create.component.ts index 11961ff..949d18c 100644 --- a/web/src/app/lore/template-create/template-create.component.ts +++ b/web/src/app/lore/template-create/template-create.component.ts @@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon } from 'lucide-angular'; +import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular'; import { LoreService } from '../../services/lore.service'; import { TemplateService } from '../../services/template.service'; import { PageService } from '../../services/page.service'; import { LayoutService } from '../../services/layout.service'; import { LoreNode } from '../../services/lore.model'; -import { FieldType, TemplateField } from '../../services/template.model'; +import { FieldType, ImageLayout, TemplateField } from '../../services/template.model'; import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper'; /** @@ -29,6 +29,8 @@ export class TemplateCreateComponent implements OnInit, OnDestroy { readonly Trash2 = Trash2; readonly Type = Type; readonly ImageIcon = ImageIcon; + readonly ChevronUp = ChevronUp; + readonly ChevronDown = ChevronDown; form: FormGroup; loreId = ''; @@ -75,7 +77,10 @@ export class TemplateCreateComponent implements OnInit, OnDestroy { if (!name) return; // Unicite par nom (on ignore le type pour eviter des collisions d'affichage). if (this.fields.some(f => f.name === name)) return; - this.fields = [...this.fields, { name, type: this.newFieldType }]; + const newField: TemplateField = this.newFieldType === 'IMAGE' + ? { name, type: 'IMAGE', layout: 'GALLERY' } + : { name, type: 'TEXT' }; + this.fields = [...this.fields, newField]; this.newFieldName = ''; // Le type reste sur la derniere valeur choisie : pratique pour enchainer // plusieurs champs du meme type. @@ -85,12 +90,33 @@ export class TemplateCreateComponent implements OnInit, OnDestroy { this.fields = this.fields.filter((_, i) => i !== index); } + /** Deplace un champ d'un cran vers le haut ou le bas. No-op aux bords. */ + moveField(index: number, direction: -1 | 1): void { + const target = index + direction; + if (target < 0 || target >= this.fields.length) return; + const next = [...this.fields]; + [next[index], next[target]] = [next[target], next[index]]; + this.fields = next; + } + /** Bascule le type d'un champ existant (TEXT <-> IMAGE). */ toggleFieldType(index: number): void { const field = this.fields[index]; if (!field) return; const nextType: FieldType = field.type === 'TEXT' ? 'IMAGE' : 'TEXT'; - this.fields = this.fields.map((f, i) => i === index ? { ...f, type: nextType } : f); + this.fields = this.fields.map((f, i) => { + if (i !== index) return f; + return nextType === 'IMAGE' + ? { name: f.name, type: 'IMAGE', layout: f.layout ?? 'GALLERY' } + : { name: f.name, type: 'TEXT' }; + }); + } + + /** Met a jour le layout d'un champ IMAGE. */ + setFieldLayout(index: number, layout: ImageLayout): void { + this.fields = this.fields.map((f, i) => + i === index && f.type === 'IMAGE' ? { ...f, layout } : f + ); } submit(): void { diff --git a/web/src/app/lore/template-edit/template-edit.component.html b/web/src/app/lore/template-edit/template-edit.component.html index f300b30..2657206 100644 --- a/web/src/app/lore/template-edit/template-edit.component.html +++ b/web/src/app/lore/template-edit/template-edit.component.html @@ -43,7 +43,21 @@
      -
    • +
    • +
      + + +
      {{ f.name }} @@ -54,6 +68,17 @@ [title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'"> {{ f.type === 'TEXT' ? 'Texte' : 'Image' }} + diff --git a/web/src/app/lore/template-edit/template-edit.component.scss b/web/src/app/lore/template-edit/template-edit.component.scss index ea1b93f..db6c1ad 100644 --- a/web/src/app/lore/template-edit/template-edit.component.scss +++ b/web/src/app/lore/template-edit/template-edit.component.scss @@ -125,7 +125,8 @@ &:hover { color: #a5b4fc; background: #1f1b3a; } } - .type-select { + .type-select, + .layout-select { background: #1a1a2e; border: 1px solid #2a2a3d; color: white; @@ -138,6 +139,12 @@ &:focus { outline: none; border-color: #6c63ff; } } + .layout-select { + height: 28px; + font-size: 0.72rem; + padding: 0 0.45rem; + } + input { flex: 1; background: #1a1a2e; @@ -167,6 +174,35 @@ &:focus { border: none; } } } + + .reorder-stack { + display: flex; + flex-direction: column; + gap: 2px; + + .btn-reorder { + width: 22px; + height: 16px; + background: transparent; + color: #6b7280; + border: 1px solid #2a2a3d; + border-radius: 3px; + padding: 0; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.12s, color 0.12s, border-color 0.12s; + + &:hover:not(:disabled) { + background: #2a2a3d; + color: white; + border-color: #6c63ff; + } + + &:disabled { opacity: 0.3; cursor: not-allowed; } + } + } } .btn-icon-ghost { diff --git a/web/src/app/lore/template-edit/template-edit.component.ts b/web/src/app/lore/template-edit/template-edit.component.ts index 0846bc7..8f6abf8 100644 --- a/web/src/app/lore/template-edit/template-edit.component.ts +++ b/web/src/app/lore/template-edit/template-edit.component.ts @@ -3,14 +3,14 @@ import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { forkJoin } from 'rxjs'; -import { LucideAngularModule, Plus, X, Trash2, Type, Image as ImageIcon } from 'lucide-angular'; +import { LucideAngularModule, Plus, X, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular'; import { LoreService } from '../../services/lore.service'; import { TemplateService } from '../../services/template.service'; import { PageService } from '../../services/page.service'; import { LayoutService } from '../../services/layout.service'; import { PageTitleService } from '../../services/page-title.service'; import { LoreNode } from '../../services/lore.model'; -import { FieldType, Template, TemplateField } from '../../services/template.model'; +import { FieldType, ImageLayout, Template, TemplateField } from '../../services/template.model'; import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper'; /** @@ -30,6 +30,8 @@ export class TemplateEditComponent implements OnInit, OnDestroy { readonly Trash2 = Trash2; readonly Type = Type; readonly ImageIcon = ImageIcon; + readonly ChevronUp = ChevronUp; + readonly ChevronDown = ChevronDown; form: FormGroup; loreId = ''; @@ -75,10 +77,12 @@ export class TemplateEditComponent implements OnInit, OnDestroy { this.template = template; // Copie defensive + normalisation du type (defaut TEXT si inconnu/manquant, // utile pour les templates legacy cote frontend meme si le backend le fait aussi). - this.fields = (template.fields ?? []).map(f => ({ - name: f.name, - type: f.type === 'IMAGE' ? 'IMAGE' : 'TEXT' - })); + this.fields = (template.fields ?? []).map(f => { + const type: FieldType = f.type === 'IMAGE' ? 'IMAGE' : 'TEXT'; + return type === 'IMAGE' + ? { name: f.name, type, layout: f.layout ?? 'GALLERY' } + : { name: f.name, type }; + }); this.form.patchValue({ name: template.name, description: template.description, @@ -91,7 +95,10 @@ export class TemplateEditComponent implements OnInit, OnDestroy { const name = this.newFieldName.trim(); if (!name) return; if (this.fields.some(f => f.name === name)) return; - this.fields = [...this.fields, { name, type: this.newFieldType }]; + const newField: TemplateField = this.newFieldType === 'IMAGE' + ? { name, type: 'IMAGE', layout: 'GALLERY' } + : { name, type: 'TEXT' }; + this.fields = [...this.fields, newField]; this.newFieldName = ''; } @@ -99,12 +106,33 @@ export class TemplateEditComponent implements OnInit, OnDestroy { this.fields = this.fields.filter((_, i) => i !== index); } + /** Deplace un champ d'un cran vers le haut ou le bas. No-op aux bords. */ + moveField(index: number, direction: -1 | 1): void { + const target = index + direction; + if (target < 0 || target >= this.fields.length) return; + const next = [...this.fields]; + [next[index], next[target]] = [next[target], next[index]]; + this.fields = next; + } + /** Bascule le type d'un champ (TEXT <-> IMAGE). */ toggleFieldType(index: number): void { const field = this.fields[index]; if (!field) return; const nextType: FieldType = field.type === 'TEXT' ? 'IMAGE' : 'TEXT'; - this.fields = this.fields.map((f, i) => i === index ? { ...f, type: nextType } : f); + this.fields = this.fields.map((f, i) => { + if (i !== index) return f; + return nextType === 'IMAGE' + ? { name: f.name, type: 'IMAGE', layout: f.layout ?? 'GALLERY' } + : { name: f.name, type: 'TEXT' }; + }); + } + + /** Met a jour le layout d'un champ IMAGE. */ + setFieldLayout(index: number, layout: ImageLayout): void { + this.fields = this.fields.map((f, i) => + i === index && f.type === 'IMAGE' ? { ...f, layout } : f + ); } save(): void { diff --git a/web/src/app/services/campaign.model.ts b/web/src/app/services/campaign.model.ts index 42050b5..3178dd5 100644 --- a/web/src/app/services/campaign.model.ts +++ b/web/src/app/services/campaign.model.ts @@ -36,8 +36,11 @@ export interface Arc { /** IDs des pages du Lore liées à cet arc (weak cross-context refs). */ relatedPageIds?: string[]; - /** IDs des images (Shared Kernel) illustrant cet arc. */ + /** IDs des images (Shared Kernel) illustrant cet arc (ambiance). */ illustrationImageIds?: string[]; + + /** IDs des images utilisees comme cartes / plans (outil de table). */ + mapImageIds?: string[]; } // Payload pour la création d'un Arc (pas d'id) @@ -55,6 +58,7 @@ export interface ArcCreate { relatedPageIds?: string[]; illustrationImageIds?: string[]; + mapImageIds?: string[]; } export interface Chapter { @@ -71,6 +75,7 @@ export interface Chapter { relatedPageIds?: string[]; illustrationImageIds?: string[]; + mapImageIds?: string[]; } export interface ChapterCreate { @@ -85,6 +90,7 @@ export interface ChapterCreate { relatedPageIds?: string[]; illustrationImageIds?: string[]; + mapImageIds?: string[]; } /** @@ -116,6 +122,7 @@ export interface Scene { relatedPageIds?: string[]; illustrationImageIds?: string[]; + mapImageIds?: string[]; /** Sorties narratives (graphe intra-chapitre). */ branches?: SceneBranch[]; @@ -138,5 +145,6 @@ export interface SceneCreate { relatedPageIds?: string[]; illustrationImageIds?: string[]; + mapImageIds?: string[]; branches?: SceneBranch[]; } diff --git a/web/src/app/services/template.model.ts b/web/src/app/services/template.model.ts index ea7fe5f..47d1894 100644 --- a/web/src/app/services/template.model.ts +++ b/web/src/app/services/template.model.ts @@ -7,6 +7,16 @@ */ export type FieldType = 'TEXT' | 'IMAGE'; +/** + * Variante de rendu pour un champ IMAGE. Miroir de + * com.loremind.domain.lorecontext.ImageLayout. Ignore pour TEXT. + * - 'GALLERY' : grille de vignettes (defaut) + * - 'HERO' : premiere image en banniere, suivantes en petit + * - 'MASONRY' : mosaique hauteurs variables + * - 'CAROUSEL' : defilement horizontal + */ +export type ImageLayout = 'GALLERY' | 'HERO' | 'MASONRY' | 'CAROUSEL' | 'EDITORIAL' | 'MAPS'; + /** * Champ d'un Template : nom + type discriminant. * Miroir de TemplateFieldDTO (backend). @@ -14,6 +24,8 @@ export type FieldType = 'TEXT' | 'IMAGE'; export interface TemplateField { name: string; type: FieldType; + /** Uniquement pour type='IMAGE'. Absent/null = 'GALLERY'. */ + layout?: ImageLayout | null; } export interface Template { diff --git a/web/src/app/shared/image-gallery/image-gallery.component.html b/web/src/app/shared/image-gallery/image-gallery.component.html index bb2a894..47f1907 100644 --- a/web/src/app/shared/image-gallery/image-gallery.component.html +++ b/web/src/app/shared/image-gallery/image-gallery.component.html @@ -1,29 +1,183 @@ - -