new tool timer & alert revamp
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-26 11:41:42 +02:00
parent 0c7ae46b4d
commit 68005ef918
38 changed files with 2048 additions and 111 deletions

View file

@ -1,6 +1,9 @@
// 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", calc: "r", marker: "k" };
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z" };
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
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 TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
const TOOLBOX_ICON_FILES = [
"toolbox.png",
@ -30,6 +33,7 @@ const DEFAULT_MODULE_TITLES = {
links: "Liens",
counters: "Compteurs",
calculator: "Calculateur",
timer: "Timer",
imageAnnotation: "Annotation d'images"
};
@ -210,6 +214,16 @@ function normalizeCalculatorValue(value) {
return Number.isFinite(parsed) ? parsed : 0;
}
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 labelFromFileName(name) {
if (!name || name === "image.png") return "";
return name.replace(/\.[^.]+$/, "").trim();
@ -230,6 +244,62 @@ export function normalizeCalculatorData(data) {
};
}
export function normalizeTimerData(data) {
const stopwatch = data?.stopwatch || {};
const legacyAlertMode = TIMER_ALERT_MODES.has(data?.alertMode) ? data.alertMode : "off";
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 : legacyAlertMode
};
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 legacyIntervalMs = (Number.parseInt(countdown?.intervalMinutes, 10) || 0) * 60000;
const intervalMs = Math.max(0, Number(countdown?.intervalMs) || legacyIntervalMs);
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 clampPercent(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return 0;
@ -339,6 +409,30 @@ export function compactModuleDataForStorage(type, value) {
if (!entries.length && !normalized.scrollResults) return null;
return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
}
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;
}
return value;
}
@ -403,6 +497,20 @@ function remapModuleDataForExport(type, data, nextId) {
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;
}
return compact;
}