Mise en place de l'anglais comme deuxième langue pour l'application
This commit is contained in:
@@ -1,45 +1,45 @@
|
||||
<div class="scene-create-page">
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Créer une nouvelle scène</h1>
|
||||
<h1>{{ 'sceneCreate.title' | translate }}</h1>
|
||||
@if (chapterName) {
|
||||
<p class="chapter-ref">Chapitre : {{ chapterName }}</p>
|
||||
<p class="chapter-ref">{{ 'sceneCreate.chapterRef' | translate:{ name: chapterName } }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="scene-form">
|
||||
|
||||
<div class="field">
|
||||
<label for="scene-create-name">Nom de la scène *</label>
|
||||
<label for="scene-create-name">{{ 'sceneCreate.nameLabel' | translate }}</label>
|
||||
<input
|
||||
id="scene-create-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: Arrivée au village"
|
||||
[placeholder]="'sceneCreate.namePlaceholder' | translate"
|
||||
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="scene-create-description">Description</label>
|
||||
<label for="scene-create-description">{{ 'sceneCreate.descriptionLabel' | translate }}</label>
|
||||
<textarea
|
||||
id="scene-create-description"
|
||||
formControlName="description"
|
||||
placeholder="Décrivez la scène, les événements clés, les PNJ présents..."
|
||||
[placeholder]="'sceneCreate.descriptionPlaceholder' | translate"
|
||||
rows="6">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Icône</label>
|
||||
<label>{{ 'sceneCreate.iconLabel' | translate }}</label>
|
||||
<app-icon-picker [options]="campaignIconOptions" [(selected)]="selectedIcon"></app-icon-picker>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-primary" [disabled]="form.invalid">
|
||||
Créer la scène
|
||||
{{ 'sceneCreate.createButton' | 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>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angula
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { LucideAngularModule } from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
import { CharacterService } from '../../../services/character.service';
|
||||
import { NpcService } from '../../../services/npc.service';
|
||||
@@ -20,7 +21,7 @@ import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-scene-create',
|
||||
imports: [ReactiveFormsModule, LucideAngularModule, IconPickerComponent],
|
||||
imports: [ReactiveFormsModule, LucideAngularModule, IconPickerComponent, TranslatePipe],
|
||||
templateUrl: './scene-create.component.html',
|
||||
styleUrls: ['./scene-create.component.scss']
|
||||
})
|
||||
@@ -44,7 +45,8 @@ export class SceneCreateComponent implements OnInit, OnDestroy {
|
||||
private npcService: NpcService,
|
||||
private randomTableService: RandomTableService,
|
||||
private enemyService: EnemyService,
|
||||
private layoutService: LayoutService
|
||||
private layoutService: LayoutService,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
@@ -69,7 +71,7 @@ export class SceneCreateComponent implements OnInit, OnDestroy {
|
||||
this.chapterName = currentChapter?.name ?? '';
|
||||
this.existingSceneCount = treeData.scenesByChapter[this.chapterId]?.length ?? 0;
|
||||
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId, this.translate));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ scene?.name || 'Scène' }}</h1>
|
||||
<p class="subtitle">Scène</p>
|
||||
<h1>{{ scene?.name || ('sceneEdit.defaultTitle' | translate) }}</h1>
|
||||
<p class="subtitle">{{ 'sceneEdit.subtitle' | translate }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-ai"
|
||||
(click)="toggleChat()"
|
||||
[class.active]="chatOpen"
|
||||
title="Ouvrir l'Assistant IA pour dialoguer autour de cette scène">
|
||||
[title]="'sceneEdit.aiAssistantTitle' | translate">
|
||||
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
||||
Assistant IA
|
||||
{{ 'sceneEdit.aiAssistant' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
|
||||
<button type="button" class="btn-danger" (click)="delete()">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
{{ 'common.delete' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-primary" (click)="submit()" [disabled]="form.invalid">
|
||||
Sauvegarder
|
||||
{{ 'common.save' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,119 +28,118 @@
|
||||
|
||||
<!-- Illustrations (galerie editable, rendu editorial) -->
|
||||
<div class="field">
|
||||
<label>Illustrations</label>
|
||||
<label>{{ 'sceneEdit.illustrationsLabel' | translate }}</label>
|
||||
<app-image-gallery
|
||||
[imageIds]="illustrationImageIds"
|
||||
[editable]="true"
|
||||
[layout]="'EDITORIAL'"
|
||||
(imageIdsChange)="illustrationImageIds = $event">
|
||||
</app-image-gallery>
|
||||
<small class="field-hint">Portraits des PNJ, ambiance visuelle, scenes evocatrices...</small>
|
||||
<small class="field-hint">{{ 'sceneEdit.illustrationsHint' | translate }}</small>
|
||||
</div>
|
||||
|
||||
<!-- Cartes & plans (galerie editable, rendu maps) -->
|
||||
<div class="field">
|
||||
<label>Cartes & plans</label>
|
||||
<label>{{ 'sceneEdit.mapsLabel' | translate }}</label>
|
||||
<app-image-gallery
|
||||
[imageIds]="mapImageIds"
|
||||
[editable]="true"
|
||||
[layout]="'MAPS'"
|
||||
(imageIdsChange)="mapImageIds = $event">
|
||||
</app-image-gallery>
|
||||
<small class="field-hint">Plans du lieu, cartes tactiques, schemas utilisables a la table.</small>
|
||||
<small class="field-hint">{{ 'sceneEdit.mapsHint' | translate }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="scene-edit-name">Titre de la scène *</label>
|
||||
<label for="scene-edit-name">{{ 'sceneEdit.nameLabel' | translate }}</label>
|
||||
<input
|
||||
id="scene-edit-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: Arrivée au village"
|
||||
[placeholder]="'sceneEdit.namePlaceholder' | translate"
|
||||
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="scene-edit-description">Description courte *</label>
|
||||
<label for="scene-edit-description">{{ 'sceneEdit.descriptionLabel' | translate }}</label>
|
||||
<textarea
|
||||
id="scene-edit-description"
|
||||
formControlName="description"
|
||||
placeholder="Résumé en une ou deux phrases de ce qui se passe..."
|
||||
[placeholder]="'sceneEdit.descriptionPlaceholder' | translate"
|
||||
rows="3">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Icône</label>
|
||||
<label>{{ 'sceneEdit.iconLabel' | translate }}</label>
|
||||
<app-icon-picker [options]="campaignIconOptions" [(selected)]="selectedIcon"></app-icon-picker>
|
||||
</div>
|
||||
|
||||
<!-- Section : Contexte et ambiance -->
|
||||
<app-expandable-section title="Contexte et ambiance" icon="📍" [initiallyOpen]="true">
|
||||
<app-expandable-section [title]="'sceneEdit.contextSectionTitle' | translate" icon="📍" [initiallyOpen]="true">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="scene-edit-location">Lieu</label>
|
||||
<input id="scene-edit-location" type="text" formControlName="location" placeholder="Ex: Taverne du Dragon d'Or" />
|
||||
<label for="scene-edit-location">{{ 'sceneEdit.locationLabel' | translate }}</label>
|
||||
<input id="scene-edit-location" type="text" formControlName="location" [placeholder]="'sceneEdit.locationPlaceholder' | translate" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="scene-edit-timing">Moment</label>
|
||||
<input id="scene-edit-timing" type="text" formControlName="timing" placeholder="Ex: Soir, à la tombée de la nuit" />
|
||||
<label for="scene-edit-timing">{{ 'sceneEdit.timingLabel' | translate }}</label>
|
||||
<input id="scene-edit-timing" type="text" formControlName="timing" [placeholder]="'sceneEdit.timingPlaceholder' | translate" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="scene-edit-atmosphere">Ambiance et atmosphère</label>
|
||||
<label for="scene-edit-atmosphere">{{ 'sceneEdit.atmosphereLabel' | translate }}</label>
|
||||
<textarea
|
||||
id="scene-edit-atmosphere"
|
||||
formControlName="atmosphere"
|
||||
placeholder="Décrivez l'ambiance générale de la scène (sons, odeurs, lumière, émotions...)"
|
||||
[placeholder]="'sceneEdit.atmospherePlaceholder' | translate"
|
||||
rows="4">
|
||||
</textarea>
|
||||
</div>
|
||||
</app-expandable-section>
|
||||
|
||||
<!-- Section : Narration pour les joueurs -->
|
||||
<app-expandable-section title="Narration pour les joueurs" icon="📖">
|
||||
<app-expandable-section [title]="'sceneEdit.narrationSectionTitle' | translate" icon="📖">
|
||||
<div class="field">
|
||||
<textarea
|
||||
formControlName="playerNarration"
|
||||
placeholder="Le texte que vous lirez aux joueurs pour planter le décor de cette scène..."
|
||||
[placeholder]="'sceneEdit.narrationPlaceholder' | translate"
|
||||
rows="6">
|
||||
</textarea>
|
||||
<small class="field-hint">Ce texte peut être lu directement à vos joueurs.</small>
|
||||
<small class="field-hint">{{ 'sceneEdit.narrationHint' | translate }}</small>
|
||||
</div>
|
||||
</app-expandable-section>
|
||||
|
||||
<!-- Section : Notes et secrets du MJ (privé) -->
|
||||
<app-expandable-section title="Notes et secrets du MJ" icon="🔒" variant="private">
|
||||
<app-expandable-section [title]="'sceneEdit.gmNotesSectionTitle' | translate" icon="🔒" variant="private">
|
||||
<div class="field">
|
||||
<textarea
|
||||
formControlName="gmSecretNotes"
|
||||
placeholder="Informations cachées, indices, éléments secrets que les joueurs ne doivent pas connaître..."
|
||||
[placeholder]="'sceneEdit.gmNotesPlaceholder' | translate"
|
||||
rows="5">
|
||||
</textarea>
|
||||
<small class="field-hint">Ces notes sont privées et visibles uniquement par le MJ.</small>
|
||||
<small class="field-hint">{{ 'sceneEdit.gmNotesHint' | translate }}</small>
|
||||
</div>
|
||||
</app-expandable-section>
|
||||
|
||||
<!-- Section : Choix et conséquences -->
|
||||
<app-expandable-section title="Choix et conséquences" icon="🔀">
|
||||
<app-expandable-section [title]="'sceneEdit.choicesSectionTitle' | translate" icon="🔀">
|
||||
<div class="field">
|
||||
<textarea
|
||||
formControlName="choicesConsequences"
|
||||
placeholder="Décrivez les différentes options qui s'offrent aux joueurs et leurs conséquences..."
|
||||
[placeholder]="'sceneEdit.choicesPlaceholder' | translate"
|
||||
rows="5">
|
||||
</textarea>
|
||||
</div>
|
||||
</app-expandable-section>
|
||||
|
||||
<!-- Section : Branches narratives (graphe intra-chapitre) -->
|
||||
<app-expandable-section title="Branches narratives" icon="🌿">
|
||||
<app-expandable-section [title]="'sceneEdit.branchesSectionTitle' | translate" icon="🌿">
|
||||
@if (siblingScenes.length === 0) {
|
||||
<div class="branches-hint">
|
||||
<small class="field-hint">
|
||||
💡 Il faut au moins une autre scène dans ce chapitre pour créer des branches.
|
||||
Créez d'abord d'autres scènes, puis revenez ici pour les connecter.
|
||||
{{ 'sceneEdit.branchesNoSibling' | translate }}
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
@@ -150,18 +149,18 @@
|
||||
@for (branch of branches; track $index; let i = $index) {
|
||||
<div class="branch-item">
|
||||
<div class="field">
|
||||
<label>Libellé du choix</label>
|
||||
<label>{{ 'sceneEdit.branchLabelLabel' | translate }}</label>
|
||||
<input
|
||||
type="text"
|
||||
[value]="branch.label"
|
||||
(input)="updateBranchLabel(i, $any($event.target).value)"
|
||||
placeholder="Ex: Si les joueurs attaquent le garde" />
|
||||
[placeholder]="'sceneEdit.branchLabelPlaceholder' | translate" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Scène de destination *</label>
|
||||
<label>{{ 'sceneEdit.branchTargetLabel' | translate }}</label>
|
||||
<select
|
||||
(change)="updateBranchTarget(i, $any($event.target).value)">
|
||||
<option value="" [selected]="!branch.targetSceneId">— Choisir une scène —</option>
|
||||
<option value="" [selected]="!branch.targetSceneId">{{ 'sceneEdit.branchTargetPlaceholder' | translate }}</option>
|
||||
@for (s of siblingScenes; track s) {
|
||||
<option
|
||||
[value]="s.id"
|
||||
@@ -170,39 +169,38 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Condition MJ (optionnel)</label>
|
||||
<label>{{ 'sceneEdit.branchConditionLabel' | translate }}</label>
|
||||
<input
|
||||
type="text"
|
||||
[value]="branch.condition || ''"
|
||||
(input)="updateBranchCondition(i, $any($event.target).value)"
|
||||
placeholder="Ex: Jet de Persuasion DD 15 réussi" />
|
||||
[placeholder]="'sceneEdit.branchConditionPlaceholder' | translate" />
|
||||
</div>
|
||||
<button type="button" class="btn-remove-branch" (click)="removeBranch(i)"
|
||||
title="Supprimer cette branche">
|
||||
[title]="'sceneEdit.branchRemoveTitle' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Retirer
|
||||
{{ 'sceneEdit.branchRemove' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-branch" (click)="addBranch()">
|
||||
+ Ajouter une branche
|
||||
{{ 'sceneEdit.branchAdd' | translate }}
|
||||
</button>
|
||||
<small class="field-hint">
|
||||
Chaque branche représente une "sortie" possible depuis cette scène selon l'action des joueurs.
|
||||
Les cibles sont limitées aux scènes du même chapitre.
|
||||
{{ 'sceneEdit.branchesHint' | translate }}
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
</app-expandable-section>
|
||||
|
||||
<!-- Section : Combat ou rencontre -->
|
||||
<app-expandable-section title="Combat ou rencontre" icon="⚔️">
|
||||
<app-expandable-section [title]="'sceneEdit.combatSectionTitle' | translate" icon="⚔️">
|
||||
<div class="field">
|
||||
<label for="scene-edit-combat-difficulty">Difficulté estimée</label>
|
||||
<input id="scene-edit-combat-difficulty" type="text" formControlName="combatDifficulty" placeholder="Ex: Moyenne, 3 gobelins niveau 2" />
|
||||
<label for="scene-edit-combat-difficulty">{{ 'sceneEdit.combatDifficultyLabel' | translate }}</label>
|
||||
<input id="scene-edit-combat-difficulty" type="text" formControlName="combatDifficulty" [placeholder]="'sceneEdit.combatDifficultyPlaceholder' | translate" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Ennemis du bestiaire</label>
|
||||
<label>{{ 'sceneEdit.bestiaryEnemiesLabel' | translate }}</label>
|
||||
<app-enemy-link-picker
|
||||
[value]="enemyIds"
|
||||
[availableEnemies]="availableEnemies"
|
||||
@@ -210,15 +208,15 @@
|
||||
(valueChange)="enemyIds = $event">
|
||||
</app-enemy-link-picker>
|
||||
<small class="field-hint">
|
||||
Référencez des fiches de votre bestiaire — cliquez sur un chip pour ouvrir la fiche.
|
||||
{{ 'sceneEdit.bestiaryEnemiesHint' | translate }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="scene-edit-enemies">Ennemis et créatures (texte libre)</label>
|
||||
<label for="scene-edit-enemies">{{ 'sceneEdit.enemiesLabel' | translate }}</label>
|
||||
<textarea
|
||||
id="scene-edit-enemies"
|
||||
formControlName="enemies"
|
||||
placeholder="Liste des ennemis présents dans cette scène..."
|
||||
[placeholder]="'sceneEdit.enemiesPlaceholder' | translate"
|
||||
rows="4">
|
||||
</textarea>
|
||||
</div>
|
||||
@@ -226,7 +224,7 @@
|
||||
|
||||
<!-- Section : Pages Lore associées (B2 cross-context) -->
|
||||
@if (loreId) {
|
||||
<app-expandable-section title="Pages Lore associées" icon="🔗">
|
||||
<app-expandable-section [title]="'sceneEdit.loreSectionTitle' | translate" icon="🔗">
|
||||
<div class="field">
|
||||
<app-lore-link-picker
|
||||
[value]="relatedPageIds"
|
||||
@@ -235,7 +233,7 @@
|
||||
(valueChange)="relatedPageIds = $event">
|
||||
</app-lore-link-picker>
|
||||
<small class="field-hint">
|
||||
Épinglez ici le lieu, les PNJ ou créatures de cette scène. Cliquez sur un chip pour ouvrir la page.
|
||||
{{ 'sceneEdit.loreHint' | translate }}
|
||||
</small>
|
||||
</div>
|
||||
</app-expandable-section>
|
||||
@@ -244,17 +242,15 @@
|
||||
@if (!loreId) {
|
||||
<div class="field">
|
||||
<small class="field-hint">
|
||||
💡 Cette campagne n'est associée à aucun univers. Associez-la à un Lore dans l'écran de la campagne
|
||||
pour pouvoir épingler des pages du Lore à cette scène.
|
||||
{{ 'sceneEdit.noLoreHint' | translate }}
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Lieu explorable : pièces / donjon -->
|
||||
<app-expandable-section title="Lieu explorable (donjon, crypte…)" icon="🏰" [initiallyOpen]="rooms.length > 0">
|
||||
<app-expandable-section [title]="'sceneEdit.dungeonSectionTitle' | translate" icon="🏰" [initiallyOpen]="rooms.length > 0">
|
||||
<small class="field-hint">
|
||||
Si cette scène représente un lieu à parcourir pièce par pièce, ajoutez-les ici.
|
||||
La scène bascule alors en mode « donjon » côté affichage.
|
||||
{{ 'sceneEdit.dungeonHint' | translate }}
|
||||
</small>
|
||||
<app-rooms-editor
|
||||
[rooms]="rooms"
|
||||
@@ -274,7 +270,7 @@
|
||||
entityType="scene"
|
||||
[entityId]="sceneId"
|
||||
[isOpen]="chatOpen"
|
||||
welcomeMessage="Je vois cette scène. Demande-moi d'enrichir son ambiance, sa narration ou ses choix."
|
||||
[welcomeMessage]="'sceneEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
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 { CharacterService } from '../../../services/character.service';
|
||||
import { NpcService } from '../../../services/npc.service';
|
||||
@@ -33,7 +34,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-scene-edit',
|
||||
imports: [ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, EnemyLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent, RoomsEditorComponent],
|
||||
imports: [ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, EnemyLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent, RoomsEditorComponent, TranslatePipe],
|
||||
templateUrl: './scene-edit.component.html',
|
||||
styleUrls: ['./scene-edit.component.scss']
|
||||
})
|
||||
@@ -45,11 +46,13 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** État drawer chat IA (b5.7 — intégration Campagne). */
|
||||
chatOpen = false;
|
||||
readonly chatQuickSuggestions = [
|
||||
'Propose une ambiance sensorielle immersive pour cette scène',
|
||||
'Suggère une narration d\'ouverture à lire aux joueurs',
|
||||
'Imagine 2 choix avec conséquences marquantes'
|
||||
];
|
||||
get chatQuickSuggestions(): string[] {
|
||||
return [
|
||||
this.translate.instant('sceneEdit.chatSuggestion1'),
|
||||
this.translate.instant('sceneEdit.chatSuggestion2'),
|
||||
this.translate.instant('sceneEdit.chatSuggestion3')
|
||||
];
|
||||
}
|
||||
|
||||
toggleChat(): void { this.chatOpen = !this.chatOpen; }
|
||||
|
||||
@@ -91,7 +94,8 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
private pageService: PageService,
|
||||
private layoutService: LayoutService,
|
||||
private pageTitleService: PageTitleService,
|
||||
private confirmDialog: ConfirmDialogService
|
||||
private confirmDialog: ConfirmDialogService,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
@@ -174,7 +178,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
enemies: scene.enemies ?? ''
|
||||
});
|
||||
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId, this.translate));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,10 +212,10 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
delete(): void {
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer la scène',
|
||||
message: `Supprimer la scène "${this.scene?.name}" ?`,
|
||||
details: ['Cette action est irréversible.'],
|
||||
confirmLabel: 'Supprimer',
|
||||
title: this.translate.instant('sceneEdit.deleteTitle'),
|
||||
message: this.translate.instant('sceneEdit.deleteMessage', { name: this.scene?.name }),
|
||||
details: [this.translate.instant('sceneEdit.deleteIrreversible')],
|
||||
confirmLabel: this.translate.instant('common.delete'),
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
}
|
||||
{{ scene.name }}
|
||||
</h1>
|
||||
<p class="view-subtitle">Scène</p>
|
||||
<p class="view-subtitle">{{ 'sceneView.subtitle' | translate }}</p>
|
||||
</div>
|
||||
<div class="view-actions">
|
||||
<button type="button" class="btn-primary" (click)="editMode()">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
{{ 'common.edit' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deleteScene()" title="Supprimer la scène">
|
||||
<button type="button" class="btn-danger" (click)="deleteScene()" [title]="'sceneView.deleteTitleAttr' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
{{ 'common.delete' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -30,17 +30,17 @@
|
||||
<!-- Cartes & plans -->
|
||||
@if ((scene.mapImageIds?.length ?? 0) > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🗺️</span> Cartes & plans</h2>
|
||||
<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> Description</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">📝</span> {{ 'sceneView.descriptionSectionTitle' | translate }}</h2>
|
||||
@if (scene.description?.trim()) {
|
||||
<p class="view-section-body">{{ scene.description }}</p>
|
||||
} @else {
|
||||
<p class="view-section-empty">Non renseigné</p>
|
||||
<p class="view-section-empty">{{ 'sceneView.empty' | translate }}</p>
|
||||
}
|
||||
</section>
|
||||
<!-- Contexte et ambiance -->
|
||||
@@ -48,13 +48,13 @@
|
||||
<div class="view-row">
|
||||
@if (scene.location?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">📍</span> Lieu</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">📍</span> {{ 'sceneView.locationSectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.location }}</p>
|
||||
</section>
|
||||
}
|
||||
@if (scene.timing?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">⏰</span> Moment</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">⏰</span> {{ 'sceneView.timingSectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.timing }}</p>
|
||||
</section>
|
||||
}
|
||||
@@ -62,21 +62,21 @@
|
||||
}
|
||||
@if (scene.atmosphere?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🌫️</span> Ambiance et atmosphère</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🌫️</span> {{ 'sceneView.atmosphereSectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.atmosphere }}</p>
|
||||
</section>
|
||||
}
|
||||
<!-- Narration pour les joueurs -->
|
||||
@if (scene.playerNarration?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">📖</span> Narration pour les joueurs</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">📖</span> {{ 'sceneView.narrationSectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.playerNarration }}</p>
|
||||
</section>
|
||||
}
|
||||
<!-- Choix et conséquences -->
|
||||
@if (scene.choicesConsequences?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🔀</span> Choix et conséquences</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🔀</span> {{ 'sceneView.choicesSectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.choicesConsequences }}</p>
|
||||
</section>
|
||||
}
|
||||
@@ -84,13 +84,13 @@
|
||||
@if (scene.combatDifficulty?.trim() || scene.enemies?.trim() || linkedEnemies.length > 0) {
|
||||
@if (scene.combatDifficulty?.trim()) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">⚔️</span> Difficulté estimée</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">⚔️</span> {{ 'sceneView.combatDifficultySectionTitle' | translate }}</h2>
|
||||
<p class="view-section-body">{{ scene.combatDifficulty }}</p>
|
||||
</section>
|
||||
}
|
||||
@if (scene.enemies?.trim() || linkedEnemies.length > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🐲</span> Ennemis et créatures</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🐲</span> {{ 'sceneView.enemiesSectionTitle' | translate }}</h2>
|
||||
@if (linkedEnemies.length > 0) {
|
||||
<div class="view-chips">
|
||||
@for (e of linkedEnemies; track e.id) {
|
||||
@@ -112,7 +112,7 @@
|
||||
<section class="view-section view-section--private">
|
||||
<h2 class="view-section-title">
|
||||
<span class="view-section-icon">🔒</span>
|
||||
Notes et secrets du MJ
|
||||
{{ 'sceneView.gmNotesSectionTitle' | translate }}
|
||||
</h2>
|
||||
<p class="view-section-body">{{ scene.gmSecretNotes }}</p>
|
||||
</section>
|
||||
@@ -120,7 +120,7 @@
|
||||
<!-- Pages Lore liées -->
|
||||
@if (loreId && (scene.relatedPageIds?.length ?? 0) > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🔗</span> Pages Lore associées</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🔗</span> {{ 'sceneView.loreSectionTitle' | translate }}</h2>
|
||||
<div class="view-chips">
|
||||
@for (relId of scene.relatedPageIds; track relId) {
|
||||
<a class="view-chip"
|
||||
@@ -134,7 +134,7 @@
|
||||
<!-- Lieu explorable : pièces (mode donjon) -->
|
||||
@if ((scene.rooms?.length ?? 0) > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🏰</span> Pièces du lieu</h2>
|
||||
<h2 class="view-section-title"><span class="view-section-icon">🏰</span> {{ 'sceneView.roomsSectionTitle' | translate }}</h2>
|
||||
<div class="rooms-readonly">
|
||||
@for (r of scene.rooms; track r; let i = $index) {
|
||||
<article class="room-readonly">
|
||||
@@ -143,7 +143,7 @@
|
||||
<span class="room-readonly-name">{{ r.name }}</span>
|
||||
@if (r.floor !== null && r.floor !== undefined) {
|
||||
<span class="room-readonly-floor">
|
||||
Étage {{ r.floor }}
|
||||
{{ 'sceneView.roomFloor' | translate:{ floor: r.floor } }}
|
||||
</span>
|
||||
}
|
||||
</header>
|
||||
@@ -153,7 +153,7 @@
|
||||
<div class="room-readonly-grid">
|
||||
@if (r.enemies?.trim() || roomLinkedEnemies(r).length > 0) {
|
||||
<div>
|
||||
<strong>⚔️ Ennemis</strong>
|
||||
<strong>{{ 'sceneView.roomEnemies' | translate }}</strong>
|
||||
@if (roomLinkedEnemies(r).length > 0) {
|
||||
<div class="view-chips">
|
||||
@for (e of roomLinkedEnemies(r); track e.id) {
|
||||
@@ -171,32 +171,32 @@
|
||||
}
|
||||
@if (r.loot?.trim()) {
|
||||
<div>
|
||||
<strong>💰 Loot</strong>
|
||||
<strong>{{ 'sceneView.roomLoot' | translate }}</strong>
|
||||
<p>{{ r.loot }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (r.traps?.trim()) {
|
||||
<div>
|
||||
<strong>⚠️ Pièges</strong>
|
||||
<strong>{{ 'sceneView.roomTraps' | translate }}</strong>
|
||||
<p>{{ r.traps }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (r.gmNotes?.trim()) {
|
||||
<div class="room-readonly-private">
|
||||
<strong>🔒 Notes MJ</strong>
|
||||
<strong>{{ 'sceneView.roomGmNotes' | translate }}</strong>
|
||||
<p>{{ r.gmNotes }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if ((r.branches?.length ?? 0) > 0) {
|
||||
<div class="room-readonly-branches">
|
||||
<strong>→ Sorties</strong>
|
||||
<strong>{{ 'sceneView.roomExits' | translate }}</strong>
|
||||
<ul>
|
||||
@for (b of r.branches; track b) {
|
||||
<li>
|
||||
<em>{{ b.label }}</em> → {{ roomNameById(scene, b.targetRoomId) }}
|
||||
@if (b.condition?.trim()) {
|
||||
<span class="branch-cond">(si : {{ b.condition }})</span>
|
||||
<span class="branch-cond">{{ 'sceneView.roomExitCondition' | translate:{ condition: b.condition } }}</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { LucideAngularModule, Pencil, Trash2 } from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { resolveCampaignIcon } from '../../campaign-icons';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
import { CharacterService } from '../../../services/character.service';
|
||||
@@ -26,7 +27,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-scene-view',
|
||||
imports: [RouterModule, LucideAngularModule, ImageGalleryComponent],
|
||||
imports: [RouterModule, LucideAngularModule, ImageGalleryComponent, TranslatePipe],
|
||||
templateUrl: './scene-view.component.html',
|
||||
styleUrls: ['./scene-view.component.scss']
|
||||
})
|
||||
@@ -57,7 +58,8 @@ export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
private pageService: PageService,
|
||||
private layoutService: LayoutService,
|
||||
private pageTitleService: PageTitleService,
|
||||
private confirmDialog: ConfirmDialogService
|
||||
private confirmDialog: ConfirmDialogService,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -98,12 +100,12 @@ export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
this.availableEnemies = treeData.enemies ?? [];
|
||||
this.pageTitleService.set(scene.name);
|
||||
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId, this.translate));
|
||||
});
|
||||
}
|
||||
|
||||
titleOfRelated(pageId: string): string {
|
||||
return this.availablePages.find(p => p.id === pageId)?.title ?? '(page supprimée)';
|
||||
return this.availablePages.find(p => p.id === pageId)?.title ?? this.translate.instant('sceneView.deletedPage');
|
||||
}
|
||||
|
||||
/** Fiches du bestiaire liées à la rencontre (IDs orphelins ignorés). */
|
||||
@@ -128,7 +130,7 @@ export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Résout le nom d'une pièce cible (pour afficher les sorties inter-pièces). */
|
||||
roomNameById(scene: Scene | null, roomId: string): string {
|
||||
return scene?.rooms?.find(r => r.id === roomId)?.name ?? '(pièce supprimée)';
|
||||
return scene?.rooms?.find(r => r.id === roomId)?.name ?? this.translate.instant('sceneView.deletedRoom');
|
||||
}
|
||||
|
||||
editMode(): void {
|
||||
@@ -143,10 +145,10 @@ export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
if (!this.scene) return;
|
||||
const scene = this.scene;
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer la scène',
|
||||
message: `Supprimer la scène "${scene.name}" ?`,
|
||||
details: ['Cette action est irréversible.'],
|
||||
confirmLabel: 'Supprimer',
|
||||
title: this.translate.instant('sceneView.deleteTitle'),
|
||||
message: this.translate.instant('sceneView.deleteMessage', { name: scene.name }),
|
||||
details: [this.translate.instant('sceneView.deleteIrreversible')],
|
||||
confirmLabel: this.translate.instant('common.delete'),
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
|
||||
Reference in New Issue
Block a user