Amélioration de l'UI : meilleur affichage des images que ce soit dans la partie lore ou la partie campagne (partie campagne : visualisation scrapbooking). Possibilité de réordonner les champs dans les templates...

Passage v0.3.0
This commit is contained in:
2026-04-21 16:56:27 +02:00
parent 1e34f7f954
commit 71449bee1b
45 changed files with 1045 additions and 90 deletions

View File

@@ -38,12 +38,18 @@ public class Arc {
private List<String> 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<String> illustrationImageIds = new ArrayList<>();
/**
* IDs des images utilisees comme cartes / plans (outil de table).
*/
@Builder.Default
private List<String> mapImageIds = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -34,11 +34,17 @@ public class Chapter {
private List<String> relatedPageIds = new ArrayList<>();
/**
* IDs des images (Shared Kernel) illustrant ce chapitre.
* IDs des images (Shared Kernel) illustrant ce chapitre (ambiance).
*/
@Builder.Default
private List<String> illustrationImageIds = new ArrayList<>();
/**
* IDs des images utilisees comme cartes / plans pour ce chapitre (outil de table).
*/
@Builder.Default
private List<String> mapImageIds = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -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<String> 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<String> 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.

View File

@@ -0,0 +1,18 @@
package com.loremind.domain.lorecontext;
/**
* Variante de rendu pour un champ de type IMAGE.
* <p>
* - 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
* <p>
* Uniquement significatif quand {@link FieldType} = IMAGE. Ignore pour TEXT.
*/
public enum ImageLayout {
GALLERY,
HERO,
MASONRY,
CAROUSEL
}

View File

@@ -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).
* <p>
* Evolution de `List<String> fields` vers `List<TemplateField> 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);
}
}

View File

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

View File

@@ -68,6 +68,12 @@ public class ArcJpaEntity {
@Builder.Default
private List<String> illustrationImageIds = new ArrayList<>();
/** IDs des images "cartes / plans". */
@Column(name = "map_image_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> mapImageIds = new ArrayList<>();
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

View File

@@ -57,6 +57,11 @@ public class ChapterJpaEntity {
@Builder.Default
private List<String> illustrationImageIds = new ArrayList<>();
@Column(name = "map_image_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> mapImageIds = new ArrayList<>();
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

View File

@@ -80,6 +80,11 @@ public class SceneJpaEntity {
@Builder.Default
private List<String> illustrationImageIds = new ArrayList<>();
@Column(name = "map_image_ids", columnDefinition = "TEXT")
@Convert(converter = StringListJsonConverter.class)
@Builder.Default
private List<String> 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")

View File

@@ -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();

View File

@@ -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();

View File

@@ -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<>())

View File

@@ -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 <img>.
// 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));
}

View File

@@ -27,6 +27,9 @@ public class ArcDTO {
/** IDs des pages du Lore liées à cet arc (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
/** IDs des images (Shared Kernel) illustrant cet arc. */
/** IDs des images (Shared Kernel) illustrant cet arc (ambiance). */
private List<String> illustrationImageIds = new ArrayList<>();
/** IDs des images utilisees comme cartes / plans. */
private List<String> mapImageIds = new ArrayList<>();
}

View File

@@ -25,6 +25,9 @@ public class ChapterDTO {
/** IDs des pages du Lore liées (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
/** IDs des images (Shared Kernel) illustrant ce chapitre. */
/** IDs des images (Shared Kernel) illustrant ce chapitre (ambiance). */
private List<String> illustrationImageIds = new ArrayList<>();
/** IDs des images utilisees comme cartes / plans. */
private List<String> mapImageIds = new ArrayList<>();
}

View File

@@ -30,9 +30,12 @@ public class SceneDTO {
/** IDs des pages du Lore liées (weak cross-context references). */
private List<String> relatedPageIds = new ArrayList<>();
/** IDs des images (Shared Kernel) illustrant cette scene. */
/** IDs des images (Shared Kernel) illustrant cette scene (ambiance). */
private List<String> illustrationImageIds = new ArrayList<>();
/** IDs des images utilisees comme cartes / plans (outil de table). */
private List<String> mapImageIds = new ArrayList<>();
/** Branches narratives : sorties possibles vers d'autres scènes du même chapitre. */
private List<SceneBranchDTO> branches = new ArrayList<>();
}

View File

@@ -9,6 +9,8 @@ import lombok.NoArgsConstructor;
* <p>
* 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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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;
* <p>
* 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);
}
}