Split toolbox storage by module
This commit is contained in:
parent
97253acb00
commit
588c1298cd
18 changed files with 1248 additions and 1180 deletions
|
|
@ -1,173 +0,0 @@
|
|||
// Rôle : fournit les compacteurs de données par type d'outil toolbox.
|
||||
export function createModuleDataCompactors(deps) {
|
||||
return {
|
||||
notepad(value) {
|
||||
const normalized = deps.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;
|
||||
},
|
||||
checklist(value) {
|
||||
const normalized = deps.normalizeChecklistData(value);
|
||||
const sections = normalized.sections.map(deps.compactChecklistSectionForStorage).filter((section) => section.items.length);
|
||||
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 };
|
||||
},
|
||||
images(value) {
|
||||
const images = (Array.isArray(value?.images) ? value.images : [])
|
||||
.filter((image) => image?.dataUrl)
|
||||
.map((image) => {
|
||||
const compact = { id: image.id || deps.uid("image"), dataUrl: image.dataUrl };
|
||||
if (image.label) compact.label = image.label;
|
||||
return compact;
|
||||
});
|
||||
return images.length ? { images } : null;
|
||||
},
|
||||
imageAnnotation(value) {
|
||||
const normalized = deps.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;
|
||||
},
|
||||
links(value) {
|
||||
const links = deps.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;
|
||||
},
|
||||
counters(value) {
|
||||
const counters = deps.normalizeCountersData(value).counters.map((counter) => ({
|
||||
id: counter.id,
|
||||
label: counter.label,
|
||||
value: counter.value
|
||||
}));
|
||||
return counters.length ? { counters } : null;
|
||||
},
|
||||
combos(value) {
|
||||
const normalized = deps.normalizeCombosData(value);
|
||||
const compact = {
|
||||
combos: normalized.combos.map((combo) => {
|
||||
const compactCombo = { id: combo.id, name: combo.name, inputs: combo.inputs };
|
||||
if (combo.category) compactCombo.category = combo.category;
|
||||
if (combo.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
})
|
||||
};
|
||||
if (normalized.device !== "playstation") compact.device = normalized.device;
|
||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
||||
if (!compact.combos.length) delete compact.combos;
|
||||
return compact.combos ? compact : null;
|
||||
},
|
||||
calculator(value) {
|
||||
const normalized = deps.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 };
|
||||
},
|
||||
table(value) {
|
||||
const normalized = deps.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;
|
||||
},
|
||||
timer(value) {
|
||||
const normalized = deps.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) => {
|
||||
const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
|
||||
if (alertMode !== "off") compactCountdown.alertMode = alertMode;
|
||||
if (autoRefresh) compactCountdown.autoRefresh = true;
|
||||
if (startMode === "time" && startTime) {
|
||||
compactCountdown.startMode = startMode;
|
||||
compactCountdown.startTime = startTime;
|
||||
}
|
||||
return compactCountdown;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
},
|
||||
taskPlanner(value) {
|
||||
const normalized = deps.normalizeTaskPlannerData(value);
|
||||
const compact = {};
|
||||
if (normalized.weeklyResetDay !== deps.defaultTaskPlannerWeeklyResetDay) compact.weeklyResetDay = normalized.weeklyResetDay;
|
||||
if (normalized.resetTime !== deps.defaultTaskPlannerResetTime) 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 === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime;
|
||||
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;
|
||||
},
|
||||
equipmentPlanner(value) {
|
||||
const normalized = deps.normalizeEquipmentPlannerData(value);
|
||||
if (!normalized.equipments.length) return null;
|
||||
const compact = {
|
||||
types: normalized.types.map((type) => {
|
||||
const compactType = { id: type.id, title: type.title };
|
||||
if (type.icon !== "shield") compactType.icon = type.icon;
|
||||
if (type.collapsed) compactType.collapsed = true;
|
||||
return compactType;
|
||||
}),
|
||||
equipments: normalized.equipments.map(deps.compactEquipmentPlannerEquipmentForStorage)
|
||||
};
|
||||
if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
|
||||
return compact;
|
||||
}
|
||||
};
|
||||
}
|
||||
33
website/src/features/toolboxes/storage/modules/calculator.js
Normal file
33
website/src/features/toolboxes/storage/modules/calculator.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Rôle : normalise et compacte les données du calculateur.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
function normalizeCalculatorValue(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
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 compactCalculatorDataForStorage(value) {
|
||||
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 };
|
||||
}
|
||||
74
website/src/features/toolboxes/storage/modules/checklist.js
Normal file
74
website/src/features/toolboxes/storage/modules/checklist.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Rôle : normalise et compacte les données de checklist.
|
||||
import { parsePositiveInt, uid } from "./shared.js";
|
||||
|
||||
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)
|
||||
: [];
|
||||
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 };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export 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 compactChecklistDataForStorage(value) {
|
||||
const normalized = normalizeChecklistData(value);
|
||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length);
|
||||
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 };
|
||||
}
|
||||
86
website/src/features/toolboxes/storage/modules/combos.js
Normal file
86
website/src/features/toolboxes/storage/modules/combos.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Rôle : normalise et compacte les combos d'inputs.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
|
||||
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||
|
||||
function normalizeComboInput(input) {
|
||||
const kind = COMBO_INPUT_KINDS.has(input?.kind) ? input.kind : "";
|
||||
const value = String(input?.value || "").trim().slice(0, 24);
|
||||
if (!kind || !value) return null;
|
||||
const holdMs = Number(input?.holdMs);
|
||||
const normalizedHoldMs = Number.isFinite(holdMs) && holdMs > 0
|
||||
? Math.min(99000, Math.round(holdMs / 1000) * 1000)
|
||||
: 0;
|
||||
const normalized = { kind, value };
|
||||
if (input?.hold === true || normalizedHoldMs > 0) normalized.hold = true;
|
||||
if (normalizedHoldMs > 0) normalized.holdMs = normalizedHoldMs;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
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 category = String(combo?.category || "").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 (category) normalized.category = category;
|
||||
if (device) normalized.device = device;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeComboCategoryOrder(value, combos) {
|
||||
const categories = combos.map((combo) => combo.category).filter(Boolean);
|
||||
const orderedCategories = (Array.isArray(value) ? value : [])
|
||||
.map((category) => String(category || "").trim().slice(0, 80))
|
||||
.filter((category, index, order) => category && categories.includes(category) && order.indexOf(category) === index);
|
||||
return [
|
||||
...orderedCategories,
|
||||
...categories.filter((category) => !orderedCategories.includes(category))
|
||||
];
|
||||
}
|
||||
|
||||
export function normalizeCombosData(data) {
|
||||
const combos = (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean);
|
||||
const categoryOrder = normalizeComboCategoryOrder(data?.categoryOrder, combos);
|
||||
return {
|
||||
device: COMBO_DEVICES.has(data?.device) ? data.device : "playstation",
|
||||
combos,
|
||||
collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : [])
|
||||
.map((category) => String(category || "").trim().slice(0, 80))
|
||||
.filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index),
|
||||
categoryOrder
|
||||
};
|
||||
}
|
||||
|
||||
export function compactCombosDataForStorage(value) {
|
||||
const normalized = normalizeCombosData(value);
|
||||
const compact = {
|
||||
combos: normalized.combos.map((combo) => {
|
||||
const compactCombo = { id: combo.id, name: combo.name, inputs: combo.inputs };
|
||||
if (combo.category) compactCombo.category = combo.category;
|
||||
if (combo.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
})
|
||||
};
|
||||
if (normalized.device !== "playstation") compact.device = normalized.device;
|
||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
||||
if (!compact.combos.length) delete compact.combos;
|
||||
return compact.combos ? compact : null;
|
||||
}
|
||||
28
website/src/features/toolboxes/storage/modules/counters.js
Normal file
28
website/src/features/toolboxes/storage/modules/counters.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Rôle : normalise et compacte les compteurs toolbox.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
export function compactCountersDataForStorage(value) {
|
||||
const counters = normalizeCountersData(value).counters.map((counter) => ({
|
||||
id: counter.id,
|
||||
label: counter.label,
|
||||
value: counter.value
|
||||
}));
|
||||
return counters.length ? { counters } : null;
|
||||
}
|
||||
34
website/src/features/toolboxes/storage/modules/drawing.js
Normal file
34
website/src/features/toolboxes/storage/modules/drawing.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Rôle : normalise les traits de dessin persistés par les outils annotables.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
export const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
// Rôle : normalise, résume et compacte les données du planificateur d'équipements.
|
||||
import { parsePositiveInt, uid } from "./shared.js";
|
||||
|
||||
const EQUIPMENT_SOCKET_SHAPES = new Set(["ball", "jewel"]);
|
||||
const EQUIPMENT_SOCKET_COLORS = new Set(["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"]);
|
||||
const EQUIPMENT_TYPE_ICONS = new Set(["boot", "chest-armor", "helmet", "glove", "shield", "sword", "gun", "earring", "necklace", "ring"]);
|
||||
const EQUIPMENT_TRAIT_ICONS = new Set(["sword", "shield"]);
|
||||
|
||||
function normalizeEquipmentPlannerType(type) {
|
||||
const title = String(type?.title || "").trim().slice(0, 80);
|
||||
if (!title) return null;
|
||||
const icon = EQUIPMENT_TYPE_ICONS.has(type?.icon) ? type.icon : "shield";
|
||||
return {
|
||||
id: type?.id || uid("equipmentType"),
|
||||
title,
|
||||
icon,
|
||||
collapsed: type?.collapsed === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerValue(value) {
|
||||
if (typeof value === "number") return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : "";
|
||||
const text = String(value ?? "").trim().slice(0, 80);
|
||||
if (!text) return "";
|
||||
const numeric = Number(text.replace(",", "."));
|
||||
return Number.isFinite(numeric) && /^[-+]?\d+(?:[,.]\d+)?$/.test(text) ? Math.round(numeric * 1000) / 1000 : text;
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerTrait(trait, prefix = "trait") {
|
||||
const name = String(trait?.name || "").trim().slice(0, 80);
|
||||
if (!name) return null;
|
||||
return {
|
||||
id: trait?.id || uid(prefix),
|
||||
category: String(trait?.category || "").trim().slice(0, 80),
|
||||
icon: EQUIPMENT_TRAIT_ICONS.has(trait?.icon) ? trait.icon : "sword",
|
||||
name,
|
||||
value: normalizeEquipmentPlannerValue(trait?.value)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerMaterial(material) {
|
||||
const name = String(material?.name || "").trim().slice(0, 80);
|
||||
if (!name) return null;
|
||||
return {
|
||||
id: material?.id || uid("material"),
|
||||
name,
|
||||
qty: Math.max(1, parsePositiveInt(material?.qty, 1))
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerSocketItem(socketItem) {
|
||||
const name = String(socketItem?.name || "").trim().slice(0, 80);
|
||||
if (!name) return null;
|
||||
const shape = EQUIPMENT_SOCKET_SHAPES.has(socketItem?.shape) ? socketItem.shape : "jewel";
|
||||
const color = EQUIPMENT_SOCKET_COLORS.has(socketItem?.color) ? socketItem.color : "yellow";
|
||||
return {
|
||||
id: socketItem?.id || uid("socketItem"),
|
||||
name,
|
||||
shape,
|
||||
color,
|
||||
bonuses: (Array.isArray(socketItem?.bonuses) ? socketItem.bonuses : [])
|
||||
.map((bonus) => normalizeEquipmentPlannerTrait(bonus, "socketBonus"))
|
||||
.filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerCategoryOrder(value, categories) {
|
||||
const available = new Set(categories.filter(Boolean));
|
||||
const ordered = [];
|
||||
(Array.isArray(value) ? value : []).forEach((category) => {
|
||||
const normalized = String(category || "").trim().slice(0, 80);
|
||||
if (normalized && available.has(normalized) && !ordered.includes(normalized)) ordered.push(normalized);
|
||||
});
|
||||
categories.forEach((category) => {
|
||||
if (category && !ordered.includes(category)) ordered.push(category);
|
||||
});
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function normalizeEquipmentPlannerEquipment(equipment, typeIds) {
|
||||
const name = String(equipment?.name || "").trim().slice(0, 100);
|
||||
const typeId = String(equipment?.typeId || "");
|
||||
if (!name || !typeIds.has(typeId)) return null;
|
||||
const characteristics = (Array.isArray(equipment?.characteristics) ? equipment.characteristics : [])
|
||||
.map((trait) => normalizeEquipmentPlannerTrait(trait, "trait"))
|
||||
.filter(Boolean);
|
||||
const socketItems = (Array.isArray(equipment?.socketItems) ? equipment.socketItems : [])
|
||||
.map(normalizeEquipmentPlannerSocketItem)
|
||||
.filter(Boolean);
|
||||
const socketItemIds = new Set(socketItems.map((socketItem) => socketItem.id));
|
||||
const socketLinks = (Array.isArray(equipment?.socketLinks) ? equipment.socketLinks : [])
|
||||
.map((link) => {
|
||||
const fromSocketItemId = String(link?.fromSocketItemId || "");
|
||||
const toSocketItemId = String(link?.toSocketItemId || "");
|
||||
if (!socketItemIds.has(fromSocketItemId) || !socketItemIds.has(toSocketItemId) || fromSocketItemId === toSocketItemId) return null;
|
||||
const pairKey = [fromSocketItemId, toSocketItemId].sort().join(":");
|
||||
return {
|
||||
id: link?.id || uid("socketLink"),
|
||||
fromSocketItemId,
|
||||
toSocketItemId,
|
||||
pairKey
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
|
||||
.map(({ pairKey: _pairKey, ...link }) => link);
|
||||
const categories = [
|
||||
...characteristics.map((trait) => trait.category),
|
||||
...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
|
||||
].filter(Boolean);
|
||||
const categoryOrder = normalizeEquipmentPlannerCategoryOrder(equipment?.categoryOrder, categories);
|
||||
return {
|
||||
id: equipment?.id || uid("equipment"),
|
||||
typeId,
|
||||
name,
|
||||
icon: EQUIPMENT_TYPE_ICONS.has(equipment?.icon) ? equipment.icon : "shield",
|
||||
active: equipment?.active !== false,
|
||||
obtain: String(equipment?.obtain || "").trim().slice(0, 500),
|
||||
characteristics,
|
||||
socketItems,
|
||||
socketLinks,
|
||||
materials: (Array.isArray(equipment?.materials) ? equipment.materials : [])
|
||||
.map(normalizeEquipmentPlannerMaterial)
|
||||
.filter(Boolean),
|
||||
categoryOrder,
|
||||
collapsedCategories: (Array.isArray(equipment?.collapsedCategories) ? equipment.collapsedCategories : [])
|
||||
.map((category) => String(category || "").trim().slice(0, 80))
|
||||
.filter((category, index, collapsedCategories) => category && categoryOrder.includes(category) && collapsedCategories.indexOf(category) === index)
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeEquipmentPlannerData(data) {
|
||||
const rawTypes = (Array.isArray(data?.types) ? data.types : []).map(normalizeEquipmentPlannerType).filter(Boolean);
|
||||
const typeIds = new Set(rawTypes.map((type) => type.id));
|
||||
const equipments = (Array.isArray(data?.equipments) ? data.equipments : [])
|
||||
.map((equipment) => normalizeEquipmentPlannerEquipment(equipment, typeIds))
|
||||
.filter(Boolean);
|
||||
const usedTypeIds = new Set(equipments.map((equipment) => equipment.typeId));
|
||||
const types = rawTypes.filter((type) => usedTypeIds.has(type.id));
|
||||
const orderedTypeIds = (Array.isArray(data?.typeOrder) ? data.typeOrder : [])
|
||||
.map((typeId) => String(typeId || ""))
|
||||
.filter((typeId, index, order) => usedTypeIds.has(typeId) && order.indexOf(typeId) === index);
|
||||
return {
|
||||
types,
|
||||
equipments,
|
||||
typeOrder: [
|
||||
...orderedTypeIds,
|
||||
...types.map((type) => type.id).filter((typeId) => !orderedTypeIds.includes(typeId))
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeEquipmentPlannerData(data) {
|
||||
const normalized = normalizeEquipmentPlannerData(data);
|
||||
const totals = new Map();
|
||||
const textBonuses = [];
|
||||
const activeEquipments = normalized.equipments.filter((equipment) => equipment.active);
|
||||
|
||||
function incrementCount(counts, key) {
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
}
|
||||
|
||||
function pickMostUsedKey(counts) {
|
||||
return [...counts.entries()].reduce((picked, [key, count]) => {
|
||||
if (!picked) return { key, count };
|
||||
if (count !== picked.count) return count > picked.count ? { key, count } : picked;
|
||||
return key.localeCompare(picked.key) < 0 ? { key, count } : picked;
|
||||
}, null);
|
||||
}
|
||||
|
||||
function parseIconKey(key) {
|
||||
const [icon, color = ""] = String(key || "").split(":");
|
||||
return { icon: icon || "sword", color };
|
||||
}
|
||||
|
||||
function pickSummaryIcon(iconSources) {
|
||||
return parseIconKey(pickMostUsedKey(iconSources.direct)?.key || pickMostUsedKey(iconSources.socket)?.key || "sword:");
|
||||
}
|
||||
|
||||
function getSummarySourceKind(sourceKinds) {
|
||||
if (sourceKinds.size !== 1) return "mixed";
|
||||
return sourceKinds.has("direct") ? "direct" : "socket";
|
||||
}
|
||||
|
||||
function collectEntry(entry, contribution, icon, sourceKind, color = "") {
|
||||
if (typeof entry.value === "number") {
|
||||
const existing = totals.get(entry.name);
|
||||
const iconKey = icon || "sword";
|
||||
const colorKey = sourceKind === "socket" ? color || "gray" : "";
|
||||
const iconSources = existing?.iconSources || { direct: new Map(), socket: new Map() };
|
||||
const sourceKinds = existing?.sourceKinds || new Set();
|
||||
const contributions = existing?.contributions || [];
|
||||
sourceKinds.add(sourceKind);
|
||||
incrementCount(iconSources[sourceKind], `${iconKey}:${colorKey}`);
|
||||
const pickedIcon = pickSummaryIcon(iconSources);
|
||||
totals.set(entry.name, {
|
||||
name: entry.name,
|
||||
icon: pickedIcon.icon,
|
||||
color: pickedIcon.color,
|
||||
sourceKind: getSummarySourceKind(sourceKinds),
|
||||
iconSources,
|
||||
sourceKinds,
|
||||
contributions: [
|
||||
...contributions,
|
||||
{
|
||||
id: entry.id,
|
||||
sourceKind,
|
||||
equipmentIcon: contribution.equipmentIcon,
|
||||
equipmentName: contribution.equipmentName,
|
||||
sourceName: contribution.sourceName,
|
||||
value: entry.value
|
||||
}
|
||||
],
|
||||
value: (existing?.value || 0) + entry.value
|
||||
});
|
||||
return;
|
||||
}
|
||||
textBonuses.push({
|
||||
id: entry.id,
|
||||
category: entry.category,
|
||||
name: entry.name,
|
||||
value: entry.value,
|
||||
source: contribution.sourceName
|
||||
});
|
||||
}
|
||||
|
||||
activeEquipments.forEach((equipment) => {
|
||||
equipment.characteristics.forEach((trait) => collectEntry(
|
||||
trait,
|
||||
{ equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: equipment.name },
|
||||
trait.icon || "sword",
|
||||
"direct"
|
||||
));
|
||||
equipment.socketItems.forEach((socketItem) => {
|
||||
socketItem.bonuses.forEach((bonus) => collectEntry(
|
||||
bonus,
|
||||
{ equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: `${equipment.name} · ${socketItem.name}` },
|
||||
socketItem.shape || "jewel",
|
||||
"socket",
|
||||
socketItem.color
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
const numericTotals = [...totals.values()]
|
||||
.map(({ iconSources: _iconSources, sourceKinds: _sourceKinds, color, sourceKind, ...total }) => ({
|
||||
...total,
|
||||
...(color ? { color } : {}),
|
||||
sourceKind
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return {
|
||||
activeEquipmentCount: activeEquipments.length,
|
||||
totals: numericTotals,
|
||||
equipmentTotals: numericTotals.filter((total) => total.sourceKind === "direct"),
|
||||
socketTotals: numericTotals.filter((total) => total.sourceKind === "socket"),
|
||||
mixedTotals: numericTotals.filter((total) => total.sourceKind === "mixed"),
|
||||
textBonuses,
|
||||
socketItems: activeEquipments.flatMap((equipment) => equipment.socketItems.map((socketItem) => ({
|
||||
...socketItem,
|
||||
equipmentId: equipment.id,
|
||||
equipmentName: equipment.name
|
||||
}))),
|
||||
socketLinks: activeEquipments.flatMap((equipment) => equipment.socketLinks.map((link) => ({
|
||||
...link,
|
||||
equipmentId: equipment.id,
|
||||
equipmentName: equipment.name
|
||||
})))
|
||||
};
|
||||
}
|
||||
|
||||
function compactEquipmentPlannerTraitForStorage(trait, prefix = "trait") {
|
||||
const normalized = normalizeEquipmentPlannerTrait(trait, prefix);
|
||||
if (!normalized) return null;
|
||||
const compact = { id: normalized.id, name: normalized.name };
|
||||
if (normalized.category) compact.category = normalized.category;
|
||||
if (normalized.icon !== "sword") compact.icon = normalized.icon;
|
||||
if (normalized.value !== "") compact.value = normalized.value;
|
||||
return compact;
|
||||
}
|
||||
|
||||
function compactEquipmentPlannerMaterialForStorage(material) {
|
||||
const normalized = normalizeEquipmentPlannerMaterial(material);
|
||||
return normalized ? normalized : null;
|
||||
}
|
||||
|
||||
function compactEquipmentPlannerSocketItemForStorage(socketItem) {
|
||||
const normalized = normalizeEquipmentPlannerSocketItem(socketItem);
|
||||
if (!normalized) return null;
|
||||
const compact = {
|
||||
id: normalized.id,
|
||||
name: normalized.name,
|
||||
shape: normalized.shape,
|
||||
color: normalized.color
|
||||
};
|
||||
if (normalized.bonuses.length) {
|
||||
compact.bonuses = normalized.bonuses
|
||||
.map((bonus) => compactEquipmentPlannerTraitForStorage(bonus, "socketBonus"))
|
||||
.filter(Boolean);
|
||||
}
|
||||
return compact;
|
||||
}
|
||||
|
||||
function compactEquipmentPlannerEquipmentForStorage(equipment) {
|
||||
const compact = {
|
||||
id: equipment.id,
|
||||
typeId: equipment.typeId,
|
||||
name: equipment.name
|
||||
};
|
||||
if (equipment.icon !== "shield") compact.icon = equipment.icon;
|
||||
if (equipment.active === false) compact.active = false;
|
||||
if (equipment.obtain) compact.obtain = equipment.obtain;
|
||||
if (equipment.characteristics.length) {
|
||||
compact.characteristics = equipment.characteristics
|
||||
.map((trait) => compactEquipmentPlannerTraitForStorage(trait, "trait"))
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (equipment.socketItems.length) {
|
||||
compact.socketItems = equipment.socketItems
|
||||
.map(compactEquipmentPlannerSocketItemForStorage)
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (equipment.socketLinks.length) compact.socketLinks = equipment.socketLinks;
|
||||
if (equipment.materials.length) compact.materials = equipment.materials.map(compactEquipmentPlannerMaterialForStorage).filter(Boolean);
|
||||
if (equipment.categoryOrder.length) compact.categoryOrder = equipment.categoryOrder;
|
||||
if (equipment.collapsedCategories.length) compact.collapsedCategories = equipment.collapsedCategories;
|
||||
return compact;
|
||||
}
|
||||
|
||||
export function compactEquipmentPlannerDataForStorage(value) {
|
||||
const normalized = normalizeEquipmentPlannerData(value);
|
||||
if (!normalized.equipments.length) return null;
|
||||
const compact = {
|
||||
types: normalized.types.map((type) => {
|
||||
const compactType = { id: type.id, title: type.title };
|
||||
if (type.icon !== "shield") compactType.icon = type.icon;
|
||||
if (type.collapsed) compactType.collapsed = true;
|
||||
return compactType;
|
||||
}),
|
||||
equipments: normalized.equipments.map(compactEquipmentPlannerEquipmentForStorage)
|
||||
};
|
||||
if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
|
||||
return compact;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Rôle : normalise et compacte les images simples et annotations d'image.
|
||||
import { normalizeDrawingData } from "./drawing.js";
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
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 }) };
|
||||
}
|
||||
|
||||
export function compactImagesDataForStorage(value) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function compactImageAnnotationDataForStorage(value) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function labelFromFileName(name) {
|
||||
if (!name || name === "image.png") return "";
|
||||
return name.replace(/\.[^.]+$/, "").trim();
|
||||
}
|
||||
27
website/src/features/toolboxes/storage/modules/index.js
Normal file
27
website/src/features/toolboxes/storage/modules/index.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Rôle : expose le registre strict des compacteurs par type d'outil toolbox.
|
||||
import { compactCalculatorDataForStorage } from "./calculator.js";
|
||||
import { compactChecklistDataForStorage } from "./checklist.js";
|
||||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
import { compactEquipmentPlannerDataForStorage } from "./equipmentPlanner.js";
|
||||
import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js";
|
||||
import { compactLinksDataForStorage } from "./links.js";
|
||||
import { compactNotepadDataForStorage } from "./notepad.js";
|
||||
import { compactTableDataForStorage } from "./table.js";
|
||||
import { compactTaskPlannerDataForStorage } from "./taskPlanner.js";
|
||||
import { compactTimerDataForStorage } from "./timer.js";
|
||||
|
||||
export const MODULE_DATA_COMPACTORS = {
|
||||
notepad: compactNotepadDataForStorage,
|
||||
checklist: compactChecklistDataForStorage,
|
||||
images: compactImagesDataForStorage,
|
||||
imageAnnotation: compactImageAnnotationDataForStorage,
|
||||
links: compactLinksDataForStorage,
|
||||
counters: compactCountersDataForStorage,
|
||||
combos: compactCombosDataForStorage,
|
||||
calculator: compactCalculatorDataForStorage,
|
||||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
equipmentPlanner: compactEquipmentPlannerDataForStorage
|
||||
};
|
||||
48
website/src/features/toolboxes/storage/modules/links.js
Normal file
48
website/src/features/toolboxes/storage/modules/links.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Rôle : normalise et compacte les liens toolbox.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
export function compactLinksDataForStorage(value) {
|
||||
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;
|
||||
}
|
||||
119
website/src/features/toolboxes/storage/modules/notepad.js
Normal file
119
website/src/features/toolboxes/storage/modules/notepad.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Rôle : normalise, nettoie et compacte les données du bloc-notes riche.
|
||||
import { NOTEPAD_ALLOWED_COLORS, normalizeDrawingData } from "./drawing.js";
|
||||
import { normalizeIsoDate } from "./shared.js";
|
||||
|
||||
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"
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.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(/<script[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<style[\s\S]*?<\/style>/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("</")) {
|
||||
if (tag !== "br") html += `</${tag}>`;
|
||||
return "";
|
||||
}
|
||||
if (tag === "br") {
|
||||
html += "<br>";
|
||||
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 ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
|
||||
.join("");
|
||||
}
|
||||
|
||||
function notepadHtmlToText(value) {
|
||||
const withBreaks = String(value || "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/(p|div|h3|h4|li)>/gi, "\n")
|
||||
.replace(/<li[^>]*>/gi, "- ");
|
||||
return decodeBasicEntities(withBreaks.replace(/<[^>]*>/g, ""))
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd())
|
||||
.join("\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
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 compactNotepadDataForStorage(value) {
|
||||
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;
|
||||
}
|
||||
56
website/src/features/toolboxes/storage/modules/shared.js
Normal file
56
website/src/features/toolboxes/storage/modules/shared.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Rôle : fournit les primitives communes de normalisation et d'identifiants du stockage toolbox.
|
||||
export 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",
|
||||
equipmentType: "y",
|
||||
equipment: "q",
|
||||
trait: "v",
|
||||
material: "n",
|
||||
socketItem: "j",
|
||||
socketLink: "u",
|
||||
socketBonus: "b"
|
||||
};
|
||||
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
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()}`;
|
||||
}
|
||||
|
||||
export function parsePositiveInt(value, fallback = 1) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
export function normalizeIsoDate(value) {
|
||||
const text = String(value || "").trim();
|
||||
if (!text) return "";
|
||||
const timestamp = Date.parse(text);
|
||||
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : "";
|
||||
}
|
||||
|
||||
export function normalizeTimestamp(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
63
website/src/features/toolboxes/storage/modules/table.js
Normal file
63
website/src/features/toolboxes/storage/modules/table.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Rôle : normalise et compacte les cellules et libellés de l'outil table.
|
||||
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;
|
||||
}
|
||||
|
||||
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] || "");
|
||||
if (value.trim()) 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 compactTableDataForStorage(value) {
|
||||
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;
|
||||
}
|
||||
129
website/src/features/toolboxes/storage/modules/taskPlanner.js
Normal file
129
website/src/features/toolboxes/storage/modules/taskPlanner.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Rôle : normalise et compacte les données persistées du task planner.
|
||||
import { normalizeTimestamp, uid } from "./shared.js";
|
||||
|
||||
export const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1;
|
||||
export const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00";
|
||||
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
|
||||
|
||||
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 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 === "daily" && isClockTimeString(task?.dailyResetTime)) normalized.dailyResetTime = task.dailyResetTime;
|
||||
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))
|
||||
};
|
||||
}
|
||||
|
||||
export function compactTaskPlannerDataForStorage(value) {
|
||||
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 === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime;
|
||||
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;
|
||||
}
|
||||
114
website/src/features/toolboxes/storage/modules/timer.js
Normal file
114
website/src/features/toolboxes/storage/modules/timer.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Rôle : normalise et compacte les chronomètres et comptes à rebours.
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
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 TIMER_INTERVAL_START_MODES = new Set(["now", "time"]);
|
||||
const TIMER_MIN_AUTO_REFRESH_MS = 5 * 60 * 1000;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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, autoRefresh: countdown?.autoRefresh === true } : 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);
|
||||
const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
|
||||
const startMode = TIMER_INTERVAL_START_MODES.has(countdown?.startMode) ? countdown.startMode : "now";
|
||||
const startTime = String(countdown?.startTime || "").trim();
|
||||
const autoRefresh = countdown?.autoRefresh === true || (countdown?.autoRefresh !== false && !targetAt && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS);
|
||||
return intervalMs > 0
|
||||
? {
|
||||
...normalized,
|
||||
intervalMs,
|
||||
anchorAt,
|
||||
targetAt: targetAt || anchorAt + intervalMs,
|
||||
startMode: startMode === "time" && isTimeString(startTime) ? "time" : "now",
|
||||
...(startMode === "time" && isTimeString(startTime) ? { startTime } : {}),
|
||||
autoRefresh: autoRefresh && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS
|
||||
}
|
||||
: 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
|
||||
};
|
||||
}
|
||||
|
||||
export function compactTimerDataForStorage(value) {
|
||||
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) => {
|
||||
const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
|
||||
if (alertMode !== "off") compactCountdown.alertMode = alertMode;
|
||||
if (autoRefresh) compactCountdown.autoRefresh = true;
|
||||
if (startMode === "time" && startTime) {
|
||||
compactCountdown.startMode = startMode;
|
||||
compactCountdown.startTime = startTime;
|
||||
}
|
||||
return compactCountdown;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue