Mise en place de l'export foundry ; modification des Arc, chapitres et scènes..... => Suppression de la section maps pour arc, chapitres ; et dans scène on importe les maps format foundry (image / vidéo + json)
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m35s
Build & Push Images / build (web) (push) Successful in 1m56s
Build & Push Images / build (core) (push) Successful in 3m37s
Build & Push Images / build-switcher (push) Successful in 19s

This commit is contained in:
2026-06-25 12:16:27 +02:00
parent 5bc038acd8
commit c0e1da78c8
72 changed files with 2274 additions and 223 deletions

View File

@@ -38,18 +38,6 @@
<small class="field-hint">{{ 'arcEdit.illustrationsHint' | translate }}</small>
</div>
<!-- Cartes & plans -->
<div class="field">
<label>{{ 'arcEdit.mapsLabel' | translate }}</label>
<app-image-gallery
[imageIds]="mapImageIds"
[editable]="true"
[layout]="'MAPS'"
(imageIdsChange)="mapImageIds = $event">
</app-image-gallery>
<small class="field-hint">{{ 'arcEdit.mapsHint' | translate }}</small>
</div>
<div class="field">
<label for="arc-edit-name">{{ 'arcEdit.nameLabel' | translate }}</label>
<input

View File

@@ -71,8 +71,6 @@ 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,
@@ -140,7 +138,6 @@ export class ArcEditComponent implements OnInit, OnDestroy {
this.relatedPageIds = [...(arc.relatedPageIds ?? [])];
this.selectedIcon = arc.icon ?? null;
this.illustrationImageIds = [...(arc.illustrationImageIds ?? [])];
this.mapImageIds = [...(arc.mapImageIds ?? [])];
this.pageTitleService.set(arc.name);
this.form.patchValue({
name: arc.name,
@@ -172,7 +169,6 @@ export class ArcEditComponent implements OnInit, OnDestroy {
resolution: this.form.value.resolution,
relatedPageIds: this.relatedPageIds,
illustrationImageIds: this.illustrationImageIds,
mapImageIds: this.mapImageIds,
icon: this.selectedIcon
}).subscribe({
next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]),

View File

@@ -29,13 +29,6 @@
<app-image-gallery [imageIds]="arc.illustrationImageIds ?? []" [layout]="'EDITORIAL'"></app-image-gallery>
</section>
}
<!-- Cartes & plans -->
@if ((arc.mapImageIds?.length ?? 0) > 0) {
<section class="view-section">
<h2 class="view-section-title"><span class="view-section-icon">🗺️</span> {{ 'arcView.mapsTitle' | translate }}</h2>
<app-image-gallery [imageIds]="arc.mapImageIds ?? []" [layout]="'MAPS'"></app-image-gallery>
</section>
}
<!-- Vue Hub (scénario) : liste les quêtes et leurs conditions, sans statut.
Le statut effectif et les actions de jeu vivent dans l'écran d'une Partie. -->
@if (arc.type === 'HUB') {

View File

@@ -28,6 +28,10 @@
</div>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="exportFoundry()" [disabled]="exportingFoundry">
<lucide-icon [img]="Download" [size]="14"></lucide-icon>
{{ (exportingFoundry ? 'campaignDetail.foundryExporting' : 'campaignDetail.foundryExport') | translate }}
</button>
<button type="button" class="btn-secondary" (click)="startEdit()">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
{{ 'common.edit' | translate }}

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, Dices, Drama, Check, Play, Upload, Sparkles } from 'lucide-angular';
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, Dices, Drama, Check, Play, Upload, Sparkles, Download } from 'lucide-angular';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { Router, RouterLink } from '@angular/router';
import { forkJoin, of } from 'rxjs';
@@ -45,6 +45,10 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
readonly Play = Play;
readonly Upload = Upload;
readonly Sparkles = Sparkles;
readonly Download = Download;
/** Export Foundry en cours (anti double-clic). */
exportingFoundry = false;
campaign: Campaign | null = null;
arcs: Arc[] = [];
@@ -251,6 +255,33 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
this.router.navigate(['/campaigns', this.campaign.id, 'arcs', 'create']);
}
/** Télécharge le bundle Foundry de la campagne via un lien temporaire. */
exportFoundry(): void {
if (!this.campaign?.id || this.exportingFoundry) return;
this.exportingFoundry = true;
this.campaignService.exportFoundry(this.campaign.id).subscribe({
next: (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'foundry-export.zip';
// Certains navigateurs ignorent un click() sur un <a> détaché : on l'attache
// au DOM le temps du déclenchement, puis on nettoie.
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Révocation différée : libère l'URL sans annuler le téléchargement en cours.
setTimeout(() => URL.revokeObjectURL(url), 1000);
this.exportingFoundry = false;
},
error: (err) => {
// Ne pas avaler l'erreur en silence : visible en console pour diagnostic.
console.error('Échec de l\'export Foundry', err);
this.exportingFoundry = false;
}
});
}
openArc(arc: Arc): void {
if (!this.campaign || !arc.id) return;
this.router.navigate(['/campaigns', this.campaign.id, 'arcs', arc.id]);

View File

@@ -55,17 +55,6 @@
<small class="field-hint">{{ 'chapterEdit.illustrationsHint' | translate }}</small>
</div>
<!-- Cartes & plans -->
<div class="field">
<label>{{ 'chapterEdit.maps' | translate }}</label>
<app-image-gallery
[imageIds]="mapImageIds"
[editable]="true"
[layout]="'MAPS'"
(imageIdsChange)="mapImageIds = $event">
</app-image-gallery>
<small class="field-hint">{{ 'chapterEdit.mapsHint' | translate }}</small>
</div>
<div class="field">
<label for="chapter-edit-name">{{ 'chapterEdit.nameLabel' | translate }}</label>

View File

@@ -69,7 +69,6 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
loreId: string | null = null;
relatedPageIds: string[] = [];
illustrationImageIds: string[] = [];
mapImageIds: string[] = [];
/** Prérequis (donnée de scénario). */
prerequisites: Prerequisite[] = [];
@@ -149,7 +148,6 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
this.relatedPageIds = [...(chapter.relatedPageIds ?? [])];
this.selectedIcon = chapter.icon ?? null;
this.illustrationImageIds = [...(chapter.illustrationImageIds ?? [])];
this.mapImageIds = [...(chapter.mapImageIds ?? [])];
this.prerequisites = [...(chapter.prerequisites ?? [])];
@@ -194,7 +192,6 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
prerequisites: this.prerequisites,
relatedPageIds: this.relatedPageIds,
illustrationImageIds: this.illustrationImageIds,
mapImageIds: this.mapImageIds,
icon: this.selectedIcon
}).subscribe({
next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId]),

View File

@@ -64,13 +64,6 @@
<app-image-gallery [imageIds]="chapter.illustrationImageIds ?? []" [layout]="'EDITORIAL'"></app-image-gallery>
</section>
}
<!-- Cartes & plans -->
@if ((chapter.mapImageIds?.length ?? 0) > 0) {
<section class="view-section">
<h2 class="view-section-title"><span class="view-section-icon">🗺️</span> {{ 'chapterView.maps' | translate }}</h2>
<app-image-gallery [imageIds]="chapter.mapImageIds ?? []" [layout]="'MAPS'"></app-image-gallery>
</section>
}
<section class="view-section">
<h2 class="view-section-title"><span class="view-section-icon">📖</span> {{ 'chapterView.synopsis' | translate }}</h2>
@if (chapter.description?.trim()) {

View File

@@ -38,16 +38,40 @@
<small class="field-hint">{{ 'sceneEdit.illustrationsHint' | translate }}</small>
</div>
<!-- Cartes & plans (galerie editable, rendu maps) -->
<!-- Battlemap Foundry : media + sidecar JSON Universal VTT (non affichee, export only) -->
<div class="field">
<label>{{ 'sceneEdit.mapsLabel' | translate }}</label>
<app-image-gallery
[imageIds]="mapImageIds"
[editable]="true"
[layout]="'MAPS'"
(imageIdsChange)="mapImageIds = $event">
</app-image-gallery>
<small class="field-hint">{{ 'sceneEdit.mapsHint' | translate }}</small>
<label>{{ 'sceneEdit.battlemapLabel' | translate }}</label>
<small class="field-hint">{{ 'sceneEdit.battlemapHint' | translate }}</small>
<div class="battlemap-slot">
<span class="battlemap-slot-label">{{ 'sceneEdit.battlemapMedia' | translate }}</span>
@if (battlemapMediaFileId) {
<span class="battlemap-file">{{ battlemapMediaName || ('sceneEdit.battlemapAttached' | translate) }}</span>
<button type="button" class="battlemap-remove" (click)="removeBattlemapMedia()">
{{ 'sceneEdit.battlemapRemove' | translate }}
</button>
} @else {
<label class="battlemap-pick">
{{ (battlemapUploadingMedia ? 'sceneEdit.battlemapUploading' : 'sceneEdit.battlemapChoose') | translate }}
<input type="file" accept="image/*,video/mp4,video/webm" hidden (change)="onBattlemapMediaSelected($event)" />
</label>
}
</div>
<div class="battlemap-slot">
<span class="battlemap-slot-label">{{ 'sceneEdit.battlemapData' | translate }}</span>
@if (battlemapDataFileId) {
<span class="battlemap-file">{{ battlemapDataName || ('sceneEdit.battlemapAttached' | translate) }}</span>
<button type="button" class="battlemap-remove" (click)="removeBattlemapData()">
{{ 'sceneEdit.battlemapRemove' | translate }}
</button>
} @else {
<label class="battlemap-pick">
{{ (battlemapUploadingData ? 'sceneEdit.battlemapUploading' : 'sceneEdit.battlemapChoose') | translate }}
<input type="file" accept=".json,.dd2vtt,.uvtt,application/json" hidden (change)="onBattlemapDataSelected($event)" />
</label>
}
</div>
</div>
<div class="field">

View File

@@ -3,6 +3,44 @@
max-width: 760px;
}
// Widget battlemap (export Foundry) : deux emplacements media + sidecar JSON.
.battlemap-slot {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
.battlemap-slot-label {
min-width: 160px;
font-size: 0.9rem;
opacity: 0.85;
}
.battlemap-file {
font-size: 0.9rem;
word-break: break-all;
}
.battlemap-pick {
cursor: pointer;
padding: 0.35rem 0.75rem;
border: 1px dashed currentColor;
border-radius: 6px;
font-size: 0.85rem;
opacity: 0.9;
}
.battlemap-remove {
background: none;
border: none;
color: inherit;
opacity: 0.7;
cursor: pointer;
text-decoration: underline;
font-size: 0.85rem;
}
}
// Header local : titre à gauche, actions (Assistant IA) à droite.
.page-header {
display: flex;

View File

@@ -7,6 +7,7 @@ import { switchMap } from 'rxjs/operators';
import { LucideAngularModule, Trash2, Sparkles } from 'lucide-angular';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { CampaignService } from '../../../services/campaign.service';
import { StoredFileService } from '../../../services/stored-file.service';
import { CharacterService } from '../../../services/character.service';
import { NpcService } from '../../../services/npc.service';
import { RandomTableService } from '../../../services/random-table.service';
@@ -70,7 +71,14 @@ export class SceneEditComponent implements OnInit, OnDestroy {
availableEnemies: Enemy[] = [];
enemyIds: string[] = [];
illustrationImageIds: string[] = [];
mapImageIds: string[] = [];
/** Battlemap Foundry : paire { media + sidecar JSON Universal VTT }. Non affichee. */
battlemapMediaFileId: string | null = null;
battlemapDataFileId: string | null = null;
battlemapMediaName: string | null = null;
battlemapDataName: string | null = null;
battlemapUploadingMedia = false;
battlemapUploadingData = false;
/** Scènes du chapitre courant (hors scène éditée) — alimente le dropdown des cibles. */
siblingScenes: Scene[] = [];
@@ -87,6 +95,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private router: Router,
private campaignService: CampaignService,
private storedFileService: StoredFileService,
private characterService: CharacterService,
private npcService: NpcService,
private randomTableService: RandomTableService,
@@ -161,7 +170,18 @@ export class SceneEditComponent implements OnInit, OnDestroy {
this.enemyIds = [...(scene.enemyIds ?? [])];
this.selectedIcon = scene.icon ?? null;
this.illustrationImageIds = [...(scene.illustrationImageIds ?? [])];
this.mapImageIds = [...(scene.mapImageIds ?? [])];
this.battlemapMediaFileId = scene.battlemapMediaFileId ?? null;
this.battlemapDataFileId = scene.battlemapDataFileId ?? null;
this.battlemapMediaName = null;
this.battlemapDataName = null;
if (this.battlemapMediaFileId) {
this.storedFileService.getById(this.battlemapMediaFileId)
.subscribe({ next: f => this.battlemapMediaName = f.filename, error: () => {} });
}
if (this.battlemapDataFileId) {
this.storedFileService.getById(this.battlemapDataFileId)
.subscribe({ next: f => this.battlemapDataName = f.filename, error: () => {} });
}
this.siblingScenes = chapterScenes.filter(s => s.id !== this.sceneId);
this.branches = (scene.branches ?? []).map(b => ({ ...b }));
this.rooms = (scene.rooms ?? []).map(r => ({ ...r, branches: [...(r.branches ?? [])] }));
@@ -200,7 +220,8 @@ export class SceneEditComponent implements OnInit, OnDestroy {
enemyIds: this.enemyIds,
relatedPageIds: this.relatedPageIds,
illustrationImageIds: this.illustrationImageIds,
mapImageIds: this.mapImageIds,
battlemapMediaFileId: this.battlemapMediaFileId,
battlemapDataFileId: this.battlemapDataFileId,
branches: this.branches,
rooms: this.rooms,
icon: this.selectedIcon
@@ -253,6 +274,53 @@ export class SceneEditComponent implements OnInit, OnDestroy {
this.branches[index].condition = value;
}
// ─────────────── Battlemap Foundry (media + sidecar JSON) ───────────────
// On NE supprime PAS le binaire au "retirer" (juste la reference locale) :
// si l'utilisateur annule le formulaire, la scene garde son fichier intact.
// Le binaire orphelin eventuel est inoffensif (nettoyage ulterieur possible).
onBattlemapMediaSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
this.battlemapUploadingMedia = true;
this.storedFileService.upload(file).subscribe({
next: f => {
this.battlemapMediaFileId = f.id;
this.battlemapMediaName = f.filename;
this.battlemapUploadingMedia = false;
},
error: () => { this.battlemapUploadingMedia = false; }
});
input.value = '';
}
onBattlemapDataSelected(event: Event): void {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
this.battlemapUploadingData = true;
this.storedFileService.upload(file).subscribe({
next: f => {
this.battlemapDataFileId = f.id;
this.battlemapDataName = f.filename;
this.battlemapUploadingData = false;
},
error: () => { this.battlemapUploadingData = false; }
});
input.value = '';
}
removeBattlemapMedia(): void {
this.battlemapMediaFileId = null;
this.battlemapMediaName = null;
}
removeBattlemapData(): void {
this.battlemapDataFileId = null;
this.battlemapDataName = null;
}
ngOnDestroy(): void {
// Volontairement vide : la sidebar reste prise en charge par le composant
// suivant (autre sous-route ou le composant detail parent) qui appellera

View File

@@ -27,13 +27,6 @@
<app-image-gallery [imageIds]="scene.illustrationImageIds ?? []" [layout]="'EDITORIAL'"></app-image-gallery>
</section>
}
<!-- Cartes & plans -->
@if ((scene.mapImageIds?.length ?? 0) > 0) {
<section class="view-section">
<h2 class="view-section-title"><span class="view-section-icon">🗺️</span> {{ 'sceneView.mapsSectionTitle' | translate }}</h2>
<app-image-gallery [imageIds]="scene.mapImageIds ?? []" [layout]="'MAPS'"></app-image-gallery>
</section>
}
<!-- Description courte -->
<section class="view-section">
<h2 class="view-section-title"><span class="view-section-icon">📝</span> {{ 'sceneView.descriptionSectionTitle' | translate }}</h2>

View File

@@ -68,9 +68,6 @@ export interface 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)
@@ -90,7 +87,6 @@ export interface ArcCreate {
relatedPageIds?: string[];
illustrationImageIds?: string[];
mapImageIds?: string[];
}
export interface Chapter {
@@ -122,7 +118,6 @@ export interface Chapter {
relatedPageIds?: string[];
illustrationImageIds?: string[];
mapImageIds?: string[];
}
export interface ChapterCreate {
@@ -140,7 +135,6 @@ export interface ChapterCreate {
relatedPageIds?: string[];
illustrationImageIds?: string[];
mapImageIds?: string[];
}
/**
@@ -238,7 +232,13 @@ export interface Scene {
relatedPageIds?: string[];
illustrationImageIds?: string[];
mapImageIds?: string[];
/**
* Battlemap Foundry : ID du fichier media (image/video) + ID du sidecar JSON
* Universal VTT. Non affichee dans l'appli ; transportee a l'export Foundry.
*/
battlemapMediaFileId?: string | null;
battlemapDataFileId?: string | null;
/** Sorties narratives (graphe intra-chapitre). */
branches?: SceneBranch[];
@@ -268,7 +268,8 @@ export interface SceneCreate {
relatedPageIds?: string[];
illustrationImageIds?: string[];
mapImageIds?: string[];
battlemapMediaFileId?: string | null;
battlemapDataFileId?: string | null;
branches?: SceneBranch[];
rooms?: Room[];
}

View File

@@ -42,6 +42,11 @@ export class CampaignService {
return this.http.get<Campaign>(`${this.apiUrl}/${id}`);
}
/** Télécharge le bundle d'export Foundry de la campagne (.zip). */
exportFoundry(id: string): Observable<Blob> {
return this.http.get(`${this.apiUrl}/${id}/foundry-export`, { responseType: 'blob' });
}
createCampaign(campaign: CampaignCreate): Observable<Campaign> {
return this.http.post<Campaign>(this.apiUrl, campaign);
}

View File

@@ -0,0 +1,13 @@
// Interface TypeScript pour StoredFileDTO (Backend Java).
// Miroir de com.loremind.infrastructure.web.dto.files.StoredFileDTO.
// Fichier generique (battlemap : media video/image + sidecar JSON Universal VTT).
export interface StoredFile {
id: string;
filename: string;
contentType: string;
sizeBytes: number;
/** URL relative du binaire, ex: "/api/files/42/content". */
url: string;
uploadedAt: string;
}

View File

@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { StoredFile } from './stored-file.model';
/**
* Service HTTP pour les fichiers generiques (/api/files).
* Pendant de {@link ImageService} pour les battlemaps : media (image/video) +
* sidecar JSON Universal VTT, qui sortent du perimetre des images (mp4, json,
* taille jusqu'a 128 Mo).
*/
@Injectable({ providedIn: 'root' })
export class StoredFileService {
readonly apiBase = '';
private apiUrl = `${this.apiBase}/api/files`;
constructor(private http: HttpClient) {}
/** Upload multipart/form-data. Le backend valide le MIME et la taille (128 Mo max). */
upload(file: File): Observable<StoredFile> {
const form = new FormData();
form.append('file', file);
return this.http.post<StoredFile>(this.apiUrl, form);
}
getById(id: string): Observable<StoredFile> {
return this.http.get<StoredFile>(`${this.apiUrl}/${id}`);
}
delete(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
/** URL absolue du binaire (peu utile : la battlemap n'est pas affichee). */
contentUrl(id: string): string {
return `${this.apiUrl}/${id}/content`;
}
}

View File

@@ -52,12 +52,6 @@
<lucide-icon [img]="Dices" [size]="16"></lucide-icon>
<span>{{ 'sidebar.gameSystems' | translate }}</span>
</button>
@if (!config.demoMode) {
<button class="tool-btn">
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
<span>{{ 'sidebar.vttExport' | translate }}</span>
</button>
}
@if (!config.demoMode) {
<button class="tool-btn" [class.active]="currentRoute.startsWith('/settings')" (click)="navigateTo('/settings')">
<lucide-icon [img]="Settings" [size]="16"></lucide-icon>

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Router } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { LucideAngularModule, Search, Download, Settings, ArrowLeft, Dices } from 'lucide-angular';
import { LucideAngularModule, Search, Settings, ArrowLeft, Dices } from 'lucide-angular';
import { LayoutService } from '../services/layout.service';
import { GlobalSearchService } from '../services/global-search.service';
import { ConfigService } from '../services/config.service';
@@ -21,7 +21,6 @@ export class SidebarComponent implements OnInit {
currentRoute = '';
readonly Search = Search;
readonly Download = Download;
readonly Settings = Settings;
readonly ArrowLeft = ArrowLeft;
readonly Dices = Dices;

View File

@@ -588,6 +588,8 @@
"newPlaythrough": "New playthrough",
"noPlaythrough": "No playthroughs. Create one to start a session with a table.",
"defaultPlaythroughName": "New playthrough {{n}}",
"foundryExport": "Export to Foundry",
"foundryExporting": "Exporting…",
"gameSystemChange": {
"title": "Change the game system?",
"message": "You are about to change the game system of this campaign. This also changes the template for PC and NPC sheets.",
@@ -909,8 +911,14 @@
"aiAssistantTitle": "Open the AI Assistant to discuss this scene",
"illustrationsLabel": "Illustrations",
"illustrationsHint": "NPC portraits, visual mood, evocative scenes...",
"mapsLabel": "Maps & plans",
"mapsHint": "Location plans, tactical maps, diagrams usable at the table.",
"battlemapLabel": "Battlemap (Foundry export)",
"battlemapHint": "Battle map to export to Foundry: media (image/video) + Universal VTT .json/.dd2vtt file (Dungeon Alchemist, Dungeondraft...). Not displayed in the app.",
"battlemapMedia": "Media (image / video)",
"battlemapData": "Data (.json / .dd2vtt)",
"battlemapChoose": "Choose a file",
"battlemapUploading": "Uploading...",
"battlemapAttached": "File attached",
"battlemapRemove": "Remove",
"nameLabel": "Scene title *",
"namePlaceholder": "E.g.: Arrival at the village",
"descriptionLabel": "Short description *",

View File

@@ -588,6 +588,8 @@
"newPlaythrough": "Nouvelle partie",
"noPlaythrough": "Aucune partie. Créez-en une pour démarrer une session avec une table.",
"defaultPlaythroughName": "Nouvelle partie {{n}}",
"foundryExport": "Exporter pour Foundry",
"foundryExporting": "Export…",
"gameSystemChange": {
"title": "Changer le système de jeu ?",
"message": "Vous êtes sur le point de changer le système de jeu de cette campagne. Cela change également le template des fiches de PJ et PNJ.",
@@ -909,8 +911,14 @@
"aiAssistantTitle": "Ouvrir l'Assistant IA pour dialoguer autour de cette scène",
"illustrationsLabel": "Illustrations",
"illustrationsHint": "Portraits des PNJ, ambiance visuelle, scenes evocatrices...",
"mapsLabel": "Cartes & plans",
"mapsHint": "Plans du lieu, cartes tactiques, schemas utilisables a la table.",
"battlemapLabel": "Battlemap (export Foundry)",
"battlemapHint": "Carte de combat a exporter vers Foundry : media (image/video) + fichier .json/.dd2vtt Universal VTT (Dungeon Alchemist, Dungeondraft...). Non affichee dans l'appli.",
"battlemapMedia": "Media (image / video)",
"battlemapData": "Donnees (.json / .dd2vtt)",
"battlemapChoose": "Choisir un fichier",
"battlemapUploading": "Envoi...",
"battlemapAttached": "Fichier attache",
"battlemapRemove": "Retirer",
"nameLabel": "Titre de la scène *",
"namePlaceholder": "Ex: Arrivée au village",
"descriptionLabel": "Description courte *",

View File

@@ -58,6 +58,8 @@
"newPlaythrough": "New playthrough",
"noPlaythrough": "No playthroughs. Create one to start a session with a table.",
"defaultPlaythroughName": "New playthrough {{n}}",
"foundryExport": "Export to Foundry",
"foundryExporting": "Exporting…",
"gameSystemChange": {
"title": "Change the game system?",
"message": "You are about to change the game system of this campaign. This also changes the template for PC and NPC sheets.",

View File

@@ -58,6 +58,8 @@
"newPlaythrough": "Nouvelle partie",
"noPlaythrough": "Aucune partie. Créez-en une pour démarrer une session avec une table.",
"defaultPlaythroughName": "Nouvelle partie {{n}}",
"foundryExport": "Exporter pour Foundry",
"foundryExporting": "Export…",
"gameSystemChange": {
"title": "Changer le système de jeu ?",
"message": "Vous êtes sur le point de changer le système de jeu de cette campagne. Cela change également le template des fiches de PJ et PNJ.",

View File

@@ -17,8 +17,14 @@
"aiAssistantTitle": "Open the AI Assistant to discuss this scene",
"illustrationsLabel": "Illustrations",
"illustrationsHint": "NPC portraits, visual mood, evocative scenes...",
"mapsLabel": "Maps & plans",
"mapsHint": "Location plans, tactical maps, diagrams usable at the table.",
"battlemapLabel": "Battlemap (Foundry export)",
"battlemapHint": "Battle map to export to Foundry: media (image/video) + Universal VTT .json/.dd2vtt file (Dungeon Alchemist, Dungeondraft...). Not displayed in the app.",
"battlemapMedia": "Media (image / video)",
"battlemapData": "Data (.json / .dd2vtt)",
"battlemapChoose": "Choose a file",
"battlemapUploading": "Uploading...",
"battlemapAttached": "File attached",
"battlemapRemove": "Remove",
"nameLabel": "Scene title *",
"namePlaceholder": "E.g.: Arrival at the village",
"descriptionLabel": "Short description *",

View File

@@ -17,8 +17,14 @@
"aiAssistantTitle": "Ouvrir l'Assistant IA pour dialoguer autour de cette scène",
"illustrationsLabel": "Illustrations",
"illustrationsHint": "Portraits des PNJ, ambiance visuelle, scenes evocatrices...",
"mapsLabel": "Cartes & plans",
"mapsHint": "Plans du lieu, cartes tactiques, schemas utilisables a la table.",
"battlemapLabel": "Battlemap (export Foundry)",
"battlemapHint": "Carte de combat a exporter vers Foundry : media (image/video) + fichier .json/.dd2vtt Universal VTT (Dungeon Alchemist, Dungeondraft...). Non affichee dans l'appli.",
"battlemapMedia": "Media (image / video)",
"battlemapData": "Donnees (.json / .dd2vtt)",
"battlemapChoose": "Choisir un fichier",
"battlemapUploading": "Envoi...",
"battlemapAttached": "Fichier attache",
"battlemapRemove": "Retirer",
"nameLabel": "Titre de la scène *",
"namePlaceholder": "Ex: Arrivée au village",
"descriptionLabel": "Description courte *",