// Rôle : regroupe les actions métier de création, import/export, lien et édition toolbox. import { compactModuleDataForStorage, createGlobalExportPayload, createToolboxExportPayload, globalModuleKey, labelFromFileName, MAX_TOOLBOX_SPACES, normalizeToolbox, remapModuleOrderIds, uid } from "./storage/toolboxStorage.js"; import { compressImage } from "../../utils/imageCompression.js"; import { getBackupFileName } from "./storage/autoBackup.js"; import { addModuleToToolboxSpace } from "./toolboxSpaceUtils.js"; function downloadJson(payload, filename) { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); } function getLibraryUrl() { if (typeof window === "undefined") return "https://sokko-g.local/#/library"; return `${window.location.origin}${window.location.pathname}#/library`; } export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) { function createToolbox(name, gameId = "", options = {}) { if (!store.ready) return null; const now = new Date().toISOString(); const memosModuleId = uid("mod"); const linksModuleId = uid("mod"); const welcomeText = creationTemplate?.memos?.text || creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox."; const libraryLink = { id: uid("link"), title: creationTemplate?.links?.title || "Librairie des outils", url: getLibraryUrl() }; const toolbox = normalizeToolbox({ id: uid("tbx"), name: name.trim() || "Nouvelle toolbox", updatedAt: now, modules: [ { id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Mémos" }, { id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" } ] }); if (options.tutorial === true) toolbox.tutorial = true; const memos = compactModuleDataForStorage("memos", { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] }); const links = compactModuleDataForStorage("links", { links: [libraryLink] }); void store.persistToolboxSnapshot((current) => ({ toolboxes: [toolbox, ...current.toolboxes], links: gameId ? { ...current.links, [gameId]: toolbox.id } : current.links, moduleData: { ...current.moduleData, [globalModuleKey(toolbox.id, memosModuleId)]: memos, [globalModuleKey(toolbox.id, linksModuleId)]: links } })); return toolbox; } function updateToolbox(nextToolbox) { const updatedAt = new Date().toISOString(); store.setToolboxes((toolboxes) => toolboxes.map((toolbox) => toolbox.id === nextToolbox.id ? { ...nextToolbox, updatedAt } : toolbox)); } function deleteToolbox(id) { return store.persistToolboxSnapshot((current) => { const toolbox = current.toolboxes.find((item) => item.id === id); const moduleData = { ...current.moduleData }; (toolbox?.modules || []).forEach((module) => delete moduleData[`${id}:${module.id}`]); return { toolboxes: current.toolboxes.filter((item) => item.id !== id), moduleData, links: Object.fromEntries(Object.entries(current.links).filter(([, toolboxId]) => toolboxId !== id)) }; }); } function linkToolboxToGame(gameId, toolboxId) { const updatedAt = new Date().toISOString(); return store.persistToolboxSnapshot((current) => { const links = { ...current.links }; const touched = new Set([links[gameId], toolboxId].filter(Boolean)); if (toolboxId) links[gameId] = toolboxId; else delete links[gameId]; return { ...current, links, toolboxes: current.toolboxes.map((toolbox) => touched.has(toolbox.id) ? { ...toolbox, updatedAt } : toolbox) }; }); } async function addImageFiles(toolboxId, moduleId, files) { const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/")); if (!imageFiles.length) return false; const images = []; for (const file of imageFiles) { const image = { id: uid("image"), dataUrl: await compressImage(file) }; const label = labelFromFileName(file.name); if (label) image.label = label; images.unshift(image); } const data = store.getModuleData(toolboxId, moduleId, { images: [] }); return store.updateModuleData(toolboxId, moduleId, { ...data, images: [...images, ...(Array.isArray(data.images) ? data.images : [])] }); } async function createChecklistFromResults(gameId, checklist) { const toolboxId = store.links[gameId] || ""; const toolbox = store.toolboxes.find((item) => item.id === toolboxId); if (!toolbox || !checklist) return false; const moduleId = uid("mod"); const sourceSections = Array.isArray(checklist.sections) ? checklist.sections : [{ title: "", items: checklist.items || [] }]; const sections = sourceSections.map((category) => ({ id: uid("section"), title: category.title || "", items: (category.items || []).map((item) => ({ id: uid("item"), label: typeof item === "object" && item !== null ? String(item.label || item.name || "").trim() : String(item || "").trim(), qtyTarget: Math.max(1, Number.parseInt(item?.quantity ?? item?.qtyTarget, 10) || 1), qtyCurrent: 0 })).filter((item) => item.label) })).filter((section) => section.items.length); if (!sections.length) return false; const title = String(checklist.title || "Checklist").trim() || "Checklist"; const updatedAt = new Date().toISOString(); const data = compactModuleDataForStorage("checklist", { sections }); let created = false; const saved = await store.persistToolboxSnapshot((current) => { if (current.links[gameId] !== toolboxId || !current.toolboxes.some((item) => item.id === toolboxId)) return current; created = true; return { ...current, toolboxes: current.toolboxes.map((item) => item.id === toolboxId ? { ...addModuleToToolboxSpace(item, { id: moduleId, type: "checklist", title }, item.activeSpaceId), updatedAt } : item), moduleData: { ...current.moduleData, [globalModuleKey(toolboxId, moduleId)]: data } }; }); if (saved && created) notify("Checklist créée dans la toolbox associée."); return Boolean(saved && created); } function createChecklistFromList(gameId, list) { const toolboxId = store.links[gameId] || ""; const toolbox = store.toolboxes.find((item) => item.id === toolboxId); if (!toolbox || !list) return false; setConfirmModal({ title: "Créer une checklist", message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`, confirmLabel: "Créer", onResolve: (confirmed) => { if (!confirmed) return; void createChecklistFromResults(gameId, { title: list.title || "Checklist", sections: (list.categories || []).map((category) => ({ title: category.title || "", items: category.items || [] })) }); } }); return true; } async function importToolboxPayload(file, gameId = "") { const payload = JSON.parse(await file.text()); if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide"); if (Array.isArray(payload.toolbox.spaces) && payload.toolbox.spaces.length > MAX_TOOLBOX_SPACES) { throw new Error(`Une toolbox ne peut pas contenir plus de ${MAX_TOOLBOX_SPACES} espaces.`); } const imported = normalizeToolbox({ ...payload.toolbox, id: uid("tbx"), updatedAt: new Date().toISOString() }); const moduleIdMap = new Map(); imported.modules = imported.modules.map((module) => { const nextId = uid("mod"); moduleIdMap.set(module.id, nextId); return { ...module, id: nextId }; }); const spaceIdMap = new Map(imported.spaces.map((space) => [space.id, uid("space")])); imported.spaces = imported.spaces.map((space) => ({ ...space, id: spaceIdMap.get(space.id), moduleOrder: remapModuleOrderIds(space.moduleOrder, moduleIdMap) })); imported.defaultSpaceId = spaceIdMap.get(imported.defaultSpaceId); imported.activeSpaceId = spaceIdMap.get(imported.activeSpaceId); const normalizedImport = normalizeToolbox(imported); const nextModuleData = {}; Object.entries(payload.modules || {}).forEach(([oldId, data]) => { const nextId = moduleIdMap.get(oldId); const module = normalizedImport.modules.find((item) => item.id === nextId); if (nextId) { const compact = compactModuleDataForStorage(module?.type, data); if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact; } }); const saved = await store.persistToolboxSnapshot((current) => ({ toolboxes: [normalizedImport, ...current.toolboxes], links: gameId ? { ...current.links, [gameId]: normalizedImport.id } : current.links, moduleData: { ...current.moduleData, ...nextModuleData } })); if (!saved) return null; return normalizedImport; } async function importAllToolboxesPayload(file) { const payload = JSON.parse(await file.text()); if (!Array.isArray(payload.toolboxes) || !payload.modules || typeof payload.modules !== "object") { throw new Error("Format d'import global invalide"); } if (payload.toolboxes.some((toolbox) => Array.isArray(toolbox.spaces) && toolbox.spaces.length > MAX_TOOLBOX_SPACES)) { throw new Error(`Une toolbox ne peut pas contenir plus de ${MAX_TOOLBOX_SPACES} espaces.`); } const toolboxIdMap = new Map(); const moduleIdMap = new Map(); const importedToolboxes = payload.toolboxes.map((toolbox) => { const nextToolboxId = uid("tbx"); toolboxIdMap.set(toolbox.id, nextToolboxId); const localModuleIdMap = new Map(); const modules = (toolbox.modules || []).map((module) => { const nextModuleId = uid("mod"); moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId); localModuleIdMap.set(module.id, nextModuleId); return { ...module, id: nextModuleId }; }); const normalizedSource = normalizeToolbox({ ...toolbox, modules: toolbox.modules || [] }); const spaceIdMap = new Map(normalizedSource.spaces.map((space) => [space.id, uid("space")])); const spaces = normalizedSource.spaces.map((space) => ({ ...space, id: spaceIdMap.get(space.id), moduleOrder: remapModuleOrderIds(space.moduleOrder, localModuleIdMap) })); return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, spaces, defaultSpaceId: spaceIdMap.get(normalizedSource.defaultSpaceId), activeSpaceId: spaceIdMap.get(normalizedSource.activeSpaceId), updatedAt: new Date().toISOString() }); }); const nextLinks = {}; Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => { const nextToolboxId = toolboxIdMap.get(oldToolboxId); if (nextToolboxId) nextLinks[gameId] = nextToolboxId; }); const nextModuleData = {}; Object.entries(payload.modules).forEach(([key, data]) => { const [oldToolboxId] = key.split(":"); const nextToolboxId = toolboxIdMap.get(oldToolboxId); const nextModuleId = moduleIdMap.get(key); const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId); const module = toolbox?.modules.find((item) => item.id === nextModuleId); const compact = compactModuleDataForStorage(module?.type, data); if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact; }); const saved = await store.persistToolboxSnapshot((current) => ({ toolboxes: [...importedToolboxes, ...current.toolboxes], links: { ...current.links, ...nextLinks }, moduleData: { ...current.moduleData, ...nextModuleData } })); if (!saved) throw new Error("Impossible d'enregistrer l'import global."); } return { createToolbox, updateToolbox, deleteToolbox, linkToolboxToGame, setCreateModal, setConfirmModal, setLinkModalGameId, setDrawerGameId, setImage, createChecklistFromResults, createChecklistFromList, addImageFiles, ensureModuleData: store.ensureModuleData, isModuleDataReady: store.isModuleDataReady, notify, removeModuleData: store.removeModuleData, updateModuleData: store.updateModuleData, updateToolboxOrder: (orderedIds) => { const order = new Map(orderedIds.map((id, index) => [id, index])); store.setToolboxes((toolboxes) => [...toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999))); }, importToolbox: async (file, gameId = "") => { if (!store.ready) return null; try { const toolbox = await importToolboxPayload(file, gameId); if (toolbox) notify("Toolbox importée."); return toolbox; } catch (error) { setConfirmModal({ title: "Import impossible", message: error.message, confirmLabel: "Compris", cancelLabel: "Fermer", danger: true }); return null; } }, importAllToolboxes: async (file) => { if (!store.ready) return false; try { await importAllToolboxesPayload(file); notify("Import global terminé."); return true; } catch (error) { setConfirmModal({ title: "Import global impossible", message: error.message, confirmLabel: "Compris", cancelLabel: "Fermer", danger: true }); return false; } }, exportToolbox: async (id) => { if (!store.ready) return false; const snapshot = await store.getExportSnapshot(id); if (!snapshot) return false; const toolbox = normalizeToolbox(snapshot.toolboxes.find((item) => item.id === id)); if (!toolbox) return; downloadJson(createToolboxExportPayload(toolbox, snapshot.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`); notify("Export de la toolbox lancé."); }, exportAllToolboxes: async () => { if (!store.ready) return false; const snapshot = await store.getExportSnapshot(); if (!snapshot) return false; downloadJson(createGlobalExportPayload(snapshot.toolboxes, snapshot.links, snapshot.moduleData), getBackupFileName()); notify("Export global lancé."); } }; }