From cabd121ac1a125bc40b4ad67386b4eaa30c46079 Mon Sep 17 00:00:00 2001 From: Rene Luria Date: Mon, 6 Jul 2026 13:25:20 +0200 Subject: [PATCH] docs: Add Flutter app design specification Add the full design specification for the MyIce Flutter mobile app. The app connects to the existing FastAPI backend and replicates the webapp functionality with OIDC auth, schedule browsing, and event detail views. --- .markdownlintignore | 1 + .pre-commit-config.yaml | 2 +- .../2026-07-06-myice-flutter-app-design.md | 350 ++++++++++++++++++ 3 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 .markdownlintignore create mode 100644 docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..1d78244 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1 @@ +docs/superpowers/specs/*.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 039a8fc..455356e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: rev: v0.13.0 hooks: - id: markdownlint - exclude: "^.github|(^docs/_sidebar\\.md$)" + exclude: "^.github|_sidebar\\.md$|superpowers/specs/.*\\.md$" - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.11.0.1 diff --git a/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md b/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md new file mode 100644 index 0000000..39c5ab0 --- /dev/null +++ b/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md @@ -0,0 +1,350 @@ + +# MyIce Flutter App - Design Specification + +**Date:** 2026-07-06 +**Context:** Application mobile Flutter (iOS/Android) reproduisant le fonctionnement de la webapp myice existante. +**Backend:** L'application se connecte a l'API FastAPI existante (myice/webapi.py) via HTTP. + +## 1. Architecture globale + +Approche : **Repository + Provider (B)** + +``` +lib/ +├── main.dart # Entry point, MaterialApp, MultiProvider +├── config/ +│ └── api_config.dart # URL base de l'API, timeouts +├── models/ +│ ├── account.dart # {name, label} +│ ├── event.dart # Game/Practice du schedule +│ ├── event_detail.dart # Details + convocation +│ ├── user_info.dart # {email} +│ └── convocation.dart # Player, Staff, Convocation +├── services/ +│ ├── api_service.dart # Dio + intercepteur Bearer +│ └── auth_service.dart # Login/logout OIDC, deep link +├── providers/ +│ ├── auth_provider.dart # Etat auth + user info +│ ├── schedule_provider.dart # Liste events + filtres +│ └── event_provider.dart # Details d'un event (optionnel) +├── repositories/ +│ └── myice_repository.dart # Appels API : accounts, schedule, game +├── screens/ +│ ├── login_screen.dart +│ ├── schedule_screen.dart +│ └── event_detail_screen.dart +└── widgets/ + ├── event_card.dart + ├── filter_dropdown.dart + └── loading_indicator.dart +``` + +**Pattern :** Repository encapsule les appels API -> Providers gerent l'etat reactif -> Screens ecoutent les Providers. + +## 2. Dependances Flutter + +```yaml +dependencies: + flutter: + sdk: flutter + dio: ^5.4.0 + provider: ^6.1.0 + shared_preferences: ^2.2.0 + flutter_web_auth: ^0.6.0 + intl: ^0.19.0 # Pour le parsing/formatage de dates si besoin +``` + +## 3. Modeles de donnees (JSON <-> Dart) + +### Account +```dart +class Account { + final String name; + final String label; + Account({required this.name, required this.label}); + factory Account.fromJson(Map json) => Account( + name: json['name'] as String, + label: json['label'] as String, + ); +} +``` + +### Event +Provenant de `/schedule`. +```dart +class Event { + final String idEvent; + final String agegroup; + final String name; + final String title; + final String opponent; + final String place; + final String start; + final String end; + final String? color; + final String eventType; // mappe depuis le champ JSON 'event' + + Event({ + required this.idEvent, + required this.agegroup, + required this.name, + required this.title, + required this.opponent, + required this.place, + required this.start, + required this.end, + this.color, + required this.eventType, + }); + + factory Event.fromJson(Map json) => Event( + idEvent: json['id_event']?.toString() ?? '', + agegroup: json['agegroup'] as String, + name: json['name'] as String? ?? '', + title: json['title'] as String, + opponent: json['opponent'] as String? ?? '', + place: json['place'] as String? ?? '', + start: json['start'] as String, + end: json['end'] as String, + color: json['color'] as String?, + eventType: json['event'] as String? ?? '', + ); +} +``` + +### Player +```dart +class Player { + final String? position; + final String? number; + final String fname; + final String lname; + final String dob; + + Player({this.position, this.number, required this.fname, required this.lname, required this.dob}); + + factory Player.fromJson(Map json) => Player( + position: json['position'] as String?, + number: json['number']?.toString(), + fname: json['fname'] as String, + lname: json['lname'] as String, + dob: json['dob'] as String, + ); +} +``` + +### Staff +```dart +class Staff { + final String role; + final String fname; + final String lname; + + Staff({required this.role, required this.fname, required this.lname}); + + factory Staff.fromJson(Map json) => Staff( + role: json['role'] as String, + fname: json['fname'] as String, + lname: json['lname'] as String, + ); +} +``` + +### Convocation +```dart +class Convocation { + final List available; + final List staff; + + Convocation({required this.available, required this.staff}); + + factory Convocation.fromJson(Map json) => Convocation( + available: (json['available'] as List? ?? []) + .map((e) => Player.fromJson(e as Map)) + .toList(), + staff: (json['staff'] as List? ?? []) + .map((e) => Staff.fromJson(e as Map)) + .toList(), + ); +} +``` + +### EventDetail +Provenant de `/game/{id}`. +```dart +class EventDetail { + final String title; + final String type; + final String place; + final String timeStart; + final String timeEnd; + final Convocation convocation; + + EventDetail({ + required this.title, + required this.type, + required this.place, + required this.timeStart, + required this.timeEnd, + required this.convocation, + }); + + factory EventDetail.fromJson(Map json) => EventDetail( + title: json['title'] as String, + type: json['type'] as String, + place: json['place'] as String? ?? '', + timeStart: json['time_start'] as String, + timeEnd: json['time_end'] as String, + convocation: Convocation.fromJson(json['convocation'] as Map), + ); +} +``` + +### UserInfo +```dart +class UserInfo { + final String email; + UserInfo({required this.email}); + factory UserInfo.fromJson(Map json) => UserInfo( + email: json['email'] as String? ?? '', + ); +} +``` + +## 4. Services + +### ApiService +Utilise `dio`. Un intercepteur ajoute automatiquement le header `Authorization: Bearer `. + +Concept: +- `setBearerToken(token)` ajoute un `InterceptorsWrapper` a Dio. +- `clearBearerToken()` retire les intercepteurs existants. +- Methodes: `getAccounts()`, `getSchedule(account)`, `getGameDetail(gameId, account)`, `getUserInfo()`. +- Retournent des objets modeles via `fromJson`. + +### AuthService +Utilise `flutter_web_auth` pour l'OIDC + `shared_preferences` pour stocker le token. + +Concept: +- `storedToken`, `storedAccount` depuis `SharedPreferences`. +- `login()`: appelle `FlutterWebAuth.authenticate(url: baseUrl/login, callbackUrlScheme: 'myice')`. Parse le fragment `#access_token=xxx` de l'URL retournee. Stocke le token. +- `setAccount(account)`: persiste le compte selectionne. +- `logout()`: supprime token, account, userInfo des prefs. + +**Configuration deep link requise:** +- Android: intent-filter dans `AndroidManifest.xml` pour scheme `myice://`. +- iOS: `CFBundleURLTypes` dans `Info.plist` avec scheme `myice`. + +## 5. Providers (Provider package) + +### AuthProvider +Gere l'etat global d'authentification. + +**Etat:** `isAuthenticated`, `userInfo`. +**Methodes:** +- `init()`: Verifie token stocke, appelle `/userinfo`. Si 401, considere deconnecte. +- `login()`: Lance AuthService.login(), met a jour le token, appelle `/userinfo`, notifie listeners. +- `logout()`: Efface le token, notifie listeners. + +### ScheduleProvider +Gere les events, les comptes, et les filtres. + +**Etat:** `accounts`, `events`, `selectedAccount`, `selectedAgegroup`, `selectedSubgroup`, `isLoading`, `error`. +**Methodes:** +- `loadAccounts()`: Appelle `/accounts`, met a jour `accounts`. Selectionne `storedAccount` si present, sinon le premier. +- `loadSchedule()`: Met `isLoading = true`, appelle `/schedule?account=selectedAccount`, filtre events ou `eventType == "Jeu"`, extrait les `agegroup` uniques pour les filtres. +- `setAccount(String)`: Met a jour `selectedAccount`, sauvegarde dans SharedPreferences, appelle `loadSchedule()`. +- `setAgegroup(String)`: Met a jour, reset `selectedSubgroup`, notifie. +- `setSubgroup(String)`: Met a jour, notifie. +- `get filteredEvents`: Getter filtre `events` par `agegroup` et `subgroup`. + +**Logique de filtre sous-groupe:** +Identique a la webapp. La fonction `extractSubgroup(name)` retire la partie apres `" - "` dans le `name` de l'event pour obtenir le sous-groupe (si applicable). + +### EventDetailProvider (Optionnel) +Gere le chargement des details d'un event. Peut etre integre dans `ScheduleProvider` si prefere pour garder un seul provider. + +**Etat:** `eventDetail`, `isLoading`, `error`. +**Methodes:** `loadEventDetail(gameId)` appelle `/game/{gameId}`. + +## 6. Ecrans + +### LoginScreen +- **AppBar:** Aucune (centre). +- **Body:** Column centree. + - Titre: "MyIce - Games" (headlineLarge, couleur indigo). + - Bouton: "Se connecter avec Infomaniak" (style degrade indigo -> violet). + - Texte d'aide: "Connectez-vous pour acceder aux evenements...". +- **Comportement:** + - Si `authProvider.isAuthenticated`, navigue automatiquement vers `ScheduleScreen`. + - Affiche `CircularProgressIndicator` pendant le login. + - SnackBar en cas d'erreur. + +### ScheduleScreen +- **AppBar:** Titre "MyIce", action logout + email utilisateur. +- **Body:** + - **Filtres:** Card avec 3 `DropdownButtonFormField` (Compte, Age, Sous-groupe). Le bouton "Charger" est optionnel (changement de compte recharge auto). + - **Liste:** `Expanded` + `ListView.builder`. Affiche `EventCard` pour chaque event filtre. + - Etats: Loading (`CircularProgressIndicator`), vide (texte info), erreur (SnackBar). + +### EventDetailScreen +- **AppBar:** Titre = `event.title`, bouton retour. +- **Body:** `SingleChildScrollView` + `Padding(16)`. + - Card recap: Type, Lieu, Heure. + - Section Joueurs: + - Si vide: Card amber "Aucun joueur convoque". + - Sinon: Card avec total + repartition positions. Liste triee par position puis numero: `[Position] #Num - Prenom Nom (DOB)`. + - Section Personnel: + - Si vide: Card bleu "Aucun personnel convoque". + - Sinon: Liste `role: Prenom Nom`. + - Si aucun joueur ET aucun personnel: Card amber principal "Aucun joueur ni personnel convoque" uniquement. + +### Widgets reutilisables +- **EventCard:** Card avec bordure laterale coloree. Titre (`agegroup - name`), sous-titre, details (adversaire, lieu, heure). onTap -> `EventDetailScreen`. +- **FilterDropdown:** `DropdownButtonFormField` generique stylise. + +## 7. Navigation & Routing + +Utilisation de `Navigator` classique (pas de package tierce pour 3 ecrans). +- `main.dart`: `MaterialApp` avec `home` determine par `Consumer` (`ScheduleScreen` ou `LoginScreen`). +- Navigation details: `Navigator.push(MaterialPageRoute(builder: (_) => EventDetailScreen(...)))`. + +## 8. Gestion des erreurs + +- Erreur reseau / API: SnackBar rouge avec message. +- 401 (Token expire): Deconnexion automatique + redirection `LoginScreen`. +- JSON malforme: La webapp gere cote backend. Cote Flutter, `jsonDecode` echoue -> message "Donnees invalides". +- Aucun event/joueur: Interface vide avec message informatif (pas d'erreur). + +## 9. Configuration requise + +### Android +Ajouter un intent-filter dans `android/app/src/main/AndroidManifest.xml` pour le deep link `myice://callback`. + +### iOS +Ajouter `CFBundleURLTypes` dans `ios/Runner/Info.plist` avec scheme `myice`. + +### Configuration API +`lib/config/api_config.dart`: +```dart +class ApiConfig { + static const String baseUrl = 'https://your-api-domain.com'; // A configurer +} +``` + +## 10. Plan d'implementation (High-level) + +1. Scaffolding du projet Flutter (`flutter create`). +2. Ajout des dependances (`pubspec.yaml`). +3. Configuration des deep links (`AndroidManifest` + `Info.plist`). +4. Implementation des modeles de donnees (`fromJson`). +5. Implementation des services (`ApiService`, `AuthService`). +6. Implementation des providers (`AuthProvider`, `ScheduleProvider`). +7. Implementation des ecrans (`LoginScreen`, `ScheduleScreen`, `EventDetailScreen`). +8. Implementation des widgets (`EventCard`, etc.). +9. Wiring dans `main.dart` avec `MultiProvider`. +10. Tests manuels de l'auth OIDC et de la navigation. + +--- + +**Prochaine etape apres validation du spec:** Creer le plan d'implementation detaille avec le skill `writing-plans`.