Get event from backend instead of local parsing (to be tested) + claude.md

This commit is contained in:
Thomas Fransolet 2026-03-25 17:42:26 +01:00
parent da6ff5177e
commit 8529bb2096
7 changed files with 173 additions and 21 deletions

54
CLAUDE.md Normal file
View File

@ -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
```

View File

@ -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": { "client_info": {
"mobilesdk_app_id": "1:1034665398515:android:b7475582b41ed32dd6a786", "mobilesdk_app_id": "1:1034665398515:android:b7475582b41ed32dd6a786",
@ -45,4 +64,4 @@
} }
], ],
"configuration_version": "1" "configuration_version": "1"
} }

3
devtools_options.yaml Normal file
View File

@ -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:

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:manager_api_new/api.dart';
class Agenda { class Agenda {
List<EventAgenda> events; List<EventAgenda> events;
@ -33,6 +34,8 @@ class EventAgenda {
String? website; String? website;
String? phone; String? phone;
String? idVideoYoutube; String? idVideoYoutube;
String? videoLink;
String? videoResourceUrl;
String? email; String? email;
String? image; String? image;
@ -48,10 +51,63 @@ class EventAgenda {
required this.website, required this.website,
required this.phone, required this.phone,
required this.idVideoYoutube, required this.idVideoYoutube,
this.videoLink,
this.videoResourceUrl,
required this.email, required this.email,
required this.image, required this.image,
}); });
factory EventAgenda.fromDto(EventAgendaDTO dto, String language) {
String? pickTranslation(List<TranslationDTO>? 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<String, dynamic> json) { factory EventAgenda.fromJson(Map<String, dynamic> json) {
return EventAgenda( return EventAgenda(
name: json['name'], name: json['name'],

View File

@ -1,10 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
//import 'dart:html';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
@ -49,26 +45,28 @@ class _AgendaView extends State<AgendaView> {
Future<Agenda?> getAndParseJsonInfo(TabletAppContext tabletAppContext) async { Future<Agenda?> getAndParseJsonInfo(TabletAppContext tabletAppContext) async {
try { try {
// Récupération du contenu JSON depuis l'URL if (agendaDTO.id == null) return null;
var httpClient = HttpClient(); final dtos = await tabletAppContext.clientAPI!.sectionAgendaApi!
.sectionAgendaGetUpcomingEvents(agendaDTO.id!);
// We need to get detail to get url from resourceId if (dtos == null) return null;
var resourceIdForSelectedLanguage = agendaDTO.resourceIds!.where((ri) => ri.language == tabletAppContext.language).first.value;
ResourceDTO? resourceDTO = await tabletAppContext.clientAPI!.resourceApi!.resourceGetDetail(resourceIdForSelectedLanguage!);
var request = await httpClient.getUrl(Uri.parse(resourceDTO!.url!)); final events = dtos
var response = await request.close(); .map((dto) => EventAgenda.fromDto(dto, tabletAppContext.language))
var jsonString = await response.transform(utf8.decoder).join(); .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 = Agenda(events: events);
agenda.events = agenda.events.where((a) => a.dateFrom != null && a.dateFrom!.isAfter(DateTime.now())).toList(); filteredAgenda.value = events;
agenda.events.sort((a, b) => a.dateFrom!.compareTo(b.dateFrom!));
filteredAgenda.value = agenda.events;
mapIcon = await getByteIcon(); mapIcon = await getByteIcon();
return agenda; return agenda;
} catch(e) { } catch (e) {
print("Erreur lors du parsing du json : ${e.toString()}"); print("Erreur lors du parsing du json : ${e.toString()}");
return null; return null;
} }

View File

@ -14,6 +14,7 @@ import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:tablet_app/Components/loading_common.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/Components/video_viewer_youtube.dart';
import 'package:tablet_app/Models/agenda.dart'; import 'package:tablet_app/Models/agenda.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -249,7 +250,7 @@ class _EventPopupState extends State<EventPopup> {
}, },
textStyle: TextStyle(fontSize: kDescriptionSize), textStyle: TextStyle(fontSize: kDescriptionSize),
), ),
widget.eventAgenda.idVideoYoutube != null && widget.eventAgenda.idVideoYoutube!.isNotEmpty ? if (widget.eventAgenda.idVideoYoutube != null && widget.eventAgenda.idVideoYoutube!.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SizedBox( child: SizedBox(
@ -257,8 +258,25 @@ class _EventPopupState extends State<EventPopup> {
width: 350, width: 350,
child: VideoViewerYoutube(videoUrl: "https://www.youtube.com/watch?v=${widget.eventAgenda.idVideoYoutube}", isAuto: false, webView: true) 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),
),
),
], ],
), ),
), ),

View File

@ -32,6 +32,9 @@ class Client {
StatsApi? _statsApi; StatsApi? _statsApi;
StatsApi? get statsApi => _statsApi; StatsApi? get statsApi => _statsApi;
SectionAgendaApi? _sectionAgendaApi;
SectionAgendaApi? get sectionAgendaApi => _sectionAgendaApi;
Client(String path, {String? apiKey}) { Client(String path, {String? apiKey}) {
_apiClient = ApiClient(basePath: path); _apiClient = ApiClient(basePath: path);
if (apiKey != null) _apiClient!.addDefaultHeader('X-Api-Key', apiKey); if (apiKey != null) _apiClient!.addDefaultHeader('X-Api-Key', apiKey);
@ -44,5 +47,6 @@ class Client {
_deviceApi = DeviceApi(_apiClient); _deviceApi = DeviceApi(_apiClient);
_applicationInstanceApi = ApplicationInstanceApi(_apiClient); _applicationInstanceApi = ApplicationInstanceApi(_apiClient);
_statsApi = StatsApi(_apiClient); _statsApi = StatsApi(_apiClient);
_sectionAgendaApi = SectionAgendaApi(_apiClient);
} }
} }