// Rôle : fournit l'outil timer avec chronomètre, comptes à rebours et échange texte. import { useEffect, useMemo, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { Tabs } from "../../../components/Tabs.jsx"; import { useDraftForm } from "../../../hooks/useDraftForm.js"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs, getNowMs, getTimePatternTargetMs, timePartsToDurationMs, timePartsToString } from "./timerUtils.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; import { CountdownControls, CountdownList } from "./TimerCountdown.jsx"; import { StopwatchControls, StopwatchLapList } from "./TimerStopwatch.jsx"; import { ALERT_MIN_DELAY_MS, AUTO_REFRESH_COUNTDOWN_TYPES, COUNTDOWN_TYPES, DEFAULT_COUNTDOWN_FORM, TIMER_TABS, TOGGLE_REFRESH_COUNTDOWN_TYPES, canToggleAutoRefresh, getAlertGuardMessage, getCountdownValidationError, hasTimePartValue, isAlertGuarded } from "./timerOptions.js"; export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen }) { const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" })); const textContent = context.moduleText?.timer || {}; const activeTab = data.activeTab; const [nowMs, setNowMs] = useState(getNowMs); const countdownDraft = useDraftForm(DEFAULT_COUNTDOWN_FORM); useEffect(() => { const intervalId = window.setInterval(() => setNowMs(getNowMs()), 50); return () => window.clearInterval(intervalId); }, []); const stopwatchElapsedMs = data.stopwatch.startedAt ? data.stopwatch.elapsedMs + Math.max(0, nowMs - data.stopwatch.startedAt) : data.stopwatch.elapsedMs; const visibleCountdowns = useMemo(() => { const countdowns = data.countdowns.map((countdown) => ({ ...countdown, targetMs: getCountdownTargetMs(countdown, nowMs) })); if (!data.sortResults) return countdowns; return [...countdowns].sort((a, b) => (a.targetMs || Number.POSITIVE_INFINITY) - (b.targetMs || Number.POSITIVE_INFINITY)); }, [data.countdowns, data.sortResults, nowMs]); const countdownReorder = useGroupedReorder({ namespace: "timer-countdowns", items: data.countdowns, getItemId: (countdown) => countdown.id, getParentId: () => moduleId, canMoveItem: () => !data.sortResults, onItemMove: (operation) => { if (data.sortResults) return; save({ ...data, countdowns: moveItem(data.countdowns, operation.sourceId, operation.targetId, operation.placement) }); }, hierarchy: { enabled: true, stickyParents: true } }); function save(nextData) { context.setModuleData(toolboxId, moduleId, nextData); } function setActiveTab(nextActiveTab) { save({ ...data, activeTab: nextActiveTab }); } function setScrollResults(scrollResults) { save({ ...data, scrollResults }); } function setSortResults(sortResults) { save({ ...data, sortResults }); } function updateStopwatch(stopwatch) { save({ ...data, stopwatch }); } function startStopwatch() { if (data.stopwatch.startedAt) return; updateStopwatch({ ...data.stopwatch, startedAt: getNowMs() }); } function pauseStopwatch() { if (!data.stopwatch.startedAt) return; updateStopwatch({ ...data.stopwatch, startedAt: 0, elapsedMs: stopwatchElapsedMs }); } function resetStopwatch() { updateStopwatch({ ...data.stopwatch, elapsedMs: 0, startedAt: 0 }); } function addLap() { if (!stopwatchElapsedMs) return; updateStopwatch({ ...data.stopwatch, laps: [ { id: context.uid("timer"), label: "", elapsedMs: stopwatchElapsedMs }, ...data.stopwatch.laps ] }); } function deleteLap(lapId) { updateStopwatch({ ...data.stopwatch, laps: data.stopwatch.laps.filter((lap) => lap.id !== lapId) }); } function renameLap(lapId, label) { updateStopwatch({ ...data.stopwatch, laps: data.stopwatch.laps.map((lap) => lap.id === lapId ? { ...lap, label: label.trim() } : lap) }); } function createCountdown(event) { countdownDraft.handleSubmit(event, (draft, { updateValues }) => { const type = COUNTDOWN_TYPES.includes(draft.type) ? draft.type : "duration"; const validationError = getCountdownValidationError(type, draft, textContent); if (validationError) return validationError; const label = draft.label.trim() || textContent.defaultCountdownLabel || "Timer"; const now = getNowMs(); let countdown = null; if (type === "duration") { const durationMs = timePartsToDurationMs(draft.duration); if (durationMs > 0) countdown = { id: context.uid("timer"), label, type, durationMs, targetAt: now + durationMs }; } if (type === "daily_time") { const time = timePartsToString(draft.time); const targetAt = getDailyTargetMs(time, now); if (time && targetAt) countdown = { id: context.uid("timer"), label, type, time, targetAt }; } if (type === "time_pattern") { const pattern = hasTimePartValue(draft.pattern) ? timePartsToString(draft.pattern, { allowWildcard: true }) : ""; if (pattern && getTimePatternTargetMs(pattern, now)) countdown = { id: context.uid("timer"), label, type, pattern }; } if (type === "interval") { const intervalMs = timePartsToDurationMs(draft.interval); const startTime = hasTimePartValue(draft.intervalStartTime) ? timePartsToString(draft.intervalStartTime) : ""; const startMode = startTime ? "time" : "now"; const anchorAt = getIntervalAnchorMs(startMode, startTime, now); const targetAt = getCountdownTargetMs({ type, intervalMs, anchorAt, autoRefresh: true }, now); if (intervalMs > 0) { countdown = { id: context.uid("timer"), label, type, intervalMs, anchorAt, targetAt, startMode, ...(startTime ? { startTime } : {}), autoRefresh: intervalMs >= ALERT_MIN_DELAY_MS }; } } if (!countdown) return; save({ ...data, countdowns: [...data.countdowns, countdown] }); updateValues({ label: "" }); }); } function deleteCountdown(countdownId) { save({ ...data, countdowns: data.countdowns.filter((countdown) => countdown.id !== countdownId) }); } function resetCountdown(countdown) { if (countdown.type !== "duration" && countdown.type !== "daily_time" && countdown.type !== "interval") return; const now = getNowMs(); let nextCountdown = null; if (countdown.type === "duration") nextCountdown = { ...countdown, targetAt: now + countdown.durationMs }; if (countdown.type === "daily_time") nextCountdown = { ...countdown, targetAt: getDailyTargetMs(countdown.time, now) }; if (countdown.type === "interval") { const anchorAt = getIntervalAnchorMs(countdown.startMode, countdown.startTime, now); const targetAt = getCountdownTargetMs({ ...countdown, anchorAt, autoRefresh: true }, now); nextCountdown = { ...countdown, anchorAt, targetAt }; } if (!nextCountdown) return; save({ ...data, countdowns: data.countdowns.map((item) => item.id === countdown.id ? nextCountdown : item) }); } function toggleCountdownAutoRefresh(countdownId) { const countdown = data.countdowns.find((item) => item.id === countdownId); if (!TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown?.type)) return; if (!countdown.autoRefresh && !canToggleAutoRefresh(countdown)) { context.notify?.(getAlertGuardMessage(textContent)); return; } const now = getNowMs(); save({ ...data, countdowns: data.countdowns.map((item) => { if (item.id !== countdownId) return item; const autoRefresh = !item.autoRefresh; if (item.type === "daily_time") return { ...item, autoRefresh, targetAt: getDailyTargetMs(item.time, now) }; if (item.type === "interval") { const anchorAt = getIntervalAnchorMs(item.startMode, item.startTime, now); const targetAt = getCountdownTargetMs({ ...item, anchorAt, autoRefresh: true }, now); return { ...item, autoRefresh, anchorAt, targetAt }; } return item; }) }); } function renameCountdown(countdownId, label) { save({ ...data, countdowns: data.countdowns.map((countdown) => countdown.id === countdownId ? { ...countdown, label: label.trim() } : countdown) }); } function setCountdownAlertMode(countdownId, alertMode) { const countdown = data.countdowns.find((item) => item.id === countdownId); if (alertMode !== "off" && AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown?.type) && isAlertGuarded(countdown)) { context.notify?.(getAlertGuardMessage(textContent)); return; } save({ ...data, countdowns: data.countdowns.map((countdown) => countdown.id === countdownId ? { ...countdown, alertMode } : countdown) }); } function resetActiveResults() { if (activeTab === "stopwatch") { resetStopwatch(); return; } save({ ...data, countdowns: [] }); } return (
{textExchangeOpen && (
save(nextData)} />
)}
{activeTab === "stopwatch" ? ( ) : ( )}
{activeTab === "countdown" && (