design(web): refonte des pages aide / statistiques / réglages
Typographie plus lisible (16.5px, interligne 1.7), cartes arrondies avec ombres douces, degradés teal, en-tête avec logo et slogan, onglets de navigation actifs, emojis dans toutes les sections, étapes numérotées sur la page d'aide, cartes de stats et graphique à barres sur la page statistiques, formulaire organisé en sections avec rappels santé stylisés, page de confirmation festive, mode sombre complet.
This commit is contained in:
+341
-216
@@ -143,11 +143,41 @@ proc t(lang: Lang, fr, en: string): string =
|
||||
# Gabarit de page
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc pageShell(lang: Lang, title, body: string): string =
|
||||
let nav = "<nav><a href=\"/help\">" & t(lang, "Aide", "Help") &
|
||||
"</a> <a href=\"/stats\">" & t(lang, "Statistiques", "Statistics") &
|
||||
"</a> <a href=\"/settings\">" & t(lang, "Réglages", "Settings") &
|
||||
"</a></nav>"
|
||||
proc statCard(emoji, label, value, unit: string): string =
|
||||
## Carte de statistique (emoji + libellé + grande valeur).
|
||||
"<div class='statcard'><div class='statemoji'>" & emoji & "</div>" &
|
||||
"<div class='statlabel'>" & label & "</div>" &
|
||||
"<div class='statvalue'>" & value &
|
||||
(if unit.len > 0: " <span class='statunit'>" & unit & "</span>" else: "") & "</div></div>"
|
||||
|
||||
proc numField(emoji, label, name: string, value, minVal: int): string =
|
||||
## Champ numérique libellé du formulaire de réglages.
|
||||
"<label class='field'><span class='flabel'>" & emoji & " " & label & "</span>" &
|
||||
"<input type='number' name='" & name & "' min='" & $minVal & "' value='" & $value & "'></label>"
|
||||
|
||||
proc checkRow(name: string, checked: bool, emoji, label: string): string =
|
||||
## Ligne de case à cocher du formulaire de réglages.
|
||||
"<label class='checkrow'><input type='checkbox' class='chk' name='" & name & "'" &
|
||||
(if checked: " checked" else: "") & "><span>" & emoji & " " & label & "</span></label>"
|
||||
|
||||
proc pageShell(lang: Lang, title, body, active: string): string =
|
||||
## Gabarit commun : en-tête (logo + slogan), onglets de navigation, corps et
|
||||
## pied de page. Design : dégradés doux, cartes arrondies, mode sombre.
|
||||
let nav = "<nav class='tabs'>" &
|
||||
"<a class='tab" & (if active == "help": " active" else: "") & "' href='/help'>📖 " & t(lang, "Aide", "Help") & "</a>" &
|
||||
"<a class='tab" & (if active == "stats": " active" else: "") & "' href='/stats'>📊 " & t(lang, "Statistiques", "Statistics") & "</a>" &
|
||||
"<a class='tab" & (if active == "settings": " active" else: "") & "' href='/settings'>⚙️ " & t(lang, "Réglages", "Settings") & "</a>" &
|
||||
"</nav>"
|
||||
let header = "<header class='hero'>" &
|
||||
"<div class='logo'>🍅</div>" &
|
||||
"<div><h1>NimPulse</h1><p class='tagline'>" &
|
||||
t(lang,
|
||||
"Le minuteur Pomodoro qui prend soin de vous",
|
||||
"The Pomodoro timer that takes care of you") &
|
||||
"</p></div></header>"
|
||||
let footer = "<footer><p>🔒 " & t(lang,
|
||||
"Pages servies localement par NimPulse — aucune donnée ne quitte votre ordinateur.",
|
||||
"Pages served locally by NimPulse — no data leaves your computer.") & "</p></footer>"
|
||||
result = """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
@@ -156,37 +186,111 @@ proc pageShell(lang: Lang, title, body: string): string =
|
||||
<title>NimPulse — """ & title & """</title>
|
||||
<style>
|
||||
:root{color-scheme:light dark}
|
||||
body{font-family:system-ui,'Segoe UI',sans-serif;max-width:800px;margin:2rem auto;padding:0 1rem;line-height:1.65;color:#1f2937;background:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0;font-family:'Segoe UI',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;
|
||||
font-size:16.5px;line-height:1.7;color:#1e293b;min-height:100vh;
|
||||
background:linear-gradient(165deg,#ecfeff 0%,#f0fdfa 30%,#f8fafc 70%,#fefce8 100%);
|
||||
}
|
||||
.wrap{max-width:900px;margin:0 auto;padding:1.6rem 1.3rem 3rem}
|
||||
.hero{display:flex;align-items:center;gap:1rem;padding:.3rem .1rem 1.1rem}
|
||||
.logo{font-size:2.7rem;line-height:1;filter:drop-shadow(0 4px 8px rgba(13,148,136,.4))}
|
||||
.hero h1{margin:0;font-size:2.1rem;letter-spacing:.3px;background:linear-gradient(90deg,#0f766e,#14b8a6);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.tagline{margin:.15rem 0 0;color:#64748b;font-size:1rem}
|
||||
.tabs{display:flex;gap:.55rem;flex-wrap:wrap;margin:0 0 1.5rem}
|
||||
.tab{display:inline-flex;align-items:center;gap:.45rem;padding:.55rem 1.15rem;border-radius:999px;text-decoration:none;font-weight:600;font-size:.98rem;color:#0f766e;background:rgba(255,255,255,.85);border:1px solid #99f6e4;box-shadow:0 2px 8px rgba(15,118,110,.08);transition:transform .15s ease, box-shadow .15s ease}
|
||||
.tab:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(15,118,110,.18)}
|
||||
.tab.active{background:linear-gradient(90deg,#0f766e,#14b8a6);color:#fff;border-color:transparent}
|
||||
.card{background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:18px;padding:1.35rem 1.55rem;margin:1.1rem 0;box-shadow:0 6px 18px rgba(15,118,110,.07)}
|
||||
h2{margin:2.3rem 0 .9rem;color:#0f766e;font-size:1.3rem;letter-spacing:.2px}
|
||||
.card h2{margin:.1rem 0 .5rem}
|
||||
p{margin:.55rem 0}
|
||||
.lead{font-size:1.12rem;color:#334155}
|
||||
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:1rem;margin:1rem 0}
|
||||
.step{display:flex;gap:.9rem;background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:16px;padding:1.1rem 1.2rem;box-shadow:0 4px 12px rgba(15,118,110,.06)}
|
||||
.step .num{flex:0 0 auto;width:2.1rem;height:2.1rem;border-radius:50%;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:1.05rem}
|
||||
.steps p{margin:.2rem 0 0;color:#64748b;font-size:.95rem}
|
||||
ul.clean{list-style:none;margin:.2rem 0;padding:0}
|
||||
ul.clean li{padding:.45rem 0}
|
||||
table{width:100%;border-collapse:collapse;margin:.7rem 0;background:rgba(255,255,255,.7);border-radius:12px;overflow:hidden}
|
||||
th{background:#ccfbf1;color:#134e4a;text-align:left;padding:.55rem .8rem;font-size:.93rem}
|
||||
td{padding:.5rem .8rem;border-top:1px solid #f1f5f9}
|
||||
code{background:#eef2f7;border-radius:6px;padding:.08rem .4rem;font-family:'Cascadia Mono',Consolas,monospace;font-size:.92em}
|
||||
.kbd{display:inline-block;background:#f1f5f9;border:1px solid #cbd5e1;border-bottom-width:3px;border-radius:7px;padding:.08rem .55rem;font-family:'Cascadia Mono',Consolas,monospace;font-size:.9em;font-weight:600}
|
||||
.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin:1.1rem 0}
|
||||
.statcard{background:rgba(255,255,255,.92);border:1px solid #e2e8f0;border-radius:18px;padding:1.15rem 1.3rem;text-align:center;box-shadow:0 6px 18px rgba(15,118,110,.07)}
|
||||
.statemoji{font-size:1.7rem;margin-bottom:.25rem}
|
||||
.statlabel{color:#64748b;font-size:.92rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
|
||||
.statvalue{font-size:2rem;font-weight:800;color:#0f766e;margin-top:.15rem;line-height:1.2}
|
||||
.statunit{font-size:1rem;font-weight:600;color:#94a3b8}
|
||||
.big-number{font-size:1.5rem;font-weight:800;color:#0f766e;margin:.3rem 0 .5rem}
|
||||
.progress{background:#e2e8f0;border-radius:999px;height:16px;overflow:hidden;margin:.6rem 0}
|
||||
.progress>div{background:linear-gradient(90deg,#0d9488,#22c55e);height:100%;border-radius:999px;transition:width .4s ease}
|
||||
.goalok{color:#059669;font-weight:700}
|
||||
.hint{color:#94a3b8;font-size:.92rem;margin:.1rem 0 .7rem}
|
||||
.chart{display:flex;align-items:flex-end;gap:.7rem;background:rgba(255,255,255,.85);border:1px solid #e2e8f0;border-radius:16px;padding:1.4rem 1.2rem .9rem;overflow-x:auto;margin:.8rem 0}
|
||||
.barcol{flex:1 1 0;min-width:52px;display:flex;flex-direction:column;align-items:center;gap:.35rem}
|
||||
.barval{font-size:.9rem;font-weight:700;color:#0f766e;min-height:1.2rem}
|
||||
.barfill{width:70%;max-width:64px;background:linear-gradient(180deg,#2dd4bf,#0d9488);border-radius:8px 8px 4px 4px;min-height:6px;transition:height .3s ease}
|
||||
.barlabel{font-size:.85rem;color:#64748b;font-weight:600}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:.9rem 1.4rem}
|
||||
@media (max-width:640px){.grid2{grid-template-columns:1fr}}
|
||||
.field{display:block;margin:.2rem 0}
|
||||
.flabel{display:block;font-weight:600;font-size:.95rem;color:#334155;margin-bottom:.3rem}
|
||||
input[type=number],select{width:100%;padding:.55rem .7rem;border:1.5px solid #cbd5e1;border-radius:10px;font-size:1rem;font-family:inherit;color:#1e293b;background:#fff;transition:border-color .15s ease, box-shadow .15s ease}
|
||||
input[type=number]:focus,select:focus{outline:none;border-color:#14b8a6;box-shadow:0 0 0 3px rgba(20,184,166,.18)}
|
||||
.checkrow{display:flex;align-items:center;gap:.65rem;padding:.5rem .2rem;font-weight:600;font-size:.98rem;cursor:pointer}
|
||||
.chk{width:1.25rem;height:1.25rem;accent-color:#0d9488;cursor:pointer}
|
||||
.btn{display:inline-flex;align-items:center;gap:.45rem;background:linear-gradient(90deg,#0f766e,#14b8a6);color:#fff;border:0;border-radius:12px;padding:.6rem 1.25rem;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;text-decoration:none;box-shadow:0 4px 12px rgba(15,118,110,.25);transition:transform .15s ease, filter .15s ease}
|
||||
.btn:hover{transform:translateY(-2px);filter:brightness(1.06)}
|
||||
.btn.big{padding:.8rem 1.8rem;font-size:1.08rem}
|
||||
.btn.ghost{background:#fff;color:#0f766e;border:1.5px dashed #5eead4;box-shadow:none}
|
||||
.savezone{text-align:center;margin-top:1.6rem}
|
||||
.empty{padding:1rem;text-align:center;color:#94a3b8;border:1.5px dashed #cbd5e1;border-radius:12px}
|
||||
.remlist{display:flex;flex-direction:column;gap:.5rem}
|
||||
.remrow{display:flex;align-items:center;gap:.7rem;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:.65rem .9rem;box-shadow:0 2px 8px rgba(15,118,110,.05)}
|
||||
.remrow.off{opacity:.62}
|
||||
.remrow.off .remtext{text-decoration:line-through}
|
||||
.remtext{flex:1;font-weight:500}
|
||||
.mini{border:1px solid #cbd5e1;background:#f8fafc;border-radius:9px;padding:.3rem .6rem;font-size:.88rem;font-weight:600;cursor:pointer;color:#334155;font-family:inherit}
|
||||
.mini:hover{background:#f0fdfa;border-color:#5eead4}
|
||||
.mini.danger{color:#b91c1c}
|
||||
.mini.danger:hover{background:#fef2f2;border-color:#fca5a5}
|
||||
.saved{text-align:center;padding:2.5rem 1.5rem}
|
||||
.savedemoji{font-size:3rem;margin-bottom:.4rem}
|
||||
footer{margin-top:2.8rem;padding-top:1.2rem;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:.9rem;text-align:center}
|
||||
@media (prefers-color-scheme: dark){
|
||||
body{background:#111827;color:#e5e7eb}
|
||||
h1{color:#5eead4} h2{color:#99f6e4;border-color:#374151}
|
||||
code,pre{background:#1f2937} .card{background:#1f2937;border-color:#374151}
|
||||
th,td{border-color:#374151} .bar{background:#374151} .muted{color:#9ca3af}
|
||||
input,select,button{color-scheme:dark}
|
||||
body{color:#e2e8f0;background:linear-gradient(165deg,#042f2e 0%,#0f172a 40%,#111827 80%,#1e1b4b 100%)}
|
||||
.card,.statcard,.step,.remrow,.chart{background:#0f172a;border-color:#1e293b}
|
||||
.tagline,.hint,.statlabel,.statunit,.barlabel,footer,.flabel{color:#94a3b8}
|
||||
.lead,.steps p{color:#94a3b8}
|
||||
.hero h1{background:linear-gradient(90deg,#2dd4bf,#5eead4);-webkit-background-clip:text;background-clip:text}
|
||||
h2{color:#2dd4bf}
|
||||
.statvalue{color:#5eead4}
|
||||
.barval{color:#5eead4}
|
||||
input[type=number],select{background:#0b1220;color:#e2e8f0;border-color:#334155}
|
||||
.tab{background:#0f172a;color:#5eead4;border-color:#164e63}
|
||||
.tab.active{background:linear-gradient(90deg,#0d9488,#14b8a6);color:#052e2b}
|
||||
th{background:#134e4a;color:#99f6e4}
|
||||
td{border-top-color:#1e293b}
|
||||
table{background:transparent}
|
||||
code{background:#1e293b}
|
||||
.kbd{background:#1e293b;border-color:#475569}
|
||||
.mini{background:#1e293b;border-color:#334155;color:#cbd5e1}
|
||||
.btn.ghost{background:transparent}
|
||||
.progress{background:#1e293b}
|
||||
.empty{color:#94a3b8;border-color:#334155}
|
||||
.remrow.off{opacity:.55}
|
||||
}
|
||||
h1{color:#0f766e} h2{margin-top:2rem;color:#115e59;border-bottom:1px solid #e5e7eb;padding-bottom:.3rem}
|
||||
code,pre{background:#f3f4f6;padding:.1rem .35rem;border-radius:4px;font-size:.92em}
|
||||
table{border-collapse:collapse;width:100%;margin:.6rem 0}
|
||||
th,td{border:1px solid #e5e7eb;padding:.4rem .6rem;text-align:left}
|
||||
nav{display:flex;gap:1.2rem;margin-bottom:1.6rem;padding:.6rem .8rem;border-radius:8px;background:#f0fdfa;border:1px solid #99f6e4}
|
||||
nav a{color:#0f766e;text-decoration:none;font-weight:600}
|
||||
.card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:1rem 1.2rem;margin:1rem 0}
|
||||
.bar{background:#e5e7eb;border-radius:6px;height:14px;overflow:hidden;min-width:30px;display:inline-block;vertical-align:middle}
|
||||
.bar>div{background:#14b8a6;height:100%}
|
||||
.muted{color:#6b7280;font-size:.92em}
|
||||
.btn{display:inline-block;background:#0f766e;color:#fff;padding:.45rem .95rem;border-radius:6px;text-decoration:none;border:0;font-size:.95rem;cursor:pointer}
|
||||
.btn:hover{background:#115e59}
|
||||
label{display:block;margin:.55rem 0 .15rem;font-weight:600}
|
||||
input[type=text],input[type=number],select{width:100%;max-width:320px;padding:.35rem .5rem;border:1px solid #d1d5db;border-radius:6px}
|
||||
input[type=checkbox]{width:auto}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 1.5rem}
|
||||
@media (max-width:640px){.grid{grid-template-columns:1fr}}
|
||||
.kbd{background:#f3f4f6;border:1px solid #d1d5db;border-bottom-width:2px;border-radius:4px;padding:.05rem .4rem;font-family:ui-monospace,monospace;font-size:.9em}
|
||||
.ok{color:#047857;font-weight:700}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
""" & nav & body & "</body></html>"
|
||||
<div class="wrap">
|
||||
""" & header & nav & body & footer & """
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page d'aide
|
||||
@@ -195,116 +299,114 @@ input[type=checkbox]{width:auto}
|
||||
proc renderHelp*(cfg: Config): string =
|
||||
let lang = resolveLang(cfg)
|
||||
var b = ""
|
||||
b.add "<h1>" & t(lang,
|
||||
"Aide — Comment fonctionne NimPulse",
|
||||
"Help — How NimPulse works") & "</h1>"
|
||||
|
||||
b.add "<div class=\"card\"><h2>" & t(lang, "Le principe", "The concept") & "</h2><p>" &
|
||||
t(lang,
|
||||
"NimPulse est un minuteur Pomodoro qui vit dans la zone de notification (tray). " &
|
||||
"Il alterne des périodes de travail concentré et des pauses, et profite des pauses " &
|
||||
"pour vous rappeler de prendre soin de votre santé (marcher, boire de l'eau, vous étirer…).",
|
||||
"NimPulse is a Pomodoro timer living in the system tray. It alternates focused work " &
|
||||
"sessions with breaks, and uses breaks to remind you to take care of your health " &
|
||||
"(walk, drink water, stretch…).") & "</p></div>"
|
||||
b.add "<div class='card lead'><p>🍅 " & t(lang,
|
||||
"NimPulse alterne des périodes de <b>travail concentré</b> et des <b>pauses</b>, et profite des pauses pour vous rappeler de prendre soin de vous.",
|
||||
"NimPulse alternates <b>focused work</b> sessions with <b>breaks</b>, and uses breaks to remind you to take care of yourself.") & "</p></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Premiers pas", "Getting started") & "</h2><ul>" &
|
||||
t(lang,
|
||||
"<li>Au premier lancement, un assistant vous guide (langue, profil, rappels santé).</li>" &
|
||||
"<li>L'application démarre minimisée : son icône colorée se trouve dans la zone de notification.</li>" &
|
||||
"<li>Un clic droit sur l'icône ouvre le menu ; un double-clic démarre ou met en pause.</li>" &
|
||||
"<li>Le raccourci global <span class=\"kbd\">Ctrl+Alt+P</span> démarre/met en pause sans toucher la souris.</li>",
|
||||
"<li>On first launch, a wizard guides you (language, profile, health reminders).</li>" &
|
||||
"<li>The app starts minimized: its colored icon lives in the notification area.</li>" &
|
||||
"<li>Right-click the icon for the menu; double-click to start or pause.</li>" &
|
||||
"<li>The global shortcut <span class=\"kbd\">Ctrl+Alt+P</span> starts/pauses without touching the mouse.</li>") &
|
||||
"</ul>"
|
||||
b.add "<h2>🧭 " & t(lang, "Comment ça marche", "How it works") & "</h2>"
|
||||
b.add "<div class='steps'>"
|
||||
b.add "<div class='step'><div class='num'>1</div><div><b>💼 " & t(lang, "Travaillez", "Work") & "</b><p>" & t(lang,
|
||||
"Lancez une période de travail concentré — 25 minutes par défaut.",
|
||||
"Start a focused work session — 25 minutes by default.") & "</p></div></div>"
|
||||
b.add "<div class='step'><div class='num'>2</div><div><b>☕ " & t(lang, "Pausez", "Break") & "</b><p>" & t(lang,
|
||||
"Reposez-vous pendant une pause courte, ou longue après plusieurs cycles.",
|
||||
"Rest during a short break, or a long one after several cycles.") & "</p></div></div>"
|
||||
b.add "<div class='step'><div class='num'>3</div><div><b>💪 " & t(lang, "Bougez", "Move") & "</b><p>" & t(lang,
|
||||
"Pendant la pause, NimPulse vous rappelle de marcher, boire ou vous étirer.",
|
||||
"During the break, NimPulse reminds you to walk, drink or stretch.") & "</p></div></div>"
|
||||
b.add "</div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Les périodes", "The periods") & "</h2><p>" &
|
||||
t(lang,
|
||||
"Un cycle classique : travail (25 min), pause courte (5 min), et après plusieurs cycles, " &
|
||||
"une pause longue (15 min). Le nombre de cycles avant la pause longue est configurable, " &
|
||||
"ainsi que les durées via les profils :",
|
||||
"A classic cycle: work (25 min), short break (5 min), and after several cycles a long " &
|
||||
"break (15 min). The number of cycles before the long break is configurable, as are the " &
|
||||
"durations via the profiles:") & "</p><table><tr><th>" &
|
||||
t(lang, "Profil", "Profile") & "</th><th>" & t(lang, "Travail", "Work") & "</th><th>" &
|
||||
t(lang, "Pause courte", "Short break") & "</th><th>" & t(lang, "Pause longue", "Long break") & "</th><th>" &
|
||||
t(lang, "Cycles", "Cycles") & "</th></tr>" &
|
||||
"<tr><td>" & t(lang, "Classique", "Classic") & "</td><td>25 min</td><td>5 min</td><td>15 min</td><td>4</td></tr>" &
|
||||
"<tr><td>Deep Work</td><td>50 min</td><td>10 min</td><td>20 min</td><td>4</td></tr>" &
|
||||
"<tr><td>Sprint</td><td>15 min</td><td>3 min</td><td>10 min</td><td>6</td></tr></table>" &
|
||||
"<p>" & t(lang,
|
||||
"Le profil actif se choisit dans le menu de l'icône (« Profils »).",
|
||||
"The active profile is chosen from the icon menu (“Profiles”).") & "</p>"
|
||||
b.add "<h2>🚀 " & t(lang, "Premiers pas", "Getting started") & "</h2>"
|
||||
b.add "<div class='card'><ul class='clean'>"
|
||||
b.add "<li>🧙 " & t(lang, "Au premier lancement, un assistant vous guide : langue, profil, rappels santé.",
|
||||
"On first launch, a wizard guides you: language, profile, health reminders.") & "</li>"
|
||||
b.add "<li>🎨 " & t(lang, "L'application démarre minimisée : son icône colorée vit dans la zone de notification.",
|
||||
"The app starts minimized: its colored icon lives in the notification area.") & "</li>"
|
||||
b.add "<li>🖱️ " & t(lang, "Clic droit sur l'icône = menu · double-clic = démarrer / pause.",
|
||||
"Right-click the icon = menu · double-click = start / pause.") & "</li>"
|
||||
b.add "<li>⌨️ " & t(lang, "Le raccourci global", "The global shortcut") &
|
||||
" <span class='kbd'>Ctrl+Alt+P</span> " &
|
||||
t(lang, "démarre ou met en pause sans toucher la souris.",
|
||||
"starts or pauses without touching the mouse.") & "</li>"
|
||||
b.add "</ul></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Pendant une pause", "During a break") & "</h2><ul>" &
|
||||
t(lang,
|
||||
"<li>« Sauter la pause » passe directement au travail suivant.</li>" &
|
||||
"<li>« Prolonger la pause » ajoute 5 minutes.</li>" &
|
||||
"<li>Les rappels santé défilent dans les notifications pendant les pauses, sans répéter deux fois le même.</li>",
|
||||
"<li>“Skip break” jumps straight to the next work session.</li>" &
|
||||
"<li>“Extend break” adds 5 minutes.</li>" &
|
||||
"<li>Health reminders rotate through notifications during breaks, never repeating the same one twice.</li>") &
|
||||
"</ul>"
|
||||
b.add "<h2>⏱️ " & t(lang, "Les périodes", "The periods") & "</h2>"
|
||||
b.add "<div class='card'><p>" & t(lang,
|
||||
"Un cycle classique : travail (25 min), pause courte (5 min) puis, après plusieurs cycles, une pause longue (15 min). Tout est configurable via les profils :",
|
||||
"A classic cycle: work (25 min), short break (5 min) then, after several cycles, a long break (15 min). Everything is configurable via the profiles:") & "</p>"
|
||||
b.add "<table><tr><th>👤 " & t(lang, "Profil", "Profile") & "</th><th>💼 " & t(lang, "Travail", "Work") &
|
||||
"</th><th>☕ " & t(lang, "Pause courte", "Short break") & "</th><th>🛋️ " & t(lang, "Pause longue", "Long break") &
|
||||
"</th><th>🔁 " & t(lang, "Cycles", "Cycles") & "</th></tr>"
|
||||
b.add "<tr><td>" & t(lang, "Classique", "Classic") & "</td><td>25 min</td><td>5 min</td><td>15 min</td><td>4</td></tr>"
|
||||
b.add "<tr><td>Deep Work</td><td>50 min</td><td>10 min</td><td>20 min</td><td>4</td></tr>"
|
||||
b.add "<tr><td>Sprint</td><td>15 min</td><td>3 min</td><td>10 min</td><td>6</td></tr></table>"
|
||||
b.add "<p class='hint'>" & t(lang,
|
||||
"Le profil actif se choisit dans le menu de l'icône (« Profils »).",
|
||||
"The active profile is chosen from the icon menu (“Profiles”).") & "</p></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Rappels santé", "Health reminders") & "</h2><p>" &
|
||||
t(lang,
|
||||
"Les rappels sont entièrement modifiables depuis la page Réglages : ajoutez, modifiez, " &
|
||||
"supprimez ou désactivez vos propres activités. Seuls les rappels activés sont affichés.",
|
||||
"Reminders are fully editable from the Settings page: add, edit, remove or disable your " &
|
||||
"own activities. Only enabled reminders are shown.") & "</p>"
|
||||
b.add "<h2>☕ " & t(lang, "Pendant une pause", "During a break") & "</h2>"
|
||||
b.add "<div class='card'><ul class='clean'>"
|
||||
b.add "<li>⏭️ " & t(lang, "« Sauter la pause » passe directement au travail suivant.",
|
||||
"“Skip break” jumps straight to the next work session.") & "</li>"
|
||||
b.add "<li>➕ " & t(lang, "« Prolonger la pause » ajoute 5 minutes.",
|
||||
"“Extend break” adds 5 minutes.") & "</li>"
|
||||
b.add "<li>🔔 " & t(lang, "Les rappels santé défilent dans les notifications, sans répéter deux fois le même.",
|
||||
"Health reminders rotate through notifications, never repeating the same one twice.") & "</li>"
|
||||
b.add "</ul></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Statistiques et objectif", "Statistics and goal") & "</h2><p>" &
|
||||
t(lang,
|
||||
"La page Statistiques affiche vos pomodoros du jour, de la semaine, votre série de jours " &
|
||||
"consécutifs et le total. Vous pouvez définir un objectif journalier dans les réglages : " &
|
||||
"une notification vous prévient quand il est atteint. Le menu de l'icône permet aussi " &
|
||||
"d'exporter l'historique en CSV.",
|
||||
"The Statistics page shows your pomodoros for today, this week, your streak of consecutive " &
|
||||
"days and the total. You can set a daily goal in settings: a notification tells you when " &
|
||||
"it is reached. The icon menu also lets you export the history as CSV.") & "</p>"
|
||||
b.add "<h2>💪 " & t(lang, "Rappels santé", "Health reminders") & "</h2>"
|
||||
b.add "<div class='card'><p>" & t(lang,
|
||||
"Entièrement modifiables depuis la page ⚙️ Réglages : ajoutez, modifiez, supprimez ou désactivez vos propres activités. Seuls les rappels activés sont affichés.",
|
||||
"Fully editable from the ⚙️ Settings page: add, edit, remove or disable your own activities. Only enabled reminders are shown.") & "</p></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Réglages", "Settings") & "</h2><ul>" &
|
||||
t(lang,
|
||||
"<li><b>Durées</b> : valeurs de base du minuteur (si aucun profil ne correspond).</li>" &
|
||||
"<li><b>Objectif journalier</b> : nombre de pomodoros par jour (0 = aucun).</li>" &
|
||||
"<li><b>Son</b> : joue un son en fin de période.</li>" &
|
||||
"<li><b>Ne pas déranger</b> : suspend les rappels santé (le minuteur continue).</li>" &
|
||||
"<li><b>Démarrage avec Windows</b> : lance NimPulse à l'ouverture de session.</li>" &
|
||||
"<li><b>Langue</b> : automatique, français ou anglais.</li>" &
|
||||
"<li><b>Fenêtre flottante</b> : petit panneau toujours au-dessus affichant la phase et le temps restant.</li>",
|
||||
"<li><b>Durations</b>: base timer values (when no profile matches).</li>" &
|
||||
"<li><b>Daily goal</b>: pomodoros per day (0 = none).</li>" &
|
||||
"<li><b>Sound</b>: plays a sound at the end of a period.</li>" &
|
||||
"<li><b>Do not disturb</b>: suspends health reminders (the timer keeps running).</li>" &
|
||||
"<li><b>Start with Windows</b>: launches NimPulse at logon.</li>" &
|
||||
"<li><b>Language</b>: automatic, French or English.</li>" &
|
||||
"<li><b>Floating window</b>: small always-on-top panel showing the phase and remaining time.</li>") &
|
||||
"</ul>"
|
||||
b.add "<h2>📊 " & t(lang, "Statistiques et objectif", "Statistics and goal") & "</h2>"
|
||||
b.add "<div class='card'><p>" & t(lang,
|
||||
"La page 📊 Statistiques affiche vos pomodoros du jour, de la semaine, votre série de jours consécutifs et le total. Définissez un objectif journalier dans les réglages : une notification vous prévient quand il est atteint 🎉. Le menu de l'icône permet aussi d'exporter l'historique en CSV.",
|
||||
"The 📊 Statistics page shows your pomodoros for today, this week, your streak of consecutive days and the total. Set a daily goal in settings: a notification tells you when it is reached 🎉. The icon menu also exports the history as CSV.") & "</p></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Astuces", "Tips") & "</h2><ul>" &
|
||||
t(lang,
|
||||
"<li>En <b>mode portable</b> (fichier vide <code>portable</code> à côté de l'exécutable), " &
|
||||
"la configuration et les statistiques sont stockées au même endroit que l'application — pratique sur clé USB.</li>" &
|
||||
"<li>Lancer NimPulse une seconde fois ne crée pas de doublon (mono-instance).</li>" &
|
||||
"<li>Ces pages web (aide, statistiques, réglages) sont servies localement par l'application : " &
|
||||
"aucune donnée ne quitte votre ordinateur.</li>",
|
||||
"<li>In <b>portable mode</b> (empty file <code>portable</code> next to the executable), " &
|
||||
"configuration and statistics are stored next to the app — handy on USB sticks.</li>" &
|
||||
"<li>Launching NimPulse a second time does not create a duplicate (single instance).</li>" &
|
||||
"<li>These web pages (help, statistics, settings) are served locally by the app: " &
|
||||
"no data leaves your computer.</li>") &
|
||||
"</ul>"
|
||||
b.add "<h2>⚙️ " & t(lang, "Réglages", "Settings") & "</h2>"
|
||||
b.add "<div class='card'><ul class='clean'>"
|
||||
b.add "<li>⏱️ <b>" & t(lang, "Durées", "Durations") & "</b> — " &
|
||||
t(lang, "valeurs de base du minuteur (si aucun profil ne correspond).", "base timer values (when no profile matches).") & "</li>"
|
||||
b.add "<li>🎯 <b>" & t(lang, "Objectif journalier", "Daily goal") & "</b> — " &
|
||||
t(lang, "pomodoros par jour (0 = aucun).", "pomodoros per day (0 = none).") & "</li>"
|
||||
b.add "<li>🔊 <b>" & t(lang, "Son", "Sound") & "</b> — " &
|
||||
t(lang, "joué en fin de période.", "played at the end of a period.") & "</li>"
|
||||
b.add "<li>🌙 <b>" & t(lang, "Ne pas déranger", "Do not disturb") & "</b> — " &
|
||||
t(lang, "suspend les rappels santé (le minuteur continue).", "suspends health reminders (the timer keeps running).") & "</li>"
|
||||
b.add "<li>🚀 <b>" & t(lang, "Démarrage avec Windows", "Start with Windows") & "</b> — " &
|
||||
t(lang, "lance NimPulse à l'ouverture de session.", "launches NimPulse at logon.") & "</li>"
|
||||
b.add "<li>🌐 <b>" & t(lang, "Langue", "Language") & "</b> — " &
|
||||
t(lang, "automatique, français ou anglais.", "automatic, French or English.") & "</li>"
|
||||
b.add "<li>🖥️ <b>" & t(lang, "Fenêtre flottante", "Floating window") & "</b> — " &
|
||||
t(lang, "petit panneau toujours au-dessus avec la phase et le temps restant.",
|
||||
"small always-on-top panel with the phase and remaining time.") & "</li>"
|
||||
b.add "</ul></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "Raccourcis", "Shortcuts") & "</h2><table>" &
|
||||
"<tr><th>" & t(lang, "Action", "Action") & "</th><th>" & t(lang, "Raccourci", "Shortcut") & "</th></tr>" &
|
||||
"<tr><td>" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td><span class=\"kbd\">Ctrl+Alt+P</span></td></tr>" &
|
||||
"<tr><td>" & t(lang, "Menu contextuel", "Context menu") & "</td><td>" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "</td></tr>" &
|
||||
"<tr><td>" & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td>" & t(lang, "Double-clic", "Double-click") & "</td></tr>" &
|
||||
"</table>"
|
||||
b.add "<h2>💡 " & t(lang, "Astuces", "Tips") & "</h2>"
|
||||
b.add "<div class='card'><ul class='clean'>"
|
||||
b.add "<li>🧳 " & t(lang,
|
||||
"En <b>mode portable</b> (fichier vide <code>portable</code> à côté de l'exécutable), configuration et statistiques suivent l'application — pratique sur clé USB.",
|
||||
"In <b>portable mode</b> (empty <code>portable</code> file next to the executable), configuration and statistics follow the app — handy on USB sticks.") & "</li>"
|
||||
b.add "<li>🪞 " & t(lang,
|
||||
"Lancer NimPulse une seconde fois ne crée pas de doublon (mono-instance).",
|
||||
"Launching NimPulse a second time does not create a duplicate (single instance).") & "</li>"
|
||||
b.add "<li>🔒 " & t(lang,
|
||||
"Ces pages sont servies localement par l'application : aucune donnée ne quitte votre ordinateur.",
|
||||
"These pages are served locally by the app: no data leaves your computer.") & "</li>"
|
||||
b.add "</ul></div>"
|
||||
|
||||
b.add "<h2>⌨️ " & t(lang, "Raccourcis", "Shortcuts") & "</h2>"
|
||||
b.add "<div class='card'><table>"
|
||||
b.add "<tr><th>" & t(lang, "Action", "Action") & "</th><th>" & t(lang, "Raccourci", "Shortcut") & "</th></tr>"
|
||||
b.add "<tr><td>▶️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td><span class='kbd'>Ctrl+Alt+P</span></td></tr>"
|
||||
b.add "<tr><td>📋 " & t(lang, "Menu contextuel", "Context menu") & "</td><td>" & t(lang, "Clic droit sur l'icône", "Right-click the icon") & "</td></tr>"
|
||||
b.add "<tr><td>🖱️ " & t(lang, "Démarrer / mettre en pause", "Start / pause") & "</td><td>" & t(lang, "Double-clic", "Double-click") & "</td></tr>"
|
||||
b.add "</table></div>"
|
||||
|
||||
pageShell(lang, t(lang, "Aide", "Help"), b, "help")
|
||||
|
||||
pageShell(lang, t(lang, "Aide", "Help"), b)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page statistiques
|
||||
@@ -324,40 +426,42 @@ proc renderStats*(cfg: Config): string =
|
||||
if n > maxv: maxv = n
|
||||
|
||||
var b = ""
|
||||
b.add "<h1>" & t(lang, "Statistiques", "Statistics") & "</h1>"
|
||||
b.add "<div class='statgrid'>"
|
||||
b.add statCard("📅", t(lang, "Aujourd'hui", "Today"), $today, t(lang, "pomodoro(s)", "pomodoro(s)"))
|
||||
b.add statCard("🗓️", t(lang, "Cette semaine", "This week"), $week, t(lang, "pomodoro(s)", "pomodoro(s)"))
|
||||
b.add statCard("🔥", t(lang, "Série", "Streak"), $streak, t(lang, "jour(s)", "day(s)"))
|
||||
b.add statCard("🏆", t(lang, "Total", "Total"), $total, t(lang, "complétés", "completed"))
|
||||
b.add "</div>"
|
||||
|
||||
b.add "<div class=\"card\"><h2>" & t(lang, "Aujourd'hui", "Today") & "</h2>" &
|
||||
"<p style=\"font-size:2.2rem;margin:.2rem 0;font-weight:700\">" & $today & " <span class=\"muted\">" &
|
||||
t(lang, "pomodoro(s)", "pomodoro(s)") & "</span></p>" &
|
||||
(if cfg.dailyGoal > 0:
|
||||
"<p class=\"muted\">" & t(lang, "Objectif", "Goal") & " : " & $today & " / " & $cfg.dailyGoal & "</p>" &
|
||||
"<div class=\"bar\" style=\"width:100%\"><div style=\"width:" &
|
||||
$(if today >= cfg.dailyGoal: 100 else: today * 100 div cfg.dailyGoal) & "%\"></div></div>"
|
||||
else: "") &
|
||||
"</div>"
|
||||
if cfg.dailyGoal > 0:
|
||||
b.add "<div class='card'><h2>🎯 " & t(lang, "Objectif journalier", "Daily goal") & "</h2>"
|
||||
b.add "<p class='big-number'>" & $today & " / " & $cfg.dailyGoal & "</p>"
|
||||
b.add "<div class='progress'><div style='width:" &
|
||||
$(if today >= cfg.dailyGoal: 100 else: today * 100 div cfg.dailyGoal) & "%'></div></div>"
|
||||
if today >= cfg.dailyGoal:
|
||||
b.add "<p class='goalok'>🎉 " & t(lang, "Objectif atteint, bravo !", "Goal reached, congratulations!") & "</p>"
|
||||
else:
|
||||
b.add "<p class='hint'>💪 " & t(lang,
|
||||
"Encore un petit effort — chaque pomodoro compte !",
|
||||
"Keep going — every pomodoro counts!") & "</p>"
|
||||
b.add "</div>"
|
||||
|
||||
b.add "<div class=\"grid\"><div class=\"card\"><h2>" & t(lang, "Cette semaine", "This week") & "</h2>" &
|
||||
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $week & "</p></div>" &
|
||||
"<div class=\"card\"><h2>" & t(lang, "Série (streak)", "Streak") & "</h2>" &
|
||||
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $streak & " <span class=\"muted\">" &
|
||||
t(lang, "jour(s)", "day(s)") & "</span></p></div></div>"
|
||||
|
||||
b.add "<div class=\"card\"><h2>" & t(lang, "Total", "Total") & "</h2>" &
|
||||
"<p style=\"font-size:1.6rem;margin:.2rem 0;font-weight:700\">" & $total & " <span class=\"muted\">" &
|
||||
t(lang, "pomodoros complétés", "completed pomodoros") & "</span></p></div>"
|
||||
|
||||
b.add "<h2>" & t(lang, "7 derniers jours", "Last 7 days") & "</h2><table>" &
|
||||
"<tr><th>" & t(lang, "Date", "Date") & "</th><th>" & t(lang, "Pomodoros", "Pomodoros") & "</th></tr>"
|
||||
b.add "<h2>📈 " & t(lang, "7 derniers jours", "Last 7 days") & "</h2>"
|
||||
b.add "<div class='chart'>"
|
||||
for (date, n) in days:
|
||||
b.add "<tr><td>" & htmlEscape(date) & "</td><td>" & $n & " <span class=\"bar\" style=\"width:120px\"><div style=\"width:" &
|
||||
$(n * 100 div maxv) & "%\"></div></span></td></tr>"
|
||||
b.add "</table>"
|
||||
let dayLabel = date[5 .. 6] & "/" & date[8 .. 9]
|
||||
let h = if n == 0: 6 else: 14 + n * 100 div maxv
|
||||
b.add "<div class='barcol'>" &
|
||||
"<div class='barval'>" & (if n > 0: $n else: "") & "</div>" &
|
||||
"<div class='barfill' style='height:" & $h & "px'></div>" &
|
||||
"<div class='barlabel'>" & htmlEscape(dayLabel) & "</div></div>"
|
||||
b.add "</div>"
|
||||
b.add "<p class='hint'>💾 " & t(lang,
|
||||
"Un export CSV est disponible depuis le menu de l'icône.",
|
||||
"A CSV export is available from the icon menu.") & "</p>"
|
||||
|
||||
b.add "<p class=\"muted\">" & t(lang,
|
||||
"Un export CSV est disponible depuis le menu de l'icône (Exporter les statistiques).",
|
||||
"A CSV export is available from the icon menu (Export statistics).") & "</p>"
|
||||
pageShell(lang, t(lang, "Statistiques", "Statistics"), b, "stats")
|
||||
|
||||
pageShell(lang, t(lang, "Statistiques", "Statistics"), b)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page réglages
|
||||
@@ -376,83 +480,101 @@ proc renderSettings*(cfg: Config): string =
|
||||
var profileOpts = ""
|
||||
for p in cfg.profiles:
|
||||
let sel = if p.name == cfg.activeProfile: " selected" else: ""
|
||||
profileOpts.add "<option value=\"" & htmlEscape(p.name) & "\"@@" & sel & ">" &
|
||||
profileOpts.add "<option value='" & htmlEscape(p.name) & "'" & sel & ">" &
|
||||
htmlEscape(p.name) & "</option>"
|
||||
|
||||
let langOpts =
|
||||
"<option value=\"auto\"@@" & (if cfg.lang == "auto": " selected" else: "") & ">" &
|
||||
"<option value='auto'" & (if cfg.lang == "auto": " selected" else: "") & ">🌐 " &
|
||||
t(lang, "Automatique", "Automatic") & "</option>" &
|
||||
"<option value=\"fr\"@@" & (if cfg.lang == "fr": " selected" else: "") & ">Français</option>" &
|
||||
"<option value=\"en\"@@" & (if cfg.lang == "en": " selected" else: "") & ">English</option>"
|
||||
"<option value='fr'" & (if cfg.lang == "fr": " selected" else: "") & ">🇫🇷 Français</option>" &
|
||||
"<option value='en'" & (if cfg.lang == "en": " selected" else: "") & ">🇬🇧 English</option>"
|
||||
|
||||
var b = ""
|
||||
b.add "<h1>" & t(lang, "Réglages", "Settings") & "</h1>"
|
||||
b.add "<form method=\"post\" action=\"/settings\" onsubmit=\"syncReminders()\">"
|
||||
b.add "<form method='post' action='/settings' onsubmit='syncReminders()'>"
|
||||
|
||||
b.add "<div class=\"grid\"><div>"
|
||||
b.add "<label>" & t(lang, "Travail (min)", "Work (min)") & "</label>" &
|
||||
"<input type=\"number\" name=\"workMin\" min=\"1\" value=\"" & $cfg.workMin & "\">"
|
||||
b.add "<label>" & t(lang, "Pause courte (min)", "Short break (min)") & "</label>" &
|
||||
"<input type=\"number\" name=\"shortBreakMin\" min=\"1\" value=\"" & $cfg.shortBreakMin & "\">"
|
||||
b.add "<label>" & t(lang, "Pause longue (min)", "Long break (min)") & "</label>" &
|
||||
"<input type=\"number\" name=\"longBreakMin\" min=\"1\" value=\"" & $cfg.longBreakMin & "\">"
|
||||
b.add "<label>" & t(lang, "Cycles avant pause longue", "Cycles before long break") & "</label>" &
|
||||
"<input type=\"number\" name=\"cyclesBeforeLongBreak\" min=\"1\" value=\"" & $cfg.cyclesBeforeLongBreak & "\">"
|
||||
b.add "<label>" & t(lang, "Objectif journalier (0 = aucun)", "Daily goal (0 = none)") & "</label>" &
|
||||
"<input type=\"number\" name=\"dailyGoal\" min=\"0\" value=\"" & $cfg.dailyGoal & "\">"
|
||||
b.add "</div><div>"
|
||||
# --- Minuteur ---
|
||||
b.add "<div class='card'><h2>⏱️ " & t(lang, "Minuteur", "Timer") & "</h2>"
|
||||
b.add "<p class='hint'>" & t(lang,
|
||||
"Valeurs de base utilisées quand aucun profil ne correspond.",
|
||||
"Base values used when no profile matches.") & "</p>"
|
||||
b.add "<div class='grid2'>"
|
||||
b.add numField("💼", t(lang, "Travail (min)", "Work (min)"), "workMin", cfg.workMin, 1)
|
||||
b.add numField("☕", t(lang, "Pause courte (min)", "Short break (min)"), "shortBreakMin", cfg.shortBreakMin, 1)
|
||||
b.add numField("🛋️", t(lang, "Pause longue (min)", "Long break (min)"), "longBreakMin", cfg.longBreakMin, 1)
|
||||
b.add numField("🔁", t(lang, "Cycles avant pause longue", "Cycles before long break"),
|
||||
"cyclesBeforeLongBreak", cfg.cyclesBeforeLongBreak, 1)
|
||||
b.add "</div></div>"
|
||||
|
||||
b.add "<label>" & t(lang, "Langue", "Language") & "</label>" &
|
||||
"<select name=\"lang\">" & langOpts & "</select>"
|
||||
b.add "<label>" & t(lang, "Profil actif", "Active profile") & "</label>" &
|
||||
"<select name=\"activeProfile\">" & profileOpts & "</select>"
|
||||
# --- Objectif et langue ---
|
||||
b.add "<div class='card'><h2>🎯 " & t(lang, "Objectif et langue", "Goal and language") & "</h2>"
|
||||
b.add "<div class='grid2'>"
|
||||
b.add numField("🎯", t(lang, "Objectif journalier (0 = aucun)", "Daily goal (0 = none)"),
|
||||
"dailyGoal", cfg.dailyGoal, 0)
|
||||
b.add "<label class='field'><span class='flabel'>🌐 " & t(lang, "Langue", "Language") & "</span>" &
|
||||
"<select name='lang'>" & langOpts & "</select></label>"
|
||||
b.add "</div></div>"
|
||||
|
||||
b.add "<p style=\"margin-top:.9rem\">"
|
||||
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"soundEnabled\"@@" &
|
||||
(if cfg.soundEnabled: " checked" else: "") & "> " & t(lang, "Son en fin de période", "Sound at end of period") & "</label>"
|
||||
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"doNotDisturb\"@@" &
|
||||
(if cfg.doNotDisturb: " checked" else: "") & "> " & t(lang, "Ne pas déranger", "Do not disturb") & "</label>"
|
||||
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"autoStart\"@@" &
|
||||
(if cfg.autoStart: " checked" else: "") & "> " & t(lang, "Démarrage avec Windows", "Start with Windows") & "</label>"
|
||||
b.add "<label style=\"display:flex;gap:.5rem;align-items:center\"><input type=\"checkbox\" name=\"floatingWindow\"@@" &
|
||||
(if cfg.floatingWindow: " checked" else: "") & "> " & t(lang, "Fenêtre flottante", "Floating window") & "</label>"
|
||||
b.add "</p></div></div>"
|
||||
# --- Profil actif ---
|
||||
b.add "<div class='card'><h2>👤 " & t(lang, "Profil actif", "Active profile") & "</h2>"
|
||||
b.add "<p class='hint'>" & t(lang,
|
||||
"Détermine les durées utilisées pour les prochaines périodes.",
|
||||
"Determines the durations used for the next periods.") & "</p>"
|
||||
b.add "<label class='field'><span class='flabel'>📦 " & t(lang, "Profil", "Profile") & "</span>" &
|
||||
"<select name='activeProfile'>" & profileOpts & "</select></label>"
|
||||
b.add "</div>"
|
||||
|
||||
# --- Notifications et système ---
|
||||
b.add "<div class='card'><h2>🔔 " & t(lang, "Notifications et système", "Notifications and system") & "</h2>"
|
||||
b.add checkRow("soundEnabled", cfg.soundEnabled, "🔊", t(lang, "Son en fin de période", "Sound at end of period"))
|
||||
b.add checkRow("doNotDisturb", cfg.doNotDisturb, "🌙", t(lang, "Ne pas déranger", "Do not disturb"))
|
||||
b.add checkRow("autoStart", cfg.autoStart, "🚀", t(lang, "Démarrage avec Windows", "Start with Windows"))
|
||||
b.add checkRow("floatingWindow", cfg.floatingWindow, "🖥️", t(lang, "Fenêtre flottante", "Floating window"))
|
||||
b.add "</div>"
|
||||
|
||||
# --- Rappels santé (éditeur client) ---
|
||||
b.add "<h2>" & t(lang, "Rappels santé", "Health reminders") & "</h2>"
|
||||
b.add "<div id=\"reminders\"></div>"
|
||||
b.add "<button type=\"button\" class=\"btn\" onclick=\"addReminder()\" style=\"margin-top:.6rem\">" &
|
||||
b.add "<div class='card'><h2>💪 " & t(lang, "Rappels santé", "Health reminders") & "</h2>"
|
||||
b.add "<p class='hint'>" & t(lang,
|
||||
"Ajoutez, modifiez ou désactivez les rappels affichés pendant les pauses.",
|
||||
"Add, edit or disable the reminders shown during breaks.") & "</p>"
|
||||
b.add "<div id='reminders'></div>"
|
||||
b.add "<button type='button' class='btn ghost' onclick='addReminder()' style='margin-top:.8rem'>➕ " &
|
||||
t(lang, "Ajouter un rappel", "Add a reminder") & "</button>"
|
||||
b.add "<input type=\"hidden\" name=\"reminders_json\" id=\"reminders_json\">"
|
||||
b.add "<input type='hidden' name='reminders_json' id='reminders_json'>"
|
||||
b.add "</div>"
|
||||
|
||||
b.add "<p style=\"margin-top:1.4rem\"><button type=\"submit\" class=\"btn\">" &
|
||||
t(lang, "Enregistrer", "Save") & "</button></p>"
|
||||
b.add "<p class='savezone'><button type='submit' class='btn big'>💾 " &
|
||||
t(lang, "Enregistrer les réglages", "Save settings") & "</button></p>"
|
||||
b.add "</form>"
|
||||
|
||||
b.add "<script>"
|
||||
b.add "var reminders = " & remindersJson(cfg.healthReminders) & ";"
|
||||
b.add """var R = function(fr, en){ return document.documentElement.lang === 'fr' ? fr : en; };
|
||||
function esc(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function renderReminders(){
|
||||
var el = document.getElementById('reminders');
|
||||
if (reminders.length === 0){ el.innerHTML = '<p class="muted">' + R('Aucun rappel.','No reminders.') + '</p>'; return; }
|
||||
var html = '<table><tr><th></th><th>' + R('Texte','Text') + '</th><th></th><th></th></tr>';
|
||||
if (reminders.length === 0){
|
||||
el.innerHTML = '<div class="empty">' + R('🌱 Aucun rappel pour le moment.','🌱 No reminders yet.') + '</div>';
|
||||
return;
|
||||
}
|
||||
var html = '<div class="remlist">';
|
||||
for (var i = 0; i < reminders.length; i++){
|
||||
var r = reminders[i];
|
||||
var safe = r.text.replace(/&/g,'&').replace(/</g,'<');
|
||||
html += '<tr><td><input type="checkbox" ' + (r.enabled ? 'checked' : '') + ' onchange="toggleReminder(' + i + ')"></td>' +
|
||||
'<td>' + safe + '</td>' +
|
||||
'<td><button type="button" onclick="editReminder(' + i + ')">' + R('Modifier','Edit') + '</button></td>' +
|
||||
'<td><button type="button" onclick="removeReminder(' + i + ')">' + R('Supprimer','Remove') + '</button></td></tr>';
|
||||
html += '<div class="remrow' + (r.enabled ? '' : ' off') + '">' +
|
||||
'<input type="checkbox" class="chk" ' + (r.enabled ? 'checked' : '') + ' onchange="toggleReminder(' + i + ')">' +
|
||||
'<span class="remtext">' + esc(r.text) + '</span>' +
|
||||
'<button type="button" class="mini" onclick="editReminder(' + i + ')">✏️ ' + R('Modifier','Edit') + '</button>' +
|
||||
'<button type="button" class="mini danger" onclick="removeReminder(' + i + ')">🗑️</button>' +
|
||||
'</div>';
|
||||
}
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
function addReminder(){
|
||||
var t = prompt(R('Nouveau rappel santé :','New health reminder:'));
|
||||
var t = prompt(R('💪 Nouveau rappel santé :','💪 New health reminder:'));
|
||||
if (t && t.trim().length > 0){ reminders.push({text: t.trim(), enabled: true}); renderReminders(); }
|
||||
}
|
||||
function editReminder(i){
|
||||
var t = prompt(R('Modifier le rappel :','Edit reminder:'), reminders[i].text);
|
||||
var t = prompt(R('✏️ Modifier le rappel :','✏️ Edit reminder:'), reminders[i].text);
|
||||
if (t && t.trim().length > 0){ reminders[i].text = t.trim(); renderReminders(); }
|
||||
}
|
||||
function removeReminder(i){ reminders.splice(i, 1); renderReminders(); }
|
||||
@@ -462,17 +584,20 @@ renderReminders();
|
||||
"""
|
||||
b.add "</script>"
|
||||
|
||||
pageShell(lang, t(lang, "Réglages", "Settings"), b)
|
||||
pageShell(lang, t(lang, "Réglages", "Settings"), b, "settings")
|
||||
|
||||
|
||||
proc renderSaved(cfg: Config): string =
|
||||
let lang = resolveLang(cfg)
|
||||
var b = "<h1>" & t(lang, "Réglages enregistrés", "Settings saved") & " ✓</h1>" &
|
||||
"<div class=\"card\"><p>" &
|
||||
t(lang, "Votre configuration a été enregistrée et appliquée.",
|
||||
"Your configuration has been saved and applied.") &
|
||||
"</p><p><a class=\"btn\" href=\"/settings\">" & t(lang, "Retour aux réglages", "Back to settings") & "</a> " &
|
||||
"<a class=\"btn\" href=\"/stats\">" & t(lang, "Voir les statistiques", "View statistics") & "</a></p></div>"
|
||||
pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b)
|
||||
var b = "<div class='card saved'><div class='savedemoji'>🎉</div>" &
|
||||
"<h2>" & t(lang, "Réglages enregistrés !", "Settings saved!") & "</h2>" &
|
||||
"<p>" & t(lang,
|
||||
"Votre configuration a été enregistrée et appliquée immédiatement.",
|
||||
"Your configuration has been saved and applied immediately.") & "</p>" &
|
||||
"<p><a class='btn' href='/settings'>⚙️ " & t(lang, "Retour aux réglages", "Back to settings") & "</a> " &
|
||||
"<a class='btn ghost' href='/stats'>📊 " & t(lang, "Voir les statistiques", "View statistics") & "</a></p></div>"
|
||||
pageShell(lang, t(lang, "Réglages enregistrés", "Settings saved"), b, "settings")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application d'un formulaire de réglages
|
||||
|
||||
@@ -76,7 +76,7 @@ suite "rendering":
|
||||
check "Pomodoro" in renderHelp(cfg)
|
||||
check "Statistiques" in renderStats(cfg)
|
||||
check "Réglages" in renderSettings(cfg)
|
||||
check "name=\"workMin\"" in renderSettings(cfg)
|
||||
check "name='workMin'" in renderSettings(cfg)
|
||||
check "reminders_json" in renderSettings(cfg)
|
||||
|
||||
suite "integration (local HTTP server)":
|
||||
@@ -105,7 +105,7 @@ suite "integration (local HTTP server)":
|
||||
check "Statistiques" in stats
|
||||
|
||||
let settings = client.getContent(base & "/settings")
|
||||
check "name=\"workMin\"" in settings
|
||||
check "name='workMin'" in settings
|
||||
|
||||
# Sauvegarde via POST (comme le ferait le navigateur).
|
||||
let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" &
|
||||
@@ -129,6 +129,6 @@ suite "integration (local HTTP server)":
|
||||
|
||||
# La page réglages reflète les nouvelles valeurs.
|
||||
let settings2 = client.getContent(base & "/settings")
|
||||
check "value=\"33\"" in settings2
|
||||
check "value='33'" in settings2
|
||||
|
||||
removeDir(dir)
|
||||
|
||||
Reference in New Issue
Block a user