// Rôle : normalise, compacte et sérialise les données toolbox persistées. const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", combo: "o", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" }; const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]); const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]); const NOTEPAD_RGB_COLOR_MAP = { "rgb(245, 247, 255)": "#f5f7ff", "rgb(180, 189, 211)": "#b4bdd3", "rgb(246, 196, 83)": "#f6c453", "rgb(34, 211, 238)": "#22d3ee", "rgb(139, 92, 246)": "#8b5cf6", "rgb(217, 70, 239)": "#d946ef", "rgb(16, 20, 38)": "#101426", "rgb(32, 39, 69)": "#202745" }; const TIMER_TABS = new Set(["stopwatch", "countdown"]); const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]); const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]); const TASK_TYPES = new Set(["unique", "daily", "weekly"]); const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]); const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]); const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1; const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00"; const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/"; const TOOLBOX_ICON_FILES = [ "toolbox.png", "ball.png", "capture.png", "card.png", "city.png", "compass.png", "crash.png", "detective.png", "fire.png", "horror.png", "jump.png", "mining.png", "parachute.png", "puzzle.png", "shield.png", "sword.png", "target.png", "tower.png", "wheel.png" ]; const DEFAULT_MODULE_TITLES = { notepad: "Bloc notes", checklist: "Checklist", images: "Images", links: "Liens", counters: "Compteurs", combos: "Combos", calculator: "Calculateur", table: "Tableau", timer: "Timer", taskPlanner: "Planificateur de tâches", imageAnnotation: "Annotation d'images" }; export const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`); export const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`; function randomToken(length = 6) { const bytes = new Uint8Array(length); if (globalThis.crypto?.getRandomValues) { globalThis.crypto.getRandomValues(bytes); return [...bytes].map((byte) => ID_ALPHABET[byte % ID_ALPHABET.length]).join(""); } return Math.random().toString(36).slice(2, 2 + length).padEnd(length, "0"); } export function uid(prefix) { return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`; } function getDefaultModuleTitle(type) { return DEFAULT_MODULE_TITLES[type] || "Outil"; } export function normalizeToolboxIcon(icon) { const value = String(icon || "").trim(); const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop(); return TOOLBOX_ICON_FILES.includes(file) ? `${TOOLBOX_ICON_BASE}${file}` : DEFAULT_TOOLBOX_ICON; } function normalizeToolboxModule(module) { if (!module || typeof module !== "object" || !module.type) return null; const title = String(module.title || "").trim(); const normalized = { id: module.id || uid("mod"), type: module.type }; if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title; if (module.scrollable === true) normalized.scrollable = true; return normalized; } export function normalizeToolbox(toolbox) { if (!toolbox || typeof toolbox !== "object") return null; return { id: toolbox.id || uid("tbx"), name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox", icon: normalizeToolboxIcon(toolbox.icon), moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2, modules: (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean), updatedAt: toolbox.updatedAt || new Date().toISOString() }; } export function compactToolboxForStorage(toolbox) { const normalized = normalizeToolbox(toolbox); if (!normalized) return null; const compact = { id: normalized.id, name: normalized.name, modules: normalized.modules, updatedAt: normalized.updatedAt }; if (normalized.moduleColumns === 1) compact.moduleColumns = 1; if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon; return compact; } export function compactToolboxesForStorage(toolboxes) { return (Array.isArray(toolboxes) ? toolboxes : []).map(compactToolboxForStorage).filter(Boolean); } export function moduleStorageKey(toolboxId, moduleId) { return `${toolboxId}:${moduleId}`; } export function globalModuleKey(toolboxId, moduleId) { return `${toolboxId}:${moduleId}`; } function parsePositiveInt(value, fallback = 1) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function escapeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function decodeBasicEntities(value) { return String(value || "") .replace(/ /gi, " ") .replace(/</gi, "<") .replace(/>/gi, ">") .replace(/"/gi, "\"") .replace(/'/g, "'") .replace(/&/gi, "&"); } function normalizeStyleAttribute(value) { const declarations = String(value || "") .split(";") .map((item) => item.trim()) .filter(Boolean); const allowed = []; declarations.forEach((declaration) => { const [property, rawValue] = declaration.split(":").map((item) => item?.trim().toLowerCase()); if (!["color", "background-color"].includes(property)) return; const color = NOTEPAD_RGB_COLOR_MAP[rawValue] || rawValue; if (!NOTEPAD_ALLOWED_COLORS.has(color)) return; allowed.push(`${property}: ${color}`); }); return allowed.join("; "); } export function sanitizeNotepadHtml(value) { const source = String(value || "") .replace(//gi, "") .replace(//gi, ""); let html = ""; let cursor = 0; source.replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttributes, offset) => { html += escapeHtml(source.slice(cursor, offset)); cursor = offset + match.length; const tag = rawTag.toLowerCase(); if (!NOTEPAD_ALLOWED_TAGS.has(tag)) return ""; if (match.startsWith("`; return ""; } if (tag === "br") { html += "
"; return ""; } const styleMatch = String(rawAttributes || "").match(/\sstyle=(?:"([^"]*)"|'([^']*)')/i); const style = normalizeStyleAttribute(styleMatch?.[1] || styleMatch?.[2] || ""); html += style ? `<${tag} style="${style}">` : `<${tag}>`; return ""; }); html += escapeHtml(source.slice(cursor)); return html.trim(); } function textToNotepadHtml(value) { return String(value || "") .split(/\r?\n/) .map((line) => line ? `

${escapeHtml(line)}

` : "


") .join(""); } function notepadHtmlToText(value) { const withBreaks = String(value || "") .replace(//gi, "\n") .replace(/<\/(p|div|h3|h4|li)>/gi, "\n") .replace(/]*>/gi, "- "); return decodeBasicEntities(withBreaks.replace(/<[^>]*>/g, "")) .split("\n") .map((line) => line.trimEnd()) .join("\n") .replace(/\n{3,}/g, "\n\n") .trim(); } function normalizeIsoDate(value) { const text = String(value || "").trim(); if (!text) return ""; const timestamp = Date.parse(text); return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : ""; } function normalizeDrawingPoint(point, options = {}) { const x = Number(point?.x); const y = Number(point?.y); if (!Number.isFinite(x) || !Number.isFinite(y)) return null; const max = Number(options.max); return { x: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(x * 100) / 100)), y: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(y * 100) / 100)) }; } function normalizeDrawingStroke(stroke, options = {}) { const points = (Array.isArray(stroke?.points) ? stroke.points : []).map((point) => normalizeDrawingPoint(point, options)).filter(Boolean); if (points.length < 2) return null; const color = NOTEPAD_ALLOWED_COLORS.has(String(stroke?.color || "").toLowerCase()) ? String(stroke.color).toLowerCase() : "#f6c453"; const width = Math.min(24, Math.max(1, Number(stroke?.width) || 4)); return { id: stroke?.id || uid("stroke"), color, width: Math.round(width * 10) / 10, points }; } export function normalizeDrawingData(data, options = {}) { return { strokes: (Array.isArray(data?.strokes) ? data.strokes : []).map((stroke) => normalizeDrawingStroke(stroke, options)).filter(Boolean) }; } export function normalizeNotepadData(data) { const source = data && typeof data === "object" ? data : {}; const legacyText = String(source.text || ""); const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText)); const text = notepadHtmlToText(html) || legacyText.trim(); const updatedAt = normalizeIsoDate(source.updatedAt); const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary"; const drawings = normalizeDrawingData(source.drawings); return { html, text, updatedAt, drawingMode, drawings }; } export function clampQty(value, target) { const parsed = Number.parseInt(value, 10); const safeValue = Number.isFinite(parsed) ? parsed : 0; return Math.min(safeValue, Math.max(1, target)); } function normalizeChecklistItem(item) { const qtyTarget = Math.max(1, parsePositiveInt(item?.qtyTarget, 1)); const qtyCurrent = clampQty(item?.qtyCurrent, qtyTarget); return { id: item?.id || uid("item"), label: String(item?.label || "").trim(), qtyTarget, qtyCurrent }; } function normalizeChecklistSection(section, fallbackTitle = "") { const normalized = { id: section?.id || uid("section"), title: String(section?.title ?? fallbackTitle).trim(), items: (section?.items || []).map(normalizeChecklistItem).filter((item) => item.label) }; if (typeof section?.hideWhenComplete === "boolean") normalized.hideWhenComplete = section.hideWhenComplete; if (section?.collapsed === true) normalized.collapsed = true; return normalized; } export function normalizeChecklistData(data) { const hideCompletedSections = Boolean(data?.hideCompletedSections); const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully); const sections = Array.isArray(data?.sections) ? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title) : []; const simpleItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label); if (!sections.length && simpleItems.length) return { hideCompletedSections, hideCompletedSectionsFully, sections: [{ id: uid("section"), title: "", items: simpleItems }], items: simpleItems }; const items = sections.flatMap((section) => section.items); return { hideCompletedSections, hideCompletedSectionsFully, sections, items }; } export function normalizeUrl(value) { const cleanValue = String(value || "").trim(); if (!cleanValue) return ""; try { const url = new URL(cleanValue.includes("://") ? cleanValue : `https://${cleanValue}`); if (url.protocol !== "http:" && url.protocol !== "https:") return ""; return url.href; } catch { return ""; } } export function hostnameFromUrl(value) { try { return new URL(value).hostname.replace(/^www\./, ""); } catch { return value; } } export function normalizeLinksData(data) { return { links: (data?.links || []) .map((link) => { const url = normalizeUrl(link?.url); if (!url) return null; return { id: link?.id || uid("link"), title: String(link?.title || "").trim(), url }; }) .filter(Boolean) }; } function normalizeCounterValue(value) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : 0; } export function normalizeCountersData(data) { return { counters: (data?.counters || []) .map((counter) => ({ id: counter?.id || uid("counter"), label: String(counter?.label || "").trim(), value: normalizeCounterValue(counter?.value) })) .filter((counter) => counter.label) }; } function normalizeComboInput(input) { const kind = COMBO_INPUT_KINDS.has(input?.kind) ? input.kind : ""; const value = String(input?.value || "").trim().slice(0, 24); return kind && value ? { kind, value } : null; } function normalizeComboSteps(inputs) { return (Array.isArray(inputs) ? inputs : []) .slice(0, 64) .map((step) => (Array.isArray(step) ? step : []) .map(normalizeComboInput) .filter(Boolean) .slice(0, 8)) .filter((step) => step.length); } function normalizeCombo(combo) { const name = String(combo?.name || "").trim().slice(0, 80); const device = COMBO_DEVICES.has(combo?.device) ? combo.device : ""; const inputs = normalizeComboSteps(combo?.inputs); if (!name && !inputs.length) return null; const normalized = { id: combo?.id || uid("combo"), name: name || "Combo", inputs }; if (device) normalized.device = device; return normalized; } function normalizeComboCategory(category) { const title = String(category?.title || "").trim().slice(0, 80); const combos = (Array.isArray(category?.combos) ? category.combos : []).map(normalizeCombo).filter(Boolean); if (!combos.length) return null; return { id: category?.id || uid("section"), title: title || "Catégorie", collapsed: category?.collapsed === true, combos }; } export function normalizeCombosData(data) { return { device: COMBO_DEVICES.has(data?.device) ? data.device : "playstation", combos: (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean), categories: (Array.isArray(data?.categories) ? data.categories : []).map(normalizeComboCategory).filter(Boolean) }; } function normalizeCalculatorValue(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; } function clampTableSize(value, fallback, max) { const parsed = Number.parseInt(value, 10); return Number.isInteger(parsed) ? Math.min(max, Math.max(1, parsed)) : fallback; } function columnIndexToName(index) { let value = Math.max(0, Number(index) || 0) + 1; let name = ""; while (value > 0) { const remainder = (value - 1) % 26; name = String.fromCharCode(65 + remainder) + name; value = Math.floor((value - 1) / 26); } return name; } function tableCellAddress(rowIndex, columnIndex) { return `${columnIndexToName(columnIndex)}${rowIndex + 1}`; } function normalizeTableLabels(labels, length, getFallback) { const sourceLabels = labels && typeof labels === "object" ? labels : {}; const normalized = {}; for (let index = 0; index < length; index += 1) { const value = String(sourceLabels[index] || "").slice(0, 80); if (value.trim() && value !== String(getFallback(index))) normalized[index] = value; } return normalized; } function isTimeString(value, allowWildcard = false) { const pattern = allowWildcard ? /^(?:\d{2}|X):(?:\d{2}|X):(?:\d{2}|X)$/ : /^\d{2}:\d{2}:\d{2}$/; if (!pattern.test(String(value || ""))) return false; return String(value).split(":").every((part, index) => { if (part === "X") return allowWildcard; const number = Number(part); return Number.isInteger(number) && number >= 0 && number <= (index === 0 ? 23 : 59); }); } function isClockTimeString(value) { if (!/^\d{2}:\d{2}$/.test(String(value || ""))) return false; const [hours, minutes] = String(value).split(":").map(Number); return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; } function normalizeWeekDay(value, fallback = DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) { const parsed = Number.parseInt(value, 10); return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback; } function normalizeTimestamp(value) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } export function labelFromFileName(name) { if (!name || name === "image.png") return ""; return name.replace(/\.[^.]+$/, "").trim(); } export function normalizeCalculatorData(data) { const entries = (data?.entries || []) .map((entry) => ({ id: entry?.id || uid("calc"), parentId: String(entry?.parentId || ""), label: String(entry?.label || "").trim(), value: normalizeCalculatorValue(entry?.value) })); const entryIds = new Set(entries.map((entry) => entry.id)); return { scrollResults: data?.scrollResults === true, entries: entries.map((entry) => entryIds.has(entry.parentId) ? entry : { ...entry, parentId: "" }) }; } export function normalizeTableData(data) { const rows = clampTableSize(data?.rows, 10, 50); const columns = clampTableSize(data?.columns, 6, 20); const sourceCells = data?.cells && typeof data.cells === "object" ? data.cells : {}; const cells = {}; for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) { for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) { const address = tableCellAddress(rowIndex, columnIndex); const value = String(sourceCells[address] || "").trim(); if (value) cells[address] = value.slice(0, 500); } } return { rows, columns, cells, rowLabels: normalizeTableLabels(data?.rowLabels, rows, (rowIndex) => rowIndex + 1), columnLabels: normalizeTableLabels(data?.columnLabels, columns, columnIndexToName) }; } export function normalizeTimerData(data) { const stopwatch = data?.stopwatch || {}; const laps = (Array.isArray(stopwatch.laps) ? stopwatch.laps : []) .map((lap) => ({ id: lap?.id || uid("timer"), label: String(lap?.label || "").trim(), elapsedMs: Math.max(0, Number(lap?.elapsedMs) || 0) })) .filter((lap) => lap.elapsedMs > 0); const countdowns = (Array.isArray(data?.countdowns) ? data.countdowns : []) .map((countdown) => { const type = COUNTDOWN_TYPES.has(countdown?.type) ? countdown.type : "duration"; const normalized = { id: countdown?.id || uid("timer"), label: String(countdown?.label || "Timer").trim() || "Timer", type, alertMode: TIMER_ALERT_MODES.has(countdown?.alertMode) ? countdown.alertMode : "off" }; if (type === "duration") { const durationMs = Math.max(0, Number(countdown?.durationMs) || 0); const targetAt = Math.max(0, Number(countdown?.targetAt) || 0); return durationMs > 0 && targetAt > 0 ? { ...normalized, durationMs, targetAt } : null; } if (type === "daily_time") { const time = String(countdown?.time || "").trim(); const targetAt = Math.max(0, Number(countdown?.targetAt) || 0); return isTimeString(time) && targetAt > 0 ? { ...normalized, time, targetAt } : null; } if (type === "time_pattern") { const pattern = String(countdown?.pattern || "").trim().toUpperCase(); return isTimeString(pattern, true) ? { ...normalized, pattern } : null; } if (type === "interval") { const intervalMs = Math.max(0, Number(countdown?.intervalMs) || 0); const anchorAt = Math.max(0, Number(countdown?.anchorAt) || 0); return intervalMs > 0 ? { ...normalized, intervalMs, anchorAt } : null; } return null; }) .filter(Boolean); return { activeTab: TIMER_TABS.has(data?.activeTab) ? data.activeTab : "stopwatch", scrollResults: data?.scrollResults === true, sortResults: data?.sortResults === true, stopwatch: { elapsedMs: Math.max(0, Number(stopwatch.elapsedMs) || 0), startedAt: Math.max(0, Number(stopwatch.startedAt) || 0), laps }, countdowns }; } function normalizeTaskPlannerTask(task) { const title = String(task?.title || "").trim() || "Tâche"; const type = TASK_TYPES.has(task?.type) ? task.type : "unique"; const category = String(task?.category || "").trim(); const normalized = { id: task?.id || uid("task"), title, description: String(task?.description || "").trim(), type, checked: task?.checked === true, checkedAt: normalizeTimestamp(task?.checkedAt) }; if (category) normalized.category = category; if (type === "weekly" && task?.weeklyResetDay !== undefined) normalized.weeklyResetDay = normalizeWeekDay(task.weeklyResetDay); if (!normalized.checked) normalized.checkedAt = 0; return normalized; } function normalizeTaskPlannerCategoryOrder(value, tasks) { const categories = new Set(tasks.map((task) => task.category).filter(Boolean)); const ordered = []; (Array.isArray(value) ? value : []).forEach((category) => { const normalized = String(category || "").trim(); if (normalized && categories.has(normalized) && !ordered.includes(normalized)) ordered.push(normalized); }); tasks.forEach((task) => { if (task.category && !ordered.includes(task.category)) ordered.push(task.category); }); return ordered; } function normalizeTaskPlannerRelations(data, taskIds) { const seen = new Set(); const seenChildren = new Set(); return (Array.isArray(data?.relations) ? data.relations : []) .map((relation) => { const fromTaskId = String(relation?.fromTaskId || ""); const toTaskId = String(relation?.toTaskId || ""); if (!taskIds.has(fromTaskId) || !taskIds.has(toTaskId) || fromTaskId === toTaskId) return null; const key = `${fromTaskId}:${toTaskId}`; if (seen.has(key) || seenChildren.has(fromTaskId)) return null; seen.add(key); seenChildren.add(fromTaskId); return { id: relation?.id || uid("relation"), fromTaskId, toTaskId, prerequisite: relation?.prerequisite === true }; }) .filter(Boolean); } export function normalizeTaskPlannerData(data) { const rawTasks = (Array.isArray(data?.tasks) ? data.tasks : []).map(normalizeTaskPlannerTask).filter(Boolean); const rawTaskIds = new Set(rawTasks.map((task) => task.id)); const relations = normalizeTaskPlannerRelations(data, rawTaskIds); const childTaskIds = new Set(relations.map((relation) => relation.fromTaskId)); const tasks = rawTasks.map((task) => { if (!childTaskIds.has(task.id) || !task.category) return task; const normalized = { ...task }; delete normalized.category; return normalized; }); const taskIds = new Set(tasks.map((task) => task.id)); const categoryOrder = normalizeTaskPlannerCategoryOrder(data?.categoryOrder, tasks); return { weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay), resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME, lastResetAt: normalizeTimestamp(data?.lastResetAt), hideCompleted: data?.hideCompleted === true, collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : []) .map((category) => String(category || "").trim()) .filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index), categoryOrder, tasks, relations: relations.filter((relation) => taskIds.has(relation.fromTaskId) && taskIds.has(relation.toTaskId)) }; } function clampPercent(value) { const parsed = Number(value); if (!Number.isFinite(parsed)) return 0; return Math.min(100, Math.max(0, parsed)); } export function normalizeImageAnnotationData(data) { const image = String(data?.image || ""); const markers = (data?.markers || []) .map((marker) => ({ id: marker?.id || uid("marker"), x: clampPercent(marker?.x), y: clampPercent(marker?.y), label: String(marker?.label || "").trim() })); return { image, markers, drawings: normalizeDrawingData(data?.drawings, { max: 100 }) }; } function compactChecklistItemForStorage(item) { const normalized = normalizeChecklistItem(item); const compact = { id: normalized.id, label: normalized.label }; if (normalized.qtyTarget !== 1) compact.qtyTarget = normalized.qtyTarget; if (normalized.qtyCurrent !== 0) compact.qtyCurrent = normalized.qtyCurrent; return compact; } function compactChecklistSectionForStorage(section) { const normalized = normalizeChecklistSection(section); const compact = { id: normalized.id, items: normalized.items.map(compactChecklistItemForStorage) }; if (normalized.title) compact.title = normalized.title; if (typeof normalized.hideWhenComplete === "boolean") compact.hideWhenComplete = normalized.hideWhenComplete; if (normalized.collapsed === true) compact.collapsed = true; return compact; } export function compactModuleDataForStorage(type, value) { if (type === "notepad") { const normalized = normalizeNotepadData(value); const compact = {}; if (normalized.html && normalized.text) compact.html = normalized.html; if (normalized.text) compact.text = normalized.text; if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt; if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent"; if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings; return Object.keys(compact).length ? compact : null; } if (type === "checklist") { const normalized = normalizeChecklistData(value); const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title); if (!sections.length) return null; const settings = { ...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}), ...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {}) }; if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items }; return { ...settings, sections }; } if (type === "images") { const images = (Array.isArray(value?.images) ? value.images : []) .filter((image) => image?.dataUrl) .map((image) => { const compact = { id: image.id || uid("image"), dataUrl: image.dataUrl }; if (image.label) compact.label = image.label; return compact; }); return images.length ? { images } : null; } if (type === "imageAnnotation") { const normalized = normalizeImageAnnotationData(value); if (!normalized.image) return null; const compact = { image: normalized.image, markers: normalized.markers.map((marker) => { const compact = { id: marker.id, x: marker.x, y: marker.y }; if (marker.label) compact.label = marker.label; return compact; }) }; if (normalized.drawings.strokes.length) compact.drawings = normalized.drawings; return compact; } if (type === "links") { const links = normalizeLinksData(value).links.map((link) => { const compact = { id: link.id, url: link.url }; if (link.title) compact.title = link.title; return compact; }); return links.length ? { links } : null; } if (type === "counters") { const counters = normalizeCountersData(value).counters.map((counter) => ({ id: counter.id, label: counter.label, value: counter.value })); return counters.length ? { counters } : null; } if (type === "combos") { const normalized = normalizeCombosData(value); const compact = { combos: normalized.combos.map((combo) => { const compactCombo = { id: combo.id, name: combo.name, inputs: combo.inputs }; if (combo.device) compactCombo.device = combo.device; return compactCombo; }), categories: normalized.categories.map((category) => { const compactCategory = { id: category.id, title: category.title, combos: category.combos.map((combo) => { const compactCombo = { id: combo.id, name: combo.name, inputs: combo.inputs }; if (combo.device) compactCombo.device = combo.device; return compactCombo; }) }; if (category.collapsed) compactCategory.collapsed = true; return compactCategory; }) }; if (normalized.device !== "playstation") compact.device = normalized.device; if (!compact.combos.length) delete compact.combos; if (!compact.categories.length) delete compact.categories; return compact.combos || compact.categories ? compact : null; } if (type === "calculator") { const normalized = normalizeCalculatorData(value); const entries = normalized.entries.map((entry) => { const compact = { id: entry.id, label: entry.label, value: entry.value }; if (entry.parentId) compact.parentId = entry.parentId; return compact; }); if (!entries.length && !normalized.scrollResults) return null; return normalized.scrollResults ? { entries, scrollResults: true } : { entries }; } if (type === "table") { const normalized = normalizeTableData(value); const compact = { cells: normalized.cells }; if (normalized.rows !== 10) compact.rows = normalized.rows; if (normalized.columns !== 6) compact.columns = normalized.columns; if (Object.keys(normalized.rowLabels).length) compact.rowLabels = normalized.rowLabels; if (Object.keys(normalized.columnLabels).length) compact.columnLabels = normalized.columnLabels; return Object.keys(compact.cells).length || compact.rows || compact.columns || compact.rowLabels || compact.columnLabels ? compact : null; } if (type === "timer") { const normalized = normalizeTimerData(value); const compact = {}; if (normalized.activeTab !== "stopwatch") compact.activeTab = normalized.activeTab; if (normalized.scrollResults) compact.scrollResults = true; if (normalized.sortResults) compact.sortResults = true; if (normalized.stopwatch.elapsedMs || normalized.stopwatch.startedAt || normalized.stopwatch.laps.length) { compact.stopwatch = { ...(normalized.stopwatch.elapsedMs ? { elapsedMs: normalized.stopwatch.elapsedMs } : {}), ...(normalized.stopwatch.startedAt ? { startedAt: normalized.stopwatch.startedAt } : {}), ...(normalized.stopwatch.laps.length ? { laps: normalized.stopwatch.laps } : {}) }; } if (normalized.countdowns.length) { compact.countdowns = normalized.countdowns.map((countdown) => { if (countdown.alertMode === "off") { const { alertMode, ...compactCountdown } = countdown; return compactCountdown; } return countdown; }); } return Object.keys(compact).length ? compact : null; } if (type === "taskPlanner") { const normalized = normalizeTaskPlannerData(value); const compact = {}; if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay; if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime; if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt; if (normalized.hideCompleted) compact.hideCompleted = true; if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories; if (normalized.tasks.length) { compact.tasks = normalized.tasks.map((task) => { const compactTask = { id: task.id, title: task.title, type: task.type }; if (task.description) compactTask.description = task.description; if (task.category) compactTask.category = task.category; if (task.checked) compactTask.checked = true; if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt; if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay; return compactTask; }); } if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder; if (normalized.relations.length) { compact.relations = normalized.relations.map((relation) => { const compactRelation = { id: relation.id, fromTaskId: relation.fromTaskId, toTaskId: relation.toTaskId }; if (relation.prerequisite) compactRelation.prerequisite = true; return compactRelation; }); } return Object.keys(compact).length ? compact : null; } return value; } export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") { const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId); return compactModuleDataForStorage(moduleType || module?.type, value); } function createExportIdFactory() { const counts = {}; return (prefix) => { counts[prefix] = (counts[prefix] || 0) + 1; return `${ID_PREFIXES[prefix] || "x"}${counts[prefix].toString(36)}`; }; } function remapModuleDataForExport(type, data, nextId) { const compact = compactModuleDataForStorage(type, data); if (!compact) return null; if (type === "checklist") { if (compact.sections) { return { sections: compact.sections.map((section) => ({ ...section, id: nextId("section"), items: section.items.map((item) => ({ ...item, id: nextId("item") })) })) }; } return { items: compact.items.map((item) => ({ ...item, id: nextId("item") })) }; } if (type === "images") { return { images: compact.images.map((image) => ({ ...image, id: nextId("image") })) }; } if (type === "imageAnnotation") { const remapped = { ...compact, markers: compact.markers.map((marker) => ({ ...marker, id: nextId("marker") })) }; if (compact.drawings?.strokes) { remapped.drawings = { strokes: compact.drawings.strokes.map((stroke) => ({ ...stroke, id: nextId("stroke") })) }; } return remapped; } if (type === "links") { return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) }; } if (type === "counters") { return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) }; } if (type === "combos") { const remapped = {}; if (compact.device) remapped.device = compact.device; if (compact.combos) remapped.combos = compact.combos.map((combo) => ({ ...combo, id: nextId("combo") })); if (compact.categories) { remapped.categories = compact.categories.map((category) => ({ ...category, id: nextId("section"), combos: category.combos.map((combo) => ({ ...combo, id: nextId("combo") })) })); } return remapped; } if (type === "calculator") { const entryIdMap = new Map(); compact.entries.forEach((entry) => entryIdMap.set(entry.id, nextId("calc"))); const remapped = { entries: compact.entries.map((entry) => { const remapped = { ...entry, id: entryIdMap.get(entry.id) }; if (entry.parentId) remapped.parentId = entryIdMap.get(entry.parentId) || ""; return remapped; }) }; if (compact.scrollResults) remapped.scrollResults = true; return remapped; } if (type === "timer") { const remapped = { ...compact }; if (compact.stopwatch?.laps) { remapped.stopwatch = { ...compact.stopwatch, laps: compact.stopwatch.laps.map((lap) => ({ ...lap, id: nextId("timer") })) }; } if (compact.countdowns) { remapped.countdowns = compact.countdowns.map((countdown) => ({ ...countdown, id: nextId("timer") })); } return remapped; } if (type === "taskPlanner") { const taskIdMap = new Map(); compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task"))); const remapped = { ...compact }; if (compact.tasks) remapped.tasks = compact.tasks.map((task) => ({ ...task, id: taskIdMap.get(task.id) })); if (compact.relations) { remapped.relations = compact.relations .map((relation) => ({ ...relation, id: nextId("relation"), fromTaskId: taskIdMap.get(relation.fromTaskId), toTaskId: taskIdMap.get(relation.toTaskId) })) .filter((relation) => relation.fromTaskId && relation.toTaskId); } return remapped; } return compact; } export function createToolboxExportPayload(toolbox, moduleData) { const source = normalizeToolbox(toolbox); const nextId = createExportIdFactory(); const moduleIdMap = new Map(); const exportedToolbox = compactToolboxForStorage({ ...source, id: nextId("tbx"), modules: source.modules.map((module) => { const id = nextId("mod"); moduleIdMap.set(module.id, id); return { ...module, id }; }) }); const modules = Object.fromEntries(source.modules .map((module) => [ moduleIdMap.get(module.id), remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId) ]) .filter(([, data]) => data != null)); return { toolbox: exportedToolbox, modules }; } export function createGlobalExportPayload(toolboxes, links, moduleData) { const modules = {}; toolboxes.forEach((toolbox) => { toolbox.modules.forEach((module) => { const data = compactModuleDataForStorage(module.type, moduleData[moduleStorageKey(toolbox.id, module.id)] || null); if (data) modules[globalModuleKey(toolbox.id, module.id)] = data; }); }); return { version: 1, exportedAt: new Date().toISOString(), toolboxes: compactToolboxesForStorage(toolboxes), modules, links }; }