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
All checks were successful
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
13
web/src/app/services/stored-file.model.ts
Normal file
13
web/src/app/services/stored-file.model.ts
Normal 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;
|
||||
}
|
||||
38
web/src/app/services/stored-file.service.ts
Normal file
38
web/src/app/services/stored-file.service.ts
Normal 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`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 *",
|
||||
|
||||
@@ -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 *",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 *",
|
||||
|
||||
@@ -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 *",
|
||||
|
||||
Reference in New Issue
Block a user