From 2e6d8a2c0c700aad898deb387c6c322afe381c8d Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Sun, 26 Jul 2026 17:48:14 +0200 Subject: [PATCH] keepsafe rework & alert ajustments --- docs/STORAGE_SCHEMA.md | 4 ++- tests/toolbox-modules.test.mjs | 11 ++++++ website/public/data/site.json | 4 +-- website/src/components/AppOverlays.jsx | 17 +++++++-- website/src/components/ToolboxModals.jsx | 11 ++++-- .../toolboxes/modules/TimerModule.jsx | 35 ++++++++++++------- .../features/toolboxes/modules/timerUtils.js | 18 ++++++++-- website/src/main.jsx | 21 +++++------ website/src/styles/_base.scss | 12 +++++++ website/src/styles/_overlays.scss | 32 ++++++++++------- website/src/styles/_toolboxes.scss | 9 +++++ 11 files changed, 129 insertions(+), 45 deletions(-) diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index bda25ce..16afe7d 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -244,6 +244,8 @@ Types de compte à rebours : - `time_pattern` : prochaine occurrence automatique correspondant à un pattern `HH:MM:SS`, où chaque segment peut valoir `X`. - `interval` : prochaine occurrence automatique toutes les X millisecondes, avec `intervalMs` et `anchorAt`. +Pour `time_pattern`, un segment `X` situé après un segment plus large fixé cible le premier instant de l'occurrence. Exemple : `X:24:X` cible `hh:24:00` et se répète toutes les heures. + Stockage compact : - `activeTab` est omis si la valeur vaut `stopwatch`. @@ -255,7 +257,7 @@ Stockage compact : - `laps` est omis tant qu'aucune étape n'est enregistrée. - `countdowns` est omis tant qu'aucun compte à rebours n'est configuré. - `countdowns[].alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`. -- L'activation d'une alerte est bloquée dans l'interface si la prochaine échéance est à moins de 5 minutes. +- L'activation d'une alerte est bloquée dans l'interface si la fréquence configurée est inférieure à 5 minutes pour les comptes à rebours récurrents (`time_pattern` et `interval`). Le blocage ne dépend pas du temps restant avant la prochaine échéance. ## Outil Liens diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index 63e97d3..7437cec 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -2,6 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js"; +import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js"; test("colon text import keeps urls intact after the first separator", () => { assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [ @@ -15,3 +16,13 @@ test("colon text import accepts labels without value", () => { { label: "Simple item", value: "" } ]); }); + +test("time pattern recurrence uses configured frequency instead of next remaining delay", () => { + const now = new Date(2026, 0, 1, 13, 23, 45, 0).getTime(); + const target = getTimePatternTargetMs("X:24:X", now); + assert.equal(new Date(target).getHours(), 13); + assert.equal(new Date(target).getMinutes(), 24); + assert.equal(new Date(target).getSeconds(), 0); + assert.equal(getTimePatternRecurrenceMs("X:24:X"), 60 * 60 * 1000); + assert.equal(getTimePatternRecurrenceMs("X:X:30"), 60 * 1000); +}); diff --git a/website/public/data/site.json b/website/public/data/site.json index 2dee787..6063a45 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -296,7 +296,7 @@ "countdownTypeLabel": "Type", "durationType": "Durée", "dailyTimeType": "Heure précise", - "timePatternType": "Horaire spécifique", + "timePatternType": "Pattern horaire", "intervalType": "Intervalle", "durationLabel": "Durée", "dailyTimeLabel": "Heure", @@ -315,7 +315,7 @@ "alertOffTitle": "Pas d'alerte", "alertVisibleTitle": "Alerte quand la toolbox est affichée", "alertSiteTitle": "Alerte sur tout le site", - "alertTooSoonTitle": "Alerte indisponible à moins de 5 minutes", + "alertTooSoonTitle": "Alerte indisponible pour une répétition inférieure à 5 minutes", "alertMessage": "{label} est terminé dans {toolbox}.", "emptySteps": "Aucune étape enregistrée.", "emptyCountdowns": "Aucun compte à rebours configuré.", diff --git a/website/src/components/AppOverlays.jsx b/website/src/components/AppOverlays.jsx index d22d1a8..06f25a2 100644 --- a/website/src/components/AppOverlays.jsx +++ b/website/src/components/AppOverlays.jsx @@ -18,8 +18,8 @@ export function AppOverlays({ setStorageError, image, setImage, - notification, - setNotification, + notifications, + dismissNotification, toastPosition, getGame, toolboxes, @@ -49,7 +49,18 @@ export function AppOverlays({ }} />} {storageError && setStorageError("")} />} {image && setImage(null)} createMarkerId={() => uid("marker")} />} - {notification && setNotification(null)} />} + {notifications.length > 0 && ( +
+ {notifications.map((notification) => ( + + ))} +
+ )} ); } diff --git a/website/src/components/ToolboxModals.jsx b/website/src/components/ToolboxModals.jsx index e62a60e..b5d96eb 100644 --- a/website/src/components/ToolboxModals.jsx +++ b/website/src/components/ToolboxModals.jsx @@ -24,11 +24,16 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance ); } -export function NotificationToast({ message, position = "right", onClose }) { +export function NotificationToast({ id, message, onClose }) { + useEffect(() => { + const timeoutId = window.setTimeout(() => onClose(id), 120000); + return () => window.clearTimeout(timeoutId); + }, [id, onClose]); + return ( -
+
{message} -
diff --git a/website/src/features/toolboxes/modules/TimerModule.jsx b/website/src/features/toolboxes/modules/TimerModule.jsx index 70d7fae..6edee89 100644 --- a/website/src/features/toolboxes/modules/TimerModule.jsx +++ b/website/src/features/toolboxes/modules/TimerModule.jsx @@ -11,6 +11,7 @@ import { getDailyTargetMs, getNowMs, getTimePatternTargetMs, + getTimePatternRecurrenceMs, timePartsToDurationMs, timePartsToString } from "./timerUtils.js"; @@ -27,6 +28,17 @@ const ALERT_MODES = [ { mode: "site", icon: "sound-max" } ]; const ALERT_MIN_DELAY_MS = 5 * 60 * 1000; +const AUTO_REFRESH_COUNTDOWN_TYPES = new Set(["time_pattern", "interval"]); + +function isAlertGuarded(countdown) { + if (countdown?.type === "interval") return Number(countdown.intervalMs) < ALERT_MIN_DELAY_MS; + if (countdown?.type === "time_pattern") return getTimePatternRecurrenceMs(countdown.pattern) < ALERT_MIN_DELAY_MS; + return false; +} + +function getAlertGuardMessage(textContent) { + return textContent.alertTooSoonTitle || "Alerte indisponible pour une répétition inférieure à 5 minutes"; +} function TimerValue({ ms, compact = false, showCentiseconds = true }) { if (!showCentiseconds) return {formatDuration(ms)}; @@ -204,9 +216,8 @@ export function TimerModule({ toolboxId, moduleId, context }) { function setCountdownAlertMode(countdownId, alertMode) { const countdown = data.countdowns.find((item) => item.id === countdownId); - const targetMs = countdown ? getCountdownTargetMs(countdown, nowMs) : 0; - if (alertMode !== "off" && targetMs && targetMs - nowMs < ALERT_MIN_DELAY_MS) { - context.notify?.(textContent.alertTooSoonTitle || "Alerte indisponible à moins de 5 minutes"); + if (alertMode !== "off" && AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown?.type) && isAlertGuarded(countdown)) { + context.notify?.(getAlertGuardMessage(textContent)); return; } save({ @@ -303,7 +314,7 @@ function TimerAlertModeSwitch({ mode, blocked, textContent, onChange }) { visible: textContent.alertVisibleTitle || "Alerte quand la toolbox est affichée", site: textContent.alertSiteTitle || "Alerte sur tout le site" }; - const blockedTitle = textContent.alertTooSoonTitle || "Alerte indisponible à moins de 5 minutes"; + const blockedTitle = getAlertGuardMessage(textContent); return (
{ALERT_MODES.map((option) => { const label = labels[option.mode]; - const disabled = blocked && option.mode !== "off"; + const disabled = Boolean(blocked); return (