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.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 java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -16,10 +22,14 @@ import java.util.Optional;
|
||||
@Service
|
||||
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.imageService = imageService;
|
||||
}
|
||||
|
||||
public record EnemyData(
|
||||
@@ -83,8 +93,12 @@ public class EnemyService {
|
||||
|
||||
// --- 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
|
||||
@@ -95,6 +109,38 @@ public class EnemyService {
|
||||
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) {}
|
||||
|
||||
/**
|
||||
@@ -124,6 +170,11 @@ public class EnemyService {
|
||||
ex.setName(m.name());
|
||||
ex.setFoundryStats(stats); // rafraîchit le snapshot
|
||||
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);
|
||||
updated++;
|
||||
} else {
|
||||
@@ -132,6 +183,7 @@ public class EnemyService {
|
||||
.foundryRef(m.foundryRef())
|
||||
.foundryStats(stats)
|
||||
.folder(folder)
|
||||
.portraitImageId(storePortrait(m.imgData(), m.name()))
|
||||
.campaignId(campaignId)
|
||||
.order(order++)
|
||||
.values(new HashMap<>())
|
||||
|
||||
@@ -27,7 +27,7 @@ public class FoundryMonsterImportController {
|
||||
public record MonsterCatalog(String system, List<MonsterEntry> monsters) {}
|
||||
|
||||
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")
|
||||
public ResponseEntity<EnemyService.MonsterImportResult> importMonsters(
|
||||
@@ -35,7 +35,7 @@ public class FoundryMonsterImportController {
|
||||
@RequestBody MonsterCatalog catalog) {
|
||||
List<EnemyService.MonsterImport> monsters = (catalog.monsters() == null ? List.<MonsterEntry>of() : catalog.monsters())
|
||||
.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();
|
||||
return ResponseEntity.ok(enemyService.importFoundryMonsters(campaignId, monsters));
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ class EnemyServiceMonsterImportTest {
|
||||
CampaignJpaEntity.builder().name("Camp").arcsCount(0).build()).getId();
|
||||
|
||||
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("Orc", "Compendium.nimble.monsters.Actor.o1", Map.of(), null)));
|
||||
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, null)));
|
||||
assertEquals(2, r1.created());
|
||||
assertEquals(0, r1.updated());
|
||||
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.
|
||||
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("Kobold", "Compendium.nimble.monsters.Actor.k1", Map.of(), "Kobolds")));
|
||||
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", null)));
|
||||
assertEquals(1, r2.created());
|
||||
assertEquals(1, r2.updated());
|
||||
assertEquals("Foundry/Briarban/Bosses", enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
||||
@@ -70,9 +70,9 @@ class EnemyServiceMonsterImportTest {
|
||||
CampaignJpaEntity.builder().name("Camp2").arcsCount(0).build()).getId();
|
||||
|
||||
var r = enemyService.importFoundryMonsters(String.valueOf(cid), List.of(
|
||||
new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null),
|
||||
new EnemyService.MonsterImport("SansRef", " ", Map.of(), null),
|
||||
new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null)));
|
||||
new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null, null),
|
||||
new EnemyService.MonsterImport("SansRef", " ", Map.of(), null, null),
|
||||
new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null, null)));
|
||||
assertEquals(1, r.created());
|
||||
assertEquals(1, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user