Get event from backend instead of local parsing (to be tested) + claude.md
This commit is contained in:
parent
da6ff5177e
commit
8529bb2096
54
CLAUDE.md
Normal file
54
CLAUDE.md
Normal 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
|
||||||
|
```
|
||||||
@ -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
3
devtools_options.yaml
Normal 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:
|
||||||
@ -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'],
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user