From 8bff7982ffce20cc3ea51f5a125f5a4fcd791957 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 10 Aug 2026 22:06:49 +0200 Subject: [PATCH] V0.3 --- .claude/settings.json | 4 +- .gitignore | 31 +- GUIDE_ADMIN.md | 292 ++--- MISE_A_JOUR.md | 148 +++ README.md | 284 +++-- Setp.txt | 5 - eslint.config.js | 48 + index.html | 34 + package-lock.json | 1482 ++++++++++++++++++++++- package.json | 25 +- scripts/installer-art.py | 123 ++ scripts/release.mjs | 236 ++++ scripts/signing.mjs | 43 + scripts/tauri-build.mjs | 49 + src-tauri/Cargo.lock | 624 +++++++++- src-tauri/Cargo.toml | 12 + src-tauri/capabilities/default.json | 27 +- src-tauri/installer/header.bmp | Bin 0 -> 25818 bytes src-tauri/installer/installer.nsi | 1007 +++++++++++++++ src-tauri/installer/sidebar.bmp | Bin 0 -> 154542 bytes src-tauri/src/installer.rs | 310 +++++ src-tauri/src/lib.rs | 78 +- src-tauri/tauri.conf.json | 55 +- src/App.tsx | 267 ++-- src/components/BugModal.tsx | 306 +++++ src/components/CommandPalette.tsx | 331 +++++ src/components/EditProfileModal.tsx | 174 +++ src/components/EntityExtras.tsx | 638 ++++++++++ src/components/Layout.tsx | 810 +++++++++---- src/components/Markdown.tsx | 149 +++ src/components/Modal.tsx | 194 +++ src/components/MultiSelect.tsx | 297 +++++ src/components/NotificationBell.tsx | 443 ++++--- src/components/PageHeader.tsx | 21 - src/components/ProjectModal.tsx | 130 ++ src/components/Select.tsx | 288 +++++ src/components/TaskModal.tsx | 794 ++++++------ src/components/UpdateBanner.tsx | 142 +++ src/components/people.tsx | 480 ++++++++ src/components/ui.tsx | 688 +++++++---- src/env.d.ts | 34 +- src/index.css | 317 +++-- src/lib/AppContext.tsx | 467 +++++-- src/lib/admin.ts | 27 +- src/lib/avatar.ts | 15 - src/lib/config.ts | 39 - src/lib/db.ts | 231 ++++ src/lib/feedback.tsx | 148 ++- src/lib/filters.ts | 40 + src/lib/installer.ts | 88 ++ src/lib/markdown.ts | 119 ++ src/lib/notify.ts | 164 ++- src/lib/storage.ts | 137 +++ src/lib/supabase.ts | 58 +- src/lib/theme.ts | 67 +- src/lib/tones.ts | 70 ++ src/lib/types.ts | 718 +++++++---- src/lib/updater.ts | 87 ++ src/lib/useMembers.ts | 18 - src/main.tsx | 38 +- src/pages/Admin.tsx | 643 +++++----- src/pages/Bugs.tsx | 876 +++++++++----- src/pages/Dashboard.tsx | 801 +++++++++--- src/pages/Docs.tsx | 642 ++++++++++ src/pages/Installer.tsx | 256 ++++ src/pages/Kanban.tsx | 1059 +++++++++++----- src/pages/Login.tsx | 175 ++- src/pages/Members.tsx | 481 +++++--- src/pages/Onboarding.tsx | 334 +++-- src/pages/Planning.tsx | 1258 ++++++++++++------- src/pages/Setup.tsx | 91 -- supabase/functions/admin-users/index.ts | 133 +- supabase/migration_v10.sql | 345 ++++++ supabase/migration_v11.sql | 46 + supabase/migration_v2.sql | 70 -- supabase/migration_v3.sql | 25 - supabase/migration_v4.sql | 18 - supabase/releases_bucket.sql | 29 + supabase/schema.sql | 765 +++++++----- supabase/update_all.sql | 719 ++++++++--- tailwind.config.js | 19 +- tsconfig.json | 50 +- vite.config.ts | 13 + 83 files changed, 17975 insertions(+), 4824 deletions(-) create mode 100644 MISE_A_JOUR.md delete mode 100644 Setp.txt create mode 100644 eslint.config.js create mode 100644 scripts/installer-art.py create mode 100644 scripts/release.mjs create mode 100644 scripts/signing.mjs create mode 100644 scripts/tauri-build.mjs create mode 100644 src-tauri/installer/header.bmp create mode 100644 src-tauri/installer/installer.nsi create mode 100644 src-tauri/installer/sidebar.bmp create mode 100644 src-tauri/src/installer.rs create mode 100644 src/components/BugModal.tsx create mode 100644 src/components/CommandPalette.tsx create mode 100644 src/components/EditProfileModal.tsx create mode 100644 src/components/EntityExtras.tsx create mode 100644 src/components/Markdown.tsx create mode 100644 src/components/Modal.tsx create mode 100644 src/components/MultiSelect.tsx delete mode 100644 src/components/PageHeader.tsx create mode 100644 src/components/ProjectModal.tsx create mode 100644 src/components/Select.tsx create mode 100644 src/components/UpdateBanner.tsx create mode 100644 src/components/people.tsx delete mode 100644 src/lib/avatar.ts delete mode 100644 src/lib/config.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/filters.ts create mode 100644 src/lib/installer.ts create mode 100644 src/lib/markdown.ts create mode 100644 src/lib/storage.ts create mode 100644 src/lib/tones.ts create mode 100644 src/lib/updater.ts delete mode 100644 src/lib/useMembers.ts create mode 100644 src/pages/Docs.tsx create mode 100644 src/pages/Installer.tsx delete mode 100644 src/pages/Setup.tsx create mode 100644 supabase/migration_v10.sql create mode 100644 supabase/migration_v11.sql delete mode 100644 supabase/migration_v2.sql delete mode 100644 supabase/migration_v3.sql delete mode 100644 supabase/migration_v4.sql create mode 100644 supabase/releases_bucket.sql diff --git a/.claude/settings.json b/.claude/settings.json index 72f11d5..32639cb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -12,7 +12,9 @@ "PowerShell(npm run tauri:build)", "PowerShell(Get-ChildItem -Path \"src-tauri\\\\target\\\\release\\\\app.exe\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\msi\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\" -File | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}} | Format-Table -AutoSize)", "PowerShell(npm run typecheck)", - "PowerShell(Get-ChildItem -Path \"src-tauri\\\\target\\\\release\\\\app.exe\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\msi\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\" -File | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}}, LastWriteTime | Format-Table -AutoSize)" + "PowerShell(Get-ChildItem -Path \"src-tauri\\\\target\\\\release\\\\app.exe\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\msi\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\" -File | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}}, LastWriteTime | Format-Table -AutoSize)", + "PowerShell(Get-ChildItem -Path \"src-tauri\\\\target\\\\release\\\\app.exe\",\"src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\" -File | Select-Object Name, @{N='MB';E={[math]::Round\\($_.Length/1MB,1\\)}}, LastWriteTime | Format-Table -AutoSize)", + "PowerShell($ErrorActionPreference='SilentlyContinue'; $v = supabase --version 2>$null; if \\($v\\) { \"CLI présent : $v\" } else { \"CLI Supabase NON installé\" }; \"scoop: $\\(\\(Get-Command scoop\\).Source\\)\"; \"npx: $\\(\\(Get-Command npx\\).Source\\)\")" ] } } diff --git a/.gitignore b/.gitignore index 827a3c2..94e887d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,20 @@ -node_modules/ -out/ -dist/ -*.local -.DS_Store -.vite - -# .env est VOLONTAIREMENT suivi par git : il ne contient que l'URL Supabase -# et la clé "anon" (toutes deux publiques). Ça permet de passer d'une machine -# à l'autre sans le recréer. Ne mets JAMAIS de secret ici : pour un vrai -# secret, utilise .env.local (ignoré via *.local). +node_modules/ +out/ +dist/ +*.local +.DS_Store +.vite + +# .env est VOLONTAIREMENT suivi par git : il ne contient que l'URL Supabase +# et la clé "anon" (toutes deux publiques). Ça permet de passer d'une machine +# à l'autre sans le recréer. Ne mets JAMAIS de secret ici : pour un vrai +# secret, utilise .env.local (ignoré via *.local). + +# Fichiers de travail de la CLI Supabase +supabase/.temp/ + +# Cles de signature des mises a jour (NE JAMAIS COMMITTER) +.tauri-signing-key.local.pub + +# Binaire distribuable produit par `npm run tauri:build` +/release/ diff --git a/GUIDE_ADMIN.md b/GUIDE_ADMIN.md index 968dc37..8d34d33 100644 --- a/GUIDE_ADMIN.md +++ b/GUIDE_ADMIN.md @@ -1,137 +1,155 @@ -# Guide de mise en place — Comptes & Panel Admin - -Ce guide explique, **étape par étape**, comment activer le panel d'administration -(création / suppression de comptes, gestion des rôles) pour ton équipe. - -Tu ne fais ça **qu'une seule fois**. Ensuite, tout se gère depuis l'appli. - ---- - -## Vue d'ensemble - -| Qui | Voit quoi | -|-----|-----------| -| Toi (admin) | Tout + un menu **« Administration »** pour gérer les comptes | -| Tes collègues | L'appli normale, juste un écran **« Se connecter »** | - -Les clés Supabase sont **déjà intégrées** dans l'appli (fichier `.env`) : tes -collègues n'ont **rien** à configurer. - ---- - -## Étape 1 — Vérifier le fichier `.env` - -Le fichier `.env` (à la racine du projet) contient déjà ton URL et ta clé : - -``` -VITE_SUPABASE_URL=https://cesjrlfqeqftayhnqjkb.supabase.co -VITE_SUPABASE_ANON_KEY=eyJhbGci... -``` - -✅ Rien à faire si c'est correct. (L'URL ne doit PAS contenir `/rest/v1/`.) - ---- - -## Étape 2 — Mettre à jour la base de données - -1. Va sur [supabase.com](https://supabase.com) → ton projet. -2. Menu **SQL Editor** → **New query**. -3. Copie **tout** le contenu du fichier `supabase/schema.sql` et colle-le. -4. Clique **Run**. - -> C'est sans danger même si tu l'as déjà exécuté : tout est « ré-exécutable ». -> Ça ajoute le champ `is_admin` et les sécurités du panel admin. - ---- - -## Étape 3 — Créer TON compte administrateur - -Comme l'inscription publique est désactivée, on crée le **tout premier compte** -(le tien) à la main : - -1. Dans Supabase → menu **Authentication** → **Users** → bouton **Add user**. -2. Mets ton **email** + un **mot de passe**, et coche **Auto Confirm User**. -3. Clique **Create user**. - -Puis on te donne les droits admin : - -4. Retourne dans **SQL Editor** → **New query**, colle ceci en remplaçant l'email : - -```sql -update public.profiles set is_admin = true -where id = (select id from auth.users where email = 'TON_EMAIL@exemple.com'); -``` - -5. Clique **Run**. - -🎉 Tu es maintenant administrateur. - ---- - -## Étape 4 — Déployer l'Edge Function (le « serveur de confiance ») - -C'est elle qui crée/supprime les comptes en sécurité. **Deux méthodes au choix.** - -### Méthode A — En ligne de commande (recommandée) - -Dans un terminal, à la racine du projet : - -```bash -# 1. Se connecter à Supabase (ouvre le navigateur) -npx supabase login - -# 2. Déployer la fonction -npx supabase functions deploy admin-users ^ - --project-ref cesjrlfqeqftayhnqjkb --no-verify-jwt -``` - -> `--no-verify-jwt` est normal ici : la fonction vérifie elle-même que -> l'appelant est bien un admin connecté (voir le code). -> La clé `service_role` est fournie automatiquement par Supabase, tu n'as -> rien à copier. - -### Méthode B — Par le tableau de bord (sans terminal) - -1. Supabase → menu **Edge Functions** → **Create a new function**. -2. Nom : `admin-users`. -3. Colle le contenu de `supabase/functions/admin-users/index.ts`. -4. Déploie. -5. Dans les **réglages** de la fonction, **désactive « Enforce JWT Verification »**. - ---- - -## Étape 5 — Construire et distribuer l'appli - -```bash -npm run tauri:build -``` - -L'installateur se trouve ensuite dans : -`src-tauri/target/release/bundle/` - -Envoie ce fichier à tes collègues. Les clés sont dedans : ils n'ont **rien** à régler. - ---- - -## Au quotidien — Gérer les comptes - -1. Ouvre l'appli, connecte-toi avec ton compte admin. -2. Menu **Administration** (visible uniquement pour les admins). -3. Tu peux : - - **Créer un compte** : nom, email, mot de passe provisoire, rôle, (admin ?) - - **Changer le rôle** d'un membre - - **Promouvoir / rétrograder** un admin - - **Supprimer** un compte - -Communique l'email + le mot de passe provisoire à chaque membre. Il se connecte, -c'est tout. ✅ - ---- - -## Notes de sécurité - -- La clé `anon` dans l'appli est **publique par nature** : aucun risque. -- La clé `service_role` (toute-puissante) **n'est jamais dans l'appli** : elle - reste cachée dans l'Edge Function, côté serveur. -- Un membre normal **ne peut pas** se promouvoir admin lui-même (bloqué côté base). -- Tu ne peux pas supprimer ni rétrograder **ton propre** compte admin (anti-blocage). +# Guide de mise en place — Comptes & Panel Admin + +Ce guide explique, **étape par étape**, comment activer le panel d'administration +(création / suppression de comptes, gestion des rôles) pour ton équipe. + +Tu ne fais ça **qu'une seule fois**. Ensuite, tout se gère depuis l'appli. + +--- + +## Vue d'ensemble + +| Qui | Voit quoi | +|-----|-----------| +| Toi (admin) | Tout + un menu **« Administration »** pour gérer les comptes | +| Tes collègues | L'appli normale, juste un écran **« Se connecter »** | + +Les clés Supabase sont **déjà intégrées** dans l'appli (fichier `.env`) : tes +collègues n'ont **rien** à configurer. + +--- + +## Étape 1 — Vérifier le fichier `.env` + +Le fichier `.env` (à la racine du projet) contient déjà ton URL et ta clé : + +``` +VITE_SUPABASE_URL=https://cesjrlfqeqftayhnqjkb.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGci... +``` + +✅ Rien à faire si c'est correct. (L'URL ne doit PAS contenir `/rest/v1/`.) + +--- + +## Étape 2 — Mettre à jour la base de données + +1. Va sur [supabase.com](https://supabase.com) → ton projet. +2. Menu **SQL Editor** → **New query**. +3. Copie **tout** le contenu du fichier : + - **base neuve** → `supabase/schema.sql` + - **base déjà en service** → `supabase/update_all.sql` +4. Colle-le et clique **Run**. + +> C'est sans danger même si tu l'as déjà exécuté : tout est « ré-exécutable ». +> Ça met en place `is_admin`, les commentaires, les pièces jointes et les +> sécurités du panel admin. + +--- + +## Étape 3 — Créer TON compte administrateur + +Comme l'inscription publique est désactivée, on crée le **tout premier compte** +(le tien) à la main : + +1. Dans Supabase → menu **Authentication** → **Users** → bouton **Add user**. +2. Mets ton **email** + un **mot de passe**, et coche **Auto Confirm User**. +3. Clique **Create user**. + +Puis on te donne les droits admin : + +4. Retourne dans **SQL Editor** → **New query**, colle ceci en remplaçant l'email : + +```sql +update public.profiles set is_admin = true +where id = (select id from auth.users where email = 'TON_EMAIL@exemple.com'); +``` + +5. Clique **Run**. + +🎉 Tu es maintenant administrateur. + +--- + +## Étape 4 — Déployer l'Edge Function (le « serveur de confiance ») + +C'est elle qui crée/supprime les comptes en sécurité. **Deux méthodes au choix.** + +### Méthode A — En ligne de commande (recommandée) + +Dans un terminal, à la racine du projet : + +```bash +# 1. Se connecter à Supabase (ouvre le navigateur) +npx supabase login + +# 2. Déployer la fonction +npx supabase functions deploy admin-users ^ + --project-ref cesjrlfqeqftayhnqjkb --no-verify-jwt +``` + +> `--no-verify-jwt` est normal ici : la fonction vérifie elle-même que +> l'appelant est bien un admin connecté (voir le code). +> La clé `service_role` est fournie automatiquement par Supabase, tu n'as +> rien à copier. + +### Méthode B — Par le tableau de bord (sans terminal) + +1. Supabase → menu **Edge Functions** → **Create a new function**. +2. Nom : `admin-users`. +3. Colle le contenu de `supabase/functions/admin-users/index.ts`. +4. Déploie. +5. Dans les **réglages** de la fonction, **désactive « Enforce JWT Verification »**. + +--- + +## Étape 5 — Construire et distribuer l'appli + +```bash +npm run tauri:build +``` + +L'installateur se trouve ensuite dans : +`src-tauri/target/release/bundle/` + +Envoie ce fichier à tes collègues. Les clés sont dedans : ils n'ont **rien** à régler. + +--- + +## Au quotidien — Gérer les comptes + +1. Ouvre l'appli, connecte-toi avec ton compte admin. +2. Menu **Administration** (visible uniquement pour les admins). +3. Tu peux : + - **Créer un compte** : nom, email, mot de passe provisoire (bouton + « Générer » si tu manques d'imagination), pôles, et droits admin + - **Modifier** un membre : nom, email, mot de passe, photo, pôles + - **Promouvoir / rétrograder** un admin (avec confirmation) + - **Supprimer** un compte + +Communique l'email + le mot de passe provisoire à chaque membre. Il se connecte, +c'est tout. ✅ + +--- + +## Notes de sécurité + +- La clé `anon` dans l'appli est **publique par nature** : aucun risque. +- La clé `service_role` (toute-puissante) **n'est jamais dans l'appli** : elle + reste cachée dans l'Edge Function, côté serveur. +- Un membre normal **ne peut pas** se promouvoir admin lui-même (bloqué côté base). +- Tu ne peux pas supprimer ni rétrograder **ton propre** compte admin (anti-blocage). +- Depuis la v0.2, la **suppression d'un projet** est réservée à son créateur ou à + un administrateur (avant, n'importe quel membre pouvait tout effacer). + +--- + +## ⚠️ Après la mise à jour v0.2 — redéployer la fonction + +La migration v10 supprime la colonne `profiles.role`, dans laquelle l'ancienne +version de l'Edge Function écrivait. **Redéploie-la**, sinon la création de +comptes échouera : + +```bash +npx supabase functions deploy admin-users --no-verify-jwt +``` diff --git a/MISE_A_JOUR.md b/MISE_A_JOUR.md new file mode 100644 index 0000000..5e750dc --- /dev/null +++ b/MISE_A_JOUR.md @@ -0,0 +1,148 @@ +# 🔄 Mises à jour automatiques + +Plus besoin d'envoyer un `.exe` à chaque correctif. L'application va chercher +elle-même la dernière version publiée. + +``` + Toi Ton collègue + ─── ──────────── + npm run release -- 0.3.0 "…" + │ + ├─ construit + signe l'installeur + ├─ le dépose sur Supabase + └─ écrit latest.json + au lancement : + lit latest.json → 0.3.0 > 0.2.0 + 🔔 « Version 0.3.0 disponible » + clic → installe → redémarre +``` + +--- + +## Installation (une seule fois) + +### 1. Créer le bucket + +Supabase → **SQL Editor** → **New query** → colle +[`supabase/releases_bucket.sql`](supabase/releases_bucket.sql) → **Run**. + +### 2. Donner au script la clé de publication + +Supabase → **Settings → API** → copie la clé **`service_role`**. + +Mets-la dans le fichier `.env.local` à la racine du projet : + +```env +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi... +``` + +> `.env.local` est ignoré par git (règle `*.local`). Cette clé sert **uniquement** +> à téléverser depuis ta machine : elle n'entre jamais dans l'application +> distribuée à l'équipe. + +### 3. 🔑 Sauvegarder la clé de signature — **le point important** + +Le fichier `.tauri-signing-key.local` à la racine du projet signe chaque mise à +jour. Les applications déjà installées **n'acceptent que** les paquets signés +avec cette clé. + +> ⚠️ **Si tu la perds, aucune mise à jour ne pourra plus être installée.** +> Il faudrait générer une nouvelle clé, reconstruire, et redistribuer un `.exe` +> à la main à chaque membre de l'équipe. + +Copie-la maintenant dans un endroit sûr — gestionnaire de mots de passe, clé USB, +disque de sauvegarde. **Pas** dans le dépôt git. + +### 4. La dernière distribution manuelle + +Les versions déjà installées chez tes collègues ne connaissent pas encore le +système de mise à jour. Il faut donc leur livrer **une dernière fois** un +installeur à la main : + +```bash +npm run tauri:build +``` + +Envoie le fichier `src-tauri/target/release/bundle/nsis/GameDev Tracker_0.2.0_x64-setup.exe`. + +C'est le dernier. À partir de cette version, tout est automatique. + +--- + +## Publier une nouvelle version + +```bash +npm run release -- 0.3.0 "Filtres multiples sur les tâches, correction du Kanban" +``` + +Le script : + +1. écrit `0.3.0` dans `package.json` et `tauri.conf.json` ; +2. construit et **signe** l'installeur (quelques minutes) ; +3. le téléverse dans le bucket `releases` ; +4. écrit `latest.json` — c'est ce fichier que les applications lisent. + +Ensuite, pense à valider les deux fichiers de version : + +```bash +git add package.json src-tauri/tauri.conf.json && git commit -m "v0.3.0" +``` + +### Quel numéro de version ? + +Format `X.Y.Z`, toujours croissant. + +| Ce que tu as fait | Numéro | +|---|---| +| Correction de bug | `0.2.0` → `0.2.1` | +| Nouvelle fonctionnalité | `0.2.1` → `0.3.0` | +| Refonte, changement cassant | `0.3.0` → `1.0.0` | + +Le texte entre guillemets s'affiche dans le bandeau chez tes collègues : écris +ce qui **change pour eux**, pas le détail technique. + +--- + +## Côté équipe : ce qu'ils voient + +Une bande fine en haut de l'application : + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ ✨ Version 0.3.0 disponible. Filtres multiples sur les tâches │ +│ [⬇ Installer et redémarrer] [✕ plus tard]│ +└────────────────────────────────────────────────────────────────────┘ +``` + +- Jamais de fenêtre bloquante : on peut ignorer et continuer à travailler. +- « Plus tard » masque le bandeau ; la proposition revient au lancement suivant. +- Vérification au démarrage, puis toutes les 2 heures pour les sessions longues. +- Le menu **Aide** (touche `?`) affiche la version installée et un bouton + « Rechercher une mise à jour ». + +--- + +## En cas de problème + +| Symptôme | Cause probable | Solution | +|---|---|---| +| Aucun bandeau n'apparaît | `latest.json` absent ou version identique | Ouvre l'URL du manifeste dans un navigateur, vérifie le champ `version` | +| « Signature invalide » | L'installeur a été signé avec une autre clé | Republie avec la bonne `.tauri-signing-key.local` | +| Le script dit « bucket not found » | Étape 1 non faite | Exécute `supabase/releases_bucket.sql` | +| Le script dit « clé introuvable » | `.tauri-signing-key.local` absent | Restaure ta sauvegarde (étape 3) | +| Le téléchargement échoue chez un membre | Bucket non public | Rejoue `releases_bucket.sql` | + +URL du manifeste (à ouvrir dans un navigateur pour vérifier) : + +``` +https://.supabase.co/storage/v1/object/public/releases/latest.json +``` + +--- + +## Coût + +Un installeur pèse ~8 Mo. L'offre gratuite Supabase donne 1 Go de stockage et +5 Go de transfert par mois : à 6 personnes, c'est environ **100 mises à jour par +mois** avant de s'en approcher. Tu peux supprimer les vieux installeurs du +bucket de temps en temps (garde le dernier). diff --git a/README.md b/README.md index e38a8db..0fe9ca5 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,194 @@ -# 🎮 GameDev Tracker - -Application **desktop** de gestion de projet de jeu vidéo, pensée pour une **petite équipe (5-6 personnes)**. -Construite avec **Tauri + React + TypeScript + Tailwind + Framer Motion**, base de données **gratuite Supabase**. - -## ✨ Fonctionnalités - -- 🔐 **Connexion / inscription** par email + mot de passe -- 👥 **Équipe, rôles & pôles** — le rôle d'un membre (programmeur, artiste, game designer…) définit son **pôle** -- 📋 **Tâches & Kanban** — glisser-déposer entre *À faire / En cours / En revue / Terminé*, tâche confiée à un **pôle** avec une ou **plusieurs personnes** assignées -- ⏱️ **Feuille de temps** — vue par semaine (7 jours en colonnes) : consigne les heures faites chaque jour sur une tâche **ou** une activité libre (réunion, veille…) -- 🐛 **Suivi de bugs** — sévérité, priorité, statut, pôle, assignation -- 🔔 **Notifications** — quand une tâche/un bug est confié à un pôle, ses membres reçoivent une notif (cloche 🔔 dans la barre latérale) -- 📊 **Tableau de bord** — avancement global, répartition des tâches, temps total, échéances -- 🔄 **Synchro temps réel** — quand un membre modifie une tâche, les autres la voient se mettre à jour - ---- - -## 🚀 Mise en route - -### 1. Créer la base de données (gratuit, une seule fois pour toute l'équipe) - -1. Va sur **[supabase.com](https://supabase.com)** et crée un compte. -2. Clique **New project** (choisis un mot de passe de base de données, garde-le quelque part). -3. Attends ~2 min que le projet se crée. -4. Dans le menu de gauche : **SQL Editor → New query**. -5. Ouvre le fichier [`supabase/schema.sql`](supabase/schema.sql) de ce dépôt, **copie tout son contenu**, colle-le, puis clique **Run**. - > Cela crée les tables (profils, projets, tâches, bugs, suivi du temps, notifications), la sécurité et la synchro temps réel. - > - > 🆙 **Tu avais déjà installé une version précédente (avec les jalons) ?** Exécute plutôt, dans l'ordre, [`supabase/migration_v2.sql`](supabase/migration_v2.sql) (supprime les jalons, ajoute pôles + assignation multiple + notifications), [`supabase/migration_v3.sql`](supabase/migration_v3.sql) (feuille de temps : entrées libres type réunion) puis [`supabase/migration_v4.sql`](supabase/migration_v4.sql) (plusieurs pôles par membre) — **sans perdre tes données**. - > - > 🔁 Après la migration v4, **redéploie l'Edge Function** pour que l'admin puisse gérer les pôles multiples : `supabase functions deploy admin-users --no-verify-jwt`. -6. Menu **Settings (⚙️) → API**. Note deux valeurs : - - **Project URL** → ressemble à `https://xxxxx.supabase.co` - - **anon public** (sous *Project API keys*) → longue chaîne commençant par `eyJ...` -7. **Recommandé pour une petite équipe** : désactive la confirmation d'email pour pouvoir se connecter tout de suite après l'inscription. - - Menu **Authentication → Sign In / Providers → Email** : désactive **« Confirm email »** et enregistre. - - (Sinon, chaque membre devra cliquer sur un lien reçu par email avant de pouvoir se connecter.) - -> ℹ️ La clé *anon public* est faite pour être distribuée à l'app cliente : pas de secret sensible exposé. - -### 2. Lancer l'application - -Prérequis (déjà installés sur cette machine) : **Node.js**, **Rust**. - -```bash -npm install -npm run tauri:dev -``` - -Au premier lancement, l'app affiche un écran de **configuration** : colle l'**URL** et la **clé anon** obtenues à l'étape 1, puis **Connecter**. -Ensuite : crée ton compte (avec ton rôle), crée un **projet**, et c'est parti. 🎉 - -### 3. Distribuer à l'équipe - -Pour générer un installeur (`.exe`/`.msi` sur Windows) : - -```bash -npm run tauri:build -``` - -Le fichier d'installation se trouve dans `src-tauri/target/release/bundle/`. -Envoie-le à tes coéquipiers. À leur premier lancement, ils saisissent **la même URL + clé anon** que toi — ils rejoignent ainsi le même projet partagé. - ---- - -## 🧰 Scripts utiles - -| Commande | Effet | -| --- | --- | -| `npm run tauri:dev` | Lance l'app en mode développement (rechargement à chaud) | -| `npm run tauri:build` | Génère l'installeur de l'app | -| `npm run dev` | Lance uniquement le front-end dans le navigateur (sans la fenêtre desktop) | -| `npm run typecheck` | Vérifie les types TypeScript | - -## 🏗️ Architecture - -```text -├─ index.html # point d'entrée du front -├─ src/ # application React (front-end) -│ ├─ pages/ # Dashboard, Kanban, Planning (feuille de temps), Bugs, Members, Admin, Login, Setup -│ ├─ components/ # Layout, UI, animations -│ └─ lib/ # client Supabase, types, contexte global, config -├─ src-tauri/ # enveloppe desktop Tauri (Rust) -└─ supabase/schema.sql # schéma de la base à exécuter dans Supabase -``` - -## 🔒 Note de sécurité - -L'app suppose une **équipe de confiance** : tout membre connecté peut lire et modifier les données du projet (politique RLS « accès complet pour les utilisateurs authentifiés »). C'est volontaire pour rester simple sur une petite équipe. Si un jour tu veux des permissions plus fines (ex. seul le lead supprime un projet), il faudra ajuster les *policies* dans `supabase/schema.sql`. +# 🎮 GameDev Tracker + +Application **desktop** de gestion de projet de jeu vidéo, pensée pour une **petite équipe (5-6 personnes)**. +**Tauri + React + TypeScript + Tailwind**, base de données **Supabase** (offre gratuite suffisante). + +--- + +## ✨ Ce que fait l'application + +| | | +|---|---| +| 📊 **Tableau de bord** | Ce que **tu** as à faire, avancement, bugs par sévérité, heures de l'équipe, dernières discussions | +| 📋 **Tâches (Kanban)** | 4 colonnes, glisser-déposer **et déplacement au clavier**, checklists, étiquettes, priorité, échéance | +| 🐛 **Bugs** | Sévérité expliquée, tri et filtres, **captures d'écran collables (Ctrl+V)**, temps passé | +| ⏱️ **Feuille de temps** | Semaine en 7 colonnes, saisie sur une tâche / un bug / une activité libre, **export CSV** | +| 📚 **Documentation** | Fiches en Markdown : GDD, conventions, notes de réunion — créées dans l'app ou **importées depuis un `.md` / `.docx`** | +| 💬 **Discussions** | Fil de commentaires sur chaque tâche et chaque bug, avec notification des personnes concernées | +| 📎 **Pièces jointes** | Fichiers et captures rattachés aux tâches et aux bugs | +| 👥 **Équipe & pôles** | Un membre appartient à un ou plusieurs pôles (programmeur, artiste, sound designer…) | +| 🔔 **Notifications** | Quand une tâche/un bug t'est confié — par ton **pôle** ou **nommément** — et à chaque message | +| 🔎 **Recherche globale** | `Ctrl + K` : tâches, bugs, personnes, actions | +| 🔄 **Temps réel** | Tout se synchronise entre les membres, sans recharger | +| 🌗 **Thème** | Clair, sombre, ou suivant le système | +| 🔄 **Mises à jour auto** | L'app se met à jour toute seule — plus de `.exe` à renvoyer ([détails](MISE_A_JOUR.md)) | + +--- + +## 🚀 Mise en route + +### 1. Créer la base de données (une seule fois, pour toute l'équipe) + +1. Crée un compte sur **[supabase.com](https://supabase.com)** puis un **New project** (offre gratuite). +2. Menu **SQL Editor → New query**. +3. Colle le contenu de **[`supabase/schema.sql`](supabase/schema.sql)** et clique **Run**. + + > 🆙 **Tu avais déjà une version précédente installée ?** + > Exécute plutôt **[`supabase/update_all.sql`](supabase/update_all.sql)** : il met la base à jour + > sans rien perdre (voir « Mise à jour v0.2 » plus bas). + +4. Menu **Authentication → Sign In / Providers → Email** : désactive **« Confirm email »** + (sinon chaque membre doit cliquer un lien reçu par mail avant de pouvoir se connecter). +5. Menu **Settings → API** : note l'**URL du projet** et la clé **anon public**. + +### 2. Configurer l'application + +Crée un fichier `.env` à la racine : + +```env +VITE_SUPABASE_URL=https://xxxxx.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOi... +``` + +> La clé *anon* est **publique par nature** : c'est la sécurité au niveau des lignes (RLS) +> de Supabase qui protège les données, pas le secret de cette clé. + +### 3. Lancer + +Prérequis : **Node.js** et **Rust**. + +```bash +npm install +npm run tauri:dev +``` + +### 4. Devenir administrateur (une seule fois) + +Crée ton compte, puis dans le **SQL Editor** de Supabase : + +```sql +update public.profiles set is_admin = true +where id = (select id from auth.users where email = 'TON_EMAIL@exemple.com'); +``` + +Déploie ensuite l'espace d'administration (voir **[GUIDE_ADMIN.md](GUIDE_ADMIN.md)**) : + +```bash +supabase functions deploy admin-users --no-verify-jwt +``` + +Tu peux alors créer les comptes de toute l'équipe depuis l'onglet **Administration**. + +### 5. Distribuer — une seule fois + +```bash +npm run tauri:build +``` + +Génère un installeur dans `src-tauri/target/release/bundle/nsis/`. Envoie-le à +ton équipe : **c'est la dernière fois que tu le fais à la main.** + +### 6. Activer les mises à jour automatiques + +Ensuite, chaque nouvelle version se publie en une commande : + +```bash +npm run release -- 0.3.0 "Ce qui change pour l'équipe" +``` + +Tes collègues voient un bandeau « Version disponible » au lancement, cliquent, et +l'application se met à jour toute seule. Mise en place (une fois) et mode +d'emploi complet : **[MISE_A_JOUR.md](MISE_A_JOUR.md)**. + +--- + +## ⌨️ Raccourcis clavier + +| Raccourci | Action | +|---|---| +| `Ctrl + K` ou `/` | Recherche globale / palette de commandes | +| `Ctrl + 1…6` | Changer d'écran | +| `Ctrl + B` | Replier / déployer le menu latéral | +| `?` | Afficher l'aide | +| `Échap` | Fermer une fenêtre | +| `Espace` puis `← → ↑ ↓` | Déplacer une carte du Kanban **au clavier** | +| `Ctrl + Entrée` | Envoyer un commentaire | +| `Ctrl + S` | Enregistrer une fiche de documentation | +| `Alt + ← / →` | Semaine précédente / suivante (feuille de temps) | + +--- + +## 🧱 Architecture + +``` +src/ + lib/ + supabase.ts Client unique, clés lues depuis .env + db.ts run/rows/mutate + useQuery + useRealtime ← toute la fiabilité est ici + AppContext.tsx Session, profil, membres, projets, écran courant + types.ts Types métier + libellés + helpers de formatage + tones.ts Couleurs sémantiques (badges, pastilles) — clair ET sombre + notify.ts Qui prévenir, et quand + storage.ts Avatars et pièces jointes + markdown.ts Rendu Markdown assaini + import .md/.docx + feedback.tsx Toasts et confirmations (le SEUL système de retour utilisateur) + theme.ts Clair / sombre / système + components/ + ui.tsx Primitives : Button, IconButton, Field, états vide/erreur/chargement + Modal.tsx Modale : en-tête et pied fixes, piège à focus, garde anti-perte de saisie + Select.tsx Menu déroulant entièrement pilotable au clavier + people.tsx Avatars, pôles, assignation, étiquettes + EntityExtras.tsx Discussion, pièces jointes, temps passé (tâches ET bugs) + Layout.tsx Barre latérale + UNE barre supérieure + raccourcis + pages/ Un fichier par écran +supabase/ + schema.sql Installation neuve + update_all.sql Mise à jour d'une base existante + migration_v10.sql La refonte v0.2 seule + migration_v11.sql La documentation seule + functions/admin-users/ Edge Function de gestion des comptes +``` + +### Trois règles à respecter en contribuant + +1. **Aucune requête sans gestion d'erreur.** On passe par `run` / `rows` / `mutate` de + `lib/db.ts`, jamais par `const { data } = await supa()...`. +2. **Aucune couleur en dur.** Pas de `bg-amber-100` ni de `text-gray-500` : on utilise les + tokens (`bg-panel`, `text-muted`…) et le système de tons de `lib/tones.ts`. +3. **Aucun bouton écrit à la main.** `Button` / `IconButton` de `components/ui.tsx`. + Un bouton-icône a toujours un `label` (infobulle **et** lecteur d'écran). + +--- + +## ✅ Vérifications + +```bash +npm run check # types + lint +npm run build # typecheck puis build de production +npm run release -- X.Y.Z "notes" # publie une version pour toute l'équipe +``` + +--- + +## 🆙 Mise à jour v0.2 + +La migration `supabase/update_all.sql` : + +- ajoute les tables **`comments`** et **`attachments`** (+ bucket de stockage) ; +- ajoute **`bugs.poles`** (plusieurs pôles par bug) et **`time_logs.bug_id`** (temps sur un bug) ; +- **supprime les colonnes de compatibilité** `profiles.role`, `tasks.pole`, `bugs.assignee_id` + après avoir recopié leur contenu dans les tableaux correspondants ; +- ajoute la fonction **`reorder_tasks`** (réordonnancement du Kanban en une transaction) ; +- réserve la **suppression d'un projet** à son créateur ou à un administrateur. + +> ⚠️ **Redéploie l'Edge Function après la migration** — elle écrivait dans `profiles.role`, +> qui n'existe plus : +> ```bash +> supabase functions deploy admin-users --no-verify-jwt +> ``` + +--- + +## 📄 Licence + +MIT. diff --git a/Setp.txt b/Setp.txt deleted file mode 100644 index 19186e6..0000000 --- a/Setp.txt +++ /dev/null @@ -1,5 +0,0 @@ -Sur ton autre PC (fixe/portable) -Première fois (si le projet n'y est pas encore) : - -npm install -npm run tauri:dev \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..62e2a5c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,48 @@ +// Configuration ESLint (format « flat », ESLint 9). +// L'application avait des commentaires `eslint-disable` un peu partout… +// sans qu'ESLint ne soit installé. Il l'est maintenant. + +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default tseslint.config( + { ignores: ['dist', 'node_modules', 'src-tauri', 'supabase/functions'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': 'off', + + // Les dépendances de hooks manquantes ont causé de vrais bugs de + // rafraîchissement : c'est une erreur, pas un avertissement. + 'react-hooks/exhaustive-deps': 'error', + + // Avertissement, pas erreur : réagir à un changement de contexte externe + // (notification qui demande d'ouvrir une fiche, ouverture d'un menu) + // passe légitimement par un setState dans un effet. + 'react-hooks/set-state-in-effect': 'warn', + + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' } + ], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + eqeqeq: ['error', 'smart'], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'prefer-const': 'error' + } + } +) diff --git a/index.html b/index.html index 8b8e7f0..c6732eb 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,40 @@ GameDev Tracker + + + +
diff --git a/package-lock.json b/package-lock.json index f2ecc52..7569997 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,43 @@ { "name": "gamedev-tracker", - "version": "0.1.0", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gamedev-tracker", - "version": "0.1.0", + "version": "0.2.5", "license": "MIT", "dependencies": { "@supabase/supabase-js": "^2.45.4", "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "date-fns": "^3.6.0", + "dompurify": "^3.4.13", "framer-motion": "^11.5.4", "lucide-react": "^0.441.0", + "mammoth": "^1.12.0", + "marked": "^18.0.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@tauri-apps/cli": "^2.0.0", "@types/node": "^20.16.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", + "eslint": "^10.8.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.9.0", "postcss": "^8.4.45", "tailwindcss": "^3.4.10", "typescript": "^5.5.4", + "typescript-eslint": "^8.66.0", "vite": "^5.4.3" } }, @@ -74,6 +85,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -716,6 +728,200 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1472,6 +1678,24 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-process": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", + "integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-updater": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", + "integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1517,6 +1741,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1524,12 +1755,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1547,6 +1786,7 @@ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1562,6 +1802,257 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1583,6 +2074,56 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1611,6 +2152,15 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/autoprefixer": { "version": "10.5.2", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", @@ -1648,6 +2198,36 @@ "postcss": "^8.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -1674,6 +2254,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1707,6 +2306,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -1797,6 +2397,27 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1845,6 +2466,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1852,6 +2480,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -1859,6 +2493,24 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.382", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz", @@ -1925,6 +2577,212 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1955,6 +2813,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -1965,6 +2837,19 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1978,6 +2863,44 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2067,6 +2990,19 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -2080,6 +3016,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -2089,6 +3042,38 @@ "node": ">=20.0.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2151,12 +3136,26 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2180,6 +3179,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2193,6 +3213,51 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2213,6 +3278,22 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2225,6 +3306,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2244,6 +3336,42 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/marked": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.9.tgz", + "integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2268,6 +3396,22 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -2321,6 +3465,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", @@ -2361,6 +3512,97 @@ "node": ">= 6" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2428,6 +3670,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -2571,6 +3814,32 @@ "dev": true, "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2597,6 +3866,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2609,6 +3879,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2637,6 +3908,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2752,6 +4038,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2771,6 +4063,35 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2781,6 +4102,21 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2929,6 +4265,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2949,6 +4286,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2962,12 +4312,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2976,6 +4340,36 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -3014,11 +4408,20 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -3027,6 +4430,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3081,12 +4485,84 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index e42fce8..ae50628 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,53 @@ { "name": "gamedev-tracker", - "version": "0.1.0", - "description": "Application de gestion de projet de jeu vidéo pour petite équipe", + "version": "0.2.7", + "description": "Gestion de projet de jeu video pour petite equipe (Tauri + React + Supabase)", "type": "module", "author": "Mathew", "license": "MIT", "scripts": { "dev": "vite", - "build": "vite build", + "build": "npm run typecheck && vite build", "preview": "vite preview", "tauri": "tauri", "tauri:dev": "tauri dev", - "tauri:build": "tauri build", - "typecheck": "tsc --noEmit -p tsconfig.json" + "tauri:build": "node scripts/tauri-build.mjs", + "typecheck": "tsc --noEmit -p tsconfig.json", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "check": "npm run typecheck && npm run lint", + "release": "node scripts/release.mjs" }, "dependencies": { "@supabase/supabase-js": "^2.45.4", "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "date-fns": "^3.6.0", + "dompurify": "^3.4.13", "framer-motion": "^11.5.4", "lucide-react": "^0.441.0", + "mammoth": "^1.12.0", + "marked": "^18.0.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@tauri-apps/cli": "^2.0.0", "@types/node": "^20.16.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", + "eslint": "^10.8.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.9.0", "postcss": "^8.4.45", "tailwindcss": "^3.4.10", "typescript": "^5.5.4", + "typescript-eslint": "^8.66.0", "vite": "^5.4.3" } } diff --git a/scripts/installer-art.py b/scripts/installer-art.py new file mode 100644 index 0000000..d20d386 --- /dev/null +++ b/scripts/installer-art.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Génère les deux images de l'installeur NSIS à partir du logo de l'app. + + python scripts/installer-art.py + +NSIS impose du BMP à des tailles fixes : + · sidebar.bmp 164 x 314 — panneau de gauche (accueil et fin) + · header.bmp 150 x 57 — bandeau des pages intermédiaires + +Les images sont versionnées dans src-tauri/installer/ : ce script n'est à +relancer que si le logo ou les couleurs de marque changent (il demande +Pillow, qui n'est PAS une dépendance du projet). +""" + +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parent.parent +OUT = ROOT / "src-tauri" / "installer" +ICON = ROOT / "src-tauri" / "icons" / "128x128@2x.png" + +# Dégradé du logo (public/logo.svg) +INDIGO = (99, 102, 241) +TEAL = (20, 184, 166) +INK = (16, 16, 24) +INK_SOFT = (30, 29, 48) + + +def font(size: int, bold: bool = False): + """Segoe UI si disponible, repli sur la police par défaut de Pillow.""" + for name in (["segoeuib.ttf", "seguisb.ttf"] if bold else ["segoeui.ttf"]): + try: + return ImageFont.truetype(f"C:/Windows/Fonts/{name}", size) + except OSError: + continue + return ImageFont.load_default() + + +def lerp(a, b, t): + return tuple(round(a[i] + (b[i] - a[i]) * t) for i in range(3)) + + +def vertical_gradient(size, top, bottom): + w, h = size + img = Image.new("RGB", size) + d = ImageDraw.Draw(img) + for y in range(h): + d.line([(0, y), (w, y)], fill=lerp(top, bottom, y / max(h - 1, 1))) + return img + + +def horizontal_gradient(size, left, right): + w, h = size + img = Image.new("RGB", size) + d = ImageDraw.Draw(img) + for x in range(w): + d.line([(x, 0), (x, h)], fill=lerp(left, right, x / max(w - 1, 1))) + return img + + +def logo(px: int) -> Image.Image: + return Image.open(ICON).convert("RGBA").resize((px, px), Image.LANCZOS) + + +# --------------------------------------------------------------------- +# Panneau latéral 164 x 314 — fond sombre, logo, nom, liseré de marque +# --------------------------------------------------------------------- +def sidebar() -> Image.Image: + W, H = 164, 314 + img = vertical_gradient((W, H), INK_SOFT, INK) + d = ImageDraw.Draw(img, "RGBA") + + # Halo indigo diffus derrière le logo, pour éviter le fond plat. + cx, cy = W // 2, 96 + for r in range(92, 0, -4): + alpha = int(30 * (1 - r / 92) ** 2) + d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(*INDIGO, alpha)) + + mark = logo(76) + img.paste(mark, (cx - 38, cy - 38), mark) + + title = font(15, bold=True) + sub = font(11) + d.text((cx, 168), "GameDev", font=title, fill=(255, 255, 255), anchor="mm") + d.text((cx, 186), "Tracker", font=title, fill=(255, 255, 255), anchor="mm") + d.text((cx, 210), "Gestion de projet", font=sub, fill=(150, 148, 170), anchor="mm") + d.text((cx, 225), "de jeu vidéo", font=sub, fill=(150, 148, 170), anchor="mm") + + # Liseré dégradé en pied de panneau : la signature visuelle de la marque. + bar = horizontal_gradient((W, 4), INDIGO, TEAL) + img.paste(bar, (0, H - 4)) + return img + + +# --------------------------------------------------------------------- +# Bandeau 150 x 57 — fond SOMBRE, accordé à MUI_BGCOLOR du template +# --------------------------------------------------------------------- +def header() -> Image.Image: + W, H = 150, 57 + img = Image.new("RGB", (W, H), INK) + d = ImageDraw.Draw(img) + + mark = logo(34) + img.paste(mark, (W - 34 - 12, (H - 34) // 2), mark) + + name = font(11, bold=True) + d.text((W - 56, H // 2 - 7), "GameDev", font=name, fill=(255, 255, 255), anchor="rm") + d.text((W - 56, H // 2 + 7), "Tracker", font=name, fill=(255, 255, 255), anchor="rm") + + bar = horizontal_gradient((W, 3), INDIGO, TEAL) + img.paste(bar, (0, H - 3)) + return img + + +OUT.mkdir(parents=True, exist_ok=True) +for name, image in (("sidebar", sidebar()), ("header", header())): + path = OUT / f"{name}.bmp" + # BMP 24 bits sans alpha : le seul format que NSIS accepte partout. + image.convert("RGB").save(path, "BMP") + print(f" {path.relative_to(ROOT)} {image.width}x{image.height}") + +print("\nImages générées.") diff --git a/scripts/release.mjs b/scripts/release.mjs new file mode 100644 index 0000000..56baa1e --- /dev/null +++ b/scripts/release.mjs @@ -0,0 +1,236 @@ +#!/usr/bin/env node +// ===================================================================== +// Publication d'une nouvelle version. +// +// npm run release -- 0.3.0 "Ce qui change pour l'équipe" +// +// Ce que fait le script, dans l'ordre : +// 1. écrit le numéro de version dans package.json et tauri.conf.json +// 2. construit et SIGNE l'installeur Windows +// 3. téléverse l'installeur dans le bucket public « releases » +// 4. écrit latest.json — c'est ce fichier que les applications lisent +// +// À partir de là, chaque membre voit un bandeau « Version X disponible » +// au lancement, clique, et c'est fait. +// ===================================================================== + +import { execSync } from 'node:child_process' +import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs' +import { join, basename } from 'node:path' +import { ROOT, KEY_PATH, signingEnv, tauriBin } from './signing.mjs' + +const BUCKET = 'releases' + +// --------------------------------------------------------------------- +// Petits utilitaires +// --------------------------------------------------------------------- +const c = { + ok: (m) => console.log(`\x1b[32m✓\x1b[0m ${m}`), + step: (m) => console.log(`\n\x1b[1m\x1b[36m▸ ${m}\x1b[0m`), + info: (m) => console.log(` ${m}`), + warn: (m) => console.log(`\x1b[33m!\x1b[0m ${m}`) +} + +function fail(message, hint) { + console.error(`\n\x1b[31m✗ ${message}\x1b[0m`) + if (hint) console.error(`\n${hint}\n`) + process.exit(1) +} + +/** Lit .env puis .env.local (le second gagne). */ +function loadEnv() { + const env = {} + for (const file of ['.env', '.env.local']) { + const path = join(ROOT, file) + if (!existsSync(path)) continue + for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) { + const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/i) + if (m) env[m[1]] = m[2].trim().replace(/^["']|["']$/g, '') + } + } + return env +} + +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')) +} + +function writeJson(path, data) { + writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8') +} + +// --------------------------------------------------------------------- +// 1) Arguments et vérifications +// --------------------------------------------------------------------- +const [version, ...noteParts] = process.argv.slice(2) +const notes = noteParts.join(' ').trim() + +if (!version || !/^\d+\.\d+\.\d+$/.test(version)) { + fail( + 'Numéro de version manquant ou invalide.', + 'Utilisation :\n' + + ' npm run release -- 1.2.3 "Ce qui change pour l\'équipe"\n\n' + + 'Le numéro doit être au format X.Y.Z.' + ) +} + +const env = loadEnv() +const SUPABASE_URL = env.VITE_SUPABASE_URL +const SERVICE_KEY = env.SUPABASE_SERVICE_ROLE_KEY + +if (!SUPABASE_URL) fail('VITE_SUPABASE_URL absent du fichier .env') +if (!SERVICE_KEY) { + fail( + 'SUPABASE_SERVICE_ROLE_KEY absent du fichier .env.local', + 'Cette clé sert UNIQUEMENT à téléverser depuis ta machine — elle ne part\n' + + "jamais dans l'application distribuée.\n\n" + + 'Récupère-la dans Supabase → Settings → API → « service_role », puis\n' + + 'crée un fichier .env.local à la racine avec :\n\n' + + ' SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...\n\n' + + '(.env.local est ignoré par git : la clé ne sera jamais publiée.)' + ) +} + +const currentPkg = readJson(join(ROOT, 'package.json')) +if (currentPkg.version === version) { + c.warn(`package.json est déjà en ${version} — on republie par-dessus.`) +} + +// --------------------------------------------------------------------- +// 2) Numéro de version +// --------------------------------------------------------------------- +c.step(`Version ${version}`) + +const pkgPath = join(ROOT, 'package.json') +const pkg = readJson(pkgPath) +pkg.version = version +writeJson(pkgPath, pkg) +c.ok('package.json') + +const confPath = join(ROOT, 'src-tauri', 'tauri.conf.json') +const conf = readJson(confPath) +conf.version = version +writeJson(confPath, conf) +c.ok('tauri.conf.json') + +// --------------------------------------------------------------------- +// 3) Construction signée +// --------------------------------------------------------------------- +c.step('Construction de l\'installeur (plusieurs minutes)') + +try { + execSync(`"${tauriBin()}" build`, { cwd: ROOT, stdio: 'inherit', env: signingEnv() }) +} catch { + fail('La construction a échoué. Corrige l\'erreur ci-dessus puis relance.') +} + +// --------------------------------------------------------------------- +// 4) Retrouver l'installeur et sa signature +// --------------------------------------------------------------------- +c.step('Recherche des fichiers produits') + +const bundleDir = join(ROOT, 'src-tauri', 'target', 'release', 'bundle') + +/** Cherche le premier fichier correspondant, dans l'ordre de préférence. */ +function findBundle() { + // NSIS d'abord : c'est le seul format que l'updater Windows sait installer + // sans intervention (le .msi exige des droits administrateur à chaque fois). + for (const [dir, ext] of [ + ['nsis', '-setup.exe'], + ['msi', '.msi'] + ]) { + const path = join(bundleDir, dir) + if (!existsSync(path)) continue + const file = readdirSync(path).find((f) => f.endsWith(ext) && f.includes(version)) + if (file) return join(path, file) + } + return null +} + +const installer = findBundle() +if (!installer) { + fail( + `Aucun installeur trouvé pour la version ${version}.`, + `Regarde dans ${bundleDir}.` + ) +} + +const sigPath = `${installer}.sig` +if (!existsSync(sigPath)) { + fail( + 'Signature absente : l\'installeur n\'a pas été signé.', + `Vérifie que ${KEY_PATH} existe et que la section "plugins.updater"\n` + + 'de tauri.conf.json contient bien "pubkey".' + ) +} + +const signature = readFileSync(sigPath, 'utf8').trim() +const installerName = basename(installer) +const installerBytes = readFileSync(installer) + +c.ok(`${installerName} (${(installerBytes.length / 1048576).toFixed(1)} Mo)`) + +// --------------------------------------------------------------------- +// 5) Téléversement +// --------------------------------------------------------------------- +c.step('Téléversement vers Supabase') + +async function upload(name, body, contentType) { + const res = await fetch( + `${SUPABASE_URL}/storage/v1/object/${BUCKET}/${encodeURIComponent(name)}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${SERVICE_KEY}`, + 'Content-Type': contentType, + 'x-upsert': 'true', // republier une version écrase l'ancienne + 'cache-control': 'max-age=60' + }, + body + } + ) + if (!res.ok) { + const text = await res.text() + if (/bucket not found/i.test(text)) { + fail( + `Le bucket « ${BUCKET} » n'existe pas.`, + 'Exécute une seule fois, dans le SQL Editor de Supabase :\n\n' + + " insert into storage.buckets (id, name, public)\n" + + ` values ('${BUCKET}', '${BUCKET}', true) on conflict (id) do nothing;\n\n` + + " create policy releases_read on storage.objects\n" + + ` for select using (bucket_id = '${BUCKET}');` + ) + } + fail(`Téléversement de ${name} refusé (HTTP ${res.status})`, text) + } +} + +await upload(installerName, installerBytes, 'application/octet-stream') +c.ok(installerName) + +// --------------------------------------------------------------------- +// 6) Le manifeste — c'est lui que les applications interrogent +// --------------------------------------------------------------------- +const manifest = { + version, + notes: notes || `Version ${version}`, + pub_date: new Date().toISOString(), + platforms: { + 'windows-x86_64': { + signature, + url: `${SUPABASE_URL}/storage/v1/object/public/${BUCKET}/${encodeURIComponent(installerName)}` + } + } +} + +await upload('latest.json', JSON.stringify(manifest, null, 2), 'application/json') +c.ok('latest.json') + +// --------------------------------------------------------------------- +c.step(`Version ${version} publiée 🎉`) +c.info('Chaque membre verra le bandeau « Mise à jour disponible » au prochain') +c.info('lancement (ou dans les 2 heures si son application reste ouverte).') +c.info('') +c.info('Pense à valider les fichiers modifiés :') +c.info(' git add package.json src-tauri/tauri.conf.json && git commit -m "v' + version + '"') +console.log() diff --git a/scripts/signing.mjs b/scripts/signing.mjs new file mode 100644 index 0000000..de74696 --- /dev/null +++ b/scripts/signing.mjs @@ -0,0 +1,43 @@ +// ===================================================================== +// Clé de signature des mises à jour — lecture commune aux scripts. +// +// Le CLI Tauri 2.11 n'honore pas TAURI_SIGNING_PRIVATE_KEY_PATH : il ne +// lit que TAURI_SIGNING_PRIVATE_KEY, qui accepte soit un chemin, soit la +// clé elle-même. On lui passe donc directement le contenu du fichier. +// ===================================================================== + +import { readFileSync, existsSync } from 'node:fs' +import { resolve, join } from 'node:path' + +export const ROOT = resolve(import.meta.dirname, '..') +export const KEY_PATH = join(ROOT, '.tauri-signing-key.local') + +/** + * Renvoie l'environnement à passer à `tauri build`, clé de signature + * incluse. Sort du programme avec un message clair si la clé manque — + * sans elle, aucune mise à jour ne serait acceptée par les installations + * existantes. + */ +export function signingEnv() { + if (!existsSync(KEY_PATH)) { + console.error( + '\n\x1b[31m✗ Clé de signature introuvable : .tauri-signing-key.local\x1b[0m\n\n' + + 'Elle signe chaque mise à jour. Restaure ta sauvegarde (MISE_A_JOUR.md, étape 3).\n\n' + + "Si tu n'as PAS encore de clé — attention, les applications déjà installées\n" + + 'avec une autre clé ne pourront plus jamais se mettre à jour :\n\n' + + ' npx tauri signer generate --ci -p "" -w .tauri-signing-key.local\n' + ) + process.exit(1) + } + + return { + ...process.env, + TAURI_SIGNING_PRIVATE_KEY: readFileSync(KEY_PATH, 'utf8').trim(), + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' + } +} + +/** Chemin du binaire Tauri local (npx inutile, et pas de shell à deviner). */ +export function tauriBin() { + return join(ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tauri.cmd' : 'tauri') +} diff --git a/scripts/tauri-build.mjs b/scripts/tauri-build.mjs new file mode 100644 index 0000000..6e7341c --- /dev/null +++ b/scripts/tauri-build.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// ===================================================================== +// `npm run tauri:build` +// +// 1. construit et SIGNE l'application +// 2. dépose LE fichier à distribuer dans `release/`, sous son vrai nom +// +// Le setup NSIS produit dans `src-tauri/target/release/bundle/nsis/` ne +// sert QU'À l'updater (installation silencieuse des mises à jour). Il +// reste enfoui volontairement : c'est `release/GameDev Tracker.exe` qu'on +// envoie à l'équipe, et lui seul. +// ===================================================================== + +import { execSync } from 'node:child_process' +import { copyFileSync, mkdirSync, statSync } from 'node:fs' +import { join } from 'node:path' +import { ROOT, signingEnv, tauriBin } from './signing.mjs' + +const extra = process.argv.slice(2).join(' ') + +execSync(`"${tauriBin()}" build ${extra}`.trim(), { + cwd: ROOT, + stdio: 'inherit', + env: signingEnv() +}) + +// --- Le fichier à distribuer ----------------------------------------- +const built = join(ROOT, 'src-tauri', 'target', 'release', 'app.exe') +const outDir = join(ROOT, 'release') +const out = join(outDir, 'GameDev Tracker.exe') + +mkdirSync(outDir, { recursive: true }) +copyFileSync(built, out) + +const mb = (statSync(out).size / 1048576).toFixed(1) +console.log( + ` +✓ À DISTRIBUER ${out} (${mb} Mo) +` + + ` C'est CE fichier qu'on envoie à l'équipe : il contient l'application +` + + ` ET son écran d'installation. + +` + + ` (le setup NSIS enfoui dans src-tauri/target/… ne sert qu'aux +` + + ` mises à jour automatiques — ne le distribue pas) +` +) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4ec0c88..d68d3d4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -79,12 +79,25 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" name = "app" version = "0.1.0" dependencies = [ + "dirs 5.0.1", "log", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-process", + "tauri-plugin-updater", + "winreg 0.52.0", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", ] [[package]] @@ -640,6 +653,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -671,13 +695,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -688,7 +733,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -815,7 +860,7 @@ dependencies = [ "rustc_version", "toml 1.1.2+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -851,6 +896,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -885,6 +940,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1419,6 +1484,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1668,6 +1748,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1798,6 +1908,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1854,6 +1970,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2095,6 +2217,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2110,6 +2233,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2173,12 +2308,32 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2530,6 +2685,17 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -2613,15 +2779,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2633,6 +2804,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -2694,6 +2879,92 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2709,6 +2980,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2772,6 +3052,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -2982,6 +3285,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -3100,6 +3413,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3184,7 +3503,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3223,6 +3542,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3238,7 +3568,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3246,7 +3576,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3288,7 +3618,7 @@ checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -3380,6 +3710,49 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -3390,7 +3763,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3413,7 +3786,7 @@ checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3480,6 +3853,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.0" @@ -3602,6 +3988,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3806,7 +4202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -3892,6 +4288,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4153,6 +4555,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4383,6 +4794,24 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4392,6 +4821,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4416,6 +4854,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4425,13 +4878,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4456,90 +4926,180 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -4564,6 +5124,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" @@ -4596,7 +5166,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dom_query", "dpi", "dunce", @@ -4604,7 +5174,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -4660,6 +5230,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.3" @@ -4724,6 +5304,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" @@ -4757,6 +5343,18 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fb4d9fc..00a894f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,3 +23,15 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "2.11.3", features = [] } tauri-plugin-log = "2" + +# Installeur intégré (src/installer.rs) : dossiers standards + registre Windows. +dirs = "5" + +[target.'cfg(windows)'.dependencies] +winreg = "0.52" + +# Mises a jour automatiques (uniquement sur les plateformes de bureau). +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-updater = "2" +tauri-plugin-process = "2" + diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c135d7f..dc55d26 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,11 +1,16 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "enables the default permissions", - "windows": [ - "main" - ], - "permissions": [ - "core:default" - ] -} +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Permissions de base + fenetre de l'installeur integre.", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "updater:default", + "process:allow-restart", + "core:window:allow-start-dragging", + "core:window:allow-minimize", + "core:window:allow-close" + ] +} diff --git a/src-tauri/installer/header.bmp b/src-tauri/installer/header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..da53e02004629e10ab692f6112a9435416927781 GIT binary patch literal 25818 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" + StrCmp $R0 0 0 wix_loop_done + StrCpy $WixMode 1 + StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" + Goto compare_version + wix_loop_done: + + ; Check if there is an existing installation, if not, abort the reinstall page + ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" + ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" + ${IfThen} "$R0$R1" == "" ${|} Abort ${|} + + ; Compare this installar version with the existing installation + ; and modify the messages presented to the user accordingly + compare_version: + StrCpy $R4 "$(older)" + ${If} $WixMode = 1 + ReadRegStr $R0 HKLM "$R6" "DisplayVersion" + ${Else} + ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" + ${EndIf} + ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} + + nsis_tauri_utils::SemverCompare "${VERSION}" $R0 + Pop $R0 + ; Reinstalling the same version + ${If} $R0 = 0 + StrCpy $R1 "$(alreadyInstalledLong)" + StrCpy $R2 "$(addOrReinstall)" + StrCpy $R3 "$(uninstallApp)" + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" + ; Upgrading + ${ElseIf} $R0 = 1 + StrCpy $R1 "$(olderOrUnknownVersionInstalled)" + StrCpy $R2 "$(uninstallBeforeInstalling)" + StrCpy $R3 "$(dontUninstall)" + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" + ; Downgrading + ${ElseIf} $R0 = -1 + StrCpy $R1 "$(newerVersionInstalled)" + StrCpy $R2 "$(uninstallBeforeInstalling)" + !if "${ALLOWDOWNGRADES}" == "true" + StrCpy $R3 "$(dontUninstall)" + !else + StrCpy $R3 "$(dontUninstallDowngrade)" + !endif + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" + ${Else} + Abort + ${EndIf} + + ; Skip showing the page if passive + ; + ; Note that we don't call this earlier at the begining + ; of this function because we need to populate some variables + ; related to current installed version if detected and whether + ; we are downgrading or not. + ${If} $PassiveMode = 1 + Call PageLeaveReinstall + ${Else} + nsDialogs::Create 1018 + Pop $R4 + ${IfThen} $(^RTL) = 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} + + ${NSD_CreateLabel} 0 0 100% 24u $R1 + Pop $R1 + + ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 + Pop $R2 + ${NSD_OnClick} $R2 PageReinstallUpdateSelection + + ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 + Pop $R3 + ; Disable this radio button if downgrading and downgrades are disabled + !if "${ALLOWDOWNGRADES}" == "false" + ${IfThen} $R0 = -1 ${|} EnableWindow $R3 0 ${|} + !endif + ${NSD_OnClick} $R3 PageReinstallUpdateSelection + + ; Check the first radio button if this the first time + ; we enter this page or if the second button wasn't + ; selected the last time we were on this page + ${If} $ReinstallPageCheck <> 2 + SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 + ${Else} + SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 + ${EndIf} + + ${NSD_SetFocus} $R2 + nsDialogs::Show + ${EndIf} +FunctionEnd +Function PageReinstallUpdateSelection + ${NSD_GetState} $R2 $R1 + ${If} $R1 == ${BST_CHECKED} + StrCpy $ReinstallPageCheck 1 + ${Else} + StrCpy $ReinstallPageCheck 2 + ${EndIf} +FunctionEnd +Function PageLeaveReinstall + ${NSD_GetState} $R2 $R1 + + ; If migrating from Wix, always uninstall + ${If} $WixMode = 1 + Goto reinst_uninstall + ${EndIf} + + ; In update mode, always proceeds without uninstalling + ${If} $UpdateMode = 1 + Goto reinst_done + ${EndIf} + + ; $R0 holds whether same(0)/upgrading(1)/downgrading(-1) version + ; $R1 holds the radio buttons state: + ; 1 => first choice was selected + ; 0 => second choice was selected + ${If} $R0 = 0 ; Same version, proceed + ${If} $R1 = 1 ; User chose to add/reinstall + Goto reinst_done + ${Else} ; User chose to uninstall + Goto reinst_uninstall + ${EndIf} + ${ElseIf} $R0 = 1 ; Upgrading + ${If} $R1 = 1 ; User chose to uninstall + Goto reinst_uninstall + ${Else} + Goto reinst_done ; User chose NOT to uninstall + ${EndIf} + ${ElseIf} $R0 = -1 ; Downgrading + ${If} $R1 = 1 ; User chose to uninstall + Goto reinst_uninstall + ${Else} + Goto reinst_done ; User chose NOT to uninstall + ${EndIf} + ${EndIf} + + reinst_uninstall: + HideWindow + ClearErrors + + ${If} $WixMode = 1 + ReadRegStr $R1 HKLM "$R6" "UninstallString" + ExecWait '$R1' $0 + ${Else} + ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" + ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" + ${IfThen} $UpdateMode = 1 ${|} StrCpy $R1 "$R1 /UPDATE" ${|} ; append /UPDATE + ${IfThen} $PassiveMode = 1 ${|} StrCpy $R1 "$R1 /P" ${|} ; append /P + StrCpy $R1 "$R1 _?=$4" ; append uninstall directory + ExecWait '$R1' $0 + ${EndIf} + + BringToFront + + ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code + + ${If} $0 <> 0 + ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" + ; User cancelled wix uninstaller? return to select un/reinstall page + ${If} $WixMode = 1 + ${AndIf} $0 = 1602 + Abort + ${EndIf} + + ; User cancelled NSIS uninstaller? return to select un/reinstall page + ${If} $0 = 1 + Abort + ${EndIf} + + ; Other erros? show generic error message and return to select un/reinstall page + MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" + Abort + ${EndIf} + reinst_done: +FunctionEnd + +; 5. Choose install directory page +; Retirée : l'app s'installe dans le profil utilisateur, personne n'a +; besoin de choisir le dossier. +!define MUI_PAGE_CUSTOMFUNCTION_PRE Skip +!insertmacro MUI_PAGE_DIRECTORY + +; 6. Start menu shortcut page +Var AppStartMenuFolder +!if "${STARTMENUFOLDER}" != "" + !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive + !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${STARTMENUFOLDER}" +!else + !define MUI_PAGE_CUSTOMFUNCTION_PRE Skip +!endif +!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder + +; 7. Installation page — seule page visible, en sombre +!define MUI_PAGE_CUSTOMFUNCTION_SHOW DarkenInstFiles +!insertmacro MUI_PAGE_INSTFILES + +; 8. Finish page +; +; Don't auto jump to finish page after installation page, +; because the installation page has useful info that can be used debug any issues with the installer. +; Use show readme button in the finish page as a button create a desktop shortcut +!define MUI_FINISHPAGE_SHOWREADME +!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)" +!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateOrUpdateDesktopShortcut +; Show run app after installation. +!define MUI_FINISHPAGE_RUN +!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary +; Page de fin retirée : le raccourci bureau est créé automatiquement et +; l'application démarre seule (voir la section Install et .onInstSuccess). +!define MUI_PAGE_CUSTOMFUNCTION_PRE Skip +!insertmacro MUI_PAGE_FINISH + +Function RunMainBinary + nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "" +FunctionEnd + +; Uninstaller Pages +; 1. Confirm uninstall page +Var DeleteAppDataCheckbox +Var DeleteAppDataCheckboxState +!define /ifndef WS_EX_LAYOUTRTL 0x00400000 +!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow +Function un.ConfirmShow ; Add add a `Delete app data` check box + ; $1 inner dialog HWND + ; $2 window DPI + ; $3 style + ; $4 x + ; $5 y + ; $6 width + ; $7 height + FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog + System::Call "user32::GetDpiForWindow(p r1) i .r2" + ${If} $(^RTL) = 1 + StrCpy $3 "${__NSD_CheckBox_EXSTYLE} | ${WS_EX_LAYOUTRTL}" + IntOp $4 50 * $2 + ${Else} + StrCpy $3 "${__NSD_CheckBox_EXSTYLE}" + IntOp $4 0 * $2 + ${EndIf} + IntOp $5 100 * $2 + IntOp $6 400 * $2 + IntOp $7 25 * $2 + IntOp $4 $4 / 96 + IntOp $5 $5 / 96 + IntOp $6 $6 / 96 + IntOp $7 $7 / 96 + System::Call 'user32::CreateWindowEx(i r3, w "${__NSD_CheckBox_CLASS}", w "$(deleteAppData)", i ${__NSD_CheckBox_STYLE}, i r4, i r5, i r6, i r7, p r1, i0, i0, i0) i .s' + Pop $DeleteAppDataCheckbox + SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 + SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 +FunctionEnd +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave +Function un.ConfirmLeave + SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState +FunctionEnd +!define MUI_PAGE_CUSTOMFUNCTION_PRE un.SkipIfPassive +!insertmacro MUI_UNPAGE_CONFIRM + +; 2. Uninstalling Page +!insertmacro MUI_UNPAGE_INSTFILES + +;Languages +{{#each languages}} +!insertmacro MUI_LANGUAGE "{{this}}" +{{/each}} +!insertmacro MUI_RESERVEFILE_LANGDLL +{{#each language_files}} + !include "{{this}}" +{{/each}} + +Function .onInit + ${GetOptions} $CMDLINE "/P" $PassiveMode + ${IfNot} ${Errors} + StrCpy $PassiveMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/NS" $NoShortcutMode + ${IfNot} ${Errors} + StrCpy $NoShortcutMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} + + !if "${DISPLAYLANGUAGESELECTOR}" == "true" + !insertmacro MUI_LANGDLL_DISPLAY + !endif + + !insertmacro SetContext + + ${If} $INSTDIR == "${PLACEHOLDER_INSTALL_DIR}" + ; Set default install location + !if "${INSTALLMODE}" == "perMachine" + ${If} ${RunningX64} + !if "${ARCH}" == "x64" + StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" + !else if "${ARCH}" == "arm64" + StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" + !else + StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" + !endif + ${Else} + StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" + ${EndIf} + !else if "${INSTALLMODE}" == "currentUser" + StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}" + !endif + + Call RestorePreviousInstallLocation + ${EndIf} + + + !if "${INSTALLMODE}" == "both" + !insertmacro MULTIUSER_INIT + !endif +FunctionEnd + + +Section EarlyChecks + ; Abort silent installer if downgrades is disabled + !if "${ALLOWDOWNGRADES}" == "false" + ${If} ${Silent} + ; If downgrading + ${If} $R0 = -1 + System::Call 'kernel32::AttachConsole(i -1)i.r0' + ${If} $0 <> 0 + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color + FileWrite $0 "$(silentDowngrades)" + ${EndIf} + Abort + ${EndIf} + ${EndIf} + !endif + +SectionEnd + +Section WebView2 + ; Check if Webview2 is already installed and skip this section + ${If} ${RunningX64} + ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" + ${Else} + ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" + ${EndIf} + ${If} $4 == "" + ReadRegStr $4 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" + ${EndIf} + + ${If} $4 == "" + ; Webview2 installation + ; + ; Skip if updating + ${If} $UpdateMode <> 1 + !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + DetailPrint "$(webview2Downloading)" + NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Pop $0 + ${If} $0 == "success" + DetailPrint "$(webview2DownloadSuccess)" + ${Else} + DetailPrint "$(webview2DownloadError)" + Abort "$(webview2AbortError)" + ${EndIf} + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif + + !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif + + !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" + Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + Goto install_webview2 + !endif + + Goto webview2_done + + install_webview2: + DetailPrint "$(installingWebview2)" + ; $6 holds the path to the webview2 installer + ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 + ${If} $1 = 0 + DetailPrint "$(webview2InstallSuccess)" + ${Else} + DetailPrint "$(webview2InstallError)" + Abort "$(webview2AbortError)" + ${EndIf} + webview2_done: + ${EndIf} + ${Else} + !if "${MINIMUMWEBVIEW2VERSION}" != "" + ${VersionCompare} "${MINIMUMWEBVIEW2VERSION}" "$4" $R0 + ${If} $R0 = 1 + update_webview: + DetailPrint "$(installingWebview2)" + ${If} ${RunningX64} + ReadRegStr $R1 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" "path" + ${Else} + ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\EdgeUpdate" "path" + ${EndIf} + ${If} $R1 == "" + ReadRegStr $R1 HKCU "SOFTWARE\Microsoft\EdgeUpdate" "path" + ${EndIf} + ${If} $R1 != "" + ; Chromium updater docs: https://source.chromium.org/chromium/chromium/src/+/main:docs/updater/user_manual.md + ; Modified from "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView\ModifyPath" + ExecWait `"$R1" /install appguid=${WEBVIEW2APPGUID}&needsadmin=true` $1 + ${If} $1 = 0 + DetailPrint "$(webview2InstallSuccess)" + ${Else} + MessageBox MB_ICONEXCLAMATION|MB_ABORTRETRYIGNORE "$(webview2InstallError)" IDIGNORE ignore IDRETRY update_webview + Quit + ignore: + ${EndIf} + ${EndIf} + ${EndIf} + !endif + ${EndIf} +SectionEnd + +Section Install + SetOutPath $INSTDIR + + !ifmacrodef NSIS_HOOK_PREINSTALL + !insertmacro NSIS_HOOK_PREINSTALL + !endif + + !insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}" + + ; Copy main executable + File "${MAINBINARYSRCPATH}" + + ; Copy resources + {{#each resources_dirs}} + CreateDirectory "$INSTDIR\\{{this}}" + {{/each}} + {{#each resources}} + File /a "/oname={{this.[1]}}" "{{no-escape @key}}" + {{/each}} + + ; Copy external binaries + {{#each binaries}} + File /a "/oname={{this}}" "{{no-escape @key}}" + {{/each}} + + ; Create file associations + {{#each file_associations as |association| ~}} + {{#each association.ext as |ext| ~}} + !insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$INSTDIR\${MAINBINARYNAME}.exe,0" "Open with ${PRODUCTNAME}" "$INSTDIR\${MAINBINARYNAME}.exe $\"%1$\"" + {{/each}} + {{/each}} + + ; Register deep links + {{#each deep_link_protocols as |protocol| ~}} + WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "URL Protocol" "" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "" "URL:${BUNDLEID} protocol" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" + {{/each}} + + ; Create uninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; Save $INSTDIR in registry for future installations + WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR + + !if "${INSTALLMODE}" == "both" + ; Save install mode to be selected by default for the next installation such as updating + ; or when uninstalling + WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 + !endif + + ; Remove old main binary if it doesn't match new main binary name + ReadRegStr $OldMainBinaryName SHCTX "${UNINSTKEY}" "MainBinaryName" + ${If} $OldMainBinaryName != "" + ${AndIf} $OldMainBinaryName != "${MAINBINARYNAME}.exe" + Delete "$INSTDIR\$OldMainBinaryName" + ${EndIf} + + ; Save current MAINBINARYNAME for future updates + WriteRegStr SHCTX "${UNINSTKEY}" "MainBinaryName" "${MAINBINARYNAME}.exe" + + ; Registry information for add/remove programs + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" + WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" + WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" + WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" + WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" + + ${GetSize} "$INSTDIR" "/M=uninstall.exe /S=0K /G=0" $0 $1 $2 + IntOp $0 $0 + ${ESTIMATEDSIZE} + IntFmt $0 "0x%08X" $0 + WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$0" + + !if "${HOMEPAGE}" != "" + WriteRegStr SHCTX "${UNINSTKEY}" "URLInfoAbout" "${HOMEPAGE}" + WriteRegStr SHCTX "${UNINSTKEY}" "URLUpdateInfo" "${HOMEPAGE}" + WriteRegStr SHCTX "${UNINSTKEY}" "HelpLink" "${HOMEPAGE}" + !endif + + ; Create start menu shortcut + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + Call CreateOrUpdateStartMenuShortcut + !insertmacro MUI_STARTMENU_WRITE_END + + ; Raccourci bureau : toujours créé, puisque la page de fin (qui portait + ; la case à cocher) n'existe plus. + Call CreateOrUpdateDesktopShortcut + + !ifmacrodef NSIS_HOOK_POSTINSTALL + !insertmacro NSIS_HOOK_POSTINSTALL + !endif + + ; Fermeture automatique : plus de page de fin à afficher. + SetAutoClose true +SectionEnd + +Function .onInstSuccess + ; Installation silencieuse (mise à jour automatique) : on relance l'app + ; uniquement si le drapeau /R est passé — c'est l'updater qui décide. + ${If} $PassiveMode = 1 + ${OrIf} ${Silent} + ${GetOptions} $CMDLINE "/R" $R0 + ${IfNot} ${Errors} + ${GetOptions} $CMDLINE "/ARGS" $R0 + nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0" + ${EndIf} + ${Else} + ; Première installation manuelle : la page de fin ayant disparu, on + ; démarre l'application directement. + nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "" + ${EndIf} +FunctionEnd + +Function un.onInit + !insertmacro SetContext + + !if "${INSTALLMODE}" == "both" + !insertmacro MULTIUSER_UNINIT + !endif + + !insertmacro MUI_UNGETLANGUAGE + + ${GetOptions} $CMDLINE "/P" $PassiveMode + ${IfNot} ${Errors} + StrCpy $PassiveMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} +FunctionEnd + +Section Uninstall + + !ifmacrodef NSIS_HOOK_PREUNINSTALL + !insertmacro NSIS_HOOK_PREUNINSTALL + !endif + + !insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}" + + ; Delete the app directory and its content from disk + ; Copy main executable + Delete "$INSTDIR\${MAINBINARYNAME}.exe" + + ; Delete resources + {{#each resources}} + Delete "$INSTDIR\\{{this.[1]}}" + {{/each}} + + ; Delete external binaries + {{#each binaries}} + Delete "$INSTDIR\\{{this}}" + {{/each}} + + ; Delete app associations + {{#each file_associations as |association| ~}} + {{#each association.ext as |ext| ~}} + !insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}" + {{/each}} + {{/each}} + + ; Delete deep links + {{#each deep_link_protocols as |protocol| ~}} + ReadRegStr $R7 SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" + ${If} $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" + DeleteRegKey SHCTX "Software\Classes\\{{protocol}}" + ${EndIf} + {{/each}} + + + ; Delete uninstaller + Delete "$INSTDIR\uninstall.exe" + + {{#each resources_ancestors}} + RMDir /REBOOTOK "$INSTDIR\\{{this}}" + {{/each}} + RMDir "$INSTDIR" + + ; Remove shortcuts if not updating + ${If} $UpdateMode <> 1 + !insertmacro DeleteAppUserModelId + + ; Remove start menu shortcut + !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder + !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + RMDir "$SMPROGRAMS\$AppStartMenuFolder" + ${EndIf} + !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" + Delete "$SMPROGRAMS\${PRODUCTNAME}.lnk" + ${EndIf} + + ; Remove desktop shortcuts + !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" + Delete "$DESKTOP\${PRODUCTNAME}.lnk" + ${EndIf} + ${EndIf} + + ; Remove registry information for add/remove programs + !if "${INSTALLMODE}" == "both" + DeleteRegKey SHCTX "${UNINSTKEY}" + !else if "${INSTALLMODE}" == "perMachine" + DeleteRegKey HKLM "${UNINSTKEY}" + !else + DeleteRegKey HKCU "${UNINSTKEY}" + !endif + + ; Removes the Autostart entry for ${PRODUCTNAME} from the HKCU Run key if it exists. + ; This ensures the program does not launch automatically after uninstallation if it exists. + ; If it doesn't exist, it does nothing. + ; We do this when not updating (to preserve the registry value on updates) + ${If} $UpdateMode <> 1 + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" + ${EndIf} + + ; Delete app data if the checkbox is selected + ; and if not updating + ${If} $DeleteAppDataCheckboxState = 1 + ${AndIf} $UpdateMode <> 1 + ; Clear the install location $INSTDIR from registry + DeleteRegKey SHCTX "${MANUPRODUCTKEY}" + DeleteRegKey /ifempty SHCTX "${MANUKEY}" + + ; Clear the install language from registry + DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" + DeleteRegKey /ifempty HKCU "${MANUPRODUCTKEY}" + DeleteRegKey /ifempty HKCU "${MANUKEY}" + + SetShellVarContext current + RmDir /r "$APPDATA\${BUNDLEID}" + RmDir /r "$LOCALAPPDATA\${BUNDLEID}" + ${EndIf} + + !ifmacrodef NSIS_HOOK_POSTUNINSTALL + !insertmacro NSIS_HOOK_POSTUNINSTALL + !endif + + ; Auto close if passive mode or updating + ${If} $PassiveMode = 1 + ${OrIf} $UpdateMode = 1 + SetAutoClose true + ${EndIf} +SectionEnd + +Function RestorePreviousInstallLocation + ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" + StrCmp $4 "" +2 0 + StrCpy $INSTDIR $4 +FunctionEnd + +Function Skip + Abort +FunctionEnd + +Function SkipIfPassive + ${IfThen} $PassiveMode = 1 ${|} Abort ${|} +FunctionEnd +Function un.SkipIfPassive + ${IfThen} $PassiveMode = 1 ${|} Abort ${|} +FunctionEnd + +Function CreateOrUpdateStartMenuShortcut + ; We used to use product name as MAINBINARYNAME + ; migrate old shortcuts to target the new MAINBINARYNAME + StrCpy $R0 0 + + !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + StrCpy $R0 1 + ${EndIf} + + !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + StrCpy $R0 1 + ${EndIf} + + ${If} $R0 = 1 + Return + ${EndIf} + + ; Skip creating shortcut if in update mode or no shortcut mode + ; but always create if migrating from wix + ${If} $WixMode = 0 + ${If} $UpdateMode = 1 + ${OrIf} $NoShortcutMode = 1 + Return + ${EndIf} + ${EndIf} + + !if "${STARTMENUFOLDER}" != "" + CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" + CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + !else + CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk" + !endif +FunctionEnd + +Function CreateOrUpdateDesktopShortcut + ; We used to use product name as MAINBINARYNAME + ; migrate old shortcuts to target the new MAINBINARYNAME + !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Return + ${EndIf} + + ; Skip creating shortcut if in update mode or no shortcut mode + ; but always create if migrating from wix + ${If} $WixMode = 0 + ${If} $UpdateMode = 1 + ${OrIf} $NoShortcutMode = 1 + Return + ${EndIf} + ${EndIf} + + CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk" +FunctionEnd diff --git a/src-tauri/installer/sidebar.bmp b/src-tauri/installer/sidebar.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a221d91fc598eb7b664f31dcd3d129d34e32a7c0 GIT binary patch literal 154542 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 literal 0 HcmV?d00001 diff --git a/src-tauri/src/installer.rs b/src-tauri/src/installer.rs new file mode 100644 index 0000000..6f06431 --- /dev/null +++ b/src-tauri/src/installer.rs @@ -0,0 +1,310 @@ +//! Installeur intégré à l'application (« self-installing portable »). +//! +//! Le `.exe` distribué EST l'application. Lancé depuis un dossier autre que +//! le dossier d'installation, il affiche son propre écran d'installation — +//! écrit en React, avec le design system de l'app — qui se recopie dans +//! `%LOCALAPPDATA%\GameDev Tracker`, crée les raccourcis, s'enregistre +//! auprès de Windows, puis se relance depuis l'emplacement installé. +//! +//! Pas de fenêtre NSIS pour la première installation. +//! +//! # Cohabitation avec les mises à jour automatiques +//! +//! Les mises à jour, elles, continuent de passer par l'installeur NSIS +//! silencieux (invisible pour l'utilisateur). Pour qu'il écrase la même +//! installation au lieu d'en créer une seconde, on reproduit ICI très +//! exactement ce que le template NSIS de Tauri écrit et relit : +//! +//! * dossier par défaut : `%LOCALAPPDATA%\{PRODUCT_NAME}` +//! (cf. `StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"`) +//! * clé de localisation : `HKCU\Software\{PUBLISHER}\{PRODUCT_NAME}`, valeur +//! par défaut (cf. `RestorePreviousInstallLocation`) +//! * clé de désinstallation : `HKCU\…\Uninstall\{PRODUCT_NAME}` +//! +//! Toute modification de ces trois constantes doit rester alignée sur +//! `src-tauri/installer/installer.nsi`, sinon une mise à jour installerait +//! une deuxième copie à côté de la première. + +#![cfg(windows)] + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde::Serialize; + +/// Doit correspondre à `productName` de tauri.conf.json (= `${PRODUCTNAME}`). +pub const PRODUCT_NAME: &str = "GameDev Tracker"; +/// Doit correspondre à `bundle.publisher` (= `${MANUFACTURER}`). +pub const PUBLISHER: &str = "Mathew"; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Nom du binaire une fois installé (= `${MAINBINARYNAME}.exe`). +const BINARY_NAME: &str = "app.exe"; + +fn uninstall_key() -> String { + format!( + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + PRODUCT_NAME + ) +} + +/// `Software\{PUBLISHER}\{PRODUCT_NAME}` — c'est là que NSIS va relire le +/// dossier d'installation existant. +fn manu_product_key() -> String { + format!("Software\\{}\\{}", PUBLISHER, PRODUCT_NAME) +} + +/// `%LOCALAPPDATA%\GameDev Tracker` — le même défaut que NSIS en mode +/// « currentUser ». +pub fn default_install_dir() -> Option { + dirs::data_local_dir().map(|d| d.join(PRODUCT_NAME)) +} + +fn installed_exe_in(dir: &Path) -> PathBuf { + dir.join(BINARY_NAME) +} + +fn canon(p: &Path) -> PathBuf { + std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()) +} + +/// Dossier d'installation enregistré, s'il existe. +pub fn registered_install_dir() -> Option { + use winreg::enums::HKEY_CURRENT_USER; + use winreg::RegKey; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + // 1) La clé que NSIS relit en priorité. + if let Ok(key) = hkcu.open_subkey(manu_product_key()) { + if let Ok(dir) = key.get_value::("") { + if !dir.is_empty() { + return Some(PathBuf::from(dir)); + } + } + } + // 2) Repli : l'entrée « Applications installées » de Windows. + // NSIS y écrit le chemin ENTRE GUILLEMETS — sans ce nettoyage, on + // obtiendrait un dossier littéralement nommé `"C:\...\GameDev Tracker"`. + let key = hkcu.open_subkey(uninstall_key()).ok()?; + let dir: String = key.get_value("InstallLocation").ok()?; + let dir = dir.trim().trim_matches('"').to_string(); + (!dir.is_empty()).then(|| PathBuf::from(dir)) +} + +/// Le binaire en cours d'exécution est-il celui qui est installé ? +/// +/// C'est ce test qui décide, au démarrage, entre « afficher l'installeur » +/// et « démarrer l'application ». +pub fn is_running_installed() -> bool { + let Ok(exe) = std::env::current_exe() else { + return false; + }; + let Some(dir) = exe.parent() else { + return false; + }; + let dir = canon(dir); + + if let Some(registered) = registered_install_dir() { + if canon(®istered) == dir { + return true; + } + } + // Même sans registre : si on tourne depuis le dossier d'installation + // par défaut, c'est qu'on est installé (cas d'une mise à jour NSIS qui + // aurait réécrit les clés autrement). + default_install_dir().map(|d| canon(&d) == dir).unwrap_or(false) +} + +/// Recopie le binaire courant dans le dossier d'installation. +fn copy_exe(dst_dir: &Path) -> Result { + let src = std::env::current_exe().map_err(|e| format!("Binaire courant introuvable : {e}"))?; + let dst = installed_exe_in(dst_dir); + + std::fs::create_dir_all(dst_dir) + .map_err(|e| format!("Création de « {} » impossible : {e}", dst_dir.display()))?; + + // Réinstallation par-dessus : on écrase. + if dst.exists() { + std::fs::remove_file(&dst).ok(); + } + std::fs::copy(&src, &dst).map_err(|e| format!("Copie impossible : {e}"))?; + Ok(dst) +} + +/// Écrit un raccourci `.lnk` via WScript.Shell — évite d'embarquer une +/// bibliothèque COM entière pour cette seule opération. +fn write_lnk(lnk: &Path, target: &Path) -> Result<(), String> { + let script = format!( + "$s=(New-Object -ComObject WScript.Shell).CreateShortcut('{}');\ + $s.TargetPath='{}';$s.WorkingDirectory='{}';$s.Description='{}';$s.Save()", + lnk.display(), + target.display(), + target.parent().map(|p| p.display().to_string()).unwrap_or_default(), + PRODUCT_NAME + ); + + let mut cmd = Command::new("powershell"); + cmd.args(["-NoProfile", "-NonInteractive", "-Command", &script]); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW + } + let out = cmd.output().map_err(|e| format!("PowerShell indisponible : {e}"))?; + if !out.status.success() { + return Err(format!( + "Raccourci « {} » non créé : {}", + lnk.display(), + String::from_utf8_lossy(&out.stderr).trim() + )); + } + Ok(()) +} + +fn create_start_menu_shortcut(target: &Path) -> Result<(), String> { + let dir = dirs::data_dir() + .ok_or("APPDATA introuvable")? + .join("Microsoft/Windows/Start Menu/Programs"); + std::fs::create_dir_all(&dir).ok(); + write_lnk(&dir.join(format!("{PRODUCT_NAME}.lnk")), target) +} + +fn create_desktop_shortcut(target: &Path) -> Result<(), String> { + let dir = dirs::desktop_dir().ok_or("Bureau introuvable")?; + write_lnk(&dir.join(format!("{PRODUCT_NAME}.lnk")), target) +} + +fn dir_size_kb(dir: &Path) -> u32 { + fn walk(p: &Path) -> u64 { + let mut total = 0; + if let Ok(entries) = std::fs::read_dir(p) { + for e in entries.flatten() { + match e.metadata() { + Ok(m) if m.is_dir() => total += walk(&e.path()), + Ok(m) => total += m.len(), + Err(_) => {} + } + } + } + total + } + ((walk(dir) / 1024) as u32).max(1) +} + +/// Enregistre l'application auprès de Windows : elle apparaît dans +/// « Paramètres → Applications », avec un bouton de désinstallation. +fn write_registry(exe: &Path, install_dir: &Path) -> Result<(), String> { + use winreg::enums::HKEY_CURRENT_USER; + use winreg::RegKey; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let exe_s = exe.display().to_string(); + let dir_s = install_dir.display().to_string(); + + // 1) Localisation — c'est CETTE clé que l'updater NSIS relit pour + // réinstaller au même endroit. + let (loc, _) = hkcu + .create_subkey(manu_product_key()) + .map_err(|e| format!("Registre (localisation) : {e}"))?; + loc.set_value("", &dir_s).map_err(|e| format!("Registre : {e}"))?; + + // 2) Entrée « Applications installées ». + let (key, _) = hkcu + .create_subkey(uninstall_key()) + .map_err(|e| format!("Registre (désinstallation) : {e}"))?; + + let set = |name: &str, value: &str| key.set_value(name, &value.to_string()); + set("DisplayName", PRODUCT_NAME).ok(); + set("DisplayVersion", VERSION).ok(); + set("Publisher", PUBLISHER).ok(); + set("InstallLocation", &dir_s).ok(); + set("DisplayIcon", &exe_s).ok(); + set("UninstallString", &format!("\"{exe_s}\" --uninstall")).ok(); + set("QuietUninstallString", &format!("\"{exe_s}\" --uninstall --silent")).ok(); + // MainBinaryName : lu par l'updater NSIS pour nettoyer l'ancien binaire. + set("MainBinaryName", BINARY_NAME).ok(); + key.set_value("NoModify", &1u32).ok(); + key.set_value("NoRepair", &1u32).ok(); + key.set_value("EstimatedSize", &dir_size_kb(install_dir)).ok(); + + Ok(()) +} + +// ===================================================================== +// Commandes exposées au front +// ===================================================================== + +#[derive(Serialize)] +pub struct InstallerStatus { + /// `true` = on tourne depuis le dossier installé → démarrer l'app. + pub running_installed: bool, + /// Une installation existe déjà (réinstallation / mise à jour manuelle). + pub already_installed: bool, + pub install_dir: String, + pub version: String, + pub product_name: String, +} + +#[tauri::command] +pub fn installer_status() -> InstallerStatus { + let dir = registered_install_dir() + .or_else(default_install_dir) + .unwrap_or_default(); + InstallerStatus { + running_installed: is_running_installed(), + already_installed: registered_install_dir() + .map(|d| installed_exe_in(&d).exists()) + .unwrap_or(false), + install_dir: dir.display().to_string(), + version: VERSION.to_string(), + product_name: PRODUCT_NAME.to_string(), + } +} + +/// Installe : copie, raccourcis, registre. Renvoie le dossier d'installation. +#[tauri::command] +pub fn installer_install(desktop_shortcut: bool) -> Result { + let dir = default_install_dir().ok_or("LOCALAPPDATA introuvable")?; + let exe = copy_exe(&dir)?; + + // Les raccourcis ne sont pas critiques : on n'annule pas une + // installation réussie parce qu'un .lnk n'a pas pu être écrit. + if let Err(e) = create_start_menu_shortcut(&exe) { + log::warn!("{e}"); + } + if desktop_shortcut { + if let Err(e) = create_desktop_shortcut(&exe) { + log::warn!("{e}"); + } + } + + write_registry(&exe, &dir)?; + Ok(dir.display().to_string()) +} + +/// Démarre la version installée puis quitte le processus courant. +#[tauri::command] +pub fn installer_launch(app: tauri::AppHandle) -> Result<(), String> { + let dir = registered_install_dir() + .or_else(default_install_dir) + .ok_or("Aucune installation enregistrée")?; + let target = installed_exe_in(&dir); + + let mut cmd = Command::new(&target); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x0000_0008); // DETACHED_PROCESS + } + cmd.spawn() + .map_err(|e| format!("Démarrage de « {} » impossible : {e}", target.display()))?; + + // On laisse au nouveau processus le temps de prendre la main : sans ce + // délai, la fenêtre disparaît avant que la suivante n'apparaisse. + let handle = app.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(200)); + handle.exit(0); + }); + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9c3118c..2886185 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,46 @@ +use tauri::{WebviewUrl, WebviewWindowBuilder}; + +#[cfg(windows)] +mod installer; + +/// L'application tourne-t-elle depuis son dossier d'installation ? +/// (Sur les autres plateformes il n'y a pas d'installeur intégré.) +fn running_installed() -> bool { + #[cfg(windows)] + { + installer::is_running_installed() + } + #[cfg(not(windows))] + { + true + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + let mut builder = tauri::Builder::default(); + + // Mises à jour automatiques : l'application va chercher elle-même la + // dernière version publiée. Plus besoin de renvoyer un .exe à l'équipe. + #[cfg(desktop)] + { + builder = builder + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()); + } + + // Installeur intégré : le front affiche l'écran d'installation ou + // l'application selon `installer_status()`. + #[cfg(windows)] + { + builder = builder.invoke_handler(tauri::generate_handler![ + installer::installer_status, + installer::installer_install, + installer::installer_launch + ]); + } + + builder .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( @@ -9,6 +49,42 @@ pub fn run() { .build(), )?; } + + // La fenêtre est créée ICI, directement à sa forme définitive. + // + // Elle n'est volontairement PAS déclarée dans tauri.conf.json : la + // déclarer là-bas imposait de la corriger ensuite — la masquer, la + // redimensionner, la maximiser, puis l'afficher. Chacune de ces + // étapes était visible, d'où l'impression que « plusieurs petites + // fenêtres » s'ouvraient avant l'application. + // + // Créée d'un coup aux bonnes dimensions, il n'y a plus rien à + // rattraper après coup : une seule apparition, la bonne. + let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + // Le glisser-déposer doit rester géré par le webview (Kanban, + // dépôt de pièces jointes) et non capté par Tauri. + .disable_drag_drop_handler(); + + if running_installed() { + window + .title("GameDev Tracker") + .inner_size(1360.0, 860.0) + .min_inner_size(900.0, 620.0) + .resizable(true) + .maximized(true) + .build()?; + } else { + // Écran d'installation : petite fenêtre sans bordure, centrée. + window + .title("Installation de GameDev Tracker") + .inner_size(520.0, 560.0) + .resizable(false) + .maximizable(false) + .decorations(false) + .center() + .build()?; + } + Ok(()) }) .run(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 35d3abe..a449dfb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "GameDev Tracker", - "version": "0.1.0", + "version": "0.2.7", "identifier": "com.gamedevtracker.app", "build": { "frontendDist": "../dist", @@ -10,33 +10,52 @@ "beforeBuildCommand": "npm run build" }, "app": { - "windows": [ - { - "title": "GameDev Tracker", - "width": 1280, - "height": 820, - "minWidth": 960, - "minHeight": 600, - "resizable": true, - "fullscreen": false, - "center": true, - "backgroundColor": "#0f1117", - "dragDropEnabled": false - } - ], + "windows": [], "security": { - "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://*.supabase.co wss://*.supabase.co; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; script-src 'self'" + "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://*.supabase.co wss://*.supabase.co; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; media-src 'self' blob: https:; font-src 'self' data:; script-src 'self'" } }, "bundle": { "active": true, - "targets": "all", + "targets": [ + "nsis" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "createUpdaterArtifacts": true, + "publisher": "Mathew", + "copyright": "© 2026 Mathew", + "shortDescription": "Gestion de projet de jeu vidéo", + "longDescription": "Tâches, bugs, feuille de temps et discussions pour une petite équipe de développement de jeu vidéo.", + "windows": { + "nsis": { + "installMode": "currentUser", + "languages": [ + "French" + ], + "displayLanguageSelector": false, + "headerImage": "installer/header.bmp", + "sidebarImage": "installer/sidebar.bmp", + "installerIcon": "icons/icon.ico", + "template": "installer/installer.nsi" + } + } + }, + "plugins": { + "updater": { + "active": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEY3OUVGOUREMUExQjA4OTUKUldTVkNCc2EzZm1lOTlMMVVCUHRVMnFtRTArN3cyQkhsQ3RPSmlEdjlKdGR3WWRYTnpmcjlYU3QK", + "endpoints": [ + "https://cesjrlfqeqftayhnqjkb.supabase.co/storage/v1/object/public/releases/latest.json" + ], + "windows": { + "installMode": "quiet" + } + } } } diff --git a/src/App.tsx b/src/App.tsx index f25c1cf..03c01d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,69 +1,198 @@ -import { useState } from 'react' -import { Loader2 } from 'lucide-react' -import { AnimatePresence } from 'framer-motion' -import { AppProvider, useApp } from './lib/AppContext' -import { PageFade } from './components/motion' -import Setup from './pages/Setup' -import Login from './pages/Login' -import Onboarding from './pages/Onboarding' -import Layout, { Page } from './components/Layout' -import Dashboard from './pages/Dashboard' -import Kanban from './pages/Kanban' -import Planning from './pages/Planning' -import Bugs from './pages/Bugs' -import Members from './pages/Members' -import Admin from './pages/Admin' - -const PAGES: Record JSX.Element> = { - dashboard: Dashboard, - kanban: Kanban, - planning: Planning, - bugs: Bugs, - members: Members, - admin: Admin -} - -function Shell(): JSX.Element { - const { stage, profile } = useApp() - const [page, setPage] = useState('dashboard') - - if (stage === 'loading') { - return ( -
- -
- ) - } - if (stage === 'setup') return - if (stage === 'login') return - - // Session ouverte mais profil pas encore chargé. - if (!profile) { - return ( -
- -
- ) - } - // Première connexion : accueil (nom, avatar, pôles) avant d'entrer dans l'app. - if (!profile.onboarded) return - - const ActivePage = PAGES[page] - return ( - - - - - - - - ) -} - -export default function App(): JSX.Element { - return ( - - - - ) -} +import { Component, ErrorInfo, ReactNode, Suspense, lazy, useEffect, useState } from 'react' +import { AnimatePresence } from 'framer-motion' +import { AlertTriangle, Database, RefreshCw } from 'lucide-react' +import { AppProvider, useApp } from './lib/AppContext' +import { Page } from './lib/types' +import { PageFade } from './components/motion' +import { Button, CenteredSpinner } from './components/ui' +import { InstallerStatus, resolveStartupMode } from './lib/installer' +import Login from './pages/Login' +import Onboarding from './pages/Onboarding' +import Layout from './components/Layout' + +// Chargement paresseux : le bundle initial ne contient que le shell +// (authentification + mise en page). Chaque écran arrive à la demande. +const Dashboard = lazy(() => import('./pages/Dashboard')) +const Kanban = lazy(() => import('./pages/Kanban')) +const Planning = lazy(() => import('./pages/Planning')) +const Bugs = lazy(() => import('./pages/Bugs')) +const Docs = lazy(() => import('./pages/Docs')) +const Members = lazy(() => import('./pages/Members')) +const Admin = lazy(() => import('./pages/Admin')) +const Installer = lazy(() => import('./pages/Installer')) + +const PAGES: Record JSX.Element>> = { + dashboard: Dashboard, + kanban: Kanban, + planning: Planning, + bugs: Bugs, + docs: Docs, + members: Members, + admin: Admin +} + +// --------------------------------------------------------------------- +// Filet de sécurité : une erreur de rendu ne doit pas laisser un écran +// blanc sans explication ni moyen de repartir. +// --------------------------------------------------------------------- + +class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> { + state = { error: null as Error | null } + + static getDerivedStateFromError(error: Error): { error: Error } { + return { error } + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error('Erreur de rendu :', error, info.componentStack) + } + + render(): ReactNode { + if (!this.state.error) return this.props.children + return ( +
+
+ +

Quelque chose s'est mal passé

+

{this.state.error.message}

+ +
+
+ ) + } +} + +// --------------------------------------------------------------------- + +/** Clés Supabase absentes du build : on explique quoi faire. */ +function Unconfigured(): JSX.Element { + return ( +
+
+
+
+ +
+

Base de données non configurée

+
+

+ L'application a été construite sans clés Supabase. Crée un fichier .env à la + racine du projet avec : +

+
+          {'VITE_SUPABASE_URL=https://xxxxx.supabase.co\nVITE_SUPABASE_ANON_KEY=eyJhbGciOi...'}
+        
+

+ Ces deux valeurs se trouvent dans Supabase → Settings → API. Reconstruis + ensuite l'application (npm run tauri:dev). +

+
+
+ ) +} + +function ProfileMissing({ message }: { message: string }): JSX.Element { + const { signOut } = useApp() + return ( +
+
+ +

Profil inaccessible

+

{message}

+
+ + +
+
+
+ ) +} + +// --------------------------------------------------------------------- + +function Shell(): JSX.Element { + const { stage, page, profile, profileError } = useApp() + + if (stage === 'unconfigured') return + if (stage === 'loading') return + if (stage === 'login') return + + // Session ouverte, profil pas encore là. + if (!profile) { + if (profileError) return + return + } + + // Première connexion : accueil (nom, photo, pôles) avant d'entrer dans l'app. + if (!profile.onboarded) return + + const ActivePage = PAGES[page] + + return ( + + + + }> + + + + + + ) +} + +/** + * Le .exe distribué est à la fois l'application ET son installeur. + * Lancé hors de son dossier d'installation, il affiche l'écran + * d'installation ; sinon, l'application normale. + * + * La forme de la fenêtre (taille, bordure) est décidée côté Rust au + * moment de sa création : ici on ne fait que choisir quoi afficher dedans. + */ +export default function App(): JSX.Element { + const [boot, setBoot] = useState<{ + mode: 'checking' | 'installer' | 'app' + status: InstallerStatus | null + }>({ mode: 'checking', status: null }) + + useEffect(() => { + let alive = true + ;(async () => { + const { mode, status } = await resolveStartupMode() + if (alive) setBoot({ mode, status }) + })() + return () => { + alive = false + } + }, []) + + if (boot.mode === 'checking') return
+ + if (boot.mode === 'installer') { + return ( + + }> + + + + ) + } + + return ( + + + + + + ) +} diff --git a/src/components/BugModal.tsx b/src/components/BugModal.tsx new file mode 100644 index 0000000..ee5a3be --- /dev/null +++ b/src/components/BugModal.tsx @@ -0,0 +1,306 @@ +import { useMemo, useState } from 'react' +import { format } from 'date-fns' +import { fr } from 'date-fns/locale' +import { supa } from '../lib/supabase' +import { humanize, mutate, run } from '../lib/db' +import { useApp } from '../lib/AppContext' +import { useFeedback } from '../lib/feedback' +import { notifyBug } from '../lib/notify' +import { + Bug, + BugSeverity, + BugStatus, + BUG_STATUS_LABELS, + BUG_STATUS_ORDER, + Pole, + SEVERITY_HINTS, + SEVERITY_LABELS, + SEVERITY_ORDER, + severityBadge +} from '../lib/types' +import { Badge, Button, Field, Input } from './ui' +import { Modal, ModalActions } from './Modal' +import { Select } from './Select' +import { AssigneePicker, PoleChips } from './people' +import { AttachmentPanel, CommentThread, TimeSection } from './EntityExtras' + +interface Draft { + title: string + description: string + severity: BugSeverity + status: BugStatus + poles: Pole[] + assignees: string[] +} + +function draftOf(bug: Bug | null): Draft { + return { + title: bug?.title ?? '', + description: bug?.description ?? '', + severity: bug?.severity ?? 'medium', + status: bug?.status ?? 'open', + poles: bug?.poles ?? [], + assignees: bug?.assignee_ids ?? [] + } +} + +export default function BugModal({ + bug, + projectId, + onClose, + onSaved +}: { + bug: Bug | null + projectId: string + onClose: () => void + onSaved: () => Promise | void +}): JSX.Element { + const { profile, members, membersLoading, memberById } = useApp() + const { toast, confirm } = useFeedback() + + const initial = useMemo(() => draftOf(bug), [bug]) + const [d, setD] = useState(initial) + const [busy, setBusy] = useState(false) + + const patch = (p: Partial): void => setD((cur) => ({ ...cur, ...p })) + const dirty = useMemo(() => JSON.stringify(d) !== JSON.stringify(initial), [d, initial]) + + const save = async (): Promise => { + if (!d.title.trim()) { + toast('Donne un titre au bug.', 'error') + return + } + setBusy(true) + const payload = { + project_id: projectId, + title: d.title.trim(), + description: d.description.trim() || null, + severity: d.severity, + status: d.status, + poles: d.poles, + assignee_ids: d.assignees + } + + try { + let entityId = bug?.id ?? null + if (bug) { + await mutate(supa().from('bugs').update(payload).eq('id', bug.id)) + } else { + const created = await run<{ id: string }>( + supa() + .from('bugs') + .insert({ ...payload, reporter_id: profile?.id ?? null }) + .select('id') + .single() + ) + entityId = created?.id ?? null + } + + if (entityId) { + const newPoles = d.poles.filter((p) => !(bug?.poles ?? []).includes(p)) + const newAssignees = d.assignees.filter((a) => !(bug?.assignee_ids ?? []).includes(a)) + if (newPoles.length || newAssignees.length) { + await notifyBug({ + projectId, + bugId: entityId, + title: d.title.trim(), + members, + poles: newPoles, + assigneeIds: newAssignees, + actorId: profile?.id ?? null, + isNew: !bug + }) + } + } + + await onSaved() + toast(bug ? 'Bug enregistré.' : 'Bug signalé.', 'success') + onClose() + } catch (e) { + toast(humanize(e), 'error') + } finally { + setBusy(false) + } + } + + const remove = async (): Promise => { + if (!bug) return + const ok = await confirm({ + title: 'Supprimer ce bug ?', + message: `« ${bug.title} » et tout ce qui y est rattaché (temps, discussion, captures) seront supprimés.`, + danger: true, + confirmLabel: 'Supprimer' + }) + if (!ok) return + setBusy(true) + try { + await mutate(supa().from('bugs').delete().eq('id', bug.id)) + await onSaved() + toast('Bug supprimé.', 'success') + onClose() + } catch (e) { + toast(humanize(e), 'error') + setBusy(false) + } + } + + /** Passe le bug en résolu en un clic : le geste le plus fréquent. */ + const resolve = async (): Promise => { + if (!bug) return + setBusy(true) + try { + await mutate(supa().from('bugs').update({ status: 'resolved' }).eq('id', bug.id)) + await onSaved() + toast('Bug marqué comme résolu.', 'success') + onClose() + } catch (e) { + toast(humanize(e), 'error') + setBusy(false) + } + } + + const reporter = memberById(bug?.reporter_id) + const subtitle = bug ? ( + + Signalé {reporter ? `par ${reporter.full_name} ` : ''} + le {format(new Date(bug.created_at), 'd MMM yyyy', { locale: fr })} + + ) : null + + return ( + + {bug && bug.status !== 'resolved' && bug.status !== 'closed' && ( + + )} + + + } + > +
+ + {({ id }) => ( + patch({ title: e.target.value })} + /> + )} + + + + {({ id }) => ( +