357 lines
15 KiB
JavaScript
357 lines
15 KiB
JavaScript
// 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é.");
|
|
}
|
|
};
|
|
}
|