From 8529bb20961fc18281d4e146c9904bd9f3f10c23 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Wed, 25 Mar 2026 17:42:26 +0100 Subject: [PATCH] Get event from backend instead of local parsing (to be tested) + claude.md --- CLAUDE.md | 54 ++++++++++++++++++++++++++++ android/app/google-services.json | 21 ++++++++++- devtools_options.yaml | 3 ++ lib/Models/agenda.dart | 56 +++++++++++++++++++++++++++++ lib/Screens/Agenda/agenda_view.dart | 32 ++++++++--------- lib/Screens/Agenda/event_popup.dart | 24 +++++++++++-- lib/client.dart | 4 +++ 7 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 CLAUDE.md create mode 100644 devtools_options.yaml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..37cfc10 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# tablet-app + +App Flutter dédiée aux tablettes fixes (kiosks). Les visiteurs viennent à la tablette — elle ne bouge pas. + +## Fonctionnement +1. Sélection de l'instance via **pincode** +2. Choix d'une **configuration** à afficher (configurée dans `manager-app`) +3. Affichage du contenu en boucle pour les visiteurs + +## Stack +- Flutter mobile (cible : tablette Android/iOS) +- State management : **Provider** + `ChangeNotifier` +- MQTT (`mqtt_client`) pour la communication temps réel avec le backend +- SQLite (`sqflite`) pour le cache local +- Firebase Storage + Core + +## Client API +Dépendance locale sur le client généré dans `manager-app` : +```yaml +manager_api_new: + path: ../manager-app/manager_api_new +``` +**Ne pas copier/dupliquer le client** — toujours pointer vers `manager-app/manager_api_new`. + +## Structure +``` +lib/ +├── api/ # Intégration OpenAPI +├── Components/ # Boutons, carrousels, loaders, players audio/vidéo +├── Helpers/ # DatabaseHelper, DeviceInfoHelper, MQTTHelper, translationHelper +├── Models/ # TabletAppContext, agenda, section, map-marker, WeatherData +├── Screens/ # Agenda, Article, Configuration, MainView, Menu, PDF, +│ # Map (Google Maps + MapBox + Flutter Map), Puzzle, Quiz, +│ # Slider, Video, Weather, Web +└── Services/ # assistantService, downloadService, statisticsService +``` + +## Fonctionnalités non supportées +La tablette étant fixe, certaines features orientées mobilité ne sont pas implémentées : +- **Escape game / parcours géolocalisé** — requiert un device mobile qui se déplace +- Pas de scanner QR, pas de beacons +- Pas de notifications push + +## Particularités vs mymuseum-visitapp +- MQTT activé (synchronisation device ↔ backend) +- Langues gérées via `translationHelper` + données backend (même pattern que visitapp) +- Support cartes : Google Maps, MapBox, Flutter Map + +## Commandes utiles +```bash +flutter run # Lancer sur device/émulateur connecté +flutter build apk # Build Android +flutter build ios # Build iOS +``` diff --git a/android/app/google-services.json b/android/app/google-services.json index f61394b..4fd3d73 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -24,6 +24,25 @@ } } }, + { + "client_info": { + "mobilesdk_app_id": "1:1034665398515:android:23d9de7735f898e5d6a786", + "android_client_info": { + "package_name": "be.unov.mymuseum.mdlf" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBVCpwP5Uxh_nDUV2b6s4TybUqPJ-lvXm0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, { "client_info": { "mobilesdk_app_id": "1:1034665398515:android:b7475582b41ed32dd6a786", @@ -45,4 +64,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/Models/agenda.dart b/lib/Models/agenda.dart index 0c2439e..fa92b1c 100644 --- a/lib/Models/agenda.dart +++ b/lib/Models/agenda.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:manager_api_new/api.dart'; class Agenda { List events; @@ -33,6 +34,8 @@ class EventAgenda { String? website; String? phone; String? idVideoYoutube; + String? videoLink; + String? videoResourceUrl; String? email; String? image; @@ -48,10 +51,63 @@ class EventAgenda { required this.website, required this.phone, required this.idVideoYoutube, + this.videoLink, + this.videoResourceUrl, required this.email, required this.image, }); + factory EventAgenda.fromDto(EventAgendaDTO dto, String language) { + String? pickTranslation(List? list) { + if (list == null || list.isEmpty) return null; + return (list.firstWhere( + (t) => t.language == language, + orElse: () => list.first, + )).value; + } + + EventAddress? address; + final a = dto.address; + if (a != null) { + final coords = a.geometry?.coordinates as List?; + address = EventAddress( + address: a.address, + lat: coords != null && coords.length >= 2 ? coords[1] : null, + lng: coords != null && coords.length >= 2 ? coords[0] : null, + zoom: a.zoom, + placeId: null, + name: null, + streetNumber: a.streetNumber, + streetName: a.streetName, + streetNameShort: null, + city: a.city, + state: a.state, + stateShort: null, + postCode: a.postCode, + country: a.country, + countryShort: null, + ); + } + + return EventAgenda( + name: pickTranslation(dto.label), + description: pickTranslation(dto.description), + type: dto.type, + dateAdded: dto.dateAdded, + dateFrom: dto.dateFrom, + dateTo: dto.dateTo, + dateHour: null, + address: address, + website: dto.website, + phone: dto.phone, + idVideoYoutube: dto.idVideoYoutube, + videoLink: dto.videoLink, + videoResourceUrl: dto.videoResource?.url, + email: dto.email, + image: dto.resource?.url, + ); + } + factory EventAgenda.fromJson(Map json) { return EventAgenda( name: json['name'], diff --git a/lib/Screens/Agenda/agenda_view.dart b/lib/Screens/Agenda/agenda_view.dart index 5887498..ced9e6f 100644 --- a/lib/Screens/Agenda/agenda_view.dart +++ b/lib/Screens/Agenda/agenda_view.dart @@ -1,10 +1,6 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -//import 'dart:html'; import 'dart:ui' as ui; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:manager_api_new/api.dart'; @@ -49,26 +45,28 @@ class _AgendaView extends State { Future getAndParseJsonInfo(TabletAppContext tabletAppContext) async { try { - // Récupération du contenu JSON depuis l'URL - var httpClient = HttpClient(); + if (agendaDTO.id == null) return null; + final dtos = await tabletAppContext.clientAPI!.sectionAgendaApi! + .sectionAgendaGetUpcomingEvents(agendaDTO.id!); - // We need to get detail to get url from resourceId - var resourceIdForSelectedLanguage = agendaDTO.resourceIds!.where((ri) => ri.language == tabletAppContext.language).first.value; - ResourceDTO? resourceDTO = await tabletAppContext.clientAPI!.resourceApi!.resourceGetDetail(resourceIdForSelectedLanguage!); + if (dtos == null) return null; - var request = await httpClient.getUrl(Uri.parse(resourceDTO!.url!)); - var response = await request.close(); - var jsonString = await response.transform(utf8.decoder).join(); + final events = dtos + .map((dto) => EventAgenda.fromDto(dto, tabletAppContext.language)) + .toList(); + events.sort((a, b) { + if (a.dateFrom == null) return 1; + if (b.dateFrom == null) return -1; + return a.dateFrom!.compareTo(b.dateFrom!); + }); - agenda = Agenda.fromJson(jsonString); - agenda.events = agenda.events.where((a) => a.dateFrom != null && a.dateFrom!.isAfter(DateTime.now())).toList(); - agenda.events.sort((a, b) => a.dateFrom!.compareTo(b.dateFrom!)); - filteredAgenda.value = agenda.events; + agenda = Agenda(events: events); + filteredAgenda.value = events; mapIcon = await getByteIcon(); return agenda; - } catch(e) { + } catch (e) { print("Erreur lors du parsing du json : ${e.toString()}"); return null; } diff --git a/lib/Screens/Agenda/event_popup.dart b/lib/Screens/Agenda/event_popup.dart index 1aa810d..ca2362e 100644 --- a/lib/Screens/Agenda/event_popup.dart +++ b/lib/Screens/Agenda/event_popup.dart @@ -14,6 +14,7 @@ import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:tablet_app/Components/loading_common.dart'; +import 'package:tablet_app/Components/video_viewer.dart'; import 'package:tablet_app/Components/video_viewer_youtube.dart'; import 'package:tablet_app/Models/agenda.dart'; import 'package:intl/intl.dart'; @@ -249,7 +250,7 @@ class _EventPopupState extends State { }, textStyle: TextStyle(fontSize: kDescriptionSize), ), - widget.eventAgenda.idVideoYoutube != null && widget.eventAgenda.idVideoYoutube!.isNotEmpty ? + if (widget.eventAgenda.idVideoYoutube != null && widget.eventAgenda.idVideoYoutube!.isNotEmpty) Padding( padding: const EdgeInsets.all(8.0), child: SizedBox( @@ -257,8 +258,25 @@ class _EventPopupState extends State { width: 350, child: VideoViewerYoutube(videoUrl: "https://www.youtube.com/watch?v=${widget.eventAgenda.idVideoYoutube}", isAuto: false, webView: true) ), - ) : - SizedBox(), + ), + if (widget.eventAgenda.videoLink != null && widget.eventAgenda.videoLink!.isNotEmpty) + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: 250, + width: 350, + child: VideoViewer(videoUrl: widget.eventAgenda.videoLink!, file: null), + ), + ), + if (widget.eventAgenda.videoResourceUrl != null && widget.eventAgenda.videoResourceUrl!.isNotEmpty) + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: 250, + width: 350, + child: VideoViewer(videoUrl: widget.eventAgenda.videoResourceUrl!, file: null), + ), + ), ], ), ), diff --git a/lib/client.dart b/lib/client.dart index bdeb64d..dacb4d9 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -32,6 +32,9 @@ class Client { StatsApi? _statsApi; StatsApi? get statsApi => _statsApi; + SectionAgendaApi? _sectionAgendaApi; + SectionAgendaApi? get sectionAgendaApi => _sectionAgendaApi; + Client(String path, {String? apiKey}) { _apiClient = ApiClient(basePath: path); if (apiKey != null) _apiClient!.addDefaultHeader('X-Api-Key', apiKey); @@ -44,5 +47,6 @@ class Client { _deviceApi = DeviceApi(_apiClient); _applicationInstanceApi = ApplicationInstanceApi(_apiClient); _statsApi = StatsApi(_apiClient); + _sectionAgendaApi = SectionAgendaApi(_apiClient); } } \ No newline at end of file