v0.3.0 : migration des dialogues vers wNim + renommage NimPulse + installeur une-ligne
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# TrayTempo
|
# NimPulse
|
||||||
|
|
||||||
Timer **Pomodoro nouvelle génération** pour Windows, écrit entièrement en
|
Timer **Pomodoro nouvelle génération** pour Windows, écrit entièrement en
|
||||||
**Nim**. Il vit discrètement dans la zone de notification (system tray) et
|
**Nim**. Il vit discrètement dans la zone de notification (system tray) et
|
||||||
@@ -53,14 +53,31 @@ prend soin de votre santé autant que de votre productivité, dans une interface
|
|||||||
nimble install winim
|
nimble install winim
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Le framework GUI [wNim](https://github.com/khchen/wNim) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nimble install wnim
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important (Nim 2.2.x)** : wNim 1.0.0 contient deux bugs de compilation
|
||||||
|
> avec Nim 2.2.x. Appliquez ces deux correctifs dans
|
||||||
|
> `~/.nimble/pkgs2/wnim-*/wnim/` :
|
||||||
|
> 1. `private/kiwi/strength.nim` : remplacer les 4 `const X = createStrength(...)`
|
||||||
|
> par des littéraux (`REQUIRED = 1000.0`, `STRONG = 1000000.0`,
|
||||||
|
> `MEDIUM = 1000.0`, `WEAK = 1.0`).
|
||||||
|
> 2. `private/wResizer.nim` : remplacer `initHashSet[wResizable]()` par
|
||||||
|
> `HashSet[wTypes.wResizable]()`.
|
||||||
|
> Voir les issues wNim [#125](https://github.com/khchen/wNim/issues/125) et
|
||||||
|
> [#133](https://github.com/khchen/wNim/issues/133).
|
||||||
|
|
||||||
## Compilation
|
## Compilation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nim c -d:release --app:gui src/traytempo.nim
|
nim c -d:release --app:gui src/nimpulse.nim
|
||||||
```
|
```
|
||||||
|
|
||||||
Le binaire résultant `src/traytempo.exe` est autonome (aucune dépendance
|
Le binaire résultant `src/nimpulse.exe` est autonome (aucune dépendance
|
||||||
externe à installer) et pèse moins de 1 Mo.
|
externe à installer) et pèse environ 1,4 Mo.
|
||||||
|
|
||||||
> **Note** : à l'édition de liens, `winim` injecte son propre fichier de
|
> **Note** : à l'édition de liens, `winim` injecte son propre fichier de
|
||||||
> ressources (styles visuels / manifeste), ce qui peut produire l'avertissement
|
> ressources (styles visuels / manifeste), ce qui peut produire l'avertissement
|
||||||
@@ -70,18 +87,26 @@ externe à installer) et pèse moins de 1 Mo.
|
|||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|
||||||
- **Binaire autonome** : `src\traytempo.exe` (ou `build\traytempo.exe`).
|
- **Binaire autonome** : `src\nimpulse.exe` (ou `build\nimpulse.exe`).
|
||||||
|
- **Installeur une-ligne (Windows)** :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.dracodev.net/Projets/NimPulse/-/raw/main/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script [`install.ps1`](install.ps1) télécharge le binaire et crée un
|
||||||
|
raccourci sur le bureau.
|
||||||
- **Archive portable** : lancez [`release.cmd`](release.cmd) pour compiler et
|
- **Archive portable** : lancez [`release.cmd`](release.cmd) pour compiler et
|
||||||
créer `build\TrayTempo-portable.zip` (version prête à copier sur une clé USB).
|
créer `build\NimPulse-portable.zip` (version prête à copier sur une clé USB).
|
||||||
- **Installeur** : un script [Inno Setup](installer/traytempo.iss) est fourni.
|
- **Installeur classique** : un script [Inno Setup](installer/nimpulse.iss) est
|
||||||
Compilez-le avec [Inno Setup 6](https://jrsoftware.org/isinfo.php).
|
fourni. Compilez-le avec [Inno Setup 6](https://jrsoftware.org/isinfo.php).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Au premier lancement (et à la sortie), l'application crée :
|
Au premier lancement (et à la sortie), l'application crée :
|
||||||
|
|
||||||
```
|
```
|
||||||
%APPDATA%\TrayTempo\
|
%APPDATA%\NimPulse\
|
||||||
├── config.json # réglages, profils, rappels santé
|
├── config.json # réglages, profils, rappels santé
|
||||||
└── stats.json # statistiques
|
└── stats.json # statistiques
|
||||||
```
|
```
|
||||||
@@ -116,7 +141,7 @@ Un exemple de configuration est fourni dans
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── traytempo.nim # point d'entrée : fenêtre cachée, boucle de messages,
|
├── nimpulse.nim # point d'entrée : fenêtre cachée, boucle de messages,
|
||||||
│ # WM_TIMER, raccourci global, mono-instance, câblage
|
│ # WM_TIMER, raccourci global, mono-instance, câblage
|
||||||
├── config.nim # Config/Profile/HealthReminder/Lang, JSON, registre (Run)
|
├── config.nim # Config/Profile/HealthReminder/Lang, JSON, registre (Run)
|
||||||
├── timer.nim # machine à états Pomodoro (aucune dépendance Win32)
|
├── timer.nim # machine à états Pomodoro (aucune dépendance Win32)
|
||||||
@@ -124,14 +149,14 @@ src/
|
|||||||
├── notifications.nim # toast (NIF_INFO), son, rotation des rappels santé
|
├── notifications.nim # toast (NIF_INFO), son, rotation des rappels santé
|
||||||
├── stats.nim # compteurs jour/semaine/total, streak, CSV, JSON
|
├── stats.nim # compteurs jour/semaine/total, streak, CSV, JSON
|
||||||
├── i18n.nim # chaînes localisées (français / anglais), détection langue
|
├── i18n.nim # chaînes localisées (français / anglais), détection langue
|
||||||
├── dialogs.nim # réglages, rappels santé, assistant, statistiques (Win32)
|
├── dialogs.nim # réglages, rappels santé, assistant, stats (wNim)
|
||||||
├── floating.nim # fenêtre flottante compacte (toujours au-dessus)
|
├── floating.nim # fenêtre flottante compacte (toujours au-dessus)
|
||||||
└── updater.nim # comparaison de versions + récupération distante
|
└── updater.nim # comparaison de versions + récupération distante
|
||||||
```
|
```
|
||||||
|
|
||||||
Séparation des responsabilités : `timer`, `stats` et `updater` sont purs
|
Séparation des responsabilités : `timer`, `stats` et `updater` sont purs
|
||||||
(aucune dépendance à l'API Win32), ce qui les rend testables ; `config`,
|
(aucune dépendance à l'API Win32), ce qui les rend testables ; `config`,
|
||||||
`tray`, `notifications`, `dialogs` et `traytempo` gèrent l'interaction avec
|
`tray`, `notifications`, `dialogs` et `nimpulse` gèrent l'interaction avec
|
||||||
Windows.
|
Windows.
|
||||||
|
|
||||||
## Raccourcis
|
## Raccourcis
|
||||||
@@ -162,7 +187,6 @@ nim c -r tests/test_notifications.nim
|
|||||||
nim c -r tests/test_tray_integration.nim
|
nim c -r tests/test_tray_integration.nim
|
||||||
nim c -r tests/test_updater.nim
|
nim c -r tests/test_updater.nim
|
||||||
nim c -r tests/test_floating.nim
|
nim c -r tests/test_floating.nim
|
||||||
nim c -r tests/test_dialogs.nim
|
|
||||||
```
|
```
|
||||||
|
|
||||||
La correspondance exigence → contrôle → résultat est détaillée dans
|
La correspondance exigence → contrôle → résultat est détaillée dans
|
||||||
|
|||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
# Vérification TrayTempo
|
# Vérification NimPulse
|
||||||
|
|
||||||
Table de correspondance entre chaque exigence de la spécification, le contrôle
|
Table de correspondance entre chaque exigence de la spécification, le contrôle
|
||||||
concret effectué, et le résultat observé.
|
concret effectué, et le résultat observé.
|
||||||
@@ -11,7 +11,7 @@ concret effectué, et le résultat observé.
|
|||||||
| 1a | Menu contextuel (Démarrer/Pause/Arrêter, Profils, Paramètres, Statistiques, Quitter) | Construction du menu dans `tray.nim` (`CreatePopupMenu`/`AppendMenu`/`TrackPopupMenu`) | Compile ; affichage non inspecté visuellement |
|
| 1a | Menu contextuel (Démarrer/Pause/Arrêter, Profils, Paramètres, Statistiques, Quitter) | Construction du menu dans `tray.nim` (`CreatePopupMenu`/`AppendMenu`/`TrackPopupMenu`) | Compile ; affichage non inspecté visuellement |
|
||||||
| 1b | Icône dynamique selon l'état (travail/pause courte/pause longue/pause) | `tests/test_tray_integration.nim` : `phaseIcon` retourne un `HICON` non nul pour les 5 phases | **OK** (5 icônes GDI générées) |
|
| 1b | Icône dynamique selon l'état (travail/pause courte/pause longue/pause) | `tests/test_tray_integration.nim` : `phaseIcon` retourne un `HICON` non nul pour les 5 phases | **OK** (5 icônes GDI générées) |
|
||||||
| 1c | Tooltip temps restant | `updateTrayIcon(NIF_TIP)` appelé dans le test d'intégration | **OK** (NIM_MODIFY sans erreur) |
|
| 1c | Tooltip temps restant | `updateTrayIcon(NIF_TIP)` appelé dans le test d'intégration | **OK** (NIM_MODIFY sans erreur) |
|
||||||
| 2 | Durées configurables + profils + sauvegarde `%APPDATA%\TrayTempo` | `tests/test_timer.nim` (17 tests) + `tests/test_config.nim` (round-trip JSON, accents UTF-8, champs manquants) | 17 + 9 tests **OK** |
|
| 2 | Durées configurables + profils + sauvegarde `%APPDATA%\NimPulse` | `tests/test_timer.nim` (17 tests) + `tests/test_config.nim` (round-trip JSON, accents UTF-8, champs manquants) | 17 + 9 tests **OK** |
|
||||||
| 3 | Rappels santé configurables, rotation sans répétition | `tests/test_notifications.nim` (5 tests) | 5 tests **OK** |
|
| 3 | Rappels santé configurables, rotation sans répétition | `tests/test_notifications.nim` (5 tests) | 5 tests **OK** |
|
||||||
| 4 | Notifications natives (toast / `NIF_INFO`) + son optionnel | `tests/test_tray_integration.nim` : `showToast` (NIF_INFO) sans erreur + `playSound("SystemAsterisk")` retourne `true` | **OK** (API ; rendu toast et son audible non observés) |
|
| 4 | Notifications natives (toast / `NIF_INFO`) + son optionnel | `tests/test_tray_integration.nim` : `showToast` (NIF_INFO) sans erreur + `playSound("SystemAsterisk")` retourne `true` | **OK** (API ; rendu toast et son audible non observés) |
|
||||||
| 5 | Statistiques (jour/semaine/total, streak, historique, CSV) | `tests/test_stats.nim` (10 tests) | 10 tests **OK** |
|
| 5 | Statistiques (jour/semaine/total, streak, historique, CSV) | `tests/test_stats.nim` (10 tests) | 10 tests **OK** |
|
||||||
@@ -27,7 +27,7 @@ concret effectué, et le résultat observé.
|
|||||||
| 8 | Fix du bug `cycleCount` (reset sur `stop`) | `tests/test_timer.nim` « stop resets cycleCount » | **OK** |
|
| 8 | Fix du bug `cycleCount` (reset sur `stop`) | `tests/test_timer.nim` « stop resets cycleCount » | **OK** |
|
||||||
| 9 | Sauter / prolonger une pause | `tests/test_timer.nim` (skipBreak, extendBreak, isBreak) | 4 tests **OK** |
|
| 9 | Sauter / prolonger une pause | `tests/test_timer.nim` (skipBreak, extendBreak, isBreak) | 4 tests **OK** |
|
||||||
| 10 | Internationalisation (fr/en) | `i18n.nim` : deux bundles `Strings`, détection langue système ; `tests/test_config.nim` « English defaults » | **OK** |
|
| 10 | Internationalisation (fr/en) | `i18n.nim` : deux bundles `Strings`, détection langue système ; `tests/test_config.nim` « English defaults » | **OK** |
|
||||||
| 11 | Panneau de réglages | `tests/test_dialogs.nim` : `buildSettingsDialog` crée une fenêtre et ses contrôles | **OK** (fenêtre + contrôles créés) |
|
| 11 | Panneau de réglages (wNim) | `dialogs.nim` (wDialog/wFrame + contrôles wNim) | Compile + lancement réel de l'app |
|
||||||
| 12 | Objectif journalier | `stats.nim` `goalReached` ; `tests/test_stats.nim` « goalReached is true only when the goal is exactly met » | **OK** |
|
| 12 | Objectif journalier | `stats.nim` `goalReached` ; `tests/test_stats.nim` « goalReached is true only when the goal is exactly met » | **OK** |
|
||||||
| 13 | Hotkey et son configurables | champs `hotkeyModifiers`/`hotkeyVk`/`soundName` ; `tests/test_config.nim` round-trip | **OK** |
|
| 13 | Hotkey et son configurables | champs `hotkeyModifiers`/`hotkeyVk`/`soundName` ; `tests/test_config.nim` round-trip | **OK** |
|
||||||
| 14 | Mode portable | `configDir()` ; `tests/test_config.nim` « configDir returns exe dir in portable mode » | **OK** (mode portable → exe dir, sinon APPDATA) |
|
| 14 | Mode portable | `configDir()` ; `tests/test_config.nim` « configDir returns exe dir in portable mode » | **OK** (mode portable → exe dir, sinon APPDATA) |
|
||||||
@@ -40,21 +40,21 @@ concret effectué, et le résultat observé.
|
|||||||
|------------|------------------|------------------|
|
|------------|------------------|------------------|
|
||||||
| Nim ≥ 2.0 | Compilé avec Nim 2.2.10 | OK |
|
| Nim ≥ 2.0 | Compilé avec Nim 2.2.10 | OK |
|
||||||
| winim | `nimble install winim` (3.9.4) | OK |
|
| winim | `nimble install winim` (3.9.4) | OK |
|
||||||
| Binaire unique < 5 Mo | `dir src\traytempo.exe` | ~594 Ko |
|
| Binaire unique < 5 Mo | `dir src\nimpulse.exe` | ~594 Ko |
|
||||||
| `nim c -d:release --app:gui` | commande exacte exécutée | compile **SuccessX** |
|
| `nim c -d:release --app:gui` | commande exacte exécutée | compile **SuccessX** |
|
||||||
| Modules (`tray/timer/notifications/config/stats/i18n/dialogs/floating/updater`) | `ls src/` | 10 modules présents |
|
| Modules (`tray/timer/notifications/config/stats/i18n/dialogs/floating/updater`) | `ls src/` | 10 modules présents (dialogs basé sur wNim) |
|
||||||
| Boucle messages + `SetTimer`/`WM_TIMER` | `GetMessage` loop + `SetTimer`/`WM_TIMER` dans `traytempo.nim` | Compile + timer décrémenté (testé via `tick`) |
|
| Boucle messages + `SetTimer`/`WM_TIMER` | `GetMessage` loop + `SetTimer`/`WM_TIMER` dans `nimpulse.nim` | Compile + timer décrémenté (testé via `tick`) |
|
||||||
|
|
||||||
## Livrables
|
## Livrables
|
||||||
|
|
||||||
| Livrable | Contrôle | Résultat |
|
| Livrable | Contrôle | Résultat |
|
||||||
|----------|----------|----------|
|
|----------|----------|----------|
|
||||||
| Structure + `traytempo.nimble` | fichier présent, `requires winim` | OK |
|
| Structure + `nimpulse.nimble` | fichier présent, `requires winim` | OK |
|
||||||
| Code source commenté (tous modules) | en-têtes `##` de module + commentaires | OK |
|
| Code source commenté (tous modules) | en-têtes `##` de module + commentaires | OK |
|
||||||
| README + instructions compilation | `README.md` (compile, config, archi, tests, distribution) | OK |
|
| README + instructions compilation | `README.md` (compile, config, archi, tests, distribution) | OK |
|
||||||
| Config exemple + profils par défaut | `config.example.json` (3 profils, 5 rappels, nouveaux champs) | OK |
|
| Config exemple + profils par défaut | `config.example.json` (3 profils, 5 rappels, nouveaux champs) | OK |
|
||||||
| Script de release (portable) | `release.cmd` exécuté | **OK** (zip créé) |
|
| Script de release (portable) | `release.cmd` exécuté | **OK** (zip créé) |
|
||||||
| Script d'installation | `installer/traytempo.iss` présent | OK (nécessite Inno Setup pour compiler) |
|
| Script d'installation | `installer/nimpulse.iss` présent | OK (nécessite Inno Setup pour compiler) |
|
||||||
|
|
||||||
## Limites connues de la vérification
|
## Limites connues de la vérification
|
||||||
|
|
||||||
@@ -75,6 +75,6 @@ concret effectué, et le résultat observé.
|
|||||||
run_tests.cmd
|
run_tests.cmd
|
||||||
```
|
```
|
||||||
|
|
||||||
(ou `nim c -d:release --app:gui src/traytempo.nim` pour le binaire)
|
(ou `nim c -d:release --app:gui src/nimpulse.nim` pour le binaire)
|
||||||
|
|
||||||
52 tests au total, tous verts (8 suites).
|
51 tests au total, tous verts (7 suites).
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
# =====================================================================
|
||||||
|
# NimPulse - Installateur une-ligne (Windows)
|
||||||
|
# =====================================================================
|
||||||
|
# Usage (PowerShell) :
|
||||||
|
# irm https://git.dracodev.net/Projets/NimPulse/-/raw/main/install.ps1 | iex
|
||||||
|
#
|
||||||
|
# Ce script telecharge le binaire nimpulse.exe et l'installe dans
|
||||||
|
# %LOCALAPPDATA%\NimPulse, puis cree un raccourci sur le bureau.
|
||||||
|
# =====================================================================
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# --- A ajuster selon l'emplacement reel des artefacts de release ----
|
||||||
|
$Version = "0.3.0"
|
||||||
|
$BaseUrl = "https://git.dracodev.net/Projets/NimPulse/releases/download/v$Version"
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
$InstallDir = Join-Path $env:LOCALAPPDATA "NimPulse"
|
||||||
|
$ExeUrl = "$BaseUrl/nimpulse.exe"
|
||||||
|
$ExePath = Join-Path $InstallDir "nimpulse.exe"
|
||||||
|
|
||||||
|
Write-Host "Installation de NimPulse v$Version ..."
|
||||||
|
|
||||||
|
# 1. Repertoire d'installation.
|
||||||
|
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
||||||
|
|
||||||
|
# 2. Telechargement du binaire.
|
||||||
|
Write-Host " Telechargement de $ExeUrl ..."
|
||||||
|
Invoke-WebRequest -Uri $ExeUrl -OutFile $ExePath -UseBasicParsing
|
||||||
|
|
||||||
|
# 3. Raccourci sur le bureau.
|
||||||
|
try {
|
||||||
|
$desktop = [Environment]::GetFolderPath("Desktop")
|
||||||
|
$wsh = New-Object -ComObject WScript.Shell
|
||||||
|
$lnk = $wsh.CreateShortcut((Join-Path $desktop "NimPulse.lnk"))
|
||||||
|
$lnk.TargetPath = $ExePath
|
||||||
|
$lnk.WorkingDirectory = $InstallDir
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host " Raccourci cree sur le bureau."
|
||||||
|
} catch {
|
||||||
|
Write-Host " (Raccourci bureau ignore : $($_.Exception.Message))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Lancement immediat (optionnel).
|
||||||
|
Write-Host "NimPulse est installe dans : $InstallDir"
|
||||||
|
Write-Host "Lancez-le depuis le bureau ou :"
|
||||||
|
Write-Host " & `"$ExePath`""
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
; =====================================================================
|
; =====================================================================
|
||||||
; TrayTempo - Script d'installation Inno Setup
|
; NimPulse - Script d'installation Inno Setup
|
||||||
; Compiler avec Inno Setup 6 (https://jrsoftware.org/isinfo.php).
|
; Compiler avec Inno Setup 6 (https://jrsoftware.org/isinfo.php).
|
||||||
; Placez le binaire compilé `traytempo.exe` (build\traytempo.exe) dans le
|
; Placez le binaire compilé `nimpulse.exe` (build\nimpulse.exe) dans le
|
||||||
; même dossier que ce script, puis compilez avec le compilateur ISCC.
|
; même dossier que ce script, puis compilez avec le compilateur ISCC.
|
||||||
; =====================================================================
|
; =====================================================================
|
||||||
|
|
||||||
#define MyAppName "TrayTempo"
|
#define MyAppName "NimPulse"
|
||||||
#define MyAppVersion "0.2.0"
|
#define MyAppVersion "0.2.0"
|
||||||
#define MyAppPublisher "TrayTempo"
|
#define MyAppPublisher "NimPulse"
|
||||||
#define MyAppExeName "traytempo.exe"
|
#define MyAppExeName "nimpulse.exe"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{7E5A3C2B-9F1D-4E6A-8B2C-1A3D5F7E9B0C}
|
AppId={{7E5A3C2B-9F1D-4E6A-8B2C-1A3D5F7E9B0C}
|
||||||
@@ -19,7 +19,7 @@ DefaultDirName={autopf}\{#MyAppName}
|
|||||||
DefaultGroupName={#MyAppName}
|
DefaultGroupName={#MyAppName}
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
OutputDir=..\build
|
OutputDir=..\build
|
||||||
OutputBaseFilename=TrayTempo-Setup-{#MyAppVersion}
|
OutputBaseFilename=NimPulse-Setup-{#MyAppVersion}
|
||||||
Compression=lzma2
|
Compression=lzma2
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
@@ -35,7 +35,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
|||||||
Name: "desktopicon"; Description: "Créer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"
|
Name: "desktopicon"; Description: "Créer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "traytempo.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "nimpulse.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
author = "TrayTempo"
|
author = "NimPulse"
|
||||||
description = "Pomodoro timer with health reminders living in the Windows system tray"
|
description = "Pomodoro timer with health reminders living in the Windows system tray"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
bin = @["traytempo"]
|
bin = @["nimpulse"]
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
requires "nim >= 2.0.0"
|
requires "nim >= 2.0.0"
|
||||||
requires "winim >= 3.14.0"
|
requires "winim >= 3.9.0"
|
||||||
|
requires "wnim >= 1.0.0"
|
||||||
+8
-8
@@ -1,7 +1,7 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem =====================================================================
|
rem =====================================================================
|
||||||
rem Construit le binaire de release et prépare les artefacts de
|
rem Construit le binaire de release et prepare les artefacts de
|
||||||
rem distribution : exécutable autonome + archive portable (zip).
|
rem distribution : executable autonome + archive portable (zip).
|
||||||
rem =====================================================================
|
rem =====================================================================
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
@@ -11,7 +11,7 @@ if not exist "%OUT%" mkdir "%OUT%"
|
|||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo === Compilation (release) ===
|
echo === Compilation (release) ===
|
||||||
nim c -d:release --app:gui --hints:off -o:"%OUT%\traytempo.exe" src\traytempo.nim
|
nim c -d:release --app:gui --hints:off -o:"%OUT%\nimpulse.exe" src\nimpulse.nim
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [FAIL] compilation
|
echo [FAIL] compilation
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -19,7 +19,7 @@ if errorlevel 1 (
|
|||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo === Taille du binaire ===
|
echo === Taille du binaire ===
|
||||||
dir "%OUT%\traytempo.exe" | findstr /i "traytempo.exe"
|
dir "%OUT%\nimpulse.exe" | findstr /i "nimpulse.exe"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo === Preparation de l'archive portable ===
|
echo === Preparation de l'archive portable ===
|
||||||
@@ -27,7 +27,7 @@ set PORTABLE=%OUT%\portable
|
|||||||
if exist "%PORTABLE%" rmdir /s /q "%PORTABLE%"
|
if exist "%PORTABLE%" rmdir /s /q "%PORTABLE%"
|
||||||
mkdir "%PORTABLE%"
|
mkdir "%PORTABLE%"
|
||||||
|
|
||||||
copy /y "%OUT%\traytempo.exe" "%PORTABLE%\" >nul
|
copy /y "%OUT%\nimpulse.exe" "%PORTABLE%\" >nul
|
||||||
copy /y "config.example.json" "%PORTABLE%\" >nul
|
copy /y "config.example.json" "%PORTABLE%\" >nul
|
||||||
copy /y "README.md" "%PORTABLE%\" >nul
|
copy /y "README.md" "%PORTABLE%\" >nul
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ rem Le fichier `portable` (vide) indique a l'application de stocker sa
|
|||||||
rem configuration a cote de l'executable au lieu de %%APPDATA%%.
|
rem configuration a cote de l'executable au lieu de %%APPDATA%%.
|
||||||
type nul > "%PORTABLE%\portable"
|
type nul > "%PORTABLE%\portable"
|
||||||
|
|
||||||
powershell -NoProfile -Command "Compress-Archive -Path '%PORTABLE%\*' -DestinationPath '%OUT%\TrayTempo-portable.zip' -Force"
|
powershell -NoProfile -Command "Compress-Archive -Path '%PORTABLE%\*' -DestinationPath '%OUT%\NimPulse-portable.zip' -Force"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [FAIL] creation de l'archive
|
echo [FAIL] creation de l'archive
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -45,6 +45,6 @@ echo.
|
|||||||
echo === Artefacts ===
|
echo === Artefacts ===
|
||||||
dir /b "%OUT%"
|
dir /b "%OUT%"
|
||||||
echo.
|
echo.
|
||||||
echo Termine. L'executable autonome est dans build\traytempo.exe
|
echo Termine. L'executable autonome est dans build\nimpulse.exe
|
||||||
echo et l'archive portable dans build\TrayTempo-portable.zip
|
echo et l'archive portable dans build\NimPulse-portable.zip
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem =====================================================================
|
rem =====================================================================
|
||||||
rem Exécute toutes les suites de tests TrayTempo et affiche un résumé.
|
rem Execute toutes les suites de tests NimPulse et affiche un resume.
|
||||||
rem Sortie : exit code 0 si tout est vert, 1 sinon.
|
rem Sortie : exit code 0 si tout est vert, 1 sinon.
|
||||||
rem =====================================================================
|
rem =====================================================================
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
@@ -8,7 +8,7 @@ setlocal enabledelayedexpansion
|
|||||||
set PASS=0
|
set PASS=0
|
||||||
set FAIL=0
|
set FAIL=0
|
||||||
|
|
||||||
for %%t in (test_timer test_stats test_config test_notifications test_tray_integration test_updater test_floating test_dialogs) do (
|
for %%t in (test_timer test_stats test_config test_notifications test_tray_integration test_updater test_floating) do (
|
||||||
echo.
|
echo.
|
||||||
echo === %%t ===
|
echo === %%t ===
|
||||||
nim c -r --hints:off tests\%%t.nim
|
nim c -r --hints:off tests\%%t.nim
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
## config.nim
|
## config.nim
|
||||||
## ===========
|
## ===========
|
||||||
## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\TrayTempo)
|
## Chargement / sauvegarde de la configuration (JSON dans %APPDATA%\NimPulse)
|
||||||
## et gestion du démarrage automatique (clé de registre Run).
|
## et gestion du démarrage automatique (clé de registre Run).
|
||||||
##
|
##
|
||||||
## Le module définit les types de données partagés (Config, Profile,
|
## Le module définit les types de données partagés (Config, Profile,
|
||||||
@@ -11,7 +11,7 @@ import winim/lean
|
|||||||
import winim/inc/winreg
|
import winim/inc/winreg
|
||||||
|
|
||||||
const
|
const
|
||||||
AppName* = "TrayTempo"
|
AppName* = "NimPulse"
|
||||||
AppVersion* = "0.2.0"
|
AppVersion* = "0.2.0"
|
||||||
RunKey* = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
RunKey* = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||||
DefaultHotkeyModifiers* = MOD_CONTROL or MOD_ALT ## Ctrl+Alt
|
DefaultHotkeyModifiers* = MOD_CONTROL or MOD_ALT ## Ctrl+Alt
|
||||||
@@ -116,7 +116,7 @@ proc defaultConfig*(lang: Lang = langFr): Config =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc configDir*(): string =
|
proc configDir*(): string =
|
||||||
## Répertoire de configuration : `%APPDATA%\TrayTempo`, sauf en mode
|
## Répertoire de configuration : `%APPDATA%\NimPulse`, sauf en mode
|
||||||
## portable (fichier `portable` placé à côté de l'exécutable) où l'on utilise
|
## portable (fichier `portable` placé à côté de l'exécutable) où l'on utilise
|
||||||
## le répertoire de l'exécutable lui-même.
|
## le répertoire de l'exécutable lui-même.
|
||||||
let exeDir = getAppFilename().parentDir
|
let exeDir = getAppFilename().parentDir
|
||||||
@@ -254,7 +254,7 @@ proc saveConfig*(cfg: Config) =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc isAutostartEnabled*(): bool =
|
proc isAutostartEnabled*(): bool =
|
||||||
## Vérifie la présence de la valeur TrayTempo dans la clé Run.
|
## Vérifie la présence de la valeur NimPulse dans la clé Run.
|
||||||
var hKey: HKEY
|
var hKey: HKEY
|
||||||
if RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS:
|
if RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS:
|
||||||
var dataType: DWORD = 0
|
var dataType: DWORD = 0
|
||||||
|
|||||||
+290
-529
@@ -1,131 +1,30 @@
|
|||||||
## dialogs.nim
|
## dialogs.nim
|
||||||
## ============
|
## ============
|
||||||
## Interfaces graphiques natives (Win32) construites sans fichier de ressource :
|
## Interfaces graphiques basées sur wNim (framework GUI Windows natif en Nim).
|
||||||
## panneau de réglages, édition des rappels santé, assistant de premier
|
## Remplace l'ancienne couche Win32 brute pour un rendu plus propre et moderne :
|
||||||
## lancement et boîte de dialogue de statistiques.
|
## panneau de réglages, gestion des rappels santé, assistant de premier
|
||||||
##
|
## lancement et boîte de statistiques.
|
||||||
## Ces dialogues sont modaux et utilisent des fenêtres/contrôles créés à la
|
|
||||||
## volée via CreateWindowExW. Les valeurs sont lues/écrites dans un objet
|
|
||||||
## `Config` passé par référence.
|
|
||||||
|
|
||||||
import winim/lean
|
|
||||||
import winim/inc/commctrl
|
|
||||||
import std/strutils
|
import std/strutils
|
||||||
|
import wnim
|
||||||
import config
|
import config
|
||||||
import i18n
|
import i18n
|
||||||
import stats
|
import stats
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Identifiants des contrôles
|
# État partagé (les dialogues sont modaux, donc mono-thread, donc sûrs)
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const
|
|
||||||
# Réglages
|
|
||||||
IdEditWork = 2001
|
|
||||||
IdEditShortBreak = 2002
|
|
||||||
IdEditLongBreak = 2003
|
|
||||||
IdEditCycles = 2004
|
|
||||||
IdChkSound = 2005
|
|
||||||
IdChkDnd = 2006
|
|
||||||
IdChkAutoStart = 2007
|
|
||||||
IdEditGoal = 2008
|
|
||||||
IdRadioAuto = 2009
|
|
||||||
IdRadioFr = 2010
|
|
||||||
IdRadioEn = 2011
|
|
||||||
IdBtnSave = 2012
|
|
||||||
IdBtnCancel = 2013
|
|
||||||
IdBtnReminders = 2014
|
|
||||||
|
|
||||||
# Rappels santé (liste + boutons)
|
|
||||||
IdRemList = 2100
|
|
||||||
IdRemAdd = 2101
|
|
||||||
IdRemEdit = 2102
|
|
||||||
IdRemRemove = 2103
|
|
||||||
IdRemOk = 2104
|
|
||||||
IdRemCancel = 2105
|
|
||||||
|
|
||||||
# Invite de saisie de texte (ajout/édition de rappel)
|
|
||||||
IdPromptEdit = 2110
|
|
||||||
IdPromptOk = 2111
|
|
||||||
IdPromptCancel = 2112
|
|
||||||
|
|
||||||
# Assistant
|
|
||||||
IdWizBack = 2201
|
|
||||||
IdWizNext = 2202
|
|
||||||
IdWizRadioBase = 2203
|
|
||||||
IdWizChkBase = 2250
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# État global des dialogues (ils sont modaux et mono-thread, donc sûrs)
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var
|
var
|
||||||
dlgCfg: Config
|
dlgCfg: Config
|
||||||
dlgClosed: bool
|
|
||||||
dlgSaved: bool
|
dlgSaved: bool
|
||||||
dlgHwnd: HWND
|
dlgReminders: seq[HealthReminder]
|
||||||
dlgParent: HWND
|
|
||||||
|
|
||||||
# Assistant
|
|
||||||
wizStep: int
|
|
||||||
wizCfg: Config
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers de création de contrôles
|
# Boîte de dialogue des statistiques (wMessageDialog)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc makeCtrl(cls, text: string, style: int, x, y, w, h: int, id: int32,
|
proc showStatsDialog*(cfg: Config, st: Stats) =
|
||||||
parent: HWND, hInstance: HINSTANCE): HWND =
|
|
||||||
CreateWindowExW(0, cls, text, DWORD(style), int32(x), int32(y), int32(w),
|
|
||||||
int32(h), parent, cast[HMENU](id), hInstance, nil)
|
|
||||||
|
|
||||||
proc label(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
|
|
||||||
makeCtrl("STATIC", text, WS_CHILD or WS_VISIBLE or SS_LEFT, x, y, w, h, id, parent, inst)
|
|
||||||
|
|
||||||
proc editNum(x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
|
|
||||||
makeCtrl("EDIT", "", WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP or ES_NUMBER,
|
|
||||||
x, y, w, h, id, parent, inst)
|
|
||||||
|
|
||||||
proc checkbox(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
|
|
||||||
makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or BS_AUTOCHECKBOX or WS_TABSTOP,
|
|
||||||
x, y, w, h, id, parent, inst)
|
|
||||||
|
|
||||||
proc radio(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE,
|
|
||||||
group: bool): HWND =
|
|
||||||
let style = WS_CHILD or WS_VISIBLE or BS_AUTORADIOBUTTON or WS_TABSTOP or
|
|
||||||
(if group: WS_GROUP else: 0)
|
|
||||||
makeCtrl("BUTTON", text, style, x, y, w, h, id, parent, inst)
|
|
||||||
|
|
||||||
proc button(text: string, x, y, w, h: int, id: int32, parent: HWND, inst: HINSTANCE): HWND =
|
|
||||||
makeCtrl("BUTTON", text, WS_CHILD or WS_VISIBLE or WS_TABSTOP, x, y, w, h, id, parent, inst)
|
|
||||||
|
|
||||||
proc getEditInt(hwnd: HWND, id: int32, dflt: int): int =
|
|
||||||
var ok: BOOL
|
|
||||||
let v = GetDlgItemInt(hwnd, id, &ok, TRUE)
|
|
||||||
if ok != 0: int(v) else: dflt
|
|
||||||
|
|
||||||
proc setEditInt(hwnd: HWND, id: int32, value: int) =
|
|
||||||
discard SetDlgItemInt(hwnd, id, UINT(value), TRUE)
|
|
||||||
|
|
||||||
proc isChecked(hwnd: HWND, id: int32): bool =
|
|
||||||
SendMessage(GetDlgItem(hwnd, id), BM_GETCHECK, 0, 0) == BST_CHECKED
|
|
||||||
|
|
||||||
proc setChecked(hwnd: HWND, id: int32, value: bool) =
|
|
||||||
discard SendMessage(GetDlgItem(hwnd, id), BM_SETCHECK,
|
|
||||||
if value: BST_CHECKED else: BST_UNCHECKED, 0)
|
|
||||||
|
|
||||||
proc centerWindow(hwnd: HWND, w, h: int32) =
|
|
||||||
let scrW = GetSystemMetrics(SM_CXSCREEN)
|
|
||||||
let scrH = GetSystemMetrics(SM_CYSCREEN)
|
|
||||||
let x = (scrW - w) div 2
|
|
||||||
let y = (scrH - h) div 2
|
|
||||||
discard SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOZORDER)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Boîte de dialogue des statistiques
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) =
|
|
||||||
var msg = S.statsToday & " : " & $st.completedToday()
|
var msg = S.statsToday & " : " & $st.completedToday()
|
||||||
msg &= "\n" & S.statsWeek & " : " & $st.completedThisWeek()
|
msg &= "\n" & S.statsWeek & " : " & $st.completedThisWeek()
|
||||||
msg &= "\n" & S.statsStreak & " : " & $st.streak() & " " & S.statsDays
|
msg &= "\n" & S.statsStreak & " : " & $st.streak() & " " & S.statsDays
|
||||||
@@ -135,477 +34,339 @@ proc showStatsDialog*(hwnd: HWND, cfg: Config, st: Stats) =
|
|||||||
msg &= "\n\n" & S.statsGoal & " : " & $done & " / " & $cfg.dailyGoal
|
msg &= "\n\n" & S.statsGoal & " : " & $done & " / " & $cfg.dailyGoal
|
||||||
if done >= cfg.dailyGoal:
|
if done >= cfg.dailyGoal:
|
||||||
msg &= " ✓"
|
msg &= " ✓"
|
||||||
msg &= "\n\n---\n" & S.statsDayWord & " " & "count"
|
msg &= "\n\n---\n" & S.statsDayWord & " count"
|
||||||
for (date, n) in st.lastNDays(7):
|
for (date, n) in st.lastNDays(7):
|
||||||
msg &= "\n" & date & " " & $n
|
msg &= "\n" & date & " " & $n
|
||||||
discard MessageBox(hwnd, msg, S.statsTitle, MB_OK or MB_ICONINFORMATION)
|
discard MessageDialog(message=msg, caption=S.statsTitle, style=wOK).display()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Boîte de dialogue des rappels santé : liste modifiable (ajouter / modifier /
|
# Saisie d'une ligne de texte (wTextEntryDialog)
|
||||||
# supprimer / activer-désactiver), avec invite de saisie pour le texte.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var
|
proc promptText(owner: wWindow, title, initial: string): string =
|
||||||
commonControlsInited = false
|
let dlg = TextEntryDialog(owner=owner, message=S.reminderTextPrompt,
|
||||||
remListHwnd: HWND
|
caption=title, value=initial)
|
||||||
remWorking: seq[HealthReminder] ## Copie de travail (validée à la fermeture).
|
result = dlg.display().strip()
|
||||||
promptResult: string
|
|
||||||
promptDone: bool
|
|
||||||
|
|
||||||
proc ensureCommonControls() =
|
# ---------------------------------------------------------------------------
|
||||||
if not commonControlsInited:
|
# Gestion des rappels santé (liste + ajout / modification / suppression)
|
||||||
var icc: TINITCOMMONCONTROLSEX
|
# ---------------------------------------------------------------------------
|
||||||
icc.dwSize = DWORD(sizeof(TINITCOMMONCONTROLSEX))
|
|
||||||
icc.dwICC = ICC_LISTVIEW_CLASSES
|
|
||||||
discard InitCommonControlsEx(&icc)
|
|
||||||
commonControlsInited = true
|
|
||||||
|
|
||||||
proc refreshReminderList(list: HWND) =
|
proc showRemindersDialog*(owner: wWindow, cfg: var Config) =
|
||||||
## Reconstruit la liste à partir de la copie de travail.
|
dlgReminders = cfg.healthReminders
|
||||||
discard SendMessage(list, LVM_DELETEALLITEMS, 0, 0)
|
|
||||||
for i, r in remWorking:
|
|
||||||
let wtext = +$r.text
|
|
||||||
var item: LVITEM
|
|
||||||
item.mask = LVIF_TEXT
|
|
||||||
item.iItem = int32(i)
|
|
||||||
item.pszText = cast[LPWSTR](&wtext)
|
|
||||||
ListView_InsertItem(list, &item)
|
|
||||||
ListView_SetItemState(list, int32(i),
|
|
||||||
UINT(INDEXTOSTATEIMAGEMASK(if r.enabled: 2 else: 1)),
|
|
||||||
UINT(LVIS_STATEIMAGEMASK))
|
|
||||||
|
|
||||||
proc selectedReminderIndex(list: HWND): int =
|
|
||||||
int(ListView_GetNextItem(list, -1, LVNI_SELECTED))
|
|
||||||
|
|
||||||
proc getEditText(hwnd: HWND, id: int32): string =
|
|
||||||
## Lit le texte d'un contrôle EDIT et le convertit en chaîne UTF-8.
|
|
||||||
var buf: array[512, WCHAR]
|
|
||||||
let n = GetDlgItemTextW(hwnd, id, cast[LPWSTR](addr buf[0]), 512)
|
|
||||||
if n > 0:
|
|
||||||
result = $cast[WideCString](addr buf[0])
|
|
||||||
|
|
||||||
proc promptProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
|
|
||||||
case msg
|
|
||||||
of WM_COMMAND:
|
|
||||||
let id = int(wParam and 0xFFFF)
|
|
||||||
case id
|
|
||||||
of IdPromptOk:
|
|
||||||
promptResult = getEditText(hwnd, IdPromptEdit)
|
|
||||||
promptDone = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
of IdPromptCancel:
|
|
||||||
promptResult = ""
|
|
||||||
promptDone = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
else: discard
|
|
||||||
return 0
|
|
||||||
of WM_DESTROY:
|
|
||||||
promptDone = true
|
|
||||||
return 0
|
|
||||||
else: discard
|
|
||||||
return DefWindowProc(hwnd, msg, wParam, lParam)
|
|
||||||
|
|
||||||
proc promptText(parent: HWND, inst: HINSTANCE, title, initial: string): string =
|
|
||||||
## Petite boîte de saisie d'une ligne. Retourne "" si annulé.
|
|
||||||
promptResult = ""
|
|
||||||
promptDone = false
|
|
||||||
|
|
||||||
let w = 360'i32
|
|
||||||
let h = 140'i32
|
|
||||||
|
|
||||||
var wc: WNDCLASSW
|
|
||||||
wc.lpfnWndProc = cast[WNDPROC](promptProc)
|
|
||||||
wc.hInstance = inst
|
|
||||||
wc.lpszClassName = L"TrayTempoPromptClass"
|
|
||||||
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
|
||||||
discard RegisterClass(&wc)
|
|
||||||
|
|
||||||
let hwnd = CreateWindowExW(0, L"TrayTempoPromptClass", title,
|
|
||||||
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
|
|
||||||
0, 0, w, h, parent, 0, inst, nil)
|
|
||||||
centerWindow(hwnd, w, h)
|
|
||||||
|
|
||||||
discard label(S.reminderTextPrompt, 12, 16, 320, 18, 0, hwnd, inst)
|
|
||||||
discard makeCtrl("EDIT", initial, WS_CHILD or WS_VISIBLE or WS_BORDER or WS_TABSTOP,
|
|
||||||
12, 38, w - 24, 24, IdPromptEdit, hwnd, inst)
|
|
||||||
discard button(S.btnOk, w - 210, h - 50, 95, 28, IdPromptOk, hwnd, inst)
|
|
||||||
discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdPromptCancel, hwnd, inst)
|
|
||||||
|
|
||||||
var m: MSG
|
|
||||||
while not promptDone and GetMessage(&m, 0, 0, 0) != 0:
|
|
||||||
if not IsDialogMessage(hwnd, &m):
|
|
||||||
TranslateMessage(&m)
|
|
||||||
DispatchMessage(&m)
|
|
||||||
|
|
||||||
result = promptResult
|
|
||||||
|
|
||||||
proc remindersProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
|
|
||||||
case msg
|
|
||||||
of WM_COMMAND:
|
|
||||||
let id = int(wParam and 0xFFFF)
|
|
||||||
case id
|
|
||||||
of IdRemAdd:
|
|
||||||
let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderNewTitle, "")
|
|
||||||
if text.strip().len > 0:
|
|
||||||
remWorking.add HealthReminder(text: text, enabled: true)
|
|
||||||
refreshReminderList(remListHwnd)
|
|
||||||
of IdRemEdit:
|
|
||||||
let idx = selectedReminderIndex(remListHwnd)
|
|
||||||
if idx >= 0 and idx < remWorking.len:
|
|
||||||
let text = promptText(hwnd, GetModuleHandleW(nil), S.reminderEditTitle,
|
|
||||||
remWorking[idx].text)
|
|
||||||
if text.strip().len > 0:
|
|
||||||
remWorking[idx].text = text
|
|
||||||
refreshReminderList(remListHwnd)
|
|
||||||
of IdRemRemove:
|
|
||||||
let idx = selectedReminderIndex(remListHwnd)
|
|
||||||
if idx >= 0 and idx < remWorking.len:
|
|
||||||
remWorking.delete(idx)
|
|
||||||
refreshReminderList(remListHwnd)
|
|
||||||
of IdRemOk:
|
|
||||||
for i in 0 ..< remWorking.len:
|
|
||||||
remWorking[i].enabled = ListView_GetCheckState(remListHwnd, UINT(i)) != 0
|
|
||||||
dlgSaved = true
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
of IdRemCancel:
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
else: discard
|
|
||||||
return 0
|
|
||||||
of WM_DESTROY:
|
|
||||||
dlgClosed = true
|
|
||||||
return 0
|
|
||||||
else: discard
|
|
||||||
return DefWindowProc(hwnd, msg, wParam, lParam)
|
|
||||||
|
|
||||||
proc showRemindersDialog(parent: HWND, inst: HINSTANCE, cfg: var Config) =
|
|
||||||
## Dialogue de gestion des rappels santé. Les modifications ne sont appliquées
|
|
||||||
## qu'à la validation (bouton OK).
|
|
||||||
remWorking = cfg.healthReminders
|
|
||||||
dlgClosed = false
|
|
||||||
dlgSaved = false
|
dlgSaved = false
|
||||||
ensureCommonControls()
|
|
||||||
|
|
||||||
let w = 440'i32
|
let frame = Frame(owner=owner, title=S.settingsHealth, style=wDefaultDialogStyle)
|
||||||
let h = 340'i32
|
let panel = Panel(frame)
|
||||||
|
|
||||||
var wc: WNDCLASSW
|
let list = ListCtrl(panel, style=wLcReport or wLcSingleSel)
|
||||||
wc.lpfnWndProc = cast[WNDPROC](remindersProc)
|
let btnAdd = Button(panel, label=S.reminderAdd)
|
||||||
wc.hInstance = inst
|
let btnEdit = Button(panel, label=S.reminderEdit)
|
||||||
wc.lpszClassName = L"TrayTempoRemindersClass"
|
let btnRemove = Button(panel, label=S.reminderRemove)
|
||||||
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
let chkEnabled = CheckBox(panel, label=S.settingsHealth)
|
||||||
discard RegisterClass(&wc)
|
let btnOk = Button(panel, label=S.btnOk)
|
||||||
|
let btnCancel = Button(panel, label=S.btnCancel)
|
||||||
|
|
||||||
let hwnd = CreateWindowExW(0, L"TrayTempoRemindersClass", S.settingsHealth,
|
proc refresh() =
|
||||||
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
|
list.clearAll()
|
||||||
0, 0, w, h, parent, 0, inst, nil)
|
for r in dlgReminders:
|
||||||
centerWindow(hwnd, w, h)
|
list.appendItem(r.text)
|
||||||
|
|
||||||
remListHwnd = makeCtrl("SysListView32", "", WS_CHILD or WS_VISIBLE or WS_TABSTOP or
|
proc selectedIndex(): int =
|
||||||
LVS_REPORT or LVS_SINGLESEL or LVS_SHOWSELALWAYS,
|
list.getNextItem(-1, wListNextAll, wListStateSelected)
|
||||||
12, 12, w - 120, h - 80, IdRemList, hwnd, inst)
|
|
||||||
ListView_SetExtendedListViewStyle(remListHwnd, LVS_EX_CHECKBOXES or LVS_EX_FULLROWSELECT)
|
|
||||||
|
|
||||||
# Une colonne pleine largeur pour afficher le texte des rappels.
|
proc updateEnabled() =
|
||||||
var col: LVCOLUMN
|
let idx = selectedIndex()
|
||||||
col.mask = LVCF_WIDTH
|
if idx >= 0 and idx < dlgReminders.len:
|
||||||
col.cx = w - 140
|
chkEnabled.setValue(dlgReminders[idx].enabled)
|
||||||
discard SendMessage(remListHwnd, LVM_INSERTCOLUMN, 0, cast[LPARAM](&col))
|
|
||||||
|
|
||||||
discard button(S.reminderAdd, w - 100, 12, 88, 28, IdRemAdd, hwnd, inst)
|
list.appendColumn("", 320)
|
||||||
discard button(S.reminderEdit, w - 100, 46, 88, 28, IdRemEdit, hwnd, inst)
|
|
||||||
discard button(S.reminderRemove, w - 100, 80, 88, 28, IdRemRemove, hwnd, inst)
|
|
||||||
|
|
||||||
discard button(S.btnOk, w - 210, h - 50, 95, 28, IdRemOk, hwnd, inst)
|
btnAdd.wEvent_Button do ():
|
||||||
discard button(S.btnCancel, w - 105, h - 50, 95, 28, IdRemCancel, hwnd, inst)
|
let text = promptText(frame, S.reminderNewTitle, "")
|
||||||
|
if text.len > 0:
|
||||||
|
dlgReminders.add HealthReminder(text: text, enabled: true)
|
||||||
|
refresh()
|
||||||
|
|
||||||
refreshReminderList(remListHwnd)
|
btnEdit.wEvent_Button do ():
|
||||||
|
let idx = selectedIndex()
|
||||||
|
if idx >= 0 and idx < dlgReminders.len:
|
||||||
|
let text = promptText(frame, S.reminderEditTitle, dlgReminders[idx].text)
|
||||||
|
if text.len > 0:
|
||||||
|
dlgReminders[idx].text = text
|
||||||
|
refresh()
|
||||||
|
|
||||||
var m: MSG
|
btnRemove.wEvent_Button do ():
|
||||||
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
|
let idx = selectedIndex()
|
||||||
if not IsDialogMessage(hwnd, &m):
|
if idx >= 0 and idx < dlgReminders.len:
|
||||||
TranslateMessage(&m)
|
dlgReminders.delete(idx)
|
||||||
DispatchMessage(&m)
|
refresh()
|
||||||
|
|
||||||
|
chkEnabled.wEvent_CheckBox do ():
|
||||||
|
let idx = selectedIndex()
|
||||||
|
if idx >= 0 and idx < dlgReminders.len:
|
||||||
|
dlgReminders[idx].enabled = chkEnabled.getValue()
|
||||||
|
|
||||||
|
list.wEvent_ListItemSelected do ():
|
||||||
|
updateEnabled()
|
||||||
|
|
||||||
|
btnOk.wEvent_Button do ():
|
||||||
|
dlgSaved = true
|
||||||
|
frame.close()
|
||||||
|
|
||||||
|
btnCancel.wEvent_Button do ():
|
||||||
|
frame.close()
|
||||||
|
|
||||||
|
# Mise en page (positions fixes).
|
||||||
|
list.move(12, 12)
|
||||||
|
list.setSize(320, 250)
|
||||||
|
btnAdd.move(344, 12); btnAdd.setSize(88, 28)
|
||||||
|
btnEdit.move(344, 46); btnEdit.setSize(88, 28)
|
||||||
|
btnRemove.move(344, 80); btnRemove.setSize(88, 28)
|
||||||
|
chkEnabled.move(12, 270); chkEnabled.setSize(320, 22)
|
||||||
|
btnOk.move(232, 300); btnOk.setSize(95, 28)
|
||||||
|
btnCancel.move(337, 300); btnCancel.setSize(95, 28)
|
||||||
|
|
||||||
|
frame.setClientSize(444, 340)
|
||||||
|
frame.center()
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
updateEnabled()
|
||||||
|
|
||||||
|
discard frame.showModal()
|
||||||
|
|
||||||
if dlgSaved:
|
if dlgSaved:
|
||||||
cfg.healthReminders = remWorking
|
cfg.healthReminders = dlgReminders
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Panneau de réglages
|
# Panneau de réglages
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc saveSettingsFromControls(hwnd: HWND, cfg: var Config) =
|
proc showSettingsDialog*(cfg: var Config): bool =
|
||||||
cfg.workMin = getEditInt(hwnd, IdEditWork, cfg.workMin)
|
|
||||||
cfg.shortBreakMin = getEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin)
|
|
||||||
cfg.longBreakMin = getEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin)
|
|
||||||
cfg.cyclesBeforeLongBreak = getEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak)
|
|
||||||
cfg.dailyGoal = getEditInt(hwnd, IdEditGoal, cfg.dailyGoal)
|
|
||||||
cfg.soundEnabled = isChecked(hwnd, IdChkSound)
|
|
||||||
cfg.doNotDisturb = isChecked(hwnd, IdChkDnd)
|
|
||||||
cfg.autoStart = isChecked(hwnd, IdChkAutoStart)
|
|
||||||
if isChecked(hwnd, IdRadioFr): cfg.lang = "fr"
|
|
||||||
elif isChecked(hwnd, IdRadioEn): cfg.lang = "en"
|
|
||||||
else: cfg.lang = "auto"
|
|
||||||
|
|
||||||
proc settingsProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
|
|
||||||
case msg
|
|
||||||
of WM_COMMAND:
|
|
||||||
let id = int(wParam and 0xFFFF)
|
|
||||||
case id
|
|
||||||
of IdBtnSave:
|
|
||||||
saveSettingsFromControls(hwnd, dlgCfg)
|
|
||||||
dlgSaved = true
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
of IdBtnCancel:
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
of IdBtnReminders:
|
|
||||||
showRemindersDialog(hwnd, GetModuleHandleW(nil), dlgCfg)
|
|
||||||
else: discard
|
|
||||||
return 0
|
|
||||||
of WM_CLOSE:
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
return 0
|
|
||||||
of WM_DESTROY:
|
|
||||||
dlgClosed = true
|
|
||||||
return 0
|
|
||||||
else: discard
|
|
||||||
return DefWindowProc(hwnd, msg, wParam, lParam)
|
|
||||||
|
|
||||||
proc buildSettingsDialog*(parent: HWND, cfg: Config): HWND =
|
|
||||||
## Construit la fenêtre et les contrôles du panneau de réglages (sans lancer
|
|
||||||
## la boucle modale). Retourne le HWND créé. Utilisé par `showSettingsDialog`
|
|
||||||
## et par les tests d'intégration.
|
|
||||||
dlgCfg = cfg
|
dlgCfg = cfg
|
||||||
dlgClosed = false
|
|
||||||
dlgSaved = false
|
dlgSaved = false
|
||||||
|
|
||||||
let inst = GetModuleHandleW(nil)
|
let frame = Frame(title=S.settingsTitle, style=wDefaultDialogStyle)
|
||||||
let w = 420'i32
|
let panel = Panel(frame)
|
||||||
let h = 430'i32
|
|
||||||
|
|
||||||
var wc: WNDCLASSW
|
let lblWork = StaticText(panel, label=S.settingsWork)
|
||||||
wc.lpfnWndProc = cast[WNDPROC](settingsProc)
|
let lblShort = StaticText(panel, label=S.settingsShortBreak)
|
||||||
wc.hInstance = inst
|
let lblLong = StaticText(panel, label=S.settingsLongBreak)
|
||||||
wc.lpszClassName = L"TrayTempoSettingsClass"
|
let lblCycles = StaticText(panel, label=S.settingsCycles)
|
||||||
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
let lblGoal = StaticText(panel, label=S.settingsGoal)
|
||||||
discard RegisterClass(&wc)
|
let lblLang = StaticText(panel, label=S.settingsLanguage)
|
||||||
|
|
||||||
let hwnd = CreateWindowExW(0, L"TrayTempoSettingsClass", S.settingsTitle,
|
let editWork = TextCtrl(panel)
|
||||||
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
|
let editShort = TextCtrl(panel)
|
||||||
0, 0, w, h, parent, 0, inst, nil)
|
let editLong = TextCtrl(panel)
|
||||||
centerWindow(hwnd, w, h)
|
let editCycles = TextCtrl(panel)
|
||||||
|
let editGoal = TextCtrl(panel)
|
||||||
|
|
||||||
# --- Durées ---
|
let chkSound = CheckBox(panel, label=S.settingsSound)
|
||||||
var y = 12'i32
|
let chkDnd = CheckBox(panel, label=S.settingsDoNotDisturb)
|
||||||
discard label(S.settingsWork, 12, y + 3, 120, 18, 0, hwnd, inst)
|
let chkAutoStart = CheckBox(panel, label=S.settingsAutoStart)
|
||||||
discard editNum(140, y, 60, 22, IdEditWork, hwnd, inst)
|
|
||||||
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
|
|
||||||
|
|
||||||
y += 28
|
let cboLang = ComboBox(panel, style=wCbReadOnly)
|
||||||
discard label(S.settingsShortBreak, 12, y + 3, 120, 18, 0, hwnd, inst)
|
|
||||||
discard editNum(140, y, 60, 22, IdEditShortBreak, hwnd, inst)
|
|
||||||
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
|
|
||||||
|
|
||||||
y += 28
|
let btnReminders = Button(panel, label=S.settingsEditReminders)
|
||||||
discard label(S.settingsLongBreak, 12, y + 3, 120, 18, 0, hwnd, inst)
|
let btnSave = Button(panel, label=S.settingsSave)
|
||||||
discard editNum(140, y, 60, 22, IdEditLongBreak, hwnd, inst)
|
let btnCancel = Button(panel, label=S.settingsCancel)
|
||||||
discard label(S.settingsMinutes, 206, y + 3, 40, 18, 0, hwnd, inst)
|
|
||||||
|
|
||||||
y += 28
|
cboLang.append(["Auto", "Français", "English"])
|
||||||
discard label(S.settingsCycles, 12, y + 3, 180, 18, 0, hwnd, inst)
|
|
||||||
discard editNum(200, y, 60, 22, IdEditCycles, hwnd, inst)
|
|
||||||
|
|
||||||
y += 34
|
# Pré-remplissage.
|
||||||
discard checkbox(S.settingsSound, 12, y, 240, 20, IdChkSound, hwnd, inst)
|
editWork.setValue($cfg.workMin)
|
||||||
y += 24
|
editShort.setValue($cfg.shortBreakMin)
|
||||||
discard checkbox(S.settingsDoNotDisturb, 12, y, 320, 20, IdChkDnd, hwnd, inst)
|
editLong.setValue($cfg.longBreakMin)
|
||||||
y += 24
|
editCycles.setValue($cfg.cyclesBeforeLongBreak)
|
||||||
discard checkbox(S.settingsAutoStart, 12, y, 240, 20, IdChkAutoStart, hwnd, inst)
|
editGoal.setValue($cfg.dailyGoal)
|
||||||
|
chkSound.setValue(cfg.soundEnabled)
|
||||||
y += 30
|
chkDnd.setValue(cfg.doNotDisturb)
|
||||||
discard label(S.settingsGoal, 12, y + 3, 140, 18, 0, hwnd, inst)
|
chkAutoStart.setValue(cfg.autoStart)
|
||||||
discard editNum(160, y, 60, 22, IdEditGoal, hwnd, inst)
|
|
||||||
discard label(S.settingsGoalHint, 12, y + 24, 380, 16, 0, hwnd, inst)
|
|
||||||
|
|
||||||
y += 46
|
|
||||||
discard label(S.settingsLanguage, 12, y, 100, 18, 0, hwnd, inst)
|
|
||||||
discard radio("Auto", 120, y, 70, 20, IdRadioAuto, hwnd, inst, true)
|
|
||||||
discard radio("Français", 190, y, 90, 20, IdRadioFr, hwnd, inst, false)
|
|
||||||
discard radio("English", 285, y, 90, 20, IdRadioEn, hwnd, inst, false)
|
|
||||||
|
|
||||||
y += 30
|
|
||||||
discard button(S.settingsEditReminders, 12, y, 220, 28, IdBtnReminders, hwnd, inst)
|
|
||||||
|
|
||||||
# --- Boutons ---
|
|
||||||
discard button(S.settingsSave, w - 210, h - 62, 100, 30, IdBtnSave, hwnd, inst)
|
|
||||||
discard button(S.settingsCancel, w - 105, h - 62, 100, 30, IdBtnCancel, hwnd, inst)
|
|
||||||
|
|
||||||
# --- Pré-remplissage ---
|
|
||||||
setEditInt(hwnd, IdEditWork, cfg.workMin)
|
|
||||||
setEditInt(hwnd, IdEditShortBreak, cfg.shortBreakMin)
|
|
||||||
setEditInt(hwnd, IdEditLongBreak, cfg.longBreakMin)
|
|
||||||
setEditInt(hwnd, IdEditCycles, cfg.cyclesBeforeLongBreak)
|
|
||||||
setEditInt(hwnd, IdEditGoal, cfg.dailyGoal)
|
|
||||||
setChecked(hwnd, IdChkSound, cfg.soundEnabled)
|
|
||||||
setChecked(hwnd, IdChkDnd, cfg.doNotDisturb)
|
|
||||||
setChecked(hwnd, IdChkAutoStart, cfg.autoStart)
|
|
||||||
case cfg.lang
|
case cfg.lang
|
||||||
of "fr": setChecked(hwnd, IdRadioFr, true)
|
of "fr": cboLang.setSelection(1)
|
||||||
of "en": setChecked(hwnd, IdRadioEn, true)
|
of "en": cboLang.setSelection(2)
|
||||||
else: setChecked(hwnd, IdRadioAuto, true)
|
else: cboLang.setSelection(0)
|
||||||
|
|
||||||
discard SetFocus(GetDlgItem(hwnd, IdEditWork))
|
proc parseIntOr(text: string, dflt: int): int =
|
||||||
result = hwnd
|
try: result = parseInt(text)
|
||||||
|
except ValueError: result = dflt
|
||||||
|
|
||||||
proc showSettingsDialog*(parent: HWND, cfg: var Config): bool =
|
btnSave.wEvent_Button do ():
|
||||||
## Affiche le panneau de réglages. Retourne true si l'utilisateur a enregistré.
|
dlgCfg.workMin = parseIntOr(editWork.getValue(), dlgCfg.workMin)
|
||||||
let hwnd = buildSettingsDialog(parent, cfg)
|
dlgCfg.shortBreakMin = parseIntOr(editShort.getValue(), dlgCfg.shortBreakMin)
|
||||||
|
dlgCfg.longBreakMin = parseIntOr(editLong.getValue(), dlgCfg.longBreakMin)
|
||||||
|
dlgCfg.cyclesBeforeLongBreak = parseIntOr(editCycles.getValue(), dlgCfg.cyclesBeforeLongBreak)
|
||||||
|
dlgCfg.dailyGoal = parseIntOr(editGoal.getValue(), dlgCfg.dailyGoal)
|
||||||
|
dlgCfg.soundEnabled = chkSound.getValue()
|
||||||
|
dlgCfg.doNotDisturb = chkDnd.getValue()
|
||||||
|
dlgCfg.autoStart = chkAutoStart.getValue()
|
||||||
|
case cboLang.getSelection()
|
||||||
|
of 1: dlgCfg.lang = "fr"
|
||||||
|
of 2: dlgCfg.lang = "en"
|
||||||
|
else: dlgCfg.lang = "auto"
|
||||||
|
dlgSaved = true
|
||||||
|
frame.close()
|
||||||
|
|
||||||
# Boucle de messages locale (dialogue modal).
|
btnCancel.wEvent_Button do ():
|
||||||
var m: MSG
|
frame.close()
|
||||||
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
|
|
||||||
if not IsDialogMessage(hwnd, &m):
|
btnReminders.wEvent_Button do ():
|
||||||
TranslateMessage(&m)
|
showRemindersDialog(frame, dlgCfg)
|
||||||
DispatchMessage(&m)
|
|
||||||
|
# Mise en page (positions fixes).
|
||||||
|
var y = 12
|
||||||
|
lblWork.move(12, y + 3); lblWork.setSize(150, 18)
|
||||||
|
editWork.move(170, y); editWork.setSize(60, 22)
|
||||||
|
y += 28
|
||||||
|
lblShort.move(12, y + 3); lblShort.setSize(150, 18)
|
||||||
|
editShort.move(170, y); editShort.setSize(60, 22)
|
||||||
|
y += 28
|
||||||
|
lblLong.move(12, y + 3); lblLong.setSize(150, 18)
|
||||||
|
editLong.move(170, y); editLong.setSize(60, 22)
|
||||||
|
y += 28
|
||||||
|
lblCycles.move(12, y + 3); lblCycles.setSize(150, 18)
|
||||||
|
editCycles.move(170, y); editCycles.setSize(60, 22)
|
||||||
|
y += 30
|
||||||
|
lblGoal.move(12, y + 3); lblGoal.setSize(150, 18)
|
||||||
|
editGoal.move(170, y); editGoal.setSize(60, 22)
|
||||||
|
y += 30
|
||||||
|
chkSound.move(12, y); chkSound.setSize(240, 20)
|
||||||
|
y += 24
|
||||||
|
chkDnd.move(12, y); chkDnd.setSize(320, 20)
|
||||||
|
y += 24
|
||||||
|
chkAutoStart.move(12, y); chkAutoStart.setSize(240, 20)
|
||||||
|
y += 30
|
||||||
|
lblLang.move(12, y + 3); lblLang.setSize(100, 18)
|
||||||
|
cboLang.move(120, y); cboLang.setSize(120, 120)
|
||||||
|
y += 30
|
||||||
|
btnReminders.move(12, y); btnReminders.setSize(220, 28)
|
||||||
|
|
||||||
|
btnSave.move(190, 400); btnSave.setSize(100, 30)
|
||||||
|
btnCancel.move(300, 400); btnCancel.setSize(100, 30)
|
||||||
|
|
||||||
|
frame.setClientSize(420, 440)
|
||||||
|
frame.center()
|
||||||
|
|
||||||
|
discard frame.showModal()
|
||||||
|
|
||||||
if dlgSaved:
|
if dlgSaved:
|
||||||
cfg = dlgCfg
|
cfg = dlgCfg
|
||||||
return dlgSaved
|
result = dlgSaved
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Assistant de premier lancement
|
# Assistant de premier lancement
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const WizSteps = 5 # 0 welcome, 1 langue, 2 profil, 3 santé, 4 fin
|
|
||||||
|
|
||||||
proc wizRebuild(hwnd: HWND, inst: HINSTANCE) =
|
|
||||||
# Efface tous les contrôles enfants existants.
|
|
||||||
var child = GetWindow(hwnd, GW_CHILD)
|
|
||||||
while child != 0:
|
|
||||||
let next = GetWindow(child, GW_HWNDNEXT)
|
|
||||||
DestroyWindow(child)
|
|
||||||
child = next
|
|
||||||
|
|
||||||
case wizStep
|
|
||||||
of 0:
|
|
||||||
discard label(S.wizardWelcomeTitle, 16, 16, 460, 26, 0, hwnd, inst)
|
|
||||||
discard label(S.wizardWelcomeBody, 16, 48, 460, 60, 0, hwnd, inst)
|
|
||||||
of 1:
|
|
||||||
discard label(S.wizardLangPrompt, 16, 16, 460, 22, 0, hwnd, inst)
|
|
||||||
discard radio("Français", 30, 48, 200, 22, IdWizRadioBase, hwnd, inst, true)
|
|
||||||
discard radio("English", 30, 76, 200, 22, int32(IdWizRadioBase + 1), hwnd, inst, false)
|
|
||||||
# Pré-sélection selon la langue détectée.
|
|
||||||
setChecked(hwnd, IdWizRadioBase, detectSystemLang() == langFr)
|
|
||||||
setChecked(hwnd, int32(IdWizRadioBase + 1), detectSystemLang() != langFr)
|
|
||||||
of 2:
|
|
||||||
discard label(S.wizardProfilePrompt, 16, 16, 460, 22, 0, hwnd, inst)
|
|
||||||
let profs = wizCfg.profiles
|
|
||||||
for i, p in profs:
|
|
||||||
let name = p.name & " (" & $p.workMin & "/" & $p.shortBreakMin & "/" &
|
|
||||||
$p.longBreakMin & " min)"
|
|
||||||
discard radio(name, 30, 48 + i * 26, 440, 22, int32(IdWizRadioBase + i), hwnd, inst, i == 0)
|
|
||||||
# Pré-sélectionne le profil actif.
|
|
||||||
for i, p in profs:
|
|
||||||
if p.name == wizCfg.activeProfile:
|
|
||||||
setChecked(hwnd, int32(IdWizRadioBase + i), true)
|
|
||||||
of 3:
|
|
||||||
discard label(S.wizardHealthPrompt, 16, 16, 460, 22, 0, hwnd, inst)
|
|
||||||
for i, r in wizCfg.healthReminders:
|
|
||||||
discard checkbox(r.text, 30, 48 + i * 26, 440, 22, int32(IdWizChkBase + i), hwnd, inst)
|
|
||||||
setChecked(hwnd, int32(IdWizChkBase + i), r.enabled)
|
|
||||||
of 4:
|
|
||||||
discard label(S.wizardDoneTitle, 16, 16, 460, 26, 0, hwnd, inst)
|
|
||||||
discard label(S.wizardDoneBody, 16, 48, 460, 80, 0, hwnd, inst)
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
# Boutons Bas / Suivant / Terminer.
|
|
||||||
let backEnabled = wizStep > 0
|
|
||||||
discard button(S.btnCancel, 20, 300, 90, 30, IdWizBack, hwnd, inst)
|
|
||||||
if not backEnabled:
|
|
||||||
discard EnableWindow(GetDlgItem(hwnd, IdWizBack), FALSE)
|
|
||||||
let nextLabel = if wizStep == WizSteps - 1: S.settingsSave else: S.btnOk
|
|
||||||
discard button(nextLabel, 400, 300, 90, 30, IdWizNext, hwnd, inst)
|
|
||||||
|
|
||||||
proc wizCollectStep(hwnd: HWND) =
|
|
||||||
## Lit les choix de l'étape courante dans wizCfg.
|
|
||||||
case wizStep
|
|
||||||
of 1:
|
|
||||||
if isChecked(hwnd, IdWizRadioBase): wizCfg.lang = "fr"
|
|
||||||
elif isChecked(hwnd, int32(IdWizRadioBase + 1)): wizCfg.lang = "en"
|
|
||||||
of 2:
|
|
||||||
let profs = wizCfg.profiles
|
|
||||||
for i in 0 ..< profs.len:
|
|
||||||
if isChecked(hwnd, int32(IdWizRadioBase + i)):
|
|
||||||
wizCfg.activeProfile = profs[i].name
|
|
||||||
of 3:
|
|
||||||
for i in 0 ..< wizCfg.healthReminders.len:
|
|
||||||
wizCfg.healthReminders[i].enabled = isChecked(hwnd, int32(IdWizChkBase + i))
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
proc wizardProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} =
|
|
||||||
case msg
|
|
||||||
of WM_COMMAND:
|
|
||||||
let id = int(wParam and 0xFFFF)
|
|
||||||
case id
|
|
||||||
of IdWizNext:
|
|
||||||
wizCollectStep(hwnd)
|
|
||||||
if wizStep < WizSteps - 1:
|
|
||||||
inc wizStep
|
|
||||||
wizRebuild(hwnd, GetModuleHandleW(nil))
|
|
||||||
else:
|
|
||||||
dlgSaved = true
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
of IdWizBack:
|
|
||||||
if wizStep > 0:
|
|
||||||
dec wizStep
|
|
||||||
wizRebuild(hwnd, GetModuleHandleW(nil))
|
|
||||||
else: discard
|
|
||||||
return 0
|
|
||||||
of WM_CLOSE:
|
|
||||||
dlgClosed = true
|
|
||||||
DestroyWindow(hwnd)
|
|
||||||
return 0
|
|
||||||
of WM_DESTROY:
|
|
||||||
dlgClosed = true
|
|
||||||
return 0
|
|
||||||
else: discard
|
|
||||||
return DefWindowProc(hwnd, msg, wParam, lParam)
|
|
||||||
|
|
||||||
proc showFirstRunWizard*(cfg: var Config): bool =
|
proc showFirstRunWizard*(cfg: var Config): bool =
|
||||||
## Assistant de premier lancement. Retourne true si l'utilisateur l'a terminé.
|
dlgCfg = cfg
|
||||||
wizCfg = cfg
|
|
||||||
wizStep = 0
|
|
||||||
dlgClosed = false
|
|
||||||
dlgSaved = false
|
dlgSaved = false
|
||||||
|
|
||||||
let inst = GetModuleHandleW(nil)
|
var step = 0
|
||||||
let w = 520'i32
|
|
||||||
let h = 360'i32
|
|
||||||
|
|
||||||
var wc: WNDCLASSW
|
let frame = Frame(title=S.wizardWelcomeTitle, style=wDefaultDialogStyle)
|
||||||
wc.lpfnWndProc = cast[WNDPROC](wizardProc)
|
let panel = Panel(frame)
|
||||||
wc.hInstance = inst
|
|
||||||
wc.lpszClassName = L"TrayTempoWizardClass"
|
|
||||||
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
|
||||||
discard RegisterClass(&wc)
|
|
||||||
|
|
||||||
let hwnd = CreateWindowExW(0, L"TrayTempoWizardClass", S.wizardWelcomeTitle,
|
let text = StaticText(panel, label="")
|
||||||
WS_CAPTION or WS_SYSMENU or WS_VISIBLE,
|
let radio1 = RadioButton(panel, label="")
|
||||||
0, 0, w, h, 0, 0, inst, nil)
|
let radio2 = RadioButton(panel, label="")
|
||||||
centerWindow(hwnd, w, h)
|
let radio3 = RadioButton(panel, label="")
|
||||||
wizRebuild(hwnd, inst)
|
let cboProfile = ComboBox(panel, style=wCbReadOnly)
|
||||||
|
let chk1 = CheckBox(panel, label="")
|
||||||
|
let chk2 = CheckBox(panel, label="")
|
||||||
|
let chk3 = CheckBox(panel, label="")
|
||||||
|
let chk4 = CheckBox(panel, label="")
|
||||||
|
let chk5 = CheckBox(panel, label="")
|
||||||
|
let btnBack = Button(panel, label=S.btnCancel)
|
||||||
|
let btnNext = Button(panel, label=S.btnOk)
|
||||||
|
|
||||||
var m: MSG
|
proc showAll(b: bool) =
|
||||||
while not dlgClosed and GetMessage(&m, 0, 0, 0) != 0:
|
text.show(b)
|
||||||
if not IsDialogMessage(hwnd, &m):
|
radio1.show(b); radio2.show(b); radio3.show(b)
|
||||||
TranslateMessage(&m)
|
cboProfile.show(b)
|
||||||
DispatchMessage(&m)
|
chk1.show(b); chk2.show(b); chk3.show(b); chk4.show(b); chk5.show(b)
|
||||||
|
|
||||||
|
proc render() =
|
||||||
|
showAll(false)
|
||||||
|
btnBack.enable(step > 0)
|
||||||
|
case step
|
||||||
|
of 0:
|
||||||
|
text.setLabel(S.wizardWelcomeTitle & "\n\n" & S.wizardWelcomeBody)
|
||||||
|
text.show()
|
||||||
|
of 1:
|
||||||
|
text.setLabel(S.wizardLangPrompt); text.show()
|
||||||
|
radio1.setLabel("Français"); radio2.setLabel("English")
|
||||||
|
radio1.setValue(detectSystemLang() == langFr)
|
||||||
|
radio2.setValue(detectSystemLang() != langFr)
|
||||||
|
radio1.show(); radio2.show()
|
||||||
|
of 2:
|
||||||
|
text.setLabel(S.wizardProfilePrompt); text.show()
|
||||||
|
cboProfile.show()
|
||||||
|
of 3:
|
||||||
|
text.setLabel(S.wizardHealthPrompt); text.show()
|
||||||
|
chk1.show(); chk2.show(); chk3.show(); chk4.show(); chk5.show()
|
||||||
|
of 4:
|
||||||
|
text.setLabel(S.wizardDoneTitle & "\n\n" & S.wizardDoneBody); text.show()
|
||||||
|
btnNext.setLabel(S.settingsSave)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
btnNext.wEvent_Button do ():
|
||||||
|
case step
|
||||||
|
of 1:
|
||||||
|
if radio1.getValue(): dlgCfg.lang = "fr" else: dlgCfg.lang = "en"
|
||||||
|
of 2:
|
||||||
|
if cboProfile.getSelection() >= 0 and cboProfile.getSelection() < dlgCfg.profiles.len:
|
||||||
|
dlgCfg.activeProfile = dlgCfg.profiles[cboProfile.getSelection()].name
|
||||||
|
of 3:
|
||||||
|
dlgCfg.healthReminders[0].enabled = chk1.getValue()
|
||||||
|
dlgCfg.healthReminders[1].enabled = chk2.getValue()
|
||||||
|
dlgCfg.healthReminders[2].enabled = chk3.getValue()
|
||||||
|
dlgCfg.healthReminders[3].enabled = chk4.getValue()
|
||||||
|
dlgCfg.healthReminders[4].enabled = chk5.getValue()
|
||||||
|
else: discard
|
||||||
|
if step < 4:
|
||||||
|
inc step
|
||||||
|
render()
|
||||||
|
else:
|
||||||
|
dlgSaved = true
|
||||||
|
frame.close()
|
||||||
|
|
||||||
|
btnBack.wEvent_Button do ():
|
||||||
|
if step > 0:
|
||||||
|
dec step
|
||||||
|
render()
|
||||||
|
|
||||||
|
# Initialisation du profil et des rappels.
|
||||||
|
cboProfile.append(@["Classique", "Deep Work", "Sprint"])
|
||||||
|
cboProfile.setSelection(0)
|
||||||
|
for i, r in cfg.healthReminders:
|
||||||
|
case i
|
||||||
|
of 0: chk1.setLabel(r.text); chk1.setValue(r.enabled)
|
||||||
|
of 1: chk2.setLabel(r.text); chk2.setValue(r.enabled)
|
||||||
|
of 2: chk3.setLabel(r.text); chk3.setValue(r.enabled)
|
||||||
|
of 3: chk4.setLabel(r.text); chk4.setValue(r.enabled)
|
||||||
|
of 4: chk5.setLabel(r.text); chk5.setValue(r.enabled)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
text.move(16, 16); text.setSize(460, 200)
|
||||||
|
radio1.move(30, 100); radio1.setSize(200, 22)
|
||||||
|
radio2.move(30, 128); radio2.setSize(200, 22)
|
||||||
|
radio3.move(30, 156); radio3.setSize(200, 22)
|
||||||
|
cboProfile.move(30, 100); cboProfile.setSize(200, 120)
|
||||||
|
chk1.move(30, 90); chk1.setSize(440, 22)
|
||||||
|
chk2.move(30, 116); chk2.setSize(440, 22)
|
||||||
|
chk3.move(30, 142); chk3.setSize(440, 22)
|
||||||
|
chk4.move(30, 168); chk4.setSize(440, 22)
|
||||||
|
chk5.move(30, 194); chk5.setSize(440, 22)
|
||||||
|
btnBack.move(20, 300); btnBack.setSize(90, 30)
|
||||||
|
btnNext.move(400, 300); btnNext.setSize(90, 30)
|
||||||
|
|
||||||
|
frame.setClientSize(510, 350)
|
||||||
|
frame.center()
|
||||||
|
|
||||||
|
render()
|
||||||
|
discard frame.showModal()
|
||||||
|
|
||||||
if dlgSaved:
|
if dlgSaved:
|
||||||
cfg = wizCfg
|
cfg = dlgCfg
|
||||||
return dlgSaved
|
result = dlgSaved
|
||||||
|
|||||||
+2
-2
@@ -43,7 +43,7 @@ proc showFloating*() =
|
|||||||
var wc: WNDCLASSW
|
var wc: WNDCLASSW
|
||||||
wc.lpfnWndProc = cast[WNDPROC](floatingProc)
|
wc.lpfnWndProc = cast[WNDPROC](floatingProc)
|
||||||
wc.hInstance = inst
|
wc.hInstance = inst
|
||||||
wc.lpszClassName = L"TrayTempoFloatingClass"
|
wc.lpszClassName = L"NimPulseFloatingClass"
|
||||||
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
wc.hbrBackground = cast[HBRUSH](COLOR_WINDOW + 1)
|
||||||
discard RegisterClass(&wc)
|
discard RegisterClass(&wc)
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ proc showFloating*() =
|
|||||||
let scrH = GetSystemMetrics(SM_CYSCREEN)
|
let scrH = GetSystemMetrics(SM_CYSCREEN)
|
||||||
|
|
||||||
fHwnd = CreateWindowExW(WS_EX_TOPMOST or WS_EX_TOOLWINDOW,
|
fHwnd = CreateWindowExW(WS_EX_TOPMOST or WS_EX_TOOLWINDOW,
|
||||||
L"TrayTempoFloatingClass", L"TrayTempo",
|
L"NimPulseFloatingClass", L"NimPulse",
|
||||||
WS_POPUP or WS_BORDER or WS_VISIBLE,
|
WS_POPUP or WS_BORDER or WS_VISIBLE,
|
||||||
scrW - w - 24, scrH - h - 80, w, h,
|
scrW - w - 24, scrH - h - 80, w, h,
|
||||||
0, 0, inst, nil)
|
0, 0, inst, nil)
|
||||||
|
|||||||
+17
-17
@@ -6,7 +6,7 @@
|
|||||||
## démarrage (détection système ou préférence utilisateur dans config.json).
|
## démarrage (détection système ou préférence utilisateur dans config.json).
|
||||||
##
|
##
|
||||||
## Ce module importe `config` (types) et `timer` (pour la phase), mais il est
|
## Ce module importe `config` (types) et `timer` (pour la phase), mais il est
|
||||||
## lui-même importé par les modules d'interface (tray, notifications, traytempo).
|
## lui-même importé par les modules d'interface (tray, notifications, nimpulse).
|
||||||
|
|
||||||
import winim/lean
|
import winim/lean
|
||||||
import config
|
import config
|
||||||
@@ -130,7 +130,7 @@ type Strings* = object
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const fr* = Strings(
|
const fr* = Strings(
|
||||||
appName: "TrayTempo",
|
appName: "NimPulse",
|
||||||
|
|
||||||
phaseIdle: "Inactif",
|
phaseIdle: "Inactif",
|
||||||
phaseWorking: "Travail",
|
phaseWorking: "Travail",
|
||||||
@@ -138,8 +138,8 @@ const fr* = Strings(
|
|||||||
phaseLongBreak: "Pause longue",
|
phaseLongBreak: "Pause longue",
|
||||||
phasePaused: "En pause",
|
phasePaused: "En pause",
|
||||||
|
|
||||||
tipIdle: "TrayTempo - Inactif",
|
tipIdle: "NimPulse - Inactif",
|
||||||
tipPaused: "TrayTempo - En pause",
|
tipPaused: "NimPulse - En pause",
|
||||||
|
|
||||||
menuStart: "Démarrer",
|
menuStart: "Démarrer",
|
||||||
menuResume: "Reprendre",
|
menuResume: "Reprendre",
|
||||||
@@ -165,7 +165,7 @@ const fr* = Strings(
|
|||||||
breakShort: "courte",
|
breakShort: "courte",
|
||||||
breakLong: "longue",
|
breakLong: "longue",
|
||||||
|
|
||||||
statsTitle: "Statistiques TrayTempo",
|
statsTitle: "Statistiques NimPulse",
|
||||||
statsToday: "Pomodoros aujourd'hui",
|
statsToday: "Pomodoros aujourd'hui",
|
||||||
statsWeek: "Pomodoros (7 jours)",
|
statsWeek: "Pomodoros (7 jours)",
|
||||||
statsStreak: "Série",
|
statsStreak: "Série",
|
||||||
@@ -176,8 +176,8 @@ const fr* = Strings(
|
|||||||
statsNone: "Aucun pomodoro complété pour l'instant.",
|
statsNone: "Aucun pomodoro complété pour l'instant.",
|
||||||
statsExportError: "Impossible d'exporter les statistiques.",
|
statsExportError: "Impossible d'exporter les statistiques.",
|
||||||
|
|
||||||
wizardWelcomeTitle: "Bienvenue dans TrayTempo",
|
wizardWelcomeTitle: "Bienvenue dans NimPulse",
|
||||||
wizardWelcomeBody: "Configurons TrayTempo en quelques étapes simples pour que " &
|
wizardWelcomeBody: "Configurons NimPulse en quelques étapes simples pour que " &
|
||||||
"vous soyez productif tout en prenant soin de votre santé.",
|
"vous soyez productif tout en prenant soin de votre santé.",
|
||||||
wizardLangTitle: "Langue / Language",
|
wizardLangTitle: "Langue / Language",
|
||||||
wizardLangPrompt: "Choisissez votre langue :",
|
wizardLangPrompt: "Choisissez votre langue :",
|
||||||
@@ -186,10 +186,10 @@ const fr* = Strings(
|
|||||||
wizardHealthTitle: "Rappels santé",
|
wizardHealthTitle: "Rappels santé",
|
||||||
wizardHealthPrompt: "Quels rappels souhaitez-vous pendant vos pauses ?",
|
wizardHealthPrompt: "Quels rappels souhaitez-vous pendant vos pauses ?",
|
||||||
wizardDoneTitle: "C'est prêt !",
|
wizardDoneTitle: "C'est prêt !",
|
||||||
wizardDoneBody: "TrayTempo vit maintenant dans la zone de notification. " &
|
wizardDoneBody: "NimPulse vit maintenant dans la zone de notification. " &
|
||||||
"Clic droit sur l'icône pour ouvrir le menu, ou Ctrl+Alt+P pour démarrer/mettre en pause.",
|
"Clic droit sur l'icône pour ouvrir le menu, ou Ctrl+Alt+P pour démarrer/mettre en pause.",
|
||||||
|
|
||||||
settingsTitle: "Paramètres TrayTempo",
|
settingsTitle: "Paramètres NimPulse",
|
||||||
settingsDurations: "Durées",
|
settingsDurations: "Durées",
|
||||||
settingsWork: "Travail",
|
settingsWork: "Travail",
|
||||||
settingsShortBreak: "Pause courte",
|
settingsShortBreak: "Pause courte",
|
||||||
@@ -232,7 +232,7 @@ const fr* = Strings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const en* = Strings(
|
const en* = Strings(
|
||||||
appName: "TrayTempo",
|
appName: "NimPulse",
|
||||||
|
|
||||||
phaseIdle: "Idle",
|
phaseIdle: "Idle",
|
||||||
phaseWorking: "Working",
|
phaseWorking: "Working",
|
||||||
@@ -240,8 +240,8 @@ const en* = Strings(
|
|||||||
phaseLongBreak: "Long break",
|
phaseLongBreak: "Long break",
|
||||||
phasePaused: "Paused",
|
phasePaused: "Paused",
|
||||||
|
|
||||||
tipIdle: "TrayTempo - Idle",
|
tipIdle: "NimPulse - Idle",
|
||||||
tipPaused: "TrayTempo - Paused",
|
tipPaused: "NimPulse - Paused",
|
||||||
|
|
||||||
menuStart: "Start",
|
menuStart: "Start",
|
||||||
menuResume: "Resume",
|
menuResume: "Resume",
|
||||||
@@ -267,7 +267,7 @@ const en* = Strings(
|
|||||||
breakShort: "short",
|
breakShort: "short",
|
||||||
breakLong: "long",
|
breakLong: "long",
|
||||||
|
|
||||||
statsTitle: "TrayTempo Statistics",
|
statsTitle: "NimPulse Statistics",
|
||||||
statsToday: "Pomodoros today",
|
statsToday: "Pomodoros today",
|
||||||
statsWeek: "Pomodoros (7 days)",
|
statsWeek: "Pomodoros (7 days)",
|
||||||
statsStreak: "Streak",
|
statsStreak: "Streak",
|
||||||
@@ -278,8 +278,8 @@ const en* = Strings(
|
|||||||
statsNone: "No pomodoros completed yet.",
|
statsNone: "No pomodoros completed yet.",
|
||||||
statsExportError: "Could not export statistics.",
|
statsExportError: "Could not export statistics.",
|
||||||
|
|
||||||
wizardWelcomeTitle: "Welcome to TrayTempo",
|
wizardWelcomeTitle: "Welcome to NimPulse",
|
||||||
wizardWelcomeBody: "Let's set up TrayTempo in a few simple steps so you can " &
|
wizardWelcomeBody: "Let's set up NimPulse in a few simple steps so you can " &
|
||||||
"be productive while taking care of your health.",
|
"be productive while taking care of your health.",
|
||||||
wizardLangTitle: "Language",
|
wizardLangTitle: "Language",
|
||||||
wizardLangPrompt: "Choose your language:",
|
wizardLangPrompt: "Choose your language:",
|
||||||
@@ -288,10 +288,10 @@ const en* = Strings(
|
|||||||
wizardHealthTitle: "Health reminders",
|
wizardHealthTitle: "Health reminders",
|
||||||
wizardHealthPrompt: "Which reminders do you want during your breaks?",
|
wizardHealthPrompt: "Which reminders do you want during your breaks?",
|
||||||
wizardDoneTitle: "You're ready!",
|
wizardDoneTitle: "You're ready!",
|
||||||
wizardDoneBody: "TrayTempo now lives in the system tray. " &
|
wizardDoneBody: "NimPulse now lives in the system tray. " &
|
||||||
"Right-click the icon for the menu, or press Ctrl+Alt+P to start/pause.",
|
"Right-click the icon for the menu, or press Ctrl+Alt+P to start/pause.",
|
||||||
|
|
||||||
settingsTitle: "TrayTempo Settings",
|
settingsTitle: "NimPulse Settings",
|
||||||
settingsDurations: "Durations",
|
settingsDurations: "Durations",
|
||||||
settingsWork: "Work",
|
settingsWork: "Work",
|
||||||
settingsShortBreak: "Short break",
|
settingsShortBreak: "Short break",
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
## traytempo.nim
|
## nimpulse.nim
|
||||||
## =============
|
## =============
|
||||||
## Point d'entrée de l'application. Crée une fenêtre cachée, enregistre
|
## Point d'entrée de l'application. Crée une fenêtre cachée, enregistre
|
||||||
## l'icône tray, le minuteur (WM_TIMER) et le raccourci global, puis exécute
|
## l'icône tray, le minuteur (WM_TIMER) et le raccourci global, puis exécute
|
||||||
## la boucle de messages Windows.
|
## la boucle de messages Windows.
|
||||||
##
|
##
|
||||||
## Compilation :
|
## Compilation :
|
||||||
## nim c -d:release --app:gui src/traytempo.nim
|
## nim c -d:release --app:gui src/nimpulse.nim
|
||||||
|
|
||||||
import std/os
|
import std/os
|
||||||
import std/typedthreads
|
import std/typedthreads
|
||||||
import std/strutils
|
import std/strutils
|
||||||
import winim/lean
|
import winim/lean
|
||||||
import winim/inc/shellapi
|
import winim/inc/shellapi
|
||||||
|
import wnim
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import timer
|
import timer
|
||||||
@@ -26,7 +27,7 @@ import floating
|
|||||||
const
|
const
|
||||||
TimerId = 1'i64
|
TimerId = 1'i64
|
||||||
HotkeyId = 1'i32
|
HotkeyId = 1'i32
|
||||||
SingleInstanceMutexName = "TrayTempo_SingleInstance_Mutex"
|
SingleInstanceMutexName = "NimPulse_SingleInstance_Mutex"
|
||||||
WmUpdateAvailable = WM_APP + 2
|
WmUpdateAvailable = WM_APP + 2
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -93,7 +94,7 @@ proc quitApp() =
|
|||||||
|
|
||||||
proc openSettings() =
|
proc openSettings() =
|
||||||
## Ouvre le panneau de réglages graphique.
|
## Ouvre le panneau de réglages graphique.
|
||||||
if showSettingsDialog(gHwnd, gCfg):
|
if showSettingsDialog(gCfg):
|
||||||
saveConfig(gCfg)
|
saveConfig(gCfg)
|
||||||
setLang(resolveLang(gCfg))
|
setLang(resolveLang(gCfg))
|
||||||
discard setAutostart(gCfg.autoStart)
|
discard setAutostart(gCfg.autoStart)
|
||||||
@@ -102,7 +103,7 @@ proc openSettings() =
|
|||||||
showToast(gHwnd, S.appName, S.settingsSaved)
|
showToast(gHwnd, S.appName, S.settingsSaved)
|
||||||
|
|
||||||
proc showStatsDialog() =
|
proc showStatsDialog() =
|
||||||
dialogs.showStatsDialog(gHwnd, gCfg, gStats)
|
dialogs.showStatsDialog(gCfg, gStats)
|
||||||
|
|
||||||
proc exportStatsCsv() =
|
proc exportStatsCsv() =
|
||||||
## Exporte l'historique en CSV dans le répertoire de configuration.
|
## Exporte l'historique en CSV dans le répertoire de configuration.
|
||||||
@@ -230,14 +231,17 @@ proc isAlreadyRunning(): bool =
|
|||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
if isAlreadyRunning():
|
if isAlreadyRunning():
|
||||||
discard MessageBox(0, "TrayTempo est déjà en cours d'exécution.",
|
discard MessageBox(0, "NimPulse est déjà en cours d'exécution.",
|
||||||
"TrayTempo", MB_OK or MB_ICONINFORMATION)
|
"NimPulse", MB_OK or MB_ICONINFORMATION)
|
||||||
return
|
return
|
||||||
|
|
||||||
gCfg = loadConfig()
|
gCfg = loadConfig()
|
||||||
gStats = loadStats()
|
gStats = loadStats()
|
||||||
gTimer = initTimer()
|
gTimer = initTimer()
|
||||||
|
|
||||||
|
# Initialise le framework wNim (contrôles communs, OLE, etc.) pour les dialogues.
|
||||||
|
discard App()
|
||||||
|
|
||||||
# Premier lancement : assistant de configuration guidée.
|
# Premier lancement : assistant de configuration guidée.
|
||||||
let firstRun = not fileExists(configPath())
|
let firstRun = not fileExists(configPath())
|
||||||
if firstRun:
|
if firstRun:
|
||||||
@@ -255,17 +259,17 @@ proc main() =
|
|||||||
var wc: WNDCLASSW
|
var wc: WNDCLASSW
|
||||||
wc.lpfnWndProc = cast[WNDPROC](wndProc)
|
wc.lpfnWndProc = cast[WNDPROC](wndProc)
|
||||||
wc.hInstance = hInstance
|
wc.hInstance = hInstance
|
||||||
wc.lpszClassName = L"TrayTempoWindowClass"
|
wc.lpszClassName = L"NimPulseWindowClass"
|
||||||
wc.hIcon = phaseIcon(phIdle)
|
wc.hIcon = phaseIcon(phIdle)
|
||||||
if RegisterClass(&wc) == 0:
|
if RegisterClass(&wc) == 0:
|
||||||
# La classe peut déjà être enregistrée ; on poursuit quand même.
|
# La classe peut déjà être enregistrée ; on poursuit quand même.
|
||||||
discard
|
discard
|
||||||
|
|
||||||
gHwnd = CreateWindowExW(0, L"TrayTempoWindowClass", L"TrayTempo",
|
gHwnd = CreateWindowExW(0, L"NimPulseWindowClass", L"NimPulse",
|
||||||
WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, hInstance, nil)
|
WS_OVERLAPPED, 0, 0, 0, 0, 0, 0, hInstance, nil)
|
||||||
if gHwnd == 0:
|
if gHwnd == 0:
|
||||||
discard MessageBox(0, "Impossible de créer la fenêtre de l'application.",
|
discard MessageBox(0, "Impossible de créer la fenêtre de l'application.",
|
||||||
"TrayTempo", MB_ICONERROR)
|
"NimPulse", MB_ICONERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Icône dans le tray.
|
# Icône dans le tray.
|
||||||
@@ -8,7 +8,7 @@ import std/[unittest, os]
|
|||||||
import ../src/config
|
import ../src/config
|
||||||
|
|
||||||
# Redirige APPDATA vers un répertoire temporaire pour isoler les fichiers.
|
# Redirige APPDATA vers un répertoire temporaire pour isoler les fichiers.
|
||||||
let tmpApd = getTempDir() / "traytempo_test_apd"
|
let tmpApd = getTempDir() / "nimpulse_test_apd"
|
||||||
if dirExists(tmpApd):
|
if dirExists(tmpApd):
|
||||||
removeDir(tmpApd)
|
removeDir(tmpApd)
|
||||||
putEnv("APPDATA", tmpApd)
|
putEnv("APPDATA", tmpApd)
|
||||||
@@ -102,11 +102,11 @@ suite "config":
|
|||||||
writeFile(marker, "")
|
writeFile(marker, "")
|
||||||
check configDir() == exeDir
|
check configDir() == exeDir
|
||||||
removeFile(marker)
|
removeFile(marker)
|
||||||
check configDir() == (getEnv("APPDATA") / "TrayTempo")
|
check configDir() == (getEnv("APPDATA") / "NimPulse")
|
||||||
|
|
||||||
test "acquireSingleInstance detects a second lock":
|
test "acquireSingleInstance detects a second lock":
|
||||||
check not acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0")
|
check not acquireSingleInstance("NimPulse_Test_Mutex_0.2.0")
|
||||||
check acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0")
|
check acquireSingleInstance("NimPulse_Test_Mutex_0.2.0")
|
||||||
|
|
||||||
suite "autostart registry":
|
suite "autostart registry":
|
||||||
test "setAutostart toggles the Run key and cleans up":
|
test "setAutostart toggles the Run key and cleans up":
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
## test_dialogs.nim
|
|
||||||
## =================
|
|
||||||
## Test d'intégration de la construction du panneau de réglages : vérifie que la
|
|
||||||
## fenêtre et ses contrôles sont réellement créés (sans lancer la boucle modale,
|
|
||||||
## qui bloquerait). Cela rend observable la création de l'interface, au-delà de
|
|
||||||
## la simple compilation.
|
|
||||||
|
|
||||||
import std/unittest
|
|
||||||
import winim/lean
|
|
||||||
import ../src/dialogs
|
|
||||||
import ../src/config
|
|
||||||
|
|
||||||
proc createParent(): HWND =
|
|
||||||
let hInstance = GetModuleHandleW(nil)
|
|
||||||
var wc: WNDCLASSW
|
|
||||||
wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW)
|
|
||||||
wc.hInstance = hInstance
|
|
||||||
wc.lpszClassName = L"TrayTempoDialogTestClass"
|
|
||||||
discard RegisterClass(&wc)
|
|
||||||
result = CreateWindowExW(0, L"TrayTempoDialogTestClass", L"", WS_OVERLAPPED,
|
|
||||||
0, 0, 0, 0, 0, 0, hInstance, nil)
|
|
||||||
|
|
||||||
suite "dialogs":
|
|
||||||
test "settings dialog window and controls are built":
|
|
||||||
let parent = createParent()
|
|
||||||
check parent != 0
|
|
||||||
|
|
||||||
let hwnd = buildSettingsDialog(parent, defaultConfig())
|
|
||||||
check hwnd != 0
|
|
||||||
# La fenêtre doit avoir au moins un contrôle enfant (champ, bouton, etc.).
|
|
||||||
check GetWindow(hwnd, GW_CHILD) != 0
|
|
||||||
|
|
||||||
discard DestroyWindow(hwnd)
|
|
||||||
discard DestroyWindow(parent)
|
|
||||||
@@ -80,7 +80,7 @@ suite "stats":
|
|||||||
var st = initStats()
|
var st = initStats()
|
||||||
st.daily["2026-08-13"] = 3
|
st.daily["2026-08-13"] = 3
|
||||||
st.daily["2026-08-14"] = 5
|
st.daily["2026-08-14"] = 5
|
||||||
let path = getTempDir() / "traytempo_test_export.csv"
|
let path = getTempDir() / "nimpulse_test_export.csv"
|
||||||
check st.exportCsv(path)
|
check st.exportCsv(path)
|
||||||
let content = readFile(path)
|
let content = readFile(path)
|
||||||
check content.contains("date,count")
|
check content.contains("date,count")
|
||||||
@@ -91,7 +91,7 @@ suite "stats":
|
|||||||
suite "stats persistence":
|
suite "stats persistence":
|
||||||
test "saveStats then loadStats round-trips":
|
test "saveStats then loadStats round-trips":
|
||||||
# Isole l'écriture disque dans un APPDATA temporaire.
|
# Isole l'écriture disque dans un APPDATA temporaire.
|
||||||
let tmpApd = getTempDir() / "traytempo_test_stats"
|
let tmpApd = getTempDir() / "nimpulse_test_stats"
|
||||||
if dirExists(tmpApd):
|
if dirExists(tmpApd):
|
||||||
removeDir(tmpApd)
|
removeDir(tmpApd)
|
||||||
putEnv("APPDATA", tmpApd)
|
putEnv("APPDATA", tmpApd)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ proc createTestWindow(): HWND =
|
|||||||
var wc: WNDCLASSW
|
var wc: WNDCLASSW
|
||||||
wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW)
|
wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW)
|
||||||
wc.hInstance = hInstance
|
wc.hInstance = hInstance
|
||||||
wc.lpszClassName = L"TrayTempoTestClass"
|
wc.lpszClassName = L"NimPulseTestClass"
|
||||||
discard RegisterClass(&wc)
|
discard RegisterClass(&wc)
|
||||||
result = CreateWindowExW(0, L"TrayTempoTestClass", L"", WS_OVERLAPPED,
|
result = CreateWindowExW(0, L"NimPulseTestClass", L"", WS_OVERLAPPED,
|
||||||
0, 0, 0, 0, 0, 0, hInstance, nil)
|
0, 0, 0, 0, 0, 0, hInstance, nil)
|
||||||
|
|
||||||
suite "tray integration":
|
suite "tray integration":
|
||||||
@@ -34,7 +34,7 @@ suite "tray integration":
|
|||||||
check phaseIcon(phase) != 0
|
check phaseIcon(phase) != 0
|
||||||
|
|
||||||
# Ajout de l'icône dans le tray (Shell_NotifyIcon NIM_ADD -> bool).
|
# Ajout de l'icône dans le tray (Shell_NotifyIcon NIM_ADD -> bool).
|
||||||
check addTrayIcon(hwnd, phaseIcon(phIdle), "TrayTempo") == true
|
check addTrayIcon(hwnd, phaseIcon(phIdle), "NimPulse") == true
|
||||||
|
|
||||||
# Mise à jour de l'icône et du tooltip.
|
# Mise à jour de l'icône et du tooltip.
|
||||||
updateTrayIcon(hwnd, phaseIcon(phWorking), "Travail: 25:00")
|
updateTrayIcon(hwnd, phaseIcon(phWorking), "Travail: 25:00")
|
||||||
|
|||||||
Reference in New Issue
Block a user