Improve timer interval refresh behavior
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
fdc81373c6
commit
a6b2388763
9 changed files with 305 additions and 39 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
getCentiseconds,
|
||||
getCountdownTargetMs,
|
||||
getDailyTargetMs,
|
||||
getIntervalAnchorMs,
|
||||
getNowMs,
|
||||
getTimePatternTargetMs,
|
||||
getTimePatternRecurrenceMs,
|
||||
|
|
@ -32,7 +33,8 @@ const DEFAULT_COUNTDOWN_FORM = {
|
|||
duration: EMPTY_TIME_PARTS,
|
||||
time: EMPTY_TIME_PARTS,
|
||||
pattern: EMPTY_TIME_PARTS,
|
||||
interval: EMPTY_TIME_PARTS
|
||||
interval: EMPTY_TIME_PARTS,
|
||||
intervalStartTime: EMPTY_TIME_PARTS
|
||||
};
|
||||
const ALERT_MODES = [
|
||||
{ mode: "off", icon: "sound-mute" },
|
||||
|
|
@ -41,6 +43,7 @@ const ALERT_MODES = [
|
|||
];
|
||||
const ALERT_MIN_DELAY_MS = 5 * 60 * 1000;
|
||||
const AUTO_REFRESH_COUNTDOWN_TYPES = new Set(["time_pattern", "interval"]);
|
||||
const TOGGLE_REFRESH_COUNTDOWN_TYPES = new Set(["daily_time", "interval"]);
|
||||
|
||||
function isAlertGuarded(countdown) {
|
||||
if (countdown?.type === "interval") return Number(countdown.intervalMs) < ALERT_MIN_DELAY_MS;
|
||||
|
|
@ -52,6 +55,10 @@ function getAlertGuardMessage(textContent) {
|
|||
return textContent.alertTooSoonTitle || "Alerte indisponible pour une répétition inférieure à 5 minutes";
|
||||
}
|
||||
|
||||
function canToggleAutoRefresh(countdown) {
|
||||
return TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown?.type) && !isAlertGuarded(countdown);
|
||||
}
|
||||
|
||||
function TimerValue({ ms, compact = false, showCentiseconds = true }) {
|
||||
if (!showCentiseconds) return <span className="timer-value">{formatDuration(ms)}</span>;
|
||||
if (!compact) return <span className="timer-value">{formatDurationWithCentiseconds(ms)}</span>;
|
||||
|
|
@ -64,12 +71,17 @@ function TimerValue({ ms, compact = false, showCentiseconds = true }) {
|
|||
);
|
||||
}
|
||||
|
||||
function getCountdownMeta(countdown, textContent) {
|
||||
if (countdown.type === "duration") return `${textContent.durationType || "Durée"} · ${formatDuration(countdown.durationMs || 0)}`;
|
||||
if (countdown.type === "daily_time") return `${textContent.dailyTimeType || "Heure précise"} · ${countdown.time}`;
|
||||
if (countdown.type === "time_pattern") return `${textContent.timePatternType || "Heure / minute / seconde"} · ${countdown.pattern}`;
|
||||
if (countdown.type === "interval") return `${textContent.intervalType || "Intervalle"} · ${formatDuration(countdown.intervalMs || 0)}`;
|
||||
return "";
|
||||
function getCountdownMetaParts(countdown, textContent) {
|
||||
if (countdown.type === "duration") return [`${textContent.durationType || "Durée"} ${formatDuration(countdown.durationMs || 0)}`];
|
||||
if (countdown.type === "daily_time") return [`${textContent.dailyTimeType || "Heure précise"} ${countdown.time}`];
|
||||
if (countdown.type === "time_pattern") return [`${textContent.timePatternType || "Heure / minute / seconde"} ${countdown.pattern}`];
|
||||
if (countdown.type === "interval") {
|
||||
const startLabel = countdown.startMode === "time" && countdown.startTime
|
||||
? `${textContent.intervalStartTimeLabel || "Départ"} ${countdown.startTime}`
|
||||
: textContent.intervalStartNowOption || "Maintenant";
|
||||
return [`${textContent.intervalType || "Intervalle"} ${formatDuration(countdown.intervalMs || 0)}`, startLabel];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function handleEditableKeyDown(event, callback) {
|
||||
|
|
@ -78,6 +90,10 @@ function handleEditableKeyDown(event, callback) {
|
|||
callback();
|
||||
}
|
||||
|
||||
function hasTimePartValue(parts) {
|
||||
return Boolean(String(parts?.hours || "").trim() || String(parts?.minutes || "").trim() || String(parts?.seconds || "").trim());
|
||||
}
|
||||
|
||||
export function TimerModule({ toolboxId, moduleId, context }) {
|
||||
const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" }));
|
||||
const textContent = context.moduleText?.timer || {};
|
||||
|
|
@ -197,7 +213,23 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
|||
}
|
||||
if (type === "interval") {
|
||||
const intervalMs = timePartsToDurationMs(draft.interval);
|
||||
if (intervalMs > 0) countdown = { id: context.uid("timer"), label, type, intervalMs, anchorAt: now };
|
||||
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;
|
||||
|
|
@ -216,11 +248,39 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
|||
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") nextCountdown = { ...countdown, anchorAt: 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,
|
||||
|
|
@ -314,7 +374,7 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
|||
{activeTab === "stopwatch" ? (
|
||||
<StopwatchLapList laps={data.stopwatch.laps} textContent={textContent} onRename={renameLap} onDelete={deleteLap} />
|
||||
) : (
|
||||
<CountdownList countdowns={visibleCountdowns} nowMs={nowMs} textContent={textContent} reorder={countdownReorder} reorderEnabled={!data.sortResults} parentId={moduleId} onRename={renameCountdown} onAlertModeChange={setCountdownAlertMode} onReset={resetCountdown} onDelete={deleteCountdown} />
|
||||
<CountdownList countdowns={visibleCountdowns} nowMs={nowMs} textContent={textContent} reorder={countdownReorder} reorderEnabled={!data.sortResults} parentId={moduleId} onRename={renameCountdown} onAlertModeChange={setCountdownAlertMode} onReset={resetCountdown} onAutoRefreshToggle={toggleCountdownAutoRefresh} onDelete={deleteCountdown} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -399,27 +459,36 @@ function CountdownControls({ textContent, form, onChange, onSubmit }) {
|
|||
</label>
|
||||
{form.type === "duration" && (
|
||||
<label>
|
||||
<span className="timer-field-spacer" aria-hidden="true" />
|
||||
<span className="sr-only">{textContent.durationLabel || "Durée"}</span>
|
||||
<TimePartsInput value={form.duration} onChange={(duration) => onChange({ duration })} />
|
||||
</label>
|
||||
)}
|
||||
{form.type === "daily_time" && (
|
||||
<label>
|
||||
<span className="timer-field-spacer" aria-hidden="true" />
|
||||
<span className="sr-only">{textContent.dailyTimeLabel || "Heure"}</span>
|
||||
<TimePartsInput value={form.time} onChange={(time) => onChange({ time })} />
|
||||
</label>
|
||||
)}
|
||||
{form.type === "time_pattern" && (
|
||||
<label>
|
||||
<span className="timer-field-spacer" aria-hidden="true" />
|
||||
<span className="sr-only">{textContent.timePatternLabel || "Heure / minute / seconde"}</span>
|
||||
<TimePartsInput value={form.pattern} onChange={(pattern) => onChange({ pattern })} allowWildcard />
|
||||
</label>
|
||||
)}
|
||||
{form.type === "interval" && (
|
||||
<label>
|
||||
<span className="sr-only">{textContent.intervalLabel || "Toutes les"}</span>
|
||||
<TimePartsInput value={form.interval} onChange={(interval) => onChange({ interval })} />
|
||||
</label>
|
||||
<div className="timer-interval-fields">
|
||||
<label>
|
||||
<span>{textContent.intervalLabel || "Toutes les"}</span>
|
||||
<TimePartsInput value={form.interval} onChange={(interval) => onChange({ interval })} />
|
||||
</label>
|
||||
<label>
|
||||
<span>{textContent.intervalStartTimeLabel || "Départ"}</span>
|
||||
<TimePartsInput value={form.intervalStartTime} onChange={(intervalStartTime) => onChange({ intervalStartTime })} />
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<button className="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
|
||||
</form>
|
||||
|
|
@ -485,7 +554,7 @@ function StopwatchLapList({ laps, textContent, onRename, onDelete }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled, parentId, onRename, onAlertModeChange, onReset, onDelete }) {
|
||||
function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled, parentId, onRename, onAlertModeChange, onReset, onAutoRefreshToggle, onDelete }) {
|
||||
const [editingId, setEditingId] = useState("");
|
||||
if (!countdowns.length) return <p className="muted">{textContent.emptyCountdowns || "Aucun compte à rebours configuré."}</p>;
|
||||
return (
|
||||
|
|
@ -493,9 +562,12 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
|
|||
{countdowns.map((countdown) => {
|
||||
const remainingMs = countdown.targetMs - nowMs;
|
||||
const expired = remainingMs <= 0;
|
||||
const canReset = countdown.type === "duration"
|
||||
|| countdown.type === "interval"
|
||||
|| (countdown.type === "daily_time" && expired);
|
||||
const canReset = countdown.type === "duration";
|
||||
const canToggleRefresh = TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown.type);
|
||||
const refreshBlocked = canToggleRefresh && !canToggleAutoRefresh(countdown);
|
||||
const refreshTitle = refreshBlocked
|
||||
? getAlertGuardMessage(textContent)
|
||||
: textContent.autoRefreshTitle || "Répéter automatiquement";
|
||||
const className = [
|
||||
"timer-list-item",
|
||||
expired ? "is-expired" : "",
|
||||
|
|
@ -508,7 +580,7 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
|
|||
|
||||
return (
|
||||
<li key={countdown.id} className={className} {...reorderProps}>
|
||||
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${canReset && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
|
||||
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${(canReset || canToggleRefresh) && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
|
||||
{reorderEnabled && (
|
||||
<button
|
||||
className="timer-drag-handle"
|
||||
|
|
@ -556,6 +628,23 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
|
|||
<Icon name="refresh" />
|
||||
</button>
|
||||
)}
|
||||
{canToggleRefresh && (
|
||||
<button
|
||||
className={`timer-inline-refresh-toggle ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onAutoRefreshToggle(countdown.id);
|
||||
}}
|
||||
disabled={refreshBlocked}
|
||||
aria-pressed={Boolean(countdown.autoRefresh)}
|
||||
aria-label={refreshTitle}
|
||||
title={refreshTitle}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
<TimerAlertModeSwitch
|
||||
mode={countdown.alertMode}
|
||||
blocked={AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown.type) && isAlertGuarded(countdown)}
|
||||
|
|
@ -571,8 +660,14 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
|
|||
</button>
|
||||
</div>
|
||||
<p className="timer-entry-meta">
|
||||
<span>{getCountdownMeta(countdown, textContent)}</span>
|
||||
<em>{expired ? textContent.finishedLabel || "Terminé" : `${textContent.targetLabel || "Prochaine occurrence"} · ${formatTargetTime(countdown.targetMs)}`}</em>
|
||||
<span>
|
||||
{getCountdownMetaParts(countdown, textContent).map((part, index) => (
|
||||
<b key={`${part}-${index}`}>{part}</b>
|
||||
))}
|
||||
</span>
|
||||
<em>
|
||||
<b>{expired ? textContent.finishedLabel || "Terminé" : `${textContent.targetLabel || "Prochaine occurrence"} ${formatTargetTime(countdown.targetMs)}`}</b>
|
||||
</em>
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -113,10 +113,22 @@ export function getIntervalTargetMs(intervalMs, anchorAt, nowMs) {
|
|||
return safeAnchor + Math.ceil((nowMs - safeAnchor + 1) / safeIntervalMs) * safeIntervalMs;
|
||||
}
|
||||
|
||||
export function getIntervalAnchorMs(startMode, startTime, nowMs) {
|
||||
if (startMode !== "time") return nowMs;
|
||||
const parsed = parseTimeString(startTime);
|
||||
if (!parsed) return nowMs;
|
||||
const anchor = new Date(nowMs);
|
||||
anchor.setHours(Number(parsed.hours), Number(parsed.minutes), Number(parsed.seconds), 0);
|
||||
return anchor.getTime();
|
||||
}
|
||||
|
||||
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 === "daily_time") return countdown.autoRefresh ? getDailyTargetMs(countdown.time, nowMs) : 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);
|
||||
if (countdown.type === "interval") {
|
||||
if (countdown.autoRefresh) return getIntervalTargetMs(countdown.intervalMs, countdown.anchorAt, nowMs);
|
||||
return Number(countdown.targetAt) || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue