Ajout de la possibilité d'importer les images des monstres issus de foundry
All checks were successful
All checks were successful
This commit is contained in:
@@ -2,8 +2,14 @@ package com.loremind.application.campaigncontext;
|
|||||||
|
|
||||||
import com.loremind.domain.campaigncontext.Enemy;
|
import com.loremind.domain.campaigncontext.Enemy;
|
||||||
import com.loremind.domain.campaigncontext.ports.EnemyRepository;
|
import com.loremind.domain.campaigncontext.ports.EnemyRepository;
|
||||||
|
import com.loremind.domain.images.Image;
|
||||||
|
import com.loremind.application.images.ImageService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -16,10 +22,14 @@ import java.util.Optional;
|
|||||||
@Service
|
@Service
|
||||||
public class EnemyService {
|
public class EnemyService {
|
||||||
|
|
||||||
private final EnemyRepository enemyRepository;
|
private static final Logger log = LoggerFactory.getLogger(EnemyService.class);
|
||||||
|
|
||||||
public EnemyService(EnemyRepository enemyRepository) {
|
private final EnemyRepository enemyRepository;
|
||||||
|
private final ImageService imageService;
|
||||||
|
|
||||||
|
public EnemyService(EnemyRepository enemyRepository, ImageService imageService) {
|
||||||
this.enemyRepository = enemyRepository;
|
this.enemyRepository = enemyRepository;
|
||||||
|
this.imageService = imageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record EnemyData(
|
public record EnemyData(
|
||||||
@@ -83,8 +93,12 @@ public class EnemyService {
|
|||||||
|
|
||||||
// --- Import de monstres depuis un compendium Foundry -------------------
|
// --- Import de monstres depuis un compendium Foundry -------------------
|
||||||
|
|
||||||
/** Un monstre du catalogue Foundry : nom + référence + snapshot de stats + dossier. */
|
/**
|
||||||
public record MonsterImport(String name, String foundryRef, Map<String, String> stats, String folder) {}
|
* Un monstre du catalogue Foundry : nom + référence + snapshot de stats + dossier
|
||||||
|
* + vignette du portrait (data URL base64, optionnelle).
|
||||||
|
*/
|
||||||
|
public record MonsterImport(String name, String foundryRef, Map<String, String> stats,
|
||||||
|
String folder, String imgData) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dossier LoreMind d'un monstre importé : on conserve l'arborescence Foundry
|
* Dossier LoreMind d'un monstre importé : on conserve l'arborescence Foundry
|
||||||
@@ -95,6 +109,38 @@ public class EnemyService {
|
|||||||
return p.isEmpty() ? "Foundry" : "Foundry/" + p;
|
return p.isEmpty() ? "Foundry" : "Foundry/" + p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Décode une vignette de portrait (data URL base64 "data:image/webp;base64,…")
|
||||||
|
* et l'enregistre comme image LoreMind. Renvoie l'id de l'image, ou null si
|
||||||
|
* absente/illisible (l'import ne doit jamais échouer à cause d'un portrait).
|
||||||
|
*/
|
||||||
|
private String storePortrait(String dataUrl, String name) {
|
||||||
|
if (dataUrl == null || !dataUrl.startsWith("data:")) return null;
|
||||||
|
int comma = dataUrl.indexOf(',');
|
||||||
|
if (comma < 0) return null;
|
||||||
|
String header = dataUrl.substring(5, comma); // "image/webp;base64"
|
||||||
|
String contentType = header.split(";")[0].toLowerCase();
|
||||||
|
byte[] bytes;
|
||||||
|
try {
|
||||||
|
bytes = Base64.getDecoder().decode(dataUrl.substring(comma + 1));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bytes.length == 0) return null;
|
||||||
|
String ext = contentType.contains("png") ? "png"
|
||||||
|
: contentType.contains("jpeg") || contentType.contains("jpg") ? "jpg"
|
||||||
|
: contentType.contains("gif") ? "gif" : "webp";
|
||||||
|
String filename = (name == null || name.isBlank() ? "monstre" : name.replaceAll("[^a-zA-Z0-9._-]", "_")) + "." + ext;
|
||||||
|
try {
|
||||||
|
Image img = imageService.upload(filename, contentType,
|
||||||
|
new ByteArrayInputStream(bytes), bytes.length);
|
||||||
|
return img.getId();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("Portrait de monstre ignoré (\"{}\") : {}", name, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record MonsterImportResult(int created, int updated) {}
|
public record MonsterImportResult(int created, int updated) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,6 +170,11 @@ public class EnemyService {
|
|||||||
ex.setName(m.name());
|
ex.setName(m.name());
|
||||||
ex.setFoundryStats(stats); // rafraîchit le snapshot
|
ex.setFoundryStats(stats); // rafraîchit le snapshot
|
||||||
ex.setFolder(folder); // ré-aligne sur l'arborescence Foundry
|
ex.setFolder(folder); // ré-aligne sur l'arborescence Foundry
|
||||||
|
// Portrait : seulement s'il manque (pas de ré-upload à chaque import).
|
||||||
|
if (ex.getPortraitImageId() == null) {
|
||||||
|
String portrait = storePortrait(m.imgData(), m.name());
|
||||||
|
if (portrait != null) ex.setPortraitImageId(portrait);
|
||||||
|
}
|
||||||
enemyRepository.save(ex);
|
enemyRepository.save(ex);
|
||||||
updated++;
|
updated++;
|
||||||
} else {
|
} else {
|
||||||
@@ -132,6 +183,7 @@ public class EnemyService {
|
|||||||
.foundryRef(m.foundryRef())
|
.foundryRef(m.foundryRef())
|
||||||
.foundryStats(stats)
|
.foundryStats(stats)
|
||||||
.folder(folder)
|
.folder(folder)
|
||||||
|
.portraitImageId(storePortrait(m.imgData(), m.name()))
|
||||||
.campaignId(campaignId)
|
.campaignId(campaignId)
|
||||||
.order(order++)
|
.order(order++)
|
||||||
.values(new HashMap<>())
|
.values(new HashMap<>())
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class FoundryMonsterImportController {
|
|||||||
public record MonsterCatalog(String system, List<MonsterEntry> monsters) {}
|
public record MonsterCatalog(String system, List<MonsterEntry> monsters) {}
|
||||||
|
|
||||||
public record MonsterEntry(String name, String uuid, String type, String img,
|
public record MonsterEntry(String name, String uuid, String type, String img,
|
||||||
java.util.Map<String, String> stats, String folder) {}
|
java.util.Map<String, String> stats, String folder, String imgData) {}
|
||||||
|
|
||||||
@PostMapping("/import-foundry-monsters")
|
@PostMapping("/import-foundry-monsters")
|
||||||
public ResponseEntity<EnemyService.MonsterImportResult> importMonsters(
|
public ResponseEntity<EnemyService.MonsterImportResult> importMonsters(
|
||||||
@@ -35,7 +35,7 @@ public class FoundryMonsterImportController {
|
|||||||
@RequestBody MonsterCatalog catalog) {
|
@RequestBody MonsterCatalog catalog) {
|
||||||
List<EnemyService.MonsterImport> monsters = (catalog.monsters() == null ? List.<MonsterEntry>of() : catalog.monsters())
|
List<EnemyService.MonsterImport> monsters = (catalog.monsters() == null ? List.<MonsterEntry>of() : catalog.monsters())
|
||||||
.stream()
|
.stream()
|
||||||
.map(m -> new EnemyService.MonsterImport(m.name(), m.uuid(), m.stats(), m.folder()))
|
.map(m -> new EnemyService.MonsterImport(m.name(), m.uuid(), m.stats(), m.folder(), m.imgData()))
|
||||||
.toList();
|
.toList();
|
||||||
return ResponseEntity.ok(enemyService.importFoundryMonsters(campaignId, monsters));
|
return ResponseEntity.ok(enemyService.importFoundryMonsters(campaignId, monsters));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class EnemyServiceMonsterImportTest {
|
|||||||
CampaignJpaEntity.builder().name("Camp").arcsCount(0).build()).getId();
|
CampaignJpaEntity.builder().name("Camp").arcsCount(0).build()).getId();
|
||||||
|
|
||||||
var r1 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
var r1 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
||||||
new EnemyService.MonsterImport("Goblin", "Compendium.nimble.monsters.Actor.g1", Map.of("level", "1"), "Briarban"),
|
new EnemyService.MonsterImport("Goblin", "Compendium.nimble.monsters.Actor.g1", Map.of("level", "1"), "Briarban", null),
|
||||||
new EnemyService.MonsterImport("Orc", "Compendium.nimble.monsters.Actor.o1", Map.of(), null)));
|
new EnemyService.MonsterImport("Orc", "Compendium.nimble.monsters.Actor.o1", Map.of(), null, null)));
|
||||||
assertEquals(2, r1.created());
|
assertEquals(2, r1.created());
|
||||||
assertEquals(0, r1.updated());
|
assertEquals(0, r1.updated());
|
||||||
assertEquals(2, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
assertEquals(2, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
||||||
@@ -49,8 +49,8 @@ class EnemyServiceMonsterImportTest {
|
|||||||
|
|
||||||
// Réimport : g1 déjà connu (renommé + redossiérisé) + un nouveau (k1). Pas de doublon.
|
// Réimport : g1 déjà connu (renommé + redossiérisé) + un nouveau (k1). Pas de doublon.
|
||||||
var r2 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
var r2 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
||||||
new EnemyService.MonsterImport("Goblin Boss", "Compendium.nimble.monsters.Actor.g1", Map.of(), "Briarban/Bosses"),
|
new EnemyService.MonsterImport("Goblin Boss", "Compendium.nimble.monsters.Actor.g1", Map.of(), "Briarban/Bosses", null),
|
||||||
new EnemyService.MonsterImport("Kobold", "Compendium.nimble.monsters.Actor.k1", Map.of(), "Kobolds")));
|
new EnemyService.MonsterImport("Kobold", "Compendium.nimble.monsters.Actor.k1", Map.of(), "Kobolds", null)));
|
||||||
assertEquals(1, r2.created());
|
assertEquals(1, r2.created());
|
||||||
assertEquals(1, r2.updated());
|
assertEquals(1, r2.updated());
|
||||||
assertEquals("Foundry/Briarban/Bosses", enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
assertEquals("Foundry/Briarban/Bosses", enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
||||||
@@ -70,9 +70,9 @@ class EnemyServiceMonsterImportTest {
|
|||||||
CampaignJpaEntity.builder().name("Camp2").arcsCount(0).build()).getId();
|
CampaignJpaEntity.builder().name("Camp2").arcsCount(0).build()).getId();
|
||||||
|
|
||||||
var r = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
var r = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
||||||
new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null),
|
new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null, null),
|
||||||
new EnemyService.MonsterImport("SansRef", " ", Map.of(), null),
|
new EnemyService.MonsterImport("SansRef", " ", Map.of(), null, null),
|
||||||
new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null)));
|
new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null, null)));
|
||||||
assertEquals(1, r.created());
|
assertEquals(1, r.created());
|
||||||
assertEquals(1, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
assertEquals(1, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user