All checks were successful
Corrections fonctionnelles - order initialisé à la création des pages et dossiers de lore (nextOrderFor) : un nouvel élément se place désormais en dernier de sa fratrie au lieu de 0 - storePortrait : type MIME canonique dérivé de l'extension du fichier (un data URL "image/jpg" n'est plus rejeté silencieusement) - takeUntilDestroyed ajouté sur les abonnements paramMap de arc-view, folder-view et campaign-detail (fuites mémoire) Factorisation (suppression de duplication) - shared/folder-grouping.util.ts : groupByFolder + byOrder + byFolderName mutualisés entre npc-list, enemy-list, campaign-detail et la sidebar (folderChildren). Le tri des dossiers est désormais cohérent entre la sidebar et les vues cartes (insensible casse/accents partout) - DataSyncService.onChange()/persist() remplacent le câblage changed$ + reorder recopié dans 5 vues - campaign-detail : loadCampaignBundle()/applyCampaignBundle() éliminent le forkJoin dupliqué entre ngOnInit et reload - ReorderSupport (domain/shared) : squelette générique remplaçant les 8 boucles de réordonnancement copiées dans les services - FoundryExportService : GameSystem résolu une seule fois dans buildBundle - module Foundry : walkScalars mutualise la récursion de flattenStats et flattenStructure (comportements préservés) ; esc() de l'importer aligné sur foundry.utils.escapeHTML Suppression de code mort - characterService/characters retirés de loadCampaignTreeData et de ses 16 appelants (arguments, injections, imports et littéraux CampaignTreeData) - CSS orphelin : .tree-row.cdk-drop-list-receiving et .btn-back - BottomPanel.initiallyOpen (jamais lu) retiré de l'interface et de l'appelant - commentaire trompeur du PDF corrigé Tests - mise à jour de 3 tests obsolètes de campaign-tree.helper.spec qui vérifiaient encore l'ancien comportement (tri alphabétique, non-classés en enfants directs) → alignés sur le modèle actuel (tri par order + pseudo-dossier "Sans dossier")
157 lines
6.2 KiB
TypeScript
157 lines
6.2 KiB
TypeScript
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
|
|
|
import { TranslatePipe } from '@ngx-translate/core';
|
|
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2, Package } from 'lucide-angular';
|
|
import { catchError, of } from 'rxjs';
|
|
import { CampaignService } from '../../services/campaign.service';
|
|
import { CharacterService } from '../../services/character.service';
|
|
import { NpcService } from '../../services/npc.service';
|
|
import { EnemyService } from '../../services/enemy.service';
|
|
import { Character } from '../../services/character.model';
|
|
import { Npc } from '../../services/npc.model';
|
|
import { Arc, Chapter, Scene } from '../../services/campaign.model';
|
|
import { loadCampaignTreeData, CampaignTreeData } from '../../campaigns/campaign-tree.helper';
|
|
import {
|
|
SessionDicePanelComponent, DiceRollResult
|
|
} from '../session-dice-panel/session-dice-panel.component';
|
|
import { SessionAiChatPanelComponent } from '../session-ai-chat-panel/session-ai-chat-panel.component';
|
|
import { SessionRandomTablesPanelComponent } from '../session-random-tables-panel/session-random-tables-panel.component';
|
|
import { SessionItemCatalogsPanelComponent } from '../session-item-catalogs-panel/session-item-catalogs-panel.component';
|
|
|
|
type TabId = 'dice' | 'tables' | 'objects' | 'characters' | 'scenes' | 'ai';
|
|
|
|
/**
|
|
* Panneau latéral du mode jeu : référence rapide en lecture seule.
|
|
*
|
|
* <p>Charge à la volée les PJ/PNJ et l'arbre de scènes de la campagne associée
|
|
* à la session. La navigation vers les fiches s'ouvre dans un nouvel onglet
|
|
* pour ne pas casser le flux de la session en cours.</p>
|
|
*
|
|
* <p>Le sous-composant {@link SessionDicePanelComponent} émet un événement
|
|
* de jet qui remonte ici puis vers le parent via {@link rolled}.</p>
|
|
*/
|
|
@Component({
|
|
selector: 'app-session-reference-panel',
|
|
imports: [LucideAngularModule, TranslatePipe, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent, SessionItemCatalogsPanelComponent],
|
|
templateUrl: './session-reference-panel.component.html',
|
|
styleUrls: ['./session-reference-panel.component.scss']
|
|
})
|
|
export class SessionReferencePanelComponent implements OnChanges {
|
|
readonly User = User;
|
|
readonly Drama = Drama;
|
|
readonly Swords = Swords;
|
|
readonly Dices = Dices;
|
|
readonly ExternalLink = ExternalLink;
|
|
readonly Sparkles = Sparkles;
|
|
readonly Table2 = Table2;
|
|
readonly Package = Package;
|
|
|
|
@Input() campaignId!: string;
|
|
/** Partie active — nécessaire pour charger les PJ (refonte Playthrough). */
|
|
@Input() playthroughId: string | null = null;
|
|
@Input() sessionId!: string;
|
|
@Input() canAddToJournal = true;
|
|
@Output() rolled = new EventEmitter<DiceRollResult>();
|
|
/** Émis quand l'IA répond et que le MJ veut sauvegarder la réponse comme entrée. */
|
|
@Output() aiReplyToJournal = new EventEmitter<string>();
|
|
/** Émis pour consigner un objet de catalogue au journal (entrée NOTE). */
|
|
@Output() noteToJournal = new EventEmitter<string>();
|
|
|
|
activeTab: TabId = 'dice';
|
|
|
|
characters: Character[] = [];
|
|
npcs: Npc[] = [];
|
|
treeData: CampaignTreeData | null = null;
|
|
|
|
loadingChars = false;
|
|
loadingTree = false;
|
|
/** True dès qu'un tab "lourd" a été chargé pour éviter de rappeler l'API en boucle. */
|
|
private charsLoaded = false;
|
|
private treeLoaded = false;
|
|
|
|
constructor(
|
|
private campaignService: CampaignService,
|
|
private characterService: CharacterService,
|
|
private npcService: NpcService,
|
|
private enemyService: EnemyService
|
|
) {}
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
if (changes['campaignId']) {
|
|
this.charsLoaded = false;
|
|
this.treeLoaded = false;
|
|
this.characters = [];
|
|
this.npcs = [];
|
|
this.treeData = null;
|
|
}
|
|
}
|
|
|
|
selectTab(tab: TabId): void {
|
|
this.activeTab = tab;
|
|
if (tab === 'characters') this.ensureCharactersLoaded();
|
|
if (tab === 'scenes') this.ensureTreeLoaded();
|
|
}
|
|
|
|
private ensureCharactersLoaded(): void {
|
|
if (this.charsLoaded || this.loadingChars || !this.campaignId) return;
|
|
this.loadingChars = true;
|
|
// PJ : propres à la Partie. PNJ : campagne-scope.
|
|
const chars$ = this.playthroughId
|
|
? this.characterService.getByPlaythrough(this.playthroughId)
|
|
: of([] as Character[]);
|
|
chars$.pipe(catchError(() => of([] as Character[])))
|
|
.subscribe(list => { this.characters = list; this.tryFinishCharsLoad(); });
|
|
this.npcService.getByCampaign(this.campaignId).pipe(catchError(() => of([] as Npc[])))
|
|
.subscribe(list => { this.npcs = list; this.tryFinishCharsLoad(); });
|
|
}
|
|
|
|
private tryFinishCharsLoad(): void {
|
|
// On considère que le chargement est fini quand au moins une des deux listes
|
|
// a été assignée (vide ou pleine). Le double subscribe ci-dessus garantit
|
|
// qu'on tombe ici deux fois ; idempotent.
|
|
this.loadingChars = false;
|
|
this.charsLoaded = true;
|
|
}
|
|
|
|
private ensureTreeLoaded(): void {
|
|
if (this.treeLoaded || this.loadingTree || !this.campaignId) return;
|
|
this.loadingTree = true;
|
|
loadCampaignTreeData(this.campaignService, this.campaignId, this.npcService, undefined, this.enemyService).pipe(
|
|
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, npcs: [], randomTables: [], enemies: [] } as CampaignTreeData))
|
|
).subscribe(data => {
|
|
this.treeData = data;
|
|
this.loadingTree = false;
|
|
this.treeLoaded = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ouvre une fiche dans un nouvel onglet pour préserver l'écran de session.
|
|
* Le MJ peut consulter sans perdre son journal ni son historique de dés.
|
|
*/
|
|
openInNewTab(path: (string | number)[]): void {
|
|
const url = path.map(p => String(p)).join('/');
|
|
window.open('/' + url, '_blank', 'noopener');
|
|
}
|
|
|
|
/** Helpers de typage pour le template (Angular n'infère pas bien sans). */
|
|
chaptersOf(arc: Arc): Chapter[] {
|
|
return this.treeData?.chaptersByArc[arc.id!] ?? [];
|
|
}
|
|
scenesOf(chapter: Chapter): Scene[] {
|
|
return this.treeData?.scenesByChapter[chapter.id!] ?? [];
|
|
}
|
|
|
|
onDiceRolled(result: DiceRollResult): void {
|
|
this.rolled.emit(result);
|
|
}
|
|
|
|
onAiSaveToJournal(content: string): void {
|
|
this.aiReplyToJournal.emit(content);
|
|
}
|
|
|
|
onItemNote(content: string): void {
|
|
this.noteToJournal.emit(content);
|
|
}
|
|
}
|