Mise en place de l'anglais comme deuxième langue pour l'application
This commit is contained in:
@@ -3,13 +3,10 @@
|
||||
<div class="page-header">
|
||||
<button type="button" class="btn-back" (click)="cancel()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
|
||||
Retour à la campagne
|
||||
{{ 'campaignImport.back' | translate }}
|
||||
</button>
|
||||
<h1>Importer un PDF de campagne</h1>
|
||||
<p class="subtitle">
|
||||
L'IA propose une arborescence arc → chapitre → scène. Vous la révisez et l'ajustez
|
||||
avant de créer quoi que ce soit.
|
||||
</p>
|
||||
<h1>{{ 'campaignImport.title' | translate }}</h1>
|
||||
<p class="subtitle">{{ 'campaignImport.subtitle' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Étape 1 : upload (masqué une fois en revue) -->
|
||||
@@ -18,7 +15,7 @@
|
||||
<input #pdfInput type="file" accept="application/pdf,.pdf" hidden (change)="onPdfSelected($event)" />
|
||||
<button type="button" class="btn-primary big" [disabled]="importing" (click)="pdfInput.click()">
|
||||
<lucide-icon [img]="Upload" [size]="16"></lucide-icon>
|
||||
{{ importing ? 'Import en cours…' : 'Choisir un PDF de campagne' }}
|
||||
{{ (importing ? 'campaignImport.importing' : 'campaignImport.choosePdf') | translate }}
|
||||
</button>
|
||||
<!-- Progression live -->
|
||||
@if (importing) {
|
||||
@@ -36,7 +33,7 @@
|
||||
}
|
||||
@if (importCounts) {
|
||||
<p class="import-counts">
|
||||
Trouvé jusqu'ici : {{ importCounts.arcs }} arc(s) · {{ importCounts.chapters }} chapitre(s) · {{ importCounts.scenes }} scène(s) · {{ importCounts.npcs }} PNJ
|
||||
{{ 'campaignImport.foundSoFar' | translate:{ arcs: importCounts.arcs, chapters: importCounts.chapters, scenes: importCounts.scenes, npcs: importCounts.npcs } }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
@@ -52,12 +49,7 @@
|
||||
<section class="review-area">
|
||||
<div class="review-header">
|
||||
<p class="review-summary">
|
||||
À créer : <strong>{{ arcCount }}</strong> nouvel(s) arc(s),
|
||||
<strong>{{ chapterCount }}</strong> chapitre(s),
|
||||
<strong>{{ sceneCount }}</strong> scène(s)@if (npcs.length > 0) {<span>,
|
||||
<strong>{{ npcCount }}</strong> PNJ</span>}.
|
||||
Les éléments <em>« déjà présent »</em> (grisés) sont affichés pour situer les ajouts ;
|
||||
ils ne seront pas recréés. Modifiez les nouveaux, puis créez.
|
||||
<span [innerHTML]="'campaignImport.summaryToCreate' | translate:{ arcs: arcCount, chapters: chapterCount, scenes: sceneCount }"></span>@if (npcs.length > 0) {<span [innerHTML]="'campaignImport.summaryNpcs' | translate:{ npcs: npcCount }"></span>}<span [innerHTML]="'campaignImport.summaryHint' | translate"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tree">
|
||||
@@ -70,12 +62,12 @@
|
||||
</button>
|
||||
<lucide-icon [img]="Swords" [size]="16" class="node-icon"></lucide-icon>
|
||||
<input type="text" class="node-name" [(ngModel)]="arc.name" [name]="'arc-' + ai"
|
||||
[readonly]="arc.existing" placeholder="Nom de l'arc" />
|
||||
[readonly]="arc.existing" [placeholder]="'campaignImport.arcNamePlaceholder' | translate" />
|
||||
@if (arc.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!arc.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeArc(ai)" title="Supprimer l'arc">
|
||||
<button type="button" class="btn-remove" (click)="removeArc(ai)" [title]="'campaignImport.removeArc' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -84,16 +76,16 @@
|
||||
<div class="node-body">
|
||||
@if (!arc.existing) {
|
||||
<div class="arc-type-toggle">
|
||||
<span class="toggle-label">Type :</span>
|
||||
<span class="toggle-label">{{ 'campaignImport.typeLabel' | translate }}</span>
|
||||
<button type="button" class="type-btn" [class.active]="arc.type === 'LINEAR'"
|
||||
(click)="setArcType(arc, 'LINEAR')">Linéaire</button>
|
||||
(click)="setArcType(arc, 'LINEAR')">{{ 'campaignImport.typeLinear' | translate }}</button>
|
||||
<button type="button" class="type-btn" [class.active]="arc.type === 'HUB'"
|
||||
(click)="setArcType(arc, 'HUB')">Hub (quêtes)</button>
|
||||
(click)="setArcType(arc, 'HUB')">{{ 'campaignImport.typeHub' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
@if (!arc.existing) {
|
||||
<textarea class="node-desc" [(ngModel)]="arc.description" [name]="'arc-desc-' + ai"
|
||||
rows="2" placeholder="Synopsis de l'arc (optionnel)"></textarea>
|
||||
rows="2" [placeholder]="'campaignImport.arcSynopsisPlaceholder' | translate"></textarea>
|
||||
}
|
||||
<!-- Chapitres -->
|
||||
@for (chapter of arc.chapters; track chapter; let ci = $index) {
|
||||
@@ -104,12 +96,12 @@
|
||||
</button>
|
||||
<lucide-icon [img]="BookOpen" [size]="14" class="node-icon"></lucide-icon>
|
||||
<input type="text" class="node-name" [(ngModel)]="chapter.name" [name]="'chap-' + ai + '-' + ci"
|
||||
[readonly]="chapter.existing" placeholder="Nom du chapitre" />
|
||||
[readonly]="chapter.existing" [placeholder]="'campaignImport.chapterNamePlaceholder' | translate" />
|
||||
@if (chapter.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!chapter.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeChapter(arc, ci)" title="Supprimer le chapitre">
|
||||
<button type="button" class="btn-remove" (click)="removeChapter(arc, ci)" [title]="'campaignImport.removeChapter' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -118,7 +110,7 @@
|
||||
<div class="node-body">
|
||||
@if (!chapter.existing) {
|
||||
<textarea class="node-desc" [(ngModel)]="chapter.description" [name]="'chap-desc-' + ai + '-' + ci"
|
||||
rows="2" placeholder="Synopsis du chapitre (optionnel)"></textarea>
|
||||
rows="2" [placeholder]="'campaignImport.chapterSynopsisPlaceholder' | translate"></textarea>
|
||||
}
|
||||
<!-- Scènes -->
|
||||
@for (scene of chapter.scenes; track scene; let si = $index) {
|
||||
@@ -129,60 +121,60 @@
|
||||
<div class="scene-fields">
|
||||
<input type="text" class="node-name" [(ngModel)]="scene.name"
|
||||
[name]="'scene-' + ai + '-' + ci + '-' + si" [readonly]="scene.existing"
|
||||
placeholder="Nom de la scène" />
|
||||
[placeholder]="'campaignImport.sceneNamePlaceholder' | translate" />
|
||||
@if (!scene.existing) {
|
||||
<input type="text" class="node-desc-inline" [(ngModel)]="scene.description"
|
||||
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" placeholder="Synopsis (optionnel)" />
|
||||
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" [placeholder]="'campaignImport.synopsisOptionalPlaceholder' | translate" />
|
||||
}
|
||||
</div>
|
||||
@if (scene.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!scene.existing) {
|
||||
<button type="button" class="btn-details" (click)="toggleDetails(scene)"
|
||||
title="Narration joueurs, notes MJ, pièces">
|
||||
[title]="'campaignImport.detailsTitle' | translate">
|
||||
<lucide-icon [img]="scene.detailsOpen ? ChevronDown : ChevronRight" [size]="12"></lucide-icon>
|
||||
Détails@if (scene.rooms.length) {
|
||||
<span> · {{ scene.rooms.length }} pièce(s)</span>
|
||||
{{ 'campaignImport.details' | translate }}@if (scene.rooms.length) {
|
||||
<span> {{ 'campaignImport.roomsCount' | translate:{ n: scene.rooms.length } }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@if (!scene.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeScene(chapter, si)" title="Supprimer la scène">
|
||||
<button type="button" class="btn-remove" (click)="removeScene(chapter, si)" [title]="'campaignImport.removeScene' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (scene.detailsOpen && !scene.existing) {
|
||||
<div class="scene-details">
|
||||
<label class="field-label">À lire aux joueurs</label>
|
||||
<label class="field-label">{{ 'campaignImport.playerNarrationLabel' | translate }}</label>
|
||||
<textarea class="node-desc" [(ngModel)]="scene.playerNarration"
|
||||
[name]="'scene-pn-' + ai + '-' + ci + '-' + si" rows="3"
|
||||
placeholder="Texte d'encadré lu aux joueurs (optionnel)"></textarea>
|
||||
<label class="field-label">Notes MJ</label>
|
||||
[placeholder]="'campaignImport.playerNarrationPlaceholder' | translate"></textarea>
|
||||
<label class="field-label">{{ 'campaignImport.gmNotesLabel' | translate }}</label>
|
||||
<textarea class="node-desc" [(ngModel)]="scene.gmNotes"
|
||||
[name]="'scene-gm-' + ai + '-' + ci + '-' + si" rows="3"
|
||||
placeholder="Secrets, développement, conséquences (optionnel)"></textarea>
|
||||
[placeholder]="'campaignImport.gmNotesPlaceholder' | translate"></textarea>
|
||||
<!-- Pièces (donjon) : vide = scène narrative classique -->
|
||||
<span class="field-label">Pièces (lieu explorable)</span>
|
||||
<span class="field-label">{{ 'campaignImport.roomsLabel' | translate }}</span>
|
||||
<div class="rooms">
|
||||
@for (room of scene.rooms; track room; let ri = $index) {
|
||||
<div class="room-row">
|
||||
<input type="text" class="room-name" [(ngModel)]="room.name"
|
||||
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Pièce" />
|
||||
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.roomPlaceholder' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.description"
|
||||
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Description" />
|
||||
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'common.description' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.enemies"
|
||||
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Ennemis" />
|
||||
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.enemiesPlaceholder' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.loot"
|
||||
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Trésor" />
|
||||
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" title="Supprimer la pièce">
|
||||
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.lootPlaceholder' | translate" />
|
||||
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" [title]="'campaignImport.removeRoom' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addRoom(scene)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Pièce
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.room' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,32 +182,29 @@
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addScene(chapter)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Scène
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.scene' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addChapter(arc)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Chapitre
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.chapter' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add" (click)="addArc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> Ajouter un arc
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> {{ 'campaignImport.addArc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PNJ détectés dans le PDF : revue par cases à cocher -->
|
||||
@if (npcs.length > 0) {
|
||||
<div class="npc-review">
|
||||
<h3>PNJ et créatures détectés ({{ npcs.length }})</h3>
|
||||
<p class="hint">
|
||||
Cochez ceux à créer dans la campagne (description issue du livre).
|
||||
Ceux déjà présents dans la campagne sont grisés et ne seront pas recréés.
|
||||
</p>
|
||||
<h3>{{ 'campaignImport.npcReviewTitle' | translate:{ n: npcs.length } }}</h3>
|
||||
<p class="hint">{{ 'campaignImport.npcReviewHint' | translate }}</p>
|
||||
<ul class="npc-list">
|
||||
@for (npc of npcs; track npc.name) {
|
||||
<li class="npc-row" [class.npc-existing]="npc.existing">
|
||||
@@ -223,7 +212,7 @@
|
||||
<input type="checkbox" [(ngModel)]="npc.selected" [disabled]="npc.existing">
|
||||
<strong>{{ npc.name }}</strong>
|
||||
@if (npc.existing) {
|
||||
<em class="npc-tag">déjà présent</em>
|
||||
<em class="npc-tag">{{ 'campaignImport.alreadyPresent' | translate }}</em>
|
||||
}
|
||||
</label>
|
||||
@if (npc.description) {
|
||||
@@ -241,9 +230,9 @@
|
||||
<div class="review-actions">
|
||||
<button type="button" class="btn-primary" [disabled]="applying || !hasNewContent" (click)="apply()">
|
||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||
{{ applying ? 'Création…' : 'Créer dans la campagne' }}
|
||||
{{ (applying ? 'campaignImport.creating' : 'campaignImport.createInCampaign') | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LucideAngularModule, ArrowLeft, Upload, Plus, Trash2,
|
||||
ChevronDown, ChevronRight, Swords, BookOpen, MapPin, Check
|
||||
} from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { CampaignImportService } from '../../../services/campaign-import.service';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
import { CharacterService } from '../../../services/character.service';
|
||||
@@ -53,7 +54,7 @@ interface NpcNode { name: string; description: string; selected: boolean; existi
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-campaign-import',
|
||||
imports: [FormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, LucideAngularModule, TranslatePipe],
|
||||
templateUrl: './campaign-import.component.html',
|
||||
styleUrls: ['./campaign-import.component.scss']
|
||||
})
|
||||
@@ -109,12 +110,13 @@ export class CampaignImportComponent implements OnInit {
|
||||
private randomTableService: RandomTableService,
|
||||
private enemyService: EnemyService,
|
||||
private campaignSidebar: CampaignSidebarService,
|
||||
private pageTitle: PageTitleService
|
||||
private pageTitle: PageTitleService,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.campaignId = this.route.snapshot.paramMap.get('campaignId')!;
|
||||
this.pageTitle.set('Importer une campagne');
|
||||
this.pageTitle.set(this.translate.instant('campaignImport.pageTitle'));
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
|
||||
// Pré-chargement de l'arborescence existante (pour fusionner à la revue).
|
||||
@@ -138,7 +140,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.reviewing = false;
|
||||
this.importError = null;
|
||||
this.applyError = null;
|
||||
this.importPhase = 'Extraction du texte…';
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseExtracting');
|
||||
this.importProgress = null;
|
||||
this.importStatus = null;
|
||||
this.importCounts = null;
|
||||
@@ -150,10 +152,10 @@ export class CampaignImportComponent implements OnInit {
|
||||
// Un morceau vient d'aboutir : le message d'attente est obsolète.
|
||||
this.importStatus = null;
|
||||
if (ev.total === 0) {
|
||||
this.importPhase = 'Extraction du texte…';
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseExtracting');
|
||||
this.importProgress = null;
|
||||
} else {
|
||||
this.importPhase = `Analyse de la campagne… (${ev.current}/${ev.total})`;
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseAnalyzing', { current: ev.current, total: ev.total });
|
||||
this.importProgress = { current: ev.current, total: ev.total };
|
||||
this.importCounts = {
|
||||
arcs: ev.arcCount, chapters: ev.chapterCount,
|
||||
@@ -168,7 +170,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.importProgress = null;
|
||||
this.importStatus = null;
|
||||
if ((ev.arcs ?? []).length === 0 && (ev.npcs ?? []).length === 0) {
|
||||
this.importError = "Aucune structure narrative détectée dans ce PDF.";
|
||||
this.importError = this.translate.instant('campaignImport.errorNoStructure');
|
||||
this.reviewing = false;
|
||||
} else {
|
||||
this.tree = this.buildMergedTree(ev.arcs ?? []);
|
||||
@@ -181,7 +183,9 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.importing = false;
|
||||
this.importPhase = '';
|
||||
this.importProgress = null;
|
||||
this.importError = err?.message ? `Échec de l'import : ${err.message}` : "Échec de l'import du PDF.";
|
||||
this.importError = err?.message
|
||||
? this.translate.instant('campaignImport.errorImportDetail', { message: err.message })
|
||||
: this.translate.instant('campaignImport.errorImport');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -413,7 +417,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
next: () => this.router.navigate(['/campaigns', this.campaignId]),
|
||||
error: () => {
|
||||
this.applying = false;
|
||||
this.applyError = "Échec de la création. La campagne existe-t-elle toujours ?";
|
||||
this.applyError = this.translate.instant('campaignImport.errorApply');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user