/** * Chroniques de l'Étrange — Système FoundryVTT * * Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions. * Ce système FoundryVTT est une implémentation indépendante et n'est pas * affilié à Antre-Monde Éditions, * mais a été réalisé avec l'autorisation d'Antre-Monde Éditions. * * @author LeRatierBretonnien * @copyright 2024–2026 LeRatierBretonnien * @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/ */ import { parseLegacyJson } from "../../migration/migrator.js" const MIGRATION_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-migration-app.html" /** * Dialog for importing legacy CDE actor JSON files into the current system. * * Accessible via the System Settings menu (registerMenu). * Supports multi-file selection and shows a preview table before importing. */ export class CDEMigrationApp extends foundry.applications.api.HandlebarsApplicationMixin( foundry.applications.api.ApplicationV2 ) { static DEFAULT_OPTIONS = { id: "cde-migration-app", classes: ["cde-migration-app"], tag: "div", window: { title: "CDE.MigrationTitle", icon: "fas fa-file-import", resizable: false, }, position: { width: 560, height: "auto" }, actions: { clearFiles: CDEMigrationApp.#clearFiles, doImport: CDEMigrationApp.#doImport, }, } static PARTS = { form: { template: MIGRATION_TEMPLATE }, } /** @type {Array<{name: string, type: string, img: string, system: object, items: object[], _srcFile: string}>} */ #pending = [] /** @type {string[]} - error messages per file */ #errors = [] async _prepareContext(options) { return { pending: this.#pending, errors: this.#errors, hasPending: this.#pending.length > 0, hasErrors: this.#errors.length > 0, count: this.#pending.length, } } /** After render, wire up the file input. */ _onRender(context, options) { super._onRender(context, options) const input = this.element.querySelector(".cde-migration-file-input") input?.addEventListener("change", this.#onFileChange.bind(this)) const dropZone = this.element.querySelector(".cde-migration-drop-zone") if (dropZone) { dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("is-dragover") }) dropZone.addEventListener("dragleave", () => dropZone.classList.remove("is-dragover")) dropZone.addEventListener("drop", (e) => { e.preventDefault() dropZone.classList.remove("is-dragover") this.#processFiles(Array.from(e.dataTransfer.files)) }) } } async #onFileChange(event) { const files = Array.from(event.target.files ?? []) event.target.value = "" await this.#processFiles(files) } async #processFiles(files) { for (const file of files) { if (!file.name.endsWith(".json")) { this.#errors.push(game.i18n.format("CDE.MigrationErrorNotJson", { file: file.name })) continue } try { const text = await file.text() const actors = parseLegacyJson(text) for (const actor of actors) { actor._srcFile = file.name // Avoid duplicates by name if (!this.#pending.some(p => p.name === actor.name)) { this.#pending.push(actor) } } } catch (err) { this.#errors.push(game.i18n.format("CDE.MigrationErrorParse", { file: file.name, error: err.message })) } } this.render() } static async #clearFiles() { this.#pending = [] this.#errors = [] this.render() } static async #doImport() { if (!this.#pending.length) return const created = [] const failed = [] for (const data of this.#pending) { try { const { _srcFile, ...actorData } = data const actor = await Actor.create(actorData) created.push(actor.name) } catch (err) { failed.push(`${data.name}: ${err.message}`) console.error(`CHRONIQUESDELETRANGE | Migration failed for "${data.name}":`, err) } } this.#pending = [] this.#errors = failed this.render() if (created.length) { ui.notifications.info( game.i18n.format("CDE.MigrationSuccess", { count: created.length, names: created.join(", ") }) ) } if (failed.length) { ui.notifications.warn( game.i18n.format("CDE.MigrationPartialError", { count: failed.length }) ) } } }