diff --git a/.gitignore b/.gitignore
index ce4b082..f8a3f24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,11 +73,6 @@ DerivedData/
*.dSYM.zip
*.dSYM
-# Generated files
-**/*.g.dart
-**/*.freezed.dart
-**/*.gr.dart
-
# Coverage
coverage/
@@ -93,3 +88,4 @@ coverage/
.ideai/background-tasks/
.ideai/live-state.json
.ideai/layouts.json
+.ideai/permissions.json
diff --git a/.ideai/memory/MEMORY.md b/.ideai/memory/MEMORY.md
index 90750aa..7553aea 100644
--- a/.ideai/memory/MEMORY.md
+++ b/.ideai/memory/MEMORY.md
@@ -3,3 +3,4 @@
- [gametime-product-scope](gametime-product-scope.md) — memory note gametime-product-scope
- [gametime-ux-conception](gametime-ux-conception.md) — memory note gametime-ux-conception
- [gametime-architecture-initial-stack-data-model](gametime-architecture-initial-stack-data-model.md) — memory note gametime-architecture-initial-stack-data-model
+- [gametime-dev-environment](gametime-dev-environment.md) — memory note gametime-dev-environment
diff --git a/.ideai/memory/gametime-dev-environment.md b/.ideai/memory/gametime-dev-environment.md
new file mode 100644
index 0000000..69d2fae
--- /dev/null
+++ b/.ideai/memory/gametime-dev-environment.md
@@ -0,0 +1,46 @@
+---
+name: gametime-dev-environment
+description: memory note gametime-dev-environment
+metadata:
+ type: project
+---
+# GameTime — Environnement de build local
+
+Flutter SDK et Android SDK sont installés et opérationnels sur la machine du projet (2026-07-17). Premier APK debug buildé avec succès le 2026-07-17.
+
+## Flutter
+- Installé via le paquet AUR `flutter-bin` (3.44.6, stable), pas `flutter` (source AUR) — ce dernier a un conflit de dépendance avec `dart` déjà présent sur le système (`dart<3.12.0` requis alors que 3.12.2 est installé).
+- Binaire `flutter` disponible dans `/usr/bin/flutter` (wrapper `flutter-bin`), SDK réel monté via unionfs sous `~/.cache/flutter_sdk`.
+
+## Android SDK
+- Installé via le paquet AUR `android-sdk-cmdline-tools-latest`, posé dans `/opt/android-sdk` (appartenait à root par défaut — il a fallu `chown -R anthony:anthony /opt/android-sdk` pour que `sdkmanager` puisse installer des composants sans sudo).
+- Composants installés : `platform-tools`, `platforms;android-34/35/36`, `build-tools;34.0.0/28.0.3/36.0.0`, NDK 28.2.13676358, CMake 3.22.1 (certains installés automatiquement par Gradle au premier build).
+- Licences acceptées via `sdkmanager --licenses`.
+- `flutter config --android-sdk /opt/android-sdk` exécuté pour lier Flutter au SDK.
+
+## JDK — point de friction important
+- Le JDK système par défaut est `java-26-openjdk` (trop récent : `Unsupported class file major version 70` avec Gradle 9.1 utilisé par le template Flutter).
+- Installé `jdk21-openjdk` (dépôt officiel Arch, pas besoin d'AUR) en complément, sans le mettre par défaut système.
+- Flutter configuré spécifiquement pour l'utiliser : `flutter config --jdk-dir=/usr/lib/jvm/java-21-openjdk`. Cette config est stockée dans la config Flutter (probablement `~/.config/flutter/settings` ou équivalent), donc persistante indépendamment du JDK système par défaut.
+
+## Variables d'environnement persistées
+Ajoutées dans `~/.zshrc`, `~/.bashrc` et `~/.config/fish/config.fish` (le shell de login est zsh, mais les agents peuvent tourner en bash) :
+```
+ANDROID_HOME=/opt/android-sdk
+ANDROID_SDK_ROOT=/opt/android-sdk
+PATH inclut $ANDROID_HOME/cmdline-tools/latest/bin et $ANDROID_HOME/platform-tools
+```
+
+## Limite connue
+- Pas de Chrome installé (web toolchain Flutter indisponible), sans impact puisque la cible du projet est mobile (Android/iOS), pas web.
+- Pas de device/émulateur Android connecté — seul `flutter build apk --debug` (sans device) est utilisable pour produire l'APK à transférer manuellement sur le téléphone de l'utilisateur (pas de `flutter run` direct sur device depuis cet environnement).
+- **Les sandbox des agents (DevBackend, DevFrontend, etc.) n'ont pas d'accès réseau à pub.dev**, contrairement à l'environnement de Main (Bash direct). Conséquence pratique : `flutter pub get`, `flutter analyze` (si dépend de packages non encore en cache) et `flutter build apk` doivent être exécutés par Main en vérification finale après le travail de code d'un agent, pas par l'agent lui-même. Les agents peuvent en revanche modifier pubspec.yaml, écrire du code Dart, etc.
+
+## Commande de build APK de référence
+```bash
+export ANDROID_HOME=/opt/android-sdk
+export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
+cd /home/anthony/Documents/Projects/GameTime
+flutter pub get && flutter analyze && flutter build apk --debug
+```
+APK produit : `build/app/outputs/flutter-apk/app-debug.apk`.
\ No newline at end of file
diff --git a/.ideai/tickets/1/carnet.md b/.ideai/tickets/1/carnet.md
index 2fb4ecd..80a5199 100644
--- a/.ideai/tickets/1/carnet.md
+++ b/.ideai/tickets/1/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#1"
-version: 3
+version: 4
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301472470
+updatedAt: 1784301585782
---
Décisions à prendre par Git : nom de la branche principale de dev (ex: develop) si on ne travaille pas directement sur main, convention de nommage des branches de feature par ticket (ex: feature/#2-scaffolding-flutter). Aucune action sortante (push distant, remote) sans validation explicite de l'utilisateur — repo local pour l'instant.
\ No newline at end of file
diff --git a/.ideai/tickets/1/issue.md b/.ideai/tickets/1/issue.md
index d0cdafb..004cd6e 100644
--- a/.ideai/tickets/1/issue.md
+++ b/.ideai/tickets/1/issue.md
@@ -2,7 +2,7 @@
id: "e6f0a540-055c-4b9d-9d14-3ceabf35c7d6"
number: 1
title: "[Git] Initialiser le dépôt et la stratégie de branches GameTime"
-status: "inProgress"
+status: "closed"
priority: "high"
sprint: null
links: []
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"8f065f64-ef6e-4a00-af9c-d00be079e3cc","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301284213
-updatedAt: 1784301472470
-version: 3
+updatedAt: 1784301585782
+version: 4
---
Mettre en place la structure de dépôt pour le projet Flutter GameTime : stratégie de branches (main protégée + branches de feature par ticket), conventions de commit, .gitignore adapté Flutter/Dart.
\ No newline at end of file
diff --git a/.ideai/tickets/10/carnet.md b/.ideai/tickets/10/carnet.md
index 026e172..4ad9f0f 100644
--- a/.ideai/tickets/10/carnet.md
+++ b/.ideai/tickets/10/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#10"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301394213
+updatedAt: 1784308662161
---
Référence : mémoire "gametime-ux-conception" section 5. Relance : utiliser la séance-modèle source si elle existe encore ; sinon relancer depuis le snapshot historique avec un message explicite ("La séance originale n'existe plus. Une copie va être utilisée."). L'historique doit rester lisible même si exercices/programmes/séances-modèles sources ont été supprimés depuis (snapshot autonome, cf. ticket #3/#4).
\ No newline at end of file
diff --git a/.ideai/tickets/10/issue.md b/.ideai/tickets/10/issue.md
index 27247d5..a6d4525 100644
--- a/.ideai/tickets/10/issue.md
+++ b/.ideai/tickets/10/issue.md
@@ -2,7 +2,7 @@
id: "4c2a9393-bf0f-4f41-a243-9ce0b4f0c4e1"
number: 10
title: "[DevFrontend] Écran Historique des séances"
-status: "open"
+status: "closed"
priority: "medium"
sprint: null
links: [{"target":"#4","kind":"dependsOn"},{"target":"#9","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301309677
-updatedAt: 1784301394213
-version: 4
+updatedAt: 1784308662161
+version: 6
---
Liste groupée par période (Aujourd'hui/Cette semaine/Plus ancien) avec résumé par séance. Détail par programme puis exercice puis série (temps/répétitions/score si suivis). Relance : utilise la séance-modèle associée si elle existe encore, sinon relance depuis le snapshot historique avec message explicite. Suppression d'historique (action destructive confirmée). Cf. mémoire "gametime-ux-conception" section 5.
\ No newline at end of file
diff --git a/.ideai/tickets/11/carnet.md b/.ideai/tickets/11/carnet.md
index c775d04..e59bb52 100644
--- a/.ideai/tickets/11/carnet.md
+++ b/.ideai/tickets/11/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#11"
-version: 7
+version: 9
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301397898
+updatedAt: 1784309299325
---
Points à couvrir en priorité : (1) impossibilité de modifier les mesures activées ou l'ordre des exercices depuis une séance-modèle, seuls setsCount et cibles numériques doivent être modifiables ; (2) reprise de séance après kill complet de l'app (pas juste mise en arrière-plan) ; (3) recalcul correct des timers de repos ajustés (+/-15s) après reprise ; (4) intégrité de l'historique quand l'exercice, le programme ou la séance-modèle source ont été supprimés/archivés entre-temps ; (5) règle "au moins une mesure active" sur un exercice. Rapport d'échec réel obligatoire (commande, sortie brute, diagnostic) — pas d'enjolivement si KO, cf. règle du cycle dans le contexte Main.
\ No newline at end of file
diff --git a/.ideai/tickets/11/issue.md b/.ideai/tickets/11/issue.md
index f7a235d..5fa4174 100644
--- a/.ideai/tickets/11/issue.md
+++ b/.ideai/tickets/11/issue.md
@@ -2,7 +2,7 @@
id: "9592e96c-2c9f-439c-87f0-42b9147c1e00"
number: 11
title: "[QA] Plan et exécution des tests fonctionnels GameTime"
-status: "open"
+status: "closed"
priority: "high"
sprint: null
links: [{"target":"#6","kind":"dependsOn"},{"target":"#7","kind":"dependsOn"},{"target":"#8","kind":"dependsOn"},{"target":"#9","kind":"dependsOn"},{"target":"#10","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301312946
-updatedAt: 1784301397898
-version: 7
+updatedAt: 1784309299325
+version: 9
---
Écrire et exécuter les tests couvrant : CRUD Exercice/Programme/Séance-modèle, règles d'overrides limités en séance, robustesse de la session active (reprise après fermeture/mise en arrière-plan de l'app, recalcul des timers depuis horodatages), génération correcte de l'historique (snapshot autonome), relance de séance (via séance-modèle et via snapshot si séance-modèle supprimée). Rapport d'échec réel (commande + sortie + diagnostic) sans enjoliver en cas de KO.
\ No newline at end of file
diff --git a/.ideai/tickets/2/carnet.md b/.ideai/tickets/2/carnet.md
index 5cf2314..dfc7fe2 100644
--- a/.ideai/tickets/2/carnet.md
+++ b/.ideai/tickets/2/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#2"
-version: 3
+version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301371028
+updatedAt: 1784304100048
---
Référence : mémoire projet "gametime-architecture-initial-stack-data-model" (Architect). Stack : Flutter + Drift (SQLite). Structure hexagonale obligatoire : domain / application / infrastructure(local) / presentation, composition root séparée. Pas de schéma de tables à ce stade (ticket #3), juste la connexion Drift vide et le wiring. Cibler iOS + Android dans le pubspec/config dès le départ même si le test utilisateur se fait sur Android.
\ No newline at end of file
diff --git a/.ideai/tickets/2/issue.md b/.ideai/tickets/2/issue.md
index d91513c..f38685f 100644
--- a/.ideai/tickets/2/issue.md
+++ b/.ideai/tickets/2/issue.md
@@ -2,7 +2,7 @@
id: "736ad58d-1020-438a-b563-5d0c30a05b04"
number: 2
title: "[DevBackend] Scaffolding projet Flutter + architecture hexagonale"
-status: "open"
+status: "closed"
priority: "critical"
sprint: null
links: [{"target":"#1","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301286030
-updatedAt: 1784301371028
-version: 3
+updatedAt: 1784304100048
+version: 5
---
Initialiser le projet Flutter (cibles iOS + Android). Mettre en place la structure de dossiers domain / application / infrastructure(local) / presentation. Config lint/format. Intégration Drift de base (connexion DB vide, pas encore de schéma). Le domaine ne doit dépendre ni de Flutter ni de Drift.
\ No newline at end of file
diff --git a/.ideai/tickets/3/carnet.md b/.ideai/tickets/3/carnet.md
index 4cf5e2e..1f396ad 100644
--- a/.ideai/tickets/3/carnet.md
+++ b/.ideai/tickets/3/carnet.md
@@ -1,8 +1,8 @@
---
issueRef: "#3"
-version: 3
+version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301376032
+updatedAt: 1784305026425
---
Référence : mémoire "gametime-architecture-initial-stack-data-model" pour le détail des entités (Exercise, MediaAsset, Program, ProgramExercise, WorkoutTemplate, WorkoutTemplateProgram, WorkoutTemplateExerciseOverride, ActiveWorkoutSession, ActiveSetResult, ActiveRestState, WorkoutHistory, WorkoutHistorySetResult, change_log).
Points de friction actés par Architect à respecter dans le schéma :
diff --git a/.ideai/tickets/3/issue.md b/.ideai/tickets/3/issue.md
index b2a50b7..e0e7d46 100644
--- a/.ideai/tickets/3/issue.md
+++ b/.ideai/tickets/3/issue.md
@@ -2,7 +2,7 @@
id: "ea99a2e0-3c07-404c-95db-13818be0ee20"
number: 3
title: "[DevBackend] Modèle de données Drift complet (entités + migrations)"
-status: "open"
+status: "closed"
priority: "critical"
sprint: null
links: [{"target":"#2","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301289668
-updatedAt: 1784301376032
-version: 3
+updatedAt: 1784305026425
+version: 5
---
Implémenter les tables Drift : Exercise, MediaAsset, Program, ProgramExercise, WorkoutTemplate, WorkoutTemplateProgram, WorkoutTemplateExerciseOverride, ActiveWorkoutSession, ActiveSetResult, ActiveRestState, WorkoutHistory, WorkoutHistorySetResult, change_log. Champs sync-ready sur chaque agrégat : id stable (UUIDv7/ULID), createdAt/updatedAt/deletedAt, schemaVersion, syncState, localRevision, originDeviceId. Voir mémoire projet "gametime-architecture-initial-stack-data-model" pour le détail des champs par entité et les invariants (score = valeur numérique + label/unité snapshotés, exercice jamais supprimé en dur, etc.).
\ No newline at end of file
diff --git a/.ideai/tickets/4/carnet.md b/.ideai/tickets/4/carnet.md
index 740ed49..e95f13b 100644
--- a/.ideai/tickets/4/carnet.md
+++ b/.ideai/tickets/4/carnet.md
@@ -1,8 +1,8 @@
---
issueRef: "#4"
-version: 3
+version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301378940
+updatedAt: 1784305595841
---
Règle produit validée avec l'utilisateur (mémoire "gametime-ux-conception" section 3) : une séance-modèle peut surcharger uniquement le nombre de séries et les valeurs cibles numériques des mesures déjà actives d'un exercice — jamais quelles mesures sont actives, jamais l'ordre/la liste des exercices. Ce garde-fou doit être appliqué au niveau des use cases, pas seulement de l'UI.
Pause/reprise de session : recalculer les temps écoulés depuis les horodatages stockés (startedAt, pausedAt, lastPersistedAt), jamais depuis un compteur en mémoire — nécessaire pour survivre à une fermeture d'app.
\ No newline at end of file
diff --git a/.ideai/tickets/4/issue.md b/.ideai/tickets/4/issue.md
index cc06008..a1c5fe3 100644
--- a/.ideai/tickets/4/issue.md
+++ b/.ideai/tickets/4/issue.md
@@ -2,7 +2,7 @@
id: "d3a8bc95-d1c7-4114-a964-7faa5b15835a"
number: 4
title: "[DevBackend] Couche application : use cases et repositories"
-status: "open"
+status: "closed"
priority: "high"
sprint: null
links: [{"target":"#3","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301293308
-updatedAt: 1784301378940
-version: 3
+updatedAt: 1784305595841
+version: 5
---
Implémenter ports/repositories et use cases pour : CRUD Exercise (règle "au moins une mesure active", avertissement si exercice déjà utilisé, archivage doux plutôt que suppression) ; CRUD Program (snapshot de l'exercice à l'ajout, mesures activées parmi celles autorisées, séries, cibles, repos rattaché à la série/l'exercice précédent) ; gestion WorkoutTemplate (composition de programmes en snapshot indépendant, overrides limités à setsCount et valeurs cibles numériques uniquement — pas de changement de mesures actives ni de structure) ; gestion ActiveWorkoutSession (démarrage, transitions série/repos, pause/reprise basées sur horodatages, pas sur un compteur mémoire) ; clôture de séance vers WorkoutHistory (snapshot autonome).
\ No newline at end of file
diff --git a/.ideai/tickets/5/carnet.md b/.ideai/tickets/5/carnet.md
index 184bf3f..0fc81d4 100644
--- a/.ideai/tickets/5/carnet.md
+++ b/.ideai/tickets/5/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#5"
-version: 3
+version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301380781
+updatedAt: 1784305875016
---
Fichiers médias hors base SQLite, seulement les métadonnées dans MediaAsset (localUri, mimeType, sizeBytes, durationMs pour vidéo, width/height, checksum). Prévoir remoteUri nullable pour la sync future sans l'implémenter.
\ No newline at end of file
diff --git a/.ideai/tickets/5/issue.md b/.ideai/tickets/5/issue.md
index a2e28a9..3c52173 100644
--- a/.ideai/tickets/5/issue.md
+++ b/.ideai/tickets/5/issue.md
@@ -2,7 +2,7 @@
id: "17cd97da-4b28-47c8-97e5-293e0173d640"
number: 5
title: "[DevBackend] Gestion des médias locaux"
-status: "open"
+status: "closed"
priority: "medium"
sprint: null
links: [{"target":"#3","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301295496
-updatedAt: 1784301380781
-version: 3
+updatedAt: 1784305875016
+version: 5
---
Import/association d'images et vidéos aux exercices (entité MediaAsset), stockage fichier dans le stockage applicatif local, référencement par métadonnées SQLite, nettoyage des fichiers orphelins. Prévoir les champs sync-ready (remoteUri nullable, checksum) sans implémenter la synchro.
\ No newline at end of file
diff --git a/.ideai/tickets/6/carnet.md b/.ideai/tickets/6/carnet.md
index 6e6eba0..67ed89f 100644
--- a/.ideai/tickets/6/carnet.md
+++ b/.ideai/tickets/6/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#6"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301382963
+updatedAt: 1784306547848
---
Référence : mémoire "gametime-ux-conception" section 1. Point critique UX : la section "Mesures disponibles" doit rester très lisible (toggles Temps/Répétitions/Score avec aide courte par toggle, champs label+unité seulement si Score activé). Règle bloquante : au moins une mesure doit être active pour enregistrer. Message non bloquant si on modifie les mesures d'un exercice déjà utilisé ailleurs (les programmes existants restent inchangés grâce aux snapshots).
\ No newline at end of file
diff --git a/.ideai/tickets/6/issue.md b/.ideai/tickets/6/issue.md
index aa3319c..9b7f9eb 100644
--- a/.ideai/tickets/6/issue.md
+++ b/.ideai/tickets/6/issue.md
@@ -2,7 +2,7 @@
id: "c748c16e-b782-47a8-accb-aa5d88b9adfb"
number: 6
title: "[DevFrontend] Écran Bibliothèque d'exercices"
-status: "open"
+status: "closed"
priority: "high"
sprint: null
links: [{"target":"#4","kind":"dependsOn"},{"target":"#5","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301298045
-updatedAt: 1784301382963
-version: 4
+updatedAt: 1784306547848
+version: 6
---
Liste avec recherche et filtres chips par mesure disponible. Écran création/édition d'exercice : nom, description, image/vidéo optionnelles, section "Mesures disponibles" (toggles Temps/Répétitions/Score cumulables, label+unité si Score). Règle : au moins une mesure obligatoire. Avertissement non bloquant si modification des mesures d'un exercice déjà utilisé. Cf. mémoire "gametime-ux-conception" section 1.
\ No newline at end of file
diff --git a/.ideai/tickets/7/carnet.md b/.ideai/tickets/7/carnet.md
index 203b3a4..da76cbd 100644
--- a/.ideai/tickets/7/carnet.md
+++ b/.ideai/tickets/7/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#7"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301385567
+updatedAt: 1784307099507
---
Référence : mémoire "gametime-ux-conception" section 2. Repos affiché comme "Repos après cette série", attaché à la série/l'exercice précédent pour suivre le réordonnancement (poignée de déplacement). Pas de repos après la toute dernière série du programme. Ligne résumé attendue sur chaque carte exercice : "X séries · [mesures actives] · Repos Ys".
\ No newline at end of file
diff --git a/.ideai/tickets/7/issue.md b/.ideai/tickets/7/issue.md
index 8e357ad..92f1360 100644
--- a/.ideai/tickets/7/issue.md
+++ b/.ideai/tickets/7/issue.md
@@ -2,7 +2,7 @@
id: "f955dede-b254-49da-95bb-f63adca76d05"
number: 7
title: "[DevFrontend] Écran Création/édition de programme"
-status: "open"
+status: "closed"
priority: "high"
sprint: null
links: [{"target":"#4","kind":"dependsOn"},{"target":"#6","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301300945
-updatedAt: 1784301385567
-version: 4
+updatedAt: 1784307099507
+version: 6
---
Ajout d'exercices depuis la bibliothèque (recherche, badges de mesures). Cartes réordonnables par exercice. Configuration par exercice : nombre de séries, mesures à suivre (sous-ensemble de celles autorisées par l'exercice), cible par mesure activée, repos après chaque série (hérite du défaut programme, surchargeable). Gestion du cas "exercice archivé/supprimé de la bibliothèque". Cf. mémoire "gametime-ux-conception" section 2.
\ No newline at end of file
diff --git a/.ideai/tickets/8/carnet.md b/.ideai/tickets/8/carnet.md
index f275b07..08e43dd 100644
--- a/.ideai/tickets/8/carnet.md
+++ b/.ideai/tickets/8/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#8"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301388397
+updatedAt: 1784307587164
---
Référence : mémoire "gametime-ux-conception" section 3 — décision produit explicitement validée par l'utilisateur. Au premier ajout d'un programme à la séance, afficher le message expliquant qu'une copie est enregistrée et que les futures modifications du programme source ne changeront pas cette séance. Les seuls champs éditables depuis cette surface sont : nombre de séries, et valeurs cibles numériques des mesures déjà actives (temps cible, répétitions cible, score cible). Ne pas permettre l'ajout/suppression/réordonnancement d'exercices ni le changement des mesures activées ici — ça reste au niveau du programme source (ticket #7).
\ No newline at end of file
diff --git a/.ideai/tickets/8/issue.md b/.ideai/tickets/8/issue.md
index b3e9873..7221698 100644
--- a/.ideai/tickets/8/issue.md
+++ b/.ideai/tickets/8/issue.md
@@ -2,7 +2,7 @@
id: "8f24db05-5d56-4324-8bef-68e4837e0e3b"
number: 8
title: "[DevFrontend] Écran Composition de séance-modèle"
-status: "open"
+status: "closed"
priority: "high"
sprint: null
links: [{"target":"#4","kind":"dependsOn"},{"target":"#7","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301303492
-updatedAt: 1784301388397
-version: 4
+updatedAt: 1784307587164
+version: 6
---
Assemblage nommé et réordonnable de un ou plusieurs programmes (copie/snapshot au moment de l'ajout, avec message explicite à l'utilisateur sur l'indépendance vis-à-vis du programme source). Overrides autorisés uniquement : nombre de séries et valeurs cibles numériques des mesures déjà actives par exercice — pas de changement de mesures actives, pas de réordonnancement/ajout/suppression d'exercice depuis la séance. Cf. mémoire "gametime-ux-conception" section 3 (décision produit tranchée avec l'utilisateur).
\ No newline at end of file
diff --git a/.ideai/tickets/9/carnet.md b/.ideai/tickets/9/carnet.md
index 0e687b7..75512e8 100644
--- a/.ideai/tickets/9/carnet.md
+++ b/.ideai/tickets/9/carnet.md
@@ -1,7 +1,7 @@
---
issueRef: "#9"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
-updatedAt: 1784301392430
+updatedAt: 1784308091100
---
Référence : mémoire "gametime-ux-conception" section 4. Écran le plus sensible du produit : grandes zones tactiles (44-48px mini), utilisable à l'effort, une main. Hiérarchie de saisie si mesures cumulées : Temps (bloc principal) > Répétitions (stepper) > Score (champ + unité, clavier adapté au type). Écran "Repos" séparé avec +15s/-15s et "Ignorer le repos". Pause : "Quitter et sauvegarder" comme action sûre recommandée, "Abandonner" comme action destructive confirmée séparément. Reprise doit fonctionner même après fermeture complète de l'app (bandeau "Séance en cours" au retour). Fin de séance écrit dans WorkoutHistory (ticket #4) avec temps total + scores par série.
\ No newline at end of file
diff --git a/.ideai/tickets/9/issue.md b/.ideai/tickets/9/issue.md
index 1881872..1a24e01 100644
--- a/.ideai/tickets/9/issue.md
+++ b/.ideai/tickets/9/issue.md
@@ -2,7 +2,7 @@
id: "311cfb2b-0a09-4c18-ba66-4053994bc109"
number: 9
title: "[DevFrontend] Écran Exécution de séance"
-status: "open"
+status: "closed"
priority: "critical"
sprint: null
links: [{"target":"#4","kind":"dependsOn"},{"target":"#8","kind":"dependsOn"}]
@@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784301307130
-updatedAt: 1784301392430
-version: 4
+updatedAt: 1784308091100
+version: 6
---
Surface critique optimisée pour l'usage à l'effort (grandes zones tactiles, une main). En-tête temps écoulé + pause. Progression Programme/Exercice/Série. Saisie hiérarchisée quand mesures cumulées (Temps > Répétitions > Score avec unité). Écran "Repos" dédié entre séries avec ajustement ±15s et "Ignorer le repos". Pause avec "Quitter et sauvegarder" vs "Abandonner" (destructif confirmé). Reprise après interruption/fermeture app, robuste à l'arrière-plan (recalcul depuis horodatages, pas depuis un état mémoire). Écran de fin de séance (récap + relance) → écriture dans l'historique. Cf. mémoire "gametime-ux-conception" section 4.
\ No newline at end of file
diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json
index fcba8e2..4395dba 100644
--- a/.ideai/tickets/index.json
+++ b/.ideai/tickets/index.json
@@ -5,133 +5,133 @@
"issueRef": "#1",
"path": "1",
"title": "[Git] Initialiser le dépôt et la stratégie de branches GameTime",
- "status": "inProgress",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"8f065f64-ef6e-4a00-af9c-d00be079e3cc"
],
- "updatedAt": 1784301472470
+ "updatedAt": 1784301585782
},
{
"issueRef": "#2",
"path": "2",
"title": "[DevBackend] Scaffolding projet Flutter + architecture hexagonale",
- "status": "open",
+ "status": "closed",
"priority": "critical",
"sprint": null,
"assignedAgentIds": [
"10ee045b-1c41-479e-ba03-dceed9edd495"
],
- "updatedAt": 1784301371028
+ "updatedAt": 1784304100048
},
{
"issueRef": "#3",
"path": "3",
"title": "[DevBackend] Modèle de données Drift complet (entités + migrations)",
- "status": "open",
+ "status": "closed",
"priority": "critical",
"sprint": null,
"assignedAgentIds": [
"10ee045b-1c41-479e-ba03-dceed9edd495"
],
- "updatedAt": 1784301376032
+ "updatedAt": 1784305026425
},
{
"issueRef": "#4",
"path": "4",
"title": "[DevBackend] Couche application : use cases et repositories",
- "status": "open",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"10ee045b-1c41-479e-ba03-dceed9edd495"
],
- "updatedAt": 1784301378940
+ "updatedAt": 1784305595841
},
{
"issueRef": "#5",
"path": "5",
"title": "[DevBackend] Gestion des médias locaux",
- "status": "open",
+ "status": "closed",
"priority": "medium",
"sprint": null,
"assignedAgentIds": [
"10ee045b-1c41-479e-ba03-dceed9edd495"
],
- "updatedAt": 1784301380781
+ "updatedAt": 1784305875016
},
{
"issueRef": "#6",
"path": "6",
"title": "[DevFrontend] Écran Bibliothèque d'exercices",
- "status": "open",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
- "updatedAt": 1784301382963
+ "updatedAt": 1784306547848
},
{
"issueRef": "#7",
"path": "7",
"title": "[DevFrontend] Écran Création/édition de programme",
- "status": "open",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
- "updatedAt": 1784301385567
+ "updatedAt": 1784307099507
},
{
"issueRef": "#8",
"path": "8",
"title": "[DevFrontend] Écran Composition de séance-modèle",
- "status": "open",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
- "updatedAt": 1784301388397
+ "updatedAt": 1784307587164
},
{
"issueRef": "#9",
"path": "9",
"title": "[DevFrontend] Écran Exécution de séance",
- "status": "open",
+ "status": "closed",
"priority": "critical",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
- "updatedAt": 1784301392430
+ "updatedAt": 1784308091100
},
{
"issueRef": "#10",
"path": "10",
"title": "[DevFrontend] Écran Historique des séances",
- "status": "open",
+ "status": "closed",
"priority": "medium",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
- "updatedAt": 1784301394213
+ "updatedAt": 1784308662161
},
{
"issueRef": "#11",
"path": "11",
"title": "[QA] Plan et exécution des tests fonctionnels GameTime",
- "status": "open",
+ "status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
"7efa512f-3b3a-47b5-ade0-a2dd13073055"
],
- "updatedAt": 1784301397898
+ "updatedAt": 1784309299325
},
{
"issueRef": "#12",
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..92bca7e
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "ee80f08bbf97172ec030b8751ceab557177a34a6"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+ base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+ - platform: android
+ create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+ base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+ - platform: ios
+ create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+ base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..620c7cb
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,9 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ exclude:
+ - '**/*.g.dart'
+
+linter:
+ rules:
+ prefer_single_quotes: true
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..9994ece
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ id("com.android.application")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.gametime.app"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ applicationId = "com.gametime.app"
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bc7d6d5
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt b/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt
new file mode 100644
index 0000000..e9ef322
--- /dev/null
+++ b/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.gametime.app
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..e96108c
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+# This newDsl flag was added by the Flutter template
+android.newDsl=false
+# This builtInKotlin flag was added by the Flutter template
+android.builtInKotlin=false
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2d428bf
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..c21f0c5
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "9.0.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.3.20" apply false
+}
+
+include(":app")
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..391a902
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..b0869ed
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,42 @@
+platform :ios, '12.0'
+
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. Run flutter pub get first."
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise 'FLUTTER_ROOT not found in Generated.xcconfig.'
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..08d509f
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,644 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gametime.app;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..c3fedb2
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..c30b367
--- /dev/null
+++ b/ios/Runner/AppDelegate.swift
@@ -0,0 +1,16 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..6ed2d93
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cd7b00
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..fe73094
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..502f463
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..e9f5fea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..0467bf1
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..b0c9c28
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,70 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Gametime
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ gametime
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift
new file mode 100644
index 0000000..b9ce8ea
--- /dev/null
+++ b/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,6 @@
+import Flutter
+import UIKit
+
+class SceneDelegate: FlutterSceneDelegate {
+
+}
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/lib/application/app_bootstrap.dart b/lib/application/app_bootstrap.dart
new file mode 100644
index 0000000..76a6116
--- /dev/null
+++ b/lib/application/app_bootstrap.dart
@@ -0,0 +1,90 @@
+import '../infrastructure/local/local.dart';
+import 'application.dart';
+
+final class AppBootstrap {
+ AppBootstrap._({
+ required this.database,
+ required this.exerciseUseCases,
+ required this.mediaUseCases,
+ required this.programUseCases,
+ required this.workoutTemplateUseCases,
+ required this.activeWorkoutSessionUseCases,
+ required this.closeWorkoutSessionUseCase,
+ required this.workoutHistoryUseCases,
+ });
+
+ final AppDatabase database;
+ final ExerciseUseCases exerciseUseCases;
+ final MediaUseCases mediaUseCases;
+ final ProgramUseCases programUseCases;
+ final WorkoutTemplateUseCases workoutTemplateUseCases;
+ final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
+ final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
+ final WorkoutHistoryUseCases workoutHistoryUseCases;
+
+ static Future create() async {
+ final database = AppDatabase.open();
+ await database.customSelect('SELECT 1').get();
+ final exerciseRepository = DriftExerciseRepository(database);
+ final mediaRepository = DriftMediaAssetRepository(database);
+ final programRepository = DriftProgramRepository(database);
+ final templateRepository = DriftWorkoutTemplateRepository(database);
+ final activeSessionRepository = DriftActiveSessionRepository(database);
+ final historyRepository = DriftWorkoutHistoryRepository(database);
+ final ids = LocalIdGenerator();
+ const clock = SystemClock();
+ const originDeviceId = 'local-device';
+
+ return AppBootstrap._(
+ database: database,
+ exerciseUseCases: ExerciseUseCases(
+ repository: exerciseRepository,
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ mediaUseCases: MediaUseCases(
+ mediaRepository: mediaRepository,
+ exerciseRepository: exerciseRepository,
+ storage: const PathProviderLocalMediaStorage(),
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ programUseCases: ProgramUseCases(
+ programRepository: programRepository,
+ exerciseRepository: exerciseRepository,
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ workoutTemplateUseCases: WorkoutTemplateUseCases(
+ templateRepository: templateRepository,
+ programRepository: programRepository,
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ activeWorkoutSessionUseCases: ActiveWorkoutSessionUseCases(
+ sessionRepository: activeSessionRepository,
+ templateRepository: templateRepository,
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ closeWorkoutSessionUseCase: CloseWorkoutSessionUseCase(
+ sessionRepository: activeSessionRepository,
+ historyRepository: historyRepository,
+ clock: clock,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ ),
+ workoutHistoryUseCases: WorkoutHistoryUseCases(
+ repository: historyRepository,
+ clock: clock,
+ ),
+ );
+ }
+
+ Future dispose() => database.close();
+}
diff --git a/lib/application/application.dart b/lib/application/application.dart
new file mode 100644
index 0000000..32e79f7
--- /dev/null
+++ b/lib/application/application.dart
@@ -0,0 +1,8 @@
+/// Application layer entry point.
+///
+/// This layer owns use cases and ports. It may depend on domain, but concrete
+/// adapters are wired only from the composition root.
+library;
+
+export 'ports.dart';
+export 'use_cases.dart';
diff --git a/lib/application/ports.dart b/lib/application/ports.dart
new file mode 100644
index 0000000..691a615
--- /dev/null
+++ b/lib/application/ports.dart
@@ -0,0 +1,93 @@
+import '../domain/domain.dart';
+
+abstract interface class Clock {
+ DateTime now();
+}
+
+final class SystemClock implements Clock {
+ const SystemClock();
+
+ @override
+ DateTime now() => DateTime.now().toUtc();
+}
+
+abstract interface class IdGenerator {
+ String newId();
+}
+
+abstract interface class ExerciseRepository {
+ Future findById(String id);
+ Future> listActive();
+ Future isReferencedByProgram(String id);
+ Future save(Exercise exercise);
+}
+
+final class StoredMediaFile {
+ const StoredMediaFile({
+ required this.localUri,
+ required this.mimeType,
+ required this.sizeBytes,
+ this.width,
+ this.height,
+ this.durationMs,
+ });
+
+ final String localUri;
+ final String? mimeType;
+ final int sizeBytes;
+ final int? width;
+ final int? height;
+ final int? durationMs;
+}
+
+abstract interface class LocalMediaStorage {
+ Future importFile({
+ required String sourcePath,
+ required MediaKind kind,
+ required String stableFileName,
+ });
+
+ Future> listManagedLocalUris();
+ Future deleteByLocalUri(String localUri);
+}
+
+abstract interface class MediaAssetRepository {
+ Future findById(String id);
+ Future> listActive();
+ Future save(MediaAsset mediaAsset);
+}
+
+abstract interface class ProgramRepository {
+ Future findById(String id);
+ Future> listActive();
+ Future save(Program program);
+ Future saveExercise(ProgramExercise exercise);
+ Future replaceExercises(Program program, DateTime deletedAt);
+}
+
+abstract interface class WorkoutTemplateRepository {
+ Future findById(String id);
+ Future> listActive();
+ Future save(WorkoutTemplate template);
+ Future saveProgram(WorkoutTemplateProgram program);
+ Future saveOverride(WorkoutTemplateExerciseOverride override);
+ Future replaceComposition(WorkoutTemplate template, DateTime deletedAt);
+}
+
+abstract interface class ActiveSessionRepository {
+ Future findById(String id);
+ Future findOpen();
+ Future save(ActiveWorkoutSession session);
+ Future saveSetResult(ActiveSetResult result);
+ Future saveRestState(ActiveRestState restState);
+ Future> listSetResults(String sessionId);
+ Future> listRestStates(String sessionId);
+}
+
+abstract interface class WorkoutHistoryRepository {
+ Future findById(String id);
+ Future> listActive();
+ Future save(WorkoutHistory history);
+ Future saveSetResult(WorkoutHistorySetResult result);
+ Future delete(String id, DateTime deletedAt);
+}
diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart
new file mode 100644
index 0000000..cd7cdb1
--- /dev/null
+++ b/lib/application/use_cases.dart
@@ -0,0 +1,1051 @@
+import 'dart:convert';
+
+import '../domain/domain.dart';
+import 'ports.dart';
+
+final class ExerciseUseCases {
+ const ExerciseUseCases({
+ required this.repository,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final ExerciseRepository repository;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future create({
+ required String name,
+ String? description,
+ String? imageMediaId,
+ String? videoMediaId,
+ required bool hasTimeMeasure,
+ required bool hasRepsMeasure,
+ required bool hasScoreMeasure,
+ String? scoreLabel,
+ String? scoreUnit,
+ }) async {
+ final now = clock.now();
+ final exercise = Exercise(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ name: name,
+ description: description,
+ imageMediaId: imageMediaId,
+ videoMediaId: videoMediaId,
+ hasTimeMeasure: hasTimeMeasure,
+ hasRepsMeasure: hasRepsMeasure,
+ hasScoreMeasure: hasScoreMeasure,
+ scoreLabel: scoreLabel,
+ scoreUnit: scoreUnit,
+ );
+ await repository.save(exercise);
+ return exercise;
+ }
+
+ Future save(Exercise exercise) => repository.save(exercise);
+
+ Future> listActive() => repository.listActive();
+
+ Future isUsedInPrograms(String id) {
+ return repository.isReferencedByProgram(id);
+ }
+
+ Future update({
+ required String id,
+ required String name,
+ String? description,
+ String? imageMediaId,
+ String? videoMediaId,
+ required bool hasTimeMeasure,
+ required bool hasRepsMeasure,
+ required bool hasScoreMeasure,
+ String? scoreLabel,
+ String? scoreUnit,
+ }) async {
+ final exercise = await repository.findById(id);
+ if (exercise == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final updated = exercise.copyWith(
+ metadata: exercise.metadata.touch(clock.now()),
+ name: name,
+ description: description,
+ imageMediaId: imageMediaId,
+ videoMediaId: videoMediaId,
+ hasTimeMeasure: hasTimeMeasure,
+ hasRepsMeasure: hasRepsMeasure,
+ hasScoreMeasure: hasScoreMeasure,
+ scoreLabel: scoreLabel,
+ scoreUnit: scoreUnit,
+ );
+ await repository.save(updated);
+ return updated;
+ }
+
+ Future attachImage({
+ required String exerciseId,
+ required String mediaAssetId,
+ }) async {
+ final exercise = await repository.findById(exerciseId);
+ if (exercise == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final updated = exercise.copyWith(
+ imageMediaId: mediaAssetId,
+ metadata: exercise.metadata.touch(clock.now()),
+ );
+ await repository.save(updated);
+ return updated;
+ }
+
+ Future attachVideo({
+ required String exerciseId,
+ required String mediaAssetId,
+ }) async {
+ final exercise = await repository.findById(exerciseId);
+ if (exercise == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final updated = exercise.copyWith(
+ videoMediaId: mediaAssetId,
+ metadata: exercise.metadata.touch(clock.now()),
+ );
+ await repository.save(updated);
+ return updated;
+ }
+
+ Future archive(String id) async {
+ final exercise = await repository.findById(id);
+ if (exercise == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final archived = exercise.archive(clock.now());
+ await repository.save(archived);
+ return archived;
+ }
+}
+
+final class MediaUseCases {
+ const MediaUseCases({
+ required this.mediaRepository,
+ required this.exerciseRepository,
+ required this.storage,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final MediaAssetRepository mediaRepository;
+ final ExerciseRepository exerciseRepository;
+ final LocalMediaStorage storage;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future importMedia({
+ required String sourcePath,
+ required MediaKind kind,
+ }) async {
+ final now = clock.now();
+ final id = ids.newId();
+ final stored = await storage.importFile(
+ sourcePath: sourcePath,
+ kind: kind,
+ stableFileName: id,
+ );
+ final asset = MediaAsset(
+ metadata: EntityMetadata(
+ id: id,
+ createdAt: now,
+ updatedAt: now,
+ originDeviceId: originDeviceId,
+ ),
+ kind: kind,
+ localUri: stored.localUri,
+ mimeType: stored.mimeType,
+ sizeBytes: stored.sizeBytes,
+ width: stored.width,
+ height: stored.height,
+ durationMs: stored.durationMs,
+ );
+ await mediaRepository.save(asset);
+ return asset;
+ }
+
+ Future attachToExercise({
+ required String exerciseId,
+ required String mediaAssetId,
+ }) async {
+ final exercise = await exerciseRepository.findById(exerciseId);
+ if (exercise == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final media = await mediaRepository.findById(mediaAssetId);
+ if (media == null) {
+ throw const DomainException('Media asset not found.');
+ }
+ final updated = switch (media.kind) {
+ MediaKind.image => exercise.copyWith(
+ imageMediaId: media.metadata.id,
+ metadata: exercise.metadata.touch(clock.now()),
+ ),
+ MediaKind.video => exercise.copyWith(
+ videoMediaId: media.metadata.id,
+ metadata: exercise.metadata.touch(clock.now()),
+ ),
+ };
+ await exerciseRepository.save(updated);
+ return updated;
+ }
+
+ Future deleteOrphanedManagedFiles() async {
+ final activeAssets = await mediaRepository.listActive();
+ final referencedUris = activeAssets.map((asset) => asset.localUri).toSet();
+ final managedUris = await storage.listManagedLocalUris();
+ final orphanedUris = managedUris.difference(referencedUris);
+ for (final uri in orphanedUris) {
+ await storage.deleteByLocalUri(uri);
+ }
+ return orphanedUris.length;
+ }
+}
+
+final class ProgramUseCases {
+ const ProgramUseCases({
+ required this.programRepository,
+ required this.exerciseRepository,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final ProgramRepository programRepository;
+ final ExerciseRepository exerciseRepository;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future findById(String id) => programRepository.findById(id);
+
+ Future> listActive() => programRepository.listActive();
+
+ Future create({
+ required String name,
+ required int defaultRestSeconds,
+ }) async {
+ final now = clock.now();
+ final program = Program(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ name: name,
+ defaultRestSeconds: defaultRestSeconds,
+ );
+ await programRepository.save(program);
+ return program;
+ }
+
+ Future saveConfigured({
+ String? id,
+ required String name,
+ required int defaultRestSeconds,
+ required List exercises,
+ }) async {
+ final now = clock.now();
+ final existing = id == null ? null : await programRepository.findById(id);
+ if (id != null && existing == null) {
+ throw const DomainException('Program not found.');
+ }
+ final programId = id ?? ids.newId();
+ final metadata =
+ existing?.metadata.touch(now) ??
+ EntityMetadata(
+ id: programId,
+ createdAt: now,
+ updatedAt: now,
+ originDeviceId: originDeviceId,
+ );
+ final programExercises = [];
+ for (var index = 0; index < exercises.length; index++) {
+ final input = exercises[index];
+ final exerciseMetadata =
+ input.existingMetadata?.touch(now) ??
+ _newMetadata(ids, originDeviceId, now);
+ programExercises.add(
+ ProgramExercise(
+ metadata: exerciseMetadata,
+ programId: programId,
+ sourceExerciseId: input.sourceExerciseId,
+ position: index,
+ exerciseNameSnapshot: input.exerciseNameSnapshot,
+ exerciseDescriptionSnapshot: input.exerciseDescriptionSnapshot,
+ exerciseImageMediaIdSnapshot: input.exerciseImageMediaIdSnapshot,
+ exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
+ exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
+ availableTimeSnapshot: input.availableTimeSnapshot,
+ availableRepsSnapshot: input.availableRepsSnapshot,
+ availableScoreSnapshot: input.availableScoreSnapshot,
+ scoreLabelSnapshot: input.scoreLabelSnapshot,
+ scoreUnitSnapshot: input.scoreUnitSnapshot,
+ setsCount: input.setsCount,
+ timeEnabled: input.enabledMeasures.contains(WorkoutMeasure.time),
+ repsEnabled: input.enabledMeasures.contains(WorkoutMeasure.reps),
+ scoreEnabled: input.enabledMeasures.contains(WorkoutMeasure.score),
+ targetTimeSeconds: input.targetTimeSeconds,
+ targetReps: input.targetReps,
+ targetScore: input.targetScore,
+ restSecondsOverride: input.restSecondsOverride,
+ ),
+ );
+ }
+ final program = Program(
+ metadata: metadata,
+ name: name,
+ defaultRestSeconds: defaultRestSeconds,
+ exercises: programExercises,
+ );
+ await programRepository.replaceExercises(program, now);
+ return program;
+ }
+
+ Future addExercise({
+ required String programId,
+ required String exerciseId,
+ required int position,
+ required int setsCount,
+ required Set enabledMeasures,
+ int? targetTimeSeconds,
+ int? targetReps,
+ double? targetScore,
+ int? restSecondsOverride,
+ }) async {
+ final source = await exerciseRepository.findById(exerciseId);
+ if (source == null) {
+ throw const DomainException('Exercise not found.');
+ }
+ final now = clock.now();
+ final programExercise = ProgramExercise.snapshotFromExercise(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ programId: programId,
+ exercise: source,
+ position: position,
+ setsCount: setsCount,
+ enabledMeasures: enabledMeasures,
+ targetTimeSeconds: targetTimeSeconds,
+ targetReps: targetReps,
+ targetScore: targetScore,
+ restSecondsOverride: restSecondsOverride,
+ );
+ await programRepository.saveExercise(programExercise);
+ return programExercise;
+ }
+}
+
+final class ProgramExerciseConfig {
+ const ProgramExerciseConfig({
+ this.existingMetadata,
+ this.sourceExerciseId,
+ required this.exerciseNameSnapshot,
+ this.exerciseDescriptionSnapshot,
+ this.exerciseImageMediaIdSnapshot,
+ this.exerciseVideoMediaIdSnapshot,
+ this.exerciseArchivedSnapshot = false,
+ required this.availableTimeSnapshot,
+ required this.availableRepsSnapshot,
+ required this.availableScoreSnapshot,
+ this.scoreLabelSnapshot,
+ this.scoreUnitSnapshot,
+ required this.setsCount,
+ required this.enabledMeasures,
+ this.targetTimeSeconds,
+ this.targetReps,
+ this.targetScore,
+ this.restSecondsOverride,
+ });
+
+ final EntityMetadata? existingMetadata;
+ final String? sourceExerciseId;
+ final String exerciseNameSnapshot;
+ final String? exerciseDescriptionSnapshot;
+ final String? exerciseImageMediaIdSnapshot;
+ final String? exerciseVideoMediaIdSnapshot;
+ final bool exerciseArchivedSnapshot;
+ final bool availableTimeSnapshot;
+ final bool availableRepsSnapshot;
+ final bool availableScoreSnapshot;
+ final String? scoreLabelSnapshot;
+ final String? scoreUnitSnapshot;
+ final int setsCount;
+ final Set enabledMeasures;
+ final int? targetTimeSeconds;
+ final int? targetReps;
+ final double? targetScore;
+ final int? restSecondsOverride;
+}
+
+final class WorkoutTemplateUseCases {
+ const WorkoutTemplateUseCases({
+ required this.templateRepository,
+ required this.programRepository,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final WorkoutTemplateRepository templateRepository;
+ final ProgramRepository programRepository;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future findById(String id) {
+ return templateRepository.findById(id);
+ }
+
+ Future> listActive() {
+ return templateRepository.listActive();
+ }
+
+ Future create({required String name}) async {
+ final now = clock.now();
+ final template = WorkoutTemplate(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ name: name,
+ );
+ await templateRepository.save(template);
+ return template;
+ }
+
+ Future saveConfigured({
+ String? id,
+ required String name,
+ required List programs,
+ required List overrides,
+ }) async {
+ final now = clock.now();
+ final existing = id == null ? null : await templateRepository.findById(id);
+ if (id != null && existing == null) {
+ throw const DomainException('Workout template not found.');
+ }
+ final templateId = id ?? ids.newId();
+ final metadata =
+ existing?.metadata.touch(now) ??
+ EntityMetadata(
+ id: templateId,
+ createdAt: now,
+ updatedAt: now,
+ originDeviceId: originDeviceId,
+ );
+ final templatePrograms = [];
+ final programIdsByClientKey = {};
+ for (var index = 0; index < programs.length; index++) {
+ final input = programs[index];
+ final programMetadata =
+ input.existingMetadata?.touch(now) ??
+ _newMetadata(ids, originDeviceId, now);
+ programIdsByClientKey[input.clientKey] = programMetadata.id;
+ templatePrograms.add(
+ WorkoutTemplateProgram(
+ metadata: programMetadata,
+ workoutTemplateId: templateId,
+ sourceProgramId: input.sourceProgramId,
+ position: index,
+ programNameSnapshot: input.programNameSnapshot,
+ defaultRestSecondsSnapshot: input.defaultRestSecondsSnapshot,
+ programSnapshotJson: input.programSnapshotJson,
+ ),
+ );
+ }
+ final templateOverrides = [];
+ for (final input in overrides) {
+ final programInput = programs.firstWhere(
+ (program) => program.clientKey == input.workoutTemplateProgramClientKey,
+ orElse: () =>
+ throw const DomainException('Workout template program not found.'),
+ );
+ final workoutTemplateProgramId =
+ programIdsByClientKey[input.workoutTemplateProgramClientKey];
+ if (workoutTemplateProgramId == null) {
+ throw const DomainException('Workout template program not found.');
+ }
+ _validateOverrideTargets(programInput.programSnapshotJson, input);
+ templateOverrides.add(
+ WorkoutTemplateExerciseOverride(
+ metadata:
+ input.existingMetadata?.touch(now) ??
+ _newMetadata(ids, originDeviceId, now),
+ workoutTemplateProgramId: workoutTemplateProgramId,
+ snapshotProgramExerciseId: input.snapshotProgramExerciseId,
+ setsCountOverride: input.setsCountOverride,
+ targetTimeSecondsOverride: input.targetTimeSecondsOverride,
+ targetRepsOverride: input.targetRepsOverride,
+ targetScoreOverride: input.targetScoreOverride,
+ ),
+ );
+ }
+ final template = WorkoutTemplate(
+ metadata: metadata,
+ name: name,
+ lastStartedAt: existing?.lastStartedAt,
+ programs: templatePrograms,
+ overrides: templateOverrides,
+ );
+ await templateRepository.replaceComposition(template, now);
+ return template;
+ }
+
+ Future addProgramSnapshot({
+ required String workoutTemplateId,
+ required String programId,
+ required int position,
+ }) async {
+ final program = await programRepository.findById(programId);
+ if (program == null) {
+ throw const DomainException('Program not found.');
+ }
+ final now = clock.now();
+ final snapshot = WorkoutTemplateProgram.snapshotFromProgram(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ workoutTemplateId: workoutTemplateId,
+ program: program,
+ position: position,
+ );
+ await templateRepository.saveProgram(snapshot);
+ return snapshot;
+ }
+
+ Future overrideExerciseTargets({
+ required String workoutTemplateProgramId,
+ required String snapshotProgramExerciseId,
+ int? setsCountOverride,
+ int? targetTimeSecondsOverride,
+ int? targetRepsOverride,
+ double? targetScoreOverride,
+ Set? attemptedMeasureOverride,
+ }) async {
+ if (attemptedMeasureOverride != null) {
+ throw const DomainException(
+ 'Workout templates cannot override active measures.',
+ );
+ }
+ final now = clock.now();
+ final override = WorkoutTemplateExerciseOverride(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ workoutTemplateProgramId: workoutTemplateProgramId,
+ snapshotProgramExerciseId: snapshotProgramExerciseId,
+ setsCountOverride: setsCountOverride,
+ targetTimeSecondsOverride: targetTimeSecondsOverride,
+ targetRepsOverride: targetRepsOverride,
+ targetScoreOverride: targetScoreOverride,
+ );
+ await templateRepository.saveOverride(override);
+ return override;
+ }
+}
+
+final class WorkoutTemplateProgramConfig {
+ const WorkoutTemplateProgramConfig({
+ required this.clientKey,
+ this.existingMetadata,
+ this.sourceProgramId,
+ required this.programNameSnapshot,
+ required this.defaultRestSecondsSnapshot,
+ required this.programSnapshotJson,
+ });
+
+ final String clientKey;
+ final EntityMetadata? existingMetadata;
+ final String? sourceProgramId;
+ final String programNameSnapshot;
+ final int defaultRestSecondsSnapshot;
+ final String programSnapshotJson;
+}
+
+final class WorkoutTemplateExerciseOverrideConfig {
+ const WorkoutTemplateExerciseOverrideConfig({
+ this.existingMetadata,
+ required this.workoutTemplateProgramClientKey,
+ required this.snapshotProgramExerciseId,
+ this.setsCountOverride,
+ this.targetTimeSecondsOverride,
+ this.targetRepsOverride,
+ this.targetScoreOverride,
+ });
+
+ final EntityMetadata? existingMetadata;
+ final String workoutTemplateProgramClientKey;
+ final String snapshotProgramExerciseId;
+ final int? setsCountOverride;
+ final int? targetTimeSecondsOverride;
+ final int? targetRepsOverride;
+ final double? targetScoreOverride;
+}
+
+final class ActiveWorkoutSessionUseCases {
+ const ActiveWorkoutSessionUseCases({
+ required this.sessionRepository,
+ required this.templateRepository,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final ActiveSessionRepository sessionRepository;
+ final WorkoutTemplateRepository templateRepository;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future findOpen() => sessionRepository.findOpen();
+
+ Future startFromTemplate(String templateId) async {
+ final template = await templateRepository.findById(templateId);
+ if (template == null) {
+ throw const DomainException('Workout template not found.');
+ }
+ final now = clock.now();
+ final session = ActiveWorkoutSession(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ sourceWorkoutTemplateId: templateId,
+ status: ActiveWorkoutStatus.running,
+ startedAt: now,
+ lastPersistedAt: now,
+ elapsedActiveMs: 0,
+ currentProgramIndex: 0,
+ currentExerciseIndex: 0,
+ currentSetIndex: 0,
+ resolvedTemplateSnapshotJson: jsonEncode({
+ 'templateId': template.metadata.id,
+ 'name': template.name,
+ 'programs': template.programs
+ .map(
+ (program) => {
+ 'id': program.metadata.id,
+ 'programNameSnapshot': program.programNameSnapshot,
+ 'programSnapshotJson': program.programSnapshotJson,
+ },
+ )
+ .toList(),
+ 'overrides': template.overrides
+ .map(
+ (override) => {
+ 'workoutTemplateProgramId': override.workoutTemplateProgramId,
+ 'snapshotProgramExerciseId': override.snapshotProgramExerciseId,
+ 'setsCountOverride': override.setsCountOverride,
+ 'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
+ 'targetRepsOverride': override.targetRepsOverride,
+ 'targetScoreOverride': override.targetScoreOverride,
+ },
+ )
+ .toList(),
+ }),
+ );
+ await sessionRepository.save(session);
+ return session;
+ }
+
+ Future startFromHistory(WorkoutHistory history) async {
+ final snapshot =
+ jsonDecode(history.historySnapshotJson) as Map;
+ final resolvedTemplateSnapshotJson =
+ snapshot['resolvedTemplateSnapshotJson'] as String;
+ final now = clock.now();
+ final session = ActiveWorkoutSession(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ sourceWorkoutTemplateId: null,
+ status: ActiveWorkoutStatus.running,
+ startedAt: now,
+ lastPersistedAt: now,
+ elapsedActiveMs: 0,
+ currentProgramIndex: 0,
+ currentExerciseIndex: 0,
+ currentSetIndex: 0,
+ resolvedTemplateSnapshotJson: resolvedTemplateSnapshotJson,
+ );
+ await sessionRepository.save(session);
+ return session;
+ }
+
+ Future pause(String sessionId) async {
+ final session = await _requiredSession(sessionId);
+ final paused = session.pause(clock.now());
+ await sessionRepository.save(paused);
+ return paused;
+ }
+
+ Future resume(String sessionId) async {
+ final session = await _requiredSession(sessionId);
+ final resumed = session.resume(clock.now());
+ await sessionRepository.save(resumed);
+ return resumed;
+ }
+
+ int elapsedActiveMilliseconds(ActiveWorkoutSession session) {
+ return session.elapsedActiveMillisecondsAt(clock.now());
+ }
+
+ Future recordSetResult(ActiveSetResult result) async {
+ await sessionRepository.saveSetResult(result);
+ return result;
+ }
+
+ Future recordCurrentSetResult({
+ required String sessionId,
+ required String programSnapshotId,
+ required String exerciseSnapshotId,
+ required int programIndex,
+ required int exerciseIndex,
+ required int setIndex,
+ int? actualTimeMs,
+ int? actualReps,
+ double? actualScore,
+ String? scoreLabelSnapshot,
+ String? scoreUnitSnapshot,
+ }) async {
+ final now = clock.now();
+ final result = ActiveSetResult(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ activeWorkoutSessionId: sessionId,
+ programSnapshotId: programSnapshotId,
+ exerciseSnapshotId: exerciseSnapshotId,
+ programIndex: programIndex,
+ exerciseIndex: exerciseIndex,
+ setIndex: setIndex,
+ completedAt: now,
+ actualTimeMs: actualTimeMs,
+ actualReps: actualReps,
+ actualScore: actualScore,
+ scoreLabelSnapshot: scoreLabelSnapshot,
+ scoreUnitSnapshot: scoreUnitSnapshot,
+ );
+ await sessionRepository.saveSetResult(result);
+ return result;
+ }
+
+ Future updateProgress({
+ required String sessionId,
+ required int programIndex,
+ required int exerciseIndex,
+ required int setIndex,
+ }) async {
+ final session = await _requiredSession(sessionId);
+ final updated = session.copyWith(
+ currentProgramIndex: programIndex,
+ currentExerciseIndex: exerciseIndex,
+ currentSetIndex: setIndex,
+ metadata: session.metadata.touch(clock.now()),
+ );
+ await sessionRepository.save(updated);
+ return updated;
+ }
+
+ Future quitAndSave(String sessionId) async {
+ final session = await _requiredSession(sessionId);
+ final now = clock.now();
+ final saved = session.copyWith(
+ status: ActiveWorkoutStatus.savedExit,
+ lastPersistedAt: now,
+ elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
+ metadata: session.metadata.touch(now),
+ );
+ await sessionRepository.save(saved);
+ return saved;
+ }
+
+ Future abandon(String sessionId) async {
+ final session = await _requiredSession(sessionId);
+ final now = clock.now();
+ final abandoned = session.copyWith(
+ status: ActiveWorkoutStatus.abandoned,
+ endedAt: now,
+ lastPersistedAt: now,
+ elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
+ metadata: session.metadata.touch(now),
+ );
+ await sessionRepository.save(abandoned);
+ return abandoned;
+ }
+
+ Future complete(String sessionId) async {
+ final session = await _requiredSession(sessionId);
+ final now = clock.now();
+ final completed = session.copyWith(
+ status: ActiveWorkoutStatus.completed,
+ endedAt: now,
+ lastPersistedAt: now,
+ elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
+ metadata: session.metadata.touch(now),
+ );
+ await sessionRepository.save(completed);
+ return completed;
+ }
+
+ Future startRestAfterSet({
+ required String sessionId,
+ required int afterProgramIndex,
+ required int afterExerciseIndex,
+ required int afterSetIndex,
+ required int plannedRestSeconds,
+ int? adjustedRestSeconds,
+ }) async {
+ final now = clock.now();
+ final rest = ActiveRestState(
+ metadata: _newMetadata(ids, originDeviceId, now),
+ activeWorkoutSessionId: sessionId,
+ afterProgramIndex: afterProgramIndex,
+ afterExerciseIndex: afterExerciseIndex,
+ afterSetIndex: afterSetIndex,
+ plannedRestSeconds: plannedRestSeconds,
+ adjustedRestSeconds: adjustedRestSeconds ?? plannedRestSeconds,
+ startedAt: now,
+ );
+ await sessionRepository.saveRestState(rest);
+ return rest;
+ }
+
+ Future _requiredSession(String sessionId) async {
+ final session = await sessionRepository.findById(sessionId);
+ if (session == null) {
+ throw const DomainException('Active workout session not found.');
+ }
+ return session;
+ }
+}
+
+final class CloseWorkoutSessionUseCase {
+ const CloseWorkoutSessionUseCase({
+ required this.sessionRepository,
+ required this.historyRepository,
+ required this.clock,
+ required this.ids,
+ required this.originDeviceId,
+ });
+
+ final ActiveSessionRepository sessionRepository;
+ final WorkoutHistoryRepository historyRepository;
+ final Clock clock;
+ final IdGenerator ids;
+ final String originDeviceId;
+
+ Future close({
+ required String sessionId,
+ required String nameSnapshot,
+ required bool completed,
+ }) async {
+ final session = await sessionRepository.findById(sessionId);
+ if (session == null) {
+ throw const DomainException('Active workout session not found.');
+ }
+ final now = clock.now();
+ final results = await sessionRepository.listSetResults(sessionId);
+ final historyId = ids.newId();
+ final historyResults = _historyResultsFromActiveResults(
+ historyId: historyId,
+ results: results,
+ resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
+ now: now,
+ ids: ids,
+ originDeviceId: originDeviceId,
+ );
+ final history = WorkoutHistory(
+ metadata: EntityMetadata(
+ id: historyId,
+ createdAt: now,
+ updatedAt: now,
+ originDeviceId: originDeviceId,
+ ),
+ sourceWorkoutTemplateId: session.sourceWorkoutTemplateId,
+ sourceActiveWorkoutSessionId: session.metadata.id,
+ nameSnapshot: nameSnapshot,
+ startedAt: session.startedAt,
+ endedAt: now,
+ totalActiveMs: session.elapsedActiveMillisecondsAt(now),
+ completed: completed,
+ historySnapshotJson: jsonEncode({
+ 'sessionId': session.metadata.id,
+ 'resolvedTemplateSnapshotJson': session.resolvedTemplateSnapshotJson,
+ 'results': results
+ .map(
+ (result) => {
+ 'programSnapshotId': result.programSnapshotId,
+ 'exerciseSnapshotId': result.exerciseSnapshotId,
+ 'programIndex': result.programIndex,
+ 'exerciseIndex': result.exerciseIndex,
+ 'setIndex': result.setIndex,
+ 'actualTimeMs': result.actualTimeMs,
+ 'actualReps': result.actualReps,
+ 'actualScore': result.actualScore,
+ 'scoreLabelSnapshot': result.scoreLabelSnapshot,
+ 'scoreUnitSnapshot': result.scoreUnitSnapshot,
+ },
+ )
+ .toList(),
+ }),
+ results: historyResults,
+ );
+ await historyRepository.save(history);
+ return history;
+ }
+}
+
+final class WorkoutHistoryUseCases {
+ const WorkoutHistoryUseCases({required this.repository, required this.clock});
+
+ final WorkoutHistoryRepository repository;
+ final Clock clock;
+
+ Future findById(String id) => repository.findById(id);
+
+ Future> listActive() => repository.listActive();
+
+ Future delete(String id) => repository.delete(id, clock.now());
+}
+
+void _validateOverrideTargets(
+ String programSnapshotJson,
+ WorkoutTemplateExerciseOverrideConfig input,
+) {
+ final snapshot = jsonDecode(programSnapshotJson) as Map;
+ final exercises = (snapshot['exercises'] as List? ?? const [])
+ .cast