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

@ -0,0 +1,108 @@
// Rôle : regroupe les calculs purs de formatage et d'échéance des timers.
export function getNowMs() {
return Date.now();
}
export function pad(value) {
return String(value).padStart(2, "0");
}
export function formatDuration(ms) {
const safeMs = Math.max(0, Math.floor(ms));
const totalSeconds = Math.floor(safeMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
export function getCentiseconds(ms) {
return pad(Math.floor((Math.max(0, Math.floor(ms)) % 1000) / 10));
}
export function formatDurationWithCentiseconds(ms) {
return `${formatDuration(ms)}:${getCentiseconds(ms)}`;
}
export function formatTargetTime(ms) {
if (!Number.isFinite(ms)) return "";
return new Intl.DateTimeFormat("fr-FR", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).format(new Date(ms));
}
export function normalizeTimePart(value, max, allowWildcard = false) {
const cleanValue = String(value || "").trim().toUpperCase();
if (allowWildcard && !cleanValue) return "X";
if (allowWildcard && /^X+$/.test(cleanValue)) return "X";
const parsed = cleanValue ? Number.parseInt(cleanValue, 10) : 0;
if (!Number.isFinite(parsed) || parsed < 0 || parsed > max) return "";
return pad(parsed);
}
export function normalizeTimeParts(parts, { allowWildcard = false, allowLargeHours = false } = {}) {
const hours = normalizeTimePart(parts?.hours, allowLargeHours ? 999 : 23, allowWildcard);
const minutes = normalizeTimePart(parts?.minutes, 59, allowWildcard);
const seconds = normalizeTimePart(parts?.seconds, 59, allowWildcard);
if (!hours || !minutes || !seconds) return null;
return { hours, minutes, seconds };
}
export function timePartsToDurationMs(parts) {
const normalized = normalizeTimeParts(parts, { allowLargeHours: true });
if (!normalized) return 0;
return ((Number(normalized.hours) * 3600) + (Number(normalized.minutes) * 60) + Number(normalized.seconds)) * 1000;
}
export function timePartsToString(parts, options) {
const normalized = normalizeTimeParts(parts, options);
return normalized ? `${normalized.hours}:${normalized.minutes}:${normalized.seconds}` : "";
}
export function parseTimeString(value, options) {
const [hours = "", minutes = "", seconds = ""] = String(value || "").split(":");
return normalizeTimeParts({ hours, minutes, seconds }, options);
}
export function getDailyTargetMs(time, nowMs) {
const parsed = parseTimeString(time);
if (!parsed) return 0;
const target = new Date(nowMs);
target.setHours(Number(parsed.hours), Number(parsed.minutes), Number(parsed.seconds), 0);
if (target.getTime() <= nowMs) target.setDate(target.getDate() + 1);
return target.getTime();
}
export function getTimePatternTargetMs(pattern, nowMs) {
const parsed = parseTimeString(pattern, { allowWildcard: true });
if (!parsed) return 0;
const now = new Date(nowMs);
const candidate = new Date(nowMs);
candidate.setMilliseconds(0);
for (let offset = 1; offset <= 86400; offset += 1) {
candidate.setTime(now.getTime() + offset * 1000);
const hours = parsed.hours === "X" || Number(parsed.hours) === candidate.getHours();
const minutes = parsed.minutes === "X" || Number(parsed.minutes) === candidate.getMinutes();
const seconds = parsed.seconds === "X" || Number(parsed.seconds) === candidate.getSeconds();
if (hours && minutes && seconds) return candidate.getTime();
}
return 0;
}
export function getIntervalTargetMs(intervalMs, anchorAt, nowMs) {
const safeIntervalMs = Math.max(1000, Number(intervalMs) || 0);
const safeAnchor = Number.isFinite(anchorAt) && anchorAt > 0 ? anchorAt : nowMs;
if (safeAnchor > nowMs) return safeAnchor;
return safeAnchor + Math.ceil((nowMs - safeAnchor + 1) / safeIntervalMs) * safeIntervalMs;
}
export function getCountdownTargetMs(countdown, nowMs) {
if (countdown.type === "duration") return Number(countdown.targetAt) || 0;
if (countdown.type === "daily_time") return Number(countdown.targetAt) || 0;
if (countdown.type === "time_pattern") return getTimePatternTargetMs(countdown.pattern, nowMs);
if (countdown.type === "interval") return getIntervalTargetMs(countdown.intervalMs, countdown.anchorAt, nowMs);
return 0;
}