diff --git a/android/app/build.gradle b/android/app/build.gradle index 9a0a51f..9e138ae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -40,7 +40,8 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"*/ android { namespace = "be.unov.mymuseum.fortsaintheribert" - compileSdkVersion 35 + compileSdkVersion 36 + ndkVersion "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -70,6 +71,10 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true + + /*ndk { + abiFilters "arm64-v8a" + }*/ } signingConfigs { diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..e37eeb2 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.experimental.enable16kApk=true +android.useNewNativePlugin=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 89e56bd..13e60b2 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip + diff --git a/android/settings.gradle b/android/settings.gradle index 02c755b..79a6ff1 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -31,8 +31,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.0" apply false + id "com.android.application" version "8.9.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" \ No newline at end of file diff --git a/l10n.yaml b/l10n.yaml index 4e6692e..15338f2 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,3 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file +output-localization-file: app_localizations.dart diff --git a/lib/Components/AssistantChatSheet.dart b/lib/Components/AssistantChatSheet.dart new file mode 100644 index 0000000..96d3ca3 --- /dev/null +++ b/lib/Components/AssistantChatSheet.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Models/AssistantResponse.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/Services/assistantService.dart'; +import 'package:mymuseum_visitapp/constants.dart'; + +class AssistantChatSheet extends StatefulWidget { + final VisitAppContext visitAppContext; + final String? configurationId; // null = scope instance, fourni = scope configuration + final void Function(String sectionId, String sectionTitle)? onNavigateToSection; + + const AssistantChatSheet({ + Key? key, + required this.visitAppContext, + this.configurationId, + this.onNavigateToSection, + }) : super(key: key); + + @override + State createState() => _AssistantChatSheetState(); +} + +class _AssistantChatSheetState extends State { + late AssistantService _assistantService; + final TextEditingController _controller = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final List _bubbles = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _assistantService = AssistantService(visitAppContext: widget.visitAppContext); + } + + Future _send() async { + final text = _controller.text.trim(); + if (text.isEmpty || _isLoading) return; + + _controller.clear(); + setState(() { + _bubbles.add(_ChatBubble(text: text, isUser: true)); + _isLoading = true; + }); + _scrollToBottom(); + + try { + final response = await _assistantService.chat( + message: text, + configurationId: widget.configurationId, + ); + setState(() { + _bubbles.add(_AssistantMessage( + response: response, + onNavigate: widget.onNavigateToSection, + )); + }); + } catch (_) { + setState(() { + _bubbles.add(_ChatBubble(text: "Une erreur est survenue, réessayez.", isUser: false)); + }); + } finally { + setState(() => _isLoading = false); + _scrollToBottom(); + } + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.92, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Handle + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: [ + Icon(Icons.chat_bubble_outline, color: kMainColor1), + const SizedBox(width: 8), + Text("Assistant", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: kSecondGrey)), + ], + ), + ), + const Divider(height: 1), + // Messages + Expanded( + child: _bubbles.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + "Bonjour ! Posez-moi vos questions sur cette visite.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[500], fontSize: 15), + ), + ), + ) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: _bubbles.length, + itemBuilder: (_, i) => _bubbles[i], + ), + ), + // Loading indicator + if (_isLoading) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: [ + SizedBox( + width: 20, height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: kMainColor1), + ), + const SizedBox(width: 8), + Text("...", style: TextStyle(color: Colors.grey[400])), + ], + ), + ), + // Input + SafeArea( + child: Padding( + padding: EdgeInsets.only( + left: 12, right: 12, bottom: MediaQuery.of(context).viewInsets.bottom + 8, top: 8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: "Votre question...", + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + ), + onSubmitted: (_) => _send(), + ), + ), + const SizedBox(width: 8), + CircleAvatar( + backgroundColor: kMainColor1, + child: IconButton( + icon: const Icon(Icons.send, color: Colors.white, size: 18), + onPressed: _send, + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _ChatBubble extends StatelessWidget { + final String text; + final bool isUser; + + const _ChatBubble({required this.text, required this.isUser}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.78), + decoration: BoxDecoration( + color: isUser ? kMainColor1 : Colors.grey[100], + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(16), + topRight: const Radius.circular(16), + bottomLeft: isUser ? const Radius.circular(16) : const Radius.circular(4), + bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(16), + ), + ), + child: Text( + text, + style: TextStyle( + color: isUser ? Colors.white : kSecondGrey, + fontSize: 14, + ), + ), + ), + ); + } +} + +class _AssistantMessage extends StatelessWidget { + final AssistantResponse response; + final void Function(String sectionId, String sectionTitle)? onNavigate; + + const _AssistantMessage({required this.response, this.onNavigate}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text bubble + _ChatBubble(text: response.reply, isUser: false), + + // Cards + if (response.cards != null && response.cards!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6, left: 4, right: 24), + child: Column( + children: response.cards! + .map((card) => _AiCardWidget(card: card)) + .toList(), + ), + ), + + // Navigation button + if (response.navigation != null && onNavigate != null) + Padding( + padding: const EdgeInsets.only(top: 8, left: 4), + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + onNavigate!( + response.navigation!.sectionId, + response.navigation!.sectionTitle, + ); + }, + icon: const Icon(Icons.arrow_forward, size: 16), + label: Text(response.navigation!.sectionTitle), + style: ElevatedButton.styleFrom( + backgroundColor: kMainColor1, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + textStyle: const TextStyle(fontSize: 13), + ), + ), + ), + ], + ), + ); + } +} + +class _AiCardWidget extends StatelessWidget { + final AiCard card; + + const _AiCardWidget({required this.card}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey[200]!), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: const Offset(0, 1)), + ], + ), + child: Row( + children: [ + if (card.icon != null) ...[ + Text(card.icon!, style: const TextStyle(fontSize: 18)), + const SizedBox(width: 8), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + card.title, + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: kSecondGrey), + ), + if (card.subtitle.isNotEmpty) + Text( + card.subtitle, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/Components/ScannerDialog.dart b/lib/Components/ScannerDialog.dart index 16b1626..6958f3d 100644 --- a/lib/Components/ScannerDialog.dart +++ b/lib/Components/ScannerDialog.dart @@ -128,11 +128,13 @@ class _ScannerDialogState extends State { VisitAppContext visitAppContext = widget.appContext!.getContext(); if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) { + visitAppContext.statisticsService?.track(VisitEventType.qrScan, metadata: {'valid': false, 'sectionId': sectionId}); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2), ); Navigator.of(context).pop(); } else { + visitAppContext.statisticsService?.track(VisitEventType.qrScan, sectionId: sectionId, metadata: {'valid': true}); dynamic rawSection = visitAppContext.currentSections!.firstWhere((cs) => cs!['id'] == sectionId)!; Navigator.of(context).pop(); Navigator.push( diff --git a/lib/Helpers/requirement_state_controller.dart b/lib/Helpers/requirement_state_controller.dart index 2d8e6da..712b2af 100644 --- a/lib/Helpers/requirement_state_controller.dart +++ b/lib/Helpers/requirement_state_controller.dart @@ -1,16 +1,16 @@ -import 'package:flutter_beacon/flutter_beacon.dart'; +// TODO // import 'package:flutter_beacon/flutter_beacon.dart'; import 'package:get/get.dart'; class RequirementStateController extends GetxController { - var bluetoothState = BluetoothState.stateOff.obs; - var authorizationStatus = AuthorizationStatus.notDetermined.obs; + var bluetoothState = false; //BluetoothState.stateOff.obs; + var authorizationStatus = false; //AuthorizationStatus.notDetermined.obs; var locationService = false.obs; var _startBroadcasting = false.obs; var _startScanning = false.obs; var _pauseScanning = false.obs; - bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn; + /*bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn; bool get authorizationStatusOk => authorizationStatus.value == AuthorizationStatus.allowed || authorizationStatus.value == AuthorizationStatus.always; @@ -22,7 +22,7 @@ class RequirementStateController extends GetxController { updateAuthorizationStatus(AuthorizationStatus status) { authorizationStatus.value = status; - } + }*/ updateLocationService(bool flag) { locationService.value = flag; diff --git a/lib/Models/AssistantResponse.dart b/lib/Models/AssistantResponse.dart new file mode 100644 index 0000000..1d470f6 --- /dev/null +++ b/lib/Models/AssistantResponse.dart @@ -0,0 +1,56 @@ +class AiCard { + final String title; + final String subtitle; + final String? icon; + + const AiCard({required this.title, required this.subtitle, this.icon}); + + factory AiCard.fromJson(Map json) => AiCard( + title: json['title'] as String? ?? '', + subtitle: json['subtitle'] as String? ?? '', + icon: json['icon'] as String?, + ); +} + +class AssistantNavigationAction { + final String sectionId; + final String sectionTitle; + final String sectionType; + + const AssistantNavigationAction({ + required this.sectionId, + required this.sectionTitle, + required this.sectionType, + }); + + factory AssistantNavigationAction.fromJson(Map json) => + AssistantNavigationAction( + sectionId: json['sectionId'] as String? ?? '', + sectionTitle: json['sectionTitle'] as String? ?? '', + sectionType: json['sectionType'] as String? ?? '', + ); +} + +class AssistantResponse { + final String reply; + final List? cards; + final AssistantNavigationAction? navigation; + + const AssistantResponse({ + required this.reply, + this.cards, + this.navigation, + }); + + factory AssistantResponse.fromJson(Map json) => + AssistantResponse( + reply: json['reply'] as String? ?? '', + cards: (json['cards'] as List?) + ?.map((e) => AiCard.fromJson(e as Map)) + .toList(), + navigation: json['navigation'] != null + ? AssistantNavigationAction.fromJson( + json['navigation'] as Map) + : null, + ); +} diff --git a/lib/Models/visitContext.dart b/lib/Models/visitContext.dart index 1a6a088..6892c5c 100644 --- a/lib/Models/visitContext.dart +++ b/lib/Models/visitContext.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Models/articleRead.dart'; +import 'package:mymuseum_visitapp/Services/statisticsService.dart'; import 'package:mymuseum_visitapp/Models/beaconSection.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/client.dart'; class VisitAppContext with ChangeNotifier { - Client clientAPI = Client("https://api.mymuseum.be"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 // https://api.myinfomate.be // http://192.168.31.228:5000 + Client clientAPI = Client("http://192.168.31.228:5000"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 // https://api.myinfomate.be // http://192.168.31.228:5000 String? id = ""; String? language = ""; @@ -27,6 +28,9 @@ class VisitAppContext with ChangeNotifier { List audiosNotWorking = []; + ApplicationInstanceDTO? applicationInstanceDTO; // null = assistant non activé + StatisticsService? statisticsService; + bool? isAdmin = false; bool? isAllLanguages = false; diff --git a/lib/Screens/ConfigurationPage/configuration_page.dart b/lib/Screens/ConfigurationPage/configuration_page.dart index 1e1d4ce..c37aac5 100644 --- a/lib/Screens/ConfigurationPage/configuration_page.dart +++ b/lib/Screens/ConfigurationPage/configuration_page.dart @@ -3,10 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_beacon/flutter_beacon.dart'; +// TODO //import 'package:flutter_beacon/flutter_beacon.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart'; import 'package:mymuseum_visitapp/Components/CustomAppBar.dart'; import 'package:mymuseum_visitapp/Components/ScannerBouton.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; @@ -43,15 +44,15 @@ class _ConfigurationPageState extends State with WidgetsBindi // Beacon specific final controller = Get.find(); - StreamSubscription? _streamBluetooth; - StreamSubscription? _streamRanging; + /*StreamSubscription? _streamBluetooth; + StreamSubscription? _streamRanging;*/ /*final _regionBeacons = >{}; final _beacons = [];*/ bool _isDialogShowing = false; DateTime? lastTimePopUpWasClosed; //bool _isArticleOpened = false; StreamSubscription? listener; - final List regions = []; + //final List regions = []; @override void initState() { @@ -59,13 +60,13 @@ class _ConfigurationPageState extends State with WidgetsBindi if (Platform.isIOS) { // iOS platform, at least set identifier and proximityUUID for region scanning - regions.add(Region( + /*regions.add(Region( identifier: 'MyMuseumB', proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825') - ); + );*/ } else { // Android platform, it can ranging out of beacon that filter all of Proximity UUID - regions.add(Region(identifier: 'MyMuseumB')); + //regions.add(Region(identifier: 'MyMuseumB')); } super.initState(); @@ -86,16 +87,16 @@ class _ConfigurationPageState extends State with WidgetsBindi listeningState() async { print('Listening to bluetooth state'); - _streamBluetooth = flutterBeacon + /*_streamBluetooth = flutterBeacon .bluetoothStateChanged() .listen((BluetoothState state) async { controller.updateBluetoothState(state); await checkAllRequirements(); - }); + });*/ } checkAllRequirements() async { - final bluetoothState = await flutterBeacon.bluetoothState; + /*final bluetoothState = await flutterBeacon.bluetoothState; controller.updateBluetoothState(bluetoothState); print('BLUETOOTH $bluetoothState'); @@ -105,7 +106,7 @@ class _ConfigurationPageState extends State with WidgetsBindi final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled; controller.updateLocationService(locationServiceEnabled); - print('LOCATION SERVICE $locationServiceEnabled'); + print('LOCATION SERVICE $locationServiceEnabled');*/ var status = await Permission.bluetoothScan.status; @@ -123,7 +124,7 @@ class _ConfigurationPageState extends State with WidgetsBindi print(statuses[Permission.bluetoothConnect]); print(status); - if (controller.bluetoothEnabled && + /*if (controller.bluetoothEnabled && controller.authorizationStatusOk && controller.locationServiceEnabled) { print('STATE READY'); @@ -143,13 +144,13 @@ class _ConfigurationPageState extends State with WidgetsBindi } else { print('STATE NOT READY'); controller.pauseScanning(); - } + }*/ } initScanBeacon(VisitAppContext visitAppContext) async { - await flutterBeacon.initializeScanning; - if (!controller.authorizationStatusOk || + //await flutterBeacon.initializeScanning; + /*if (!controller.authorizationStatusOk || !controller.locationServiceEnabled || !controller.bluetoothEnabled) { print( @@ -157,16 +158,16 @@ class _ConfigurationPageState extends State with WidgetsBindi 'locationServiceEnabled=${controller.locationServiceEnabled}, ' 'bluetoothEnabled=${controller.bluetoothEnabled}'); return; - } + }*/ - if (_streamRanging != null) { + /*if (_streamRanging != null) { if (_streamRanging!.isPaused) { _streamRanging?.resume(); return; } - } + }*/ - _streamRanging = + /*_streamRanging = flutterBeacon.ranging(regions).listen((RangingResult result) { //print(result); if (mounted) { @@ -229,7 +230,7 @@ class _ConfigurationPageState extends State with WidgetsBindi //}); } }); - +*/ } /*pauseScanBeacon() async { @@ -259,14 +260,14 @@ class _ConfigurationPageState extends State with WidgetsBindi void didChangeAppLifecycleState(AppLifecycleState state) async { print('AppLifecycleState = $state'); if (state == AppLifecycleState.resumed) { - if (_streamBluetooth != null) { + /*if (_streamBluetooth != null) { if (_streamBluetooth!.isPaused) { _streamBluetooth?.resume(); } } - await checkAllRequirements(); + await checkAllRequirements();*/ } else if (state == AppLifecycleState.paused) { - _streamBluetooth?.pause(); + //_streamBluetooth?.pause(); } } @@ -352,6 +353,39 @@ class _ConfigurationPageState extends State with WidgetsBindi body: Body(configuration: widget.configuration), floatingActionButton: Stack( children: [ + if (visitAppContext.applicationInstanceDTO?.isAssistant == true) + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(right: 90, bottom: 1), + child: FloatingActionButton( + heroTag: 'assistant_config', + backgroundColor: kMainColor1, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AssistantChatSheet( + visitAppContext: visitAppContext, + configurationId: widget.configuration.id, + onNavigateToSection: (sectionId, sectionTitle) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => SectionPage( + configuration: widget.configuration, + rawSection: null, + visitAppContextIn: visitAppContext, + sectionId: sectionId, + ), + )); + }, + ), + ); + }, + child: const Icon(Icons.chat_bubble_outline, color: Colors.white), + ), + ), + ), visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == visitAppContext.configuration!.id).isNotEmpty ? Align( alignment: Alignment.bottomRight, child: Padding( @@ -360,7 +394,7 @@ class _ConfigurationPageState extends State with WidgetsBindi onTap: () async { bool isCancel = false; - if(!controller.authorizationStatusOk) { + /*if(!controller.authorizationStatusOk) { //await handleOpenLocationSettings(); await showDialog( @@ -469,12 +503,12 @@ class _ConfigurationPageState extends State with WidgetsBindi } if(!visitAppContext.isScanningBeacons) { print("Start Scan"); - print(_streamRanging); + /*print(_streamRanging); if (_streamRanging != null) { _streamRanging?.resume(); } else { await initScanBeacon(visitAppContext); - } + }*/ controller.startScanning(); visitAppContext.isScanningBeacons = true; @@ -486,7 +520,7 @@ class _ConfigurationPageState extends State with WidgetsBindi visitAppContext.isScanningBeacons = false; appContext.setContext(visitAppContext); } - } + }*/ }, child: Container( decoration: BoxDecoration( @@ -512,7 +546,7 @@ class _ConfigurationPageState extends State with WidgetsBindi handleOpenLocationSettings() async { if (Platform.isAndroid) { - await flutterBeacon.openLocationSettings; + //await flutterBeacon.openLocationSettings; } else if (Platform.isIOS) { await showDialog( context: context, @@ -537,7 +571,7 @@ class _ConfigurationPageState extends State with WidgetsBindi handleOpenBluetooth() async { if (Platform.isAndroid) { try { - await flutterBeacon.openBluetoothSettings; + //await flutterBeacon.openBluetoothSettings; } on PlatformException catch (e) { print(e); } diff --git a/lib/Screens/Home/home_3.0.dart b/lib/Screens/Home/home_3.0.dart index f9af7ab..4f2184c 100644 --- a/lib/Screens/Home/home_3.0.dart +++ b/lib/Screens/Home/home_3.0.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; @@ -7,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart'; import 'package:mymuseum_visitapp/Components/CustomAppBar.dart'; import 'package:mymuseum_visitapp/Components/LanguageSelection.dart'; import 'package:mymuseum_visitapp/Components/ScannerBouton.dart'; @@ -21,11 +21,11 @@ import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart'; import 'package:mymuseum_visitapp/Services/apiService.dart'; import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart'; +import 'package:mymuseum_visitapp/Services/statisticsService.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:provider/provider.dart'; -import 'configurations_list.dart'; class HomePage3 extends StatefulWidget { const HomePage3({Key? key}) : super(key: key); @@ -51,6 +51,102 @@ class _HomePage3State extends State with WidgetsBindingObserver { _futureConfigurations = getConfigurationsCall(appContext); } + Widget _buildCard(BuildContext context, int index) { + final lang = visitAppContext.language ?? "FR"; + final config = configurations[index]; + final titleEntry = config.title?.firstWhere( + (t) => t.language == lang, + orElse: () => config.title!.first, + ); + final cleanedTitle = (titleEntry?.value ?? '').replaceAll('\n', ' ').replaceAll('
', ' '); + + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ConfigurationPage( + configuration: config, + isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed, + ), + )); + }, + child: Hero( + tag: config.id!, + child: Material( + type: MaterialType.transparency, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + color: kSecondGrey, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black38, + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + image: config.imageSource != null + ? DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(config.imageSource!), + ) + : null, + ), + child: Stack( + children: [ + // Gradient overlay for text readability + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.72), + ], + stops: const [0.4, 1.0], + ), + ), + ), + ), + // Title at bottom-left + Positioned( + bottom: 10, + left: 10, + right: 28, + child: HtmlWidget( + cleanedTitle, + textStyle: const TextStyle( + color: Colors.white, + fontFamily: 'Roboto', + fontSize: 14, + fontWeight: FontWeight.w600, + ), + customStylesBuilder: (_) => { + 'font-family': 'Roboto', + '-webkit-line-clamp': '2', + }, + ), + ), + // Chevron + const Positioned( + bottom: 10, + right: 8, + child: Icon(Icons.chevron_right, size: 18, color: Colors.white), + ), + ], + ), + ), + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; @@ -60,56 +156,40 @@ class _HomePage3State extends State with WidgetsBindingObserver { return Scaffold( extendBody: true, body: FutureBuilder( - future: _futureConfigurations,//getConfigurationsCall(visitAppContext.clientAPI, appContext), + future: _futureConfigurations, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { - configurations = List.from(snapshot.data).where((configuration) => configuration.isMobile!).toList(); + configurations = List.from(snapshot.data) + .where((c) => c.isMobile!) + .toList(); - //String cleanedTitle = configurations[0].title.replaceAll('\n', ' ').replaceAll('
', ' '); // TODO + final layoutType = visitAppContext.applicationInstanceDTO?.layoutMainPage; + final isMasonry = layoutType == null || + layoutType.value == LayoutMainPageType.MasonryGrid.value; + + final lang = visitAppContext.language ?? "FR"; + final headerTitleEntry = configurations.isNotEmpty + ? configurations[0].title?.firstWhere( + (t) => t.language == lang, + orElse: () => configurations[0].title!.first, + ) + : null; return Stack( children: [ - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.grey, Colors.lightGreen], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - ), - /*SizedBox( - height: size.height * 0.35, - width: size.width, - child: Image.network( - configurations[2].imageSource!, - fit: BoxFit.cover, - ) - ),*/ + // Dark background + const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()), SafeArea( top: false, bottom: false, child: CustomScrollView( slivers: [ - /*SliverAppBar( - backgroundColor: Colors.transparent, - pinned: true, - expandedHeight: 200.0, - collapsedHeight: 100.0, - flexibleSpace: FlexibleSpaceBar( - collapseMode: CollapseMode.none, // 👈 Optionnel pour éviter le fade - background: Image.network( - configurations[2].imageSource!, - fit: BoxFit.cover, - ), - ), - ),*/ SliverAppBar( backgroundColor: Colors.transparent, pinned: false, expandedHeight: 235.0, flexibleSpace: FlexibleSpaceBar( - collapseMode: CollapseMode.pin, // 👈 Optionnel pour éviter le fade + collapseMode: CollapseMode.pin, centerTitle: true, background: Container( padding: const EdgeInsets.only(bottom: 25.0), @@ -120,10 +200,10 @@ class _HomePage3State extends State with WidgetsBindingObserver { ), boxShadow: [ BoxShadow( - color: Colors.black26, - spreadRadius: 0.35, - blurRadius: 2, - offset: Offset(0, -22), + color: Colors.black38, + spreadRadius: 0.5, + blurRadius: 8, + offset: Offset(0, 4), ), ], ), @@ -135,12 +215,21 @@ class _HomePage3State extends State with WidgetsBindingObserver { child: Stack( fit: StackFit.expand, children: [ - Opacity( - opacity: 0.85, - child: Image.network( + if (configurations.isNotEmpty && configurations[0].imageSource != null) + Image.network( configurations[0].imageSource!, fit: BoxFit.cover, ), + // Bottom gradient for title readability + const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black54], + stops: [0.5, 1.0], + ), + ), ), Positioned( top: 35, @@ -148,7 +237,7 @@ class _HomePage3State extends State with WidgetsBindingObserver { child: SizedBox( width: 75, height: 75, - child: LanguageSelection() + child: LanguageSelection(), ), ), ], @@ -156,240 +245,111 @@ class _HomePage3State extends State with WidgetsBindingObserver { ), ), title: SizedBox( - width: /*widget.isHomeButton ?*/ size.width * 1.0 /*: null*/, + width: size.width * 1.0, height: 120, child: Center( - child: HtmlWidget( - configurations[0].title!.firstWhere((t) => t.language == "FR").value!, - textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20), - customStylesBuilder: (element) - { - return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"}; - }, - ), + child: headerTitleEntry != null + ? HtmlWidget( + headerTitleEntry.value!, + textStyle: const TextStyle( + color: Colors.white, + fontFamily: 'Roboto', + fontSize: 20, + fontWeight: FontWeight.bold, + ), + customStylesBuilder: (_) => { + 'text-align': 'center', + 'font-family': 'Roboto', + '-webkit-line-clamp': '2', + }, + ) + : const SizedBox(), ), ), - ), // plus de FlexibleSpaceBar - ), - SliverPadding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0, bottom: 20.0), - sliver: SliverMasonryGrid.count( - crossAxisCount: 2, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childCount: configurations.length, - itemBuilder: (context, index) { - //return buildTile(configurations[index]); - String cleanedTitle = configurations[index].title!.firstWhere((t) => t.language == "FR").value!.replaceAll('\n', ' ').replaceAll('
', ' '); - - return InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => - ConfigurationPage(configuration: configurations[index], isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed), - )); - }, - child: Hero( - tag: configurations[index].id!, - child: Material( - type: MaterialType.transparency, - child: Container( - height: 200 + (index % 3) * 55, - decoration: BoxDecoration( - color: kSecondGrey, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: kMainGrey, - spreadRadius: 0.5, - blurRadius: 5, - offset: Offset(0, 1), // changes position of shadow - ), - ], - image: configurations[index].imageSource != null ? DecorationImage( - fit: BoxFit.cover, - opacity: 0.5, - image: NetworkImage( - configurations[index].imageSource!, - ), - ): null, - ), - child: Stack( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: HtmlWidget( - cleanedTitle, - textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20), - customStylesBuilder: (element) - { - return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"}; - }, - ), - ), - ), - Positioned( - bottom: 10, - right: 10, - child: Container( - width: 20, - height: 20, - decoration: const BoxDecoration( - //color: kMainColor, - shape: BoxShape.circle, - ), - child: const Icon(Icons.chevron_right, size: 18, color: Colors.white) - ), - ) - ], - ), - ), - ), - ), - ); - }, ), ), - - /*SliverToBoxAdapter( - child: Image.network(configurations[0].imageSource!), // ou NetworkImage - ),*/ - + SliverPadding( + padding: const EdgeInsets.only( + left: 8.0, right: 8.0, top: 8.0, bottom: 20.0), + sliver: isMasonry + ? SliverMasonryGrid.count( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childCount: configurations.length, + itemBuilder: (context, index) => SizedBox( + height: 160 + (index % 3) * 50, + child: _buildCard(context, index), + ), + ) + : SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.82, + ), + delegate: SliverChildBuilderDelegate( + _buildCard, + childCount: configurations.length, + ), + ), + ), ], ), ), - /*FutureBuilder( - future: getConfigurationsCall(visitAppContext.clientAPI, appContext), - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - - - return /*RefreshIndicator ( - onRefresh: () { - setState(() {}); - return Future(() => null); - }, - color: kSecondColor, - child:*/ ;/*ConfigurationsList( - alreadyDownloaded: alreadyDownloaded, - configurations: configurations, - requestRefresh: () { - setState(() {}); // For refresh + if (visitAppContext.applicationInstanceDTO?.isAssistant == true) + Positioned( + bottom: 24, + right: 16, + child: FloatingActionButton( + heroTag: 'assistant_home', + backgroundColor: kMainColor1, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AssistantChatSheet( + visitAppContext: visitAppContext, + onNavigateToSection: (configurationId, _) { + final config = configurations + .where((c) => c.id == configurationId) + .firstOrNull; + if (config != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ConfigurationPage( + configuration: config, + isAlreadyAllowed: + visitAppContext.isScanBeaconAlreadyAllowed, + ), + ), + ); + } }, - ),*/ - //); - } - } - ),*/ + ), + ); + }, + child: const Icon(Icons.chat_bubble_outline, color: Colors.white), + ), + ), ], ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(TranslationHelper.getFromLocale("noData", appContext.getContext())); } else { return Center( - child: SizedBox( - height: size.height * 0.15, - child: const LoadingCommon() - ) + child: SizedBox( + height: size.height * 0.15, + child: const LoadingCommon(), + ), ); } - } - ), - ); - - return Scaffold( - /*appBar: CustomAppBar( - title: TranslationHelper.getFromLocale("visitTitle", appContext.getContext()), - isHomeButton: false, - ),*/ - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox( - width: size.width, - height: size.height, - child: FutureBuilder( - future: getConfigurationsCall(appContext), - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - configurations = List.from(snapshot.data).where((configuration) => configuration.isMobile!).toList(); - return RefreshIndicator ( - onRefresh: () { - setState(() {}); - return Future(() => null); - }, - color: kSecondColor, - child: ConfigurationsList( - alreadyDownloaded: alreadyDownloaded, - configurations: configurations, - requestRefresh: () { - setState(() {}); // For refresh - }, - ), - ); - } else if (snapshot.connectionState == ConnectionState.none) { - return Text(TranslationHelper.getFromLocale("noData", appContext.getContext())); - } else { - return Center( - child: Container( - height: size.height * 0.15, - child: LoadingCommon() - ) - ); - } - } - ), - ), - /*InkWell( - onTap: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return TestAR(); - //return XRWithQRScannerPage(); - }, - ), - ); - - }, - child: const SizedBox( - height: 50, - width: 10, - child: Text('TEST XR'), - ), - ),*/ - ], - ) - ), - // floatingActionButton: ScannerBouton(appContext: appContext), - //floatingActionButtonLocation: FloatingActionButtonLocation.miniCenterFloat, - /*bottomNavigationBar: BottomNavigationBar( - currentIndex: currentIndex, - onTap: (index) { - setState(() { - currentIndex = index; - }); - - if (currentIndex == 0) { - controller.startScanning(); - } else { - controller.pauseScanning(); - controller.startBroadcasting(); - } }, - items: [ - BottomNavigationBarItem( - icon: Icon(Icons.list), - label: 'Scan', - ), - BottomNavigationBarItem( - icon: Icon(Icons.bluetooth_audio), - label: 'Broadcast', - ), - ], - ),*/ + ), ); } @@ -397,9 +357,14 @@ class _HomePage3State extends State with WidgetsBindingObserver { bool isOnline = await hasNetwork(); VisitAppContext visitAppContext = appContext.getContext(); + + isOnline = true; // Todo remove if not local test List? configurations; configurations = List.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations)); alreadyDownloaded = configurations.map((c) => c.id).toList(); + print("GOT configurations from LOCAL"); + print(configurations.length); + print(configurations); if(!isOnline) { ScaffoldMessenger.of(context).showSnackBar( @@ -449,6 +414,30 @@ class _HomePage3State extends State with WidgetsBindingObserver { } } + // Charge l'ApplicationInstance Mobile pour savoir si l'assistant/statistiques sont activés + if (visitAppContext.applicationInstanceDTO == null && visitAppContext.instanceId != null) { + try { + final instances = await visitAppContext.clientAPI.applicationInstanceApi! + .applicationInstanceGet(instanceId: visitAppContext.instanceId); + final mobileInstance = instances?.where((e) => e.appType == AppType.Mobile).firstOrNull; + if (mobileInstance != null) { + visitAppContext.applicationInstanceDTO = mobileInstance; + if (mobileInstance.isStatistic ?? false) { + visitAppContext.statisticsService = StatisticsService( + clientAPI: visitAppContext.clientAPI, + instanceId: visitAppContext.instanceId, + configurationId: visitAppContext.configuration?.id, + appType: 'Mobile', + language: visitAppContext.language, + ); + } + } + appContext.setContext(visitAppContext); + } catch (e) { + print("Could not load applicationInstance: $e"); + } + } + return await ApiService.getConfigurations(visitAppContext.clientAPI, visitAppContext); } } \ No newline at end of file diff --git a/lib/Screens/Sections/Agenda/agenda_page.dart b/lib/Screens/Sections/Agenda/agenda_page.dart index 63021b9..0466d89 100644 --- a/lib/Screens/Sections/Agenda/agenda_page.dart +++ b/lib/Screens/Sections/Agenda/agenda_page.dart @@ -130,6 +130,11 @@ class _AgendaPage extends State { return GestureDetector( onTap: () { print("${eventAgenda.name}"); + (Provider.of(context, listen: false).getContext() as VisitAppContext) + .statisticsService?.track( + VisitEventType.agendaEventTap, + metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name}, + ); showDialog( context: context, builder: (BuildContext context) { diff --git a/lib/Screens/Sections/Puzzle/correct_overlay.dart b/lib/Screens/Sections/Game/correct_overlay.dart similarity index 100% rename from lib/Screens/Sections/Puzzle/correct_overlay.dart rename to lib/Screens/Sections/Game/correct_overlay.dart diff --git a/lib/Screens/Sections/Puzzle/puzzle_page.dart b/lib/Screens/Sections/Game/game_page.dart similarity index 97% rename from lib/Screens/Sections/Puzzle/puzzle_page.dart rename to lib/Screens/Sections/Game/game_page.dart index 9c650db..677dd90 100644 --- a/lib/Screens/Sections/Puzzle/puzzle_page.dart +++ b/lib/Screens/Sections/Game/game_page.dart @@ -30,6 +30,7 @@ class _PuzzlePage extends State { int allInPlaceCount = 0; bool isFinished = false; + DateTime? _puzzleStartTime; GlobalKey _widgetKey = GlobalKey(); Size? realWidgetSize; List pieces = []; @@ -42,6 +43,7 @@ class _PuzzlePage extends State { puzzleDTO = widget.section; puzzleDTO.rows = puzzleDTO.rows ?? 3; puzzleDTO.cols = puzzleDTO.cols ?? 3; + _puzzleStartTime = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) async { Size size = MediaQuery.of(context).size; @@ -196,6 +198,11 @@ class _PuzzlePage extends State { Size size = MediaQuery.of(context).size; final appContext = Provider.of(context, listen: false); VisitAppContext visitAppContext = appContext.getContext(); + final duration = _puzzleStartTime != null ? DateTime.now().difference(_puzzleStartTime!).inSeconds : 0; + visitAppContext.statisticsService?.track( + VisitEventType.gameComplete, + metadata: {'gameType': 'Puzzle', 'durationSeconds': duration}, + ); TranslationAndResourceDTO? messageFin = puzzleDTO.messageFin != null && puzzleDTO.messageFin!.isNotEmpty ? puzzleDTO.messageFin!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null; if(messageFin != null) { diff --git a/lib/Screens/Sections/Puzzle/message_dialog.dart b/lib/Screens/Sections/Game/message_dialog.dart similarity index 100% rename from lib/Screens/Sections/Puzzle/message_dialog.dart rename to lib/Screens/Sections/Game/message_dialog.dart diff --git a/lib/Screens/Sections/Puzzle/puzzle_piece.dart b/lib/Screens/Sections/Game/puzzle_piece.dart similarity index 100% rename from lib/Screens/Sections/Puzzle/puzzle_piece.dart rename to lib/Screens/Sections/Game/puzzle_piece.dart diff --git a/lib/Screens/Sections/Puzzle/score_widget.dart b/lib/Screens/Sections/Game/score_widget.dart similarity index 100% rename from lib/Screens/Sections/Puzzle/score_widget.dart rename to lib/Screens/Sections/Game/score_widget.dart diff --git a/lib/Screens/Sections/Map/google_map_view.dart b/lib/Screens/Sections/Map/google_map_view.dart index b7c09e0..053a382 100644 --- a/lib/Screens/Sections/Map/google_map_view.dart +++ b/lib/Screens/Sections/Map/google_map_view.dart @@ -69,6 +69,14 @@ class _GoogleMapViewState extends State { mapContext.setSelectedPoint(point); //mapContext.setSelectedPointForNavigate(point); //}); + (Provider.of(context, listen: false).getContext() as VisitAppContext) + .statisticsService?.track( + VisitEventType.mapPoiTap, + metadata: { + 'geoPointId': point.id, + 'geoPointTitle': parse(textSansHTML.body!.text).documentElement!.text, + }, + ); }, infoWindow: InfoWindow.noText)); } diff --git a/lib/Screens/Sections/Menu/menu_page.dart b/lib/Screens/Sections/Menu/menu_page.dart index e5cdcd2..c38d9fc 100644 --- a/lib/Screens/Sections/Menu/menu_page.dart +++ b/lib/Screens/Sections/Menu/menu_page.dart @@ -206,6 +206,10 @@ class _MenuPageState extends State { //SectionDTO? section = await (appContext.getContext() as TabletAppContext).clientAPI!.sectionApi!.sectionGetDetail(menuDTO.sections![index].id!); SectionDTO section = value[index]; var rawSectionData = rawSubSectionsData[index]; + (appContext.getContext() as VisitAppContext).statisticsService?.track( + VisitEventType.menuItemTap, + metadata: {'targetSectionId': section.id, 'menuItemTitle': section.title?.firstOrNull?.value}, + ); Navigator.push( context, SlideFromRightRoute(page: SectionPage( diff --git a/lib/Screens/Sections/Quiz/quizz_page.dart b/lib/Screens/Sections/Quiz/quizz_page.dart index 56f1850..e3221d9 100644 --- a/lib/Screens/Sections/Quiz/quizz_page.dart +++ b/lib/Screens/Sections/Quiz/quizz_page.dart @@ -23,11 +23,12 @@ import 'package:mymuseum_visitapp/constants.dart'; import 'package:provider/provider.dart'; class QuizPage extends StatefulWidget { - const QuizPage({Key? key, required this.visitAppContextIn, required this.quizDTO, required this.resourcesModel}) : super(key: key); + const QuizPage({Key? key, required this.visitAppContextIn, required this.quizDTO, required this.resourcesModel, this.sectionId}) : super(key: key); final QuizDTO quizDTO; final VisitAppContext visitAppContextIn; final List resourcesModel; + final String? sectionId; @override State createState() => _QuizPageState(); @@ -161,6 +162,11 @@ class _QuizPageState extends State { } } log("goodResponses =" + goodResponses.toString()); + widget.visitAppContextIn.statisticsService?.track( + VisitEventType.quizComplete, + sectionId: widget.sectionId, + metadata: {'score': goodResponses, 'totalQuestions': widget.quizDTO.questions!.length}, + ); List levelToShow = []; var test = goodResponses/widget.quizDTO.questions!.length; diff --git a/lib/Screens/section_page.dart b/lib/Screens/section_page.dart index f7da8fa..d9feaf3 100644 --- a/lib/Screens/section_page.dart +++ b/lib/Screens/section_page.dart @@ -20,12 +20,13 @@ import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Map/map_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Menu/menu_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/PDF/pdf_page.dart'; -import 'package:mymuseum_visitapp/Screens/Sections/Puzzle/puzzle_page.dart'; +import 'package:mymuseum_visitapp/Screens/Sections/Game/game_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Slider/slider_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Video/video_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Weather/weather_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Web/web_page.dart'; +import 'package:mymuseum_visitapp/Services/statisticsService.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/client.dart'; import 'package:provider/provider.dart'; @@ -57,16 +58,34 @@ class _SectionPageState extends State { List>? icons; String? mainAudioId; + DateTime? _sectionOpenTime; @override void initState() { widget.visitAppContextIn.isContentCurrentlyShown = true; + _sectionOpenTime = DateTime.now(); super.initState(); + // Track SectionView (fire-and-forget) + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.visitAppContextIn.statisticsService?.track( + VisitEventType.sectionView, + sectionId: widget.sectionId, + ); + }); } @override void dispose() { visitAppContext.isContentCurrentlyShown = false; + // Track SectionLeave with duration + final duration = _sectionOpenTime != null + ? DateTime.now().difference(_sectionOpenTime!).inSeconds + : null; + widget.visitAppContextIn.statisticsService?.track( + VisitEventType.sectionLeave, + sectionId: widget.sectionId, + durationSeconds: duration, + ); super.dispose(); } @@ -124,6 +143,7 @@ class _SectionPageState extends State { visitAppContextIn: widget.visitAppContextIn, quizDTO: quizDTO, resourcesModel: resourcesModel, + sectionId: widget.sectionId, ); case SectionType.Slider: SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!; diff --git a/lib/Services/apiService.dart b/lib/Services/apiService.dart index de751b8..df9d4bb 100644 --- a/lib/Services/apiService.dart +++ b/lib/Services/apiService.dart @@ -15,7 +15,7 @@ class ApiService { try { List? configurations; bool isOnline = await hasNetwork(); - if(isOnline) { + if(true) { // TODO isOnline configurations = await client.configurationApi!.configurationGet(instanceId: visitAppContext?.instanceId); if(configurations != null) { diff --git a/lib/Services/assistantService.dart b/lib/Services/assistantService.dart new file mode 100644 index 0000000..ca7631d --- /dev/null +++ b/lib/Services/assistantService.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/Models/AssistantResponse.dart'; + +class AiChatMessage { + final String role; + final String content; + AiChatMessage({required this.role, required this.content}); + Map toJson() => {'role': role, 'content': content}; +} + +class AssistantService { + final VisitAppContext visitAppContext; + final List history = []; + + AssistantService({required this.visitAppContext}); + + Future chat({ + required String message, + String? configurationId, + }) async { + final baseUrl = visitAppContext.clientAPI.apiApi?.basePath ?? 'https://api.mymuseum.be'; + + final body = { + 'message': message, + 'instanceId': visitAppContext.instanceId, + 'appType': 'Mobile', + 'configurationId': configurationId, + 'language': visitAppContext.language?.toUpperCase() ?? 'FR', + 'history': history.map((m) => m.toJson()).toList(), + }; + + final response = await http.post( + Uri.parse('$baseUrl/api/ai/chat'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final result = AssistantResponse.fromJson(data); + history.add(AiChatMessage(role: 'user', content: message)); + history.add(AiChatMessage(role: 'assistant', content: result.reply)); + return result; + } else { + throw Exception('Erreur assistant: ${response.statusCode}'); + } + } + + void clearHistory() => history.clear(); +} diff --git a/lib/Services/statisticsService.dart b/lib/Services/statisticsService.dart new file mode 100644 index 0000000..cd4ec33 --- /dev/null +++ b/lib/Services/statisticsService.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/client.dart'; + +class StatisticsService { + final Client clientAPI; + final String? instanceId; + final String? configurationId; + final String? appType; // "Mobile" or "Tablet" + final String? language; + final String sessionId; + + StatisticsService({ + required this.clientAPI, + required this.instanceId, + required this.configurationId, + this.appType = 'Mobile', + this.language, + }) : sessionId = _generateSessionId(); + + static String _generateSessionId() { + final rand = Random(); + final bytes = List.generate(16, (_) => rand.nextInt(256)); + return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + } + + Future track( + String eventType, { + String? sectionId, + int? durationSeconds, + Map? metadata, + }) async { + try { + await clientAPI.statsApi!.statsTrackEvent(VisitEventDTO( + instanceId: instanceId ?? '', + configurationId: configurationId, + sectionId: sectionId, + sessionId: sessionId, + eventType: eventType, + appType: appType, + language: language, + durationSeconds: durationSeconds, + metadata: metadata != null ? jsonEncode(metadata) : null, + timestamp: DateTime.now(), + )); + } catch (_) { + // fire-and-forget — never block the UI on stats errors + } + } +} diff --git a/lib/client.dart b/lib/client.dart index 886f6d4..a089a6e 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -25,6 +25,15 @@ class Client { DeviceApi? _deviceApi; DeviceApi? get deviceApi => _deviceApi; + InstanceApi? _instanceApi; + InstanceApi? get instanceApi => _instanceApi; + + ApplicationInstanceApi? _applicationInstanceApi; + ApplicationInstanceApi? get applicationInstanceApi => _applicationInstanceApi; + + StatsApi? _statsApi; + StatsApi? get statsApi => _statsApi; + Client(String path) { _apiClient = ApiClient( basePath: path); // "http://192.168.31.96" @@ -35,5 +44,8 @@ class Client { _sectionApi = SectionApi(_apiClient); _resourceApi = ResourceApi(_apiClient); _deviceApi = DeviceApi(_apiClient); + _instanceApi = InstanceApi(_apiClient); + _applicationInstanceApi = ApplicationInstanceApi(_apiClient); + _statsApi = StatsApi(_apiClient); } } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..e878925 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_fr.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('fr') + ]; + + /// No description provided for @visitTitle. + /// + /// In en, this message translates to: + /// **'List of tours'** + String get visitTitle; + + /// No description provided for @visitDownloadWarning. + /// + /// In en, this message translates to: + /// **'To follow this tour, you must first download it'** + String get visitDownloadWarning; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fr'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'fr': + return AppLocalizationsFr(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..c26d6c7 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,17 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get visitTitle => 'List of tours'; + + @override + String get visitDownloadWarning => + 'To follow this tour, you must first download it'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..33334ef --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,17 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get visitTitle => 'Liste des visites'; + + @override + String get visitDownloadWarning => + 'Pour suivre cette visite, il faut d\'abord la télécharger'; +} diff --git a/lib/main.dart b/lib/main.dart index 9702dd7..9c354eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,20 +3,21 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_beacon/flutter_beacon.dart'; +// TODO // import 'package:flutter_beacon/flutter_beacon.dart'; import 'package:get/get.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; import 'package:mymuseum_visitapp/Models/articleRead.dart'; import 'package:mymuseum_visitapp/Screens/Home/home.dart'; import 'package:mymuseum_visitapp/Screens/Home/home_3.0.dart'; +import 'package:mymuseum_visitapp/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'Helpers/DatabaseHelper.dart'; import 'Models/visitContext.dart'; import 'app_context.dart'; import 'constants.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'l10n/app_localizations.dart'; import 'package:path_provider/path_provider.dart'; void main() async { diff --git a/pubspec.lock b/pubspec.lock index b34dca3..9e0c95b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -326,14 +326,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_beacon: - dependency: "direct main" - description: - name: flutter_beacon - sha256: c93ee7a2e572a2dba31eb8db9f7a2c514b2b3e22584ea7d78d8cc854fdbded79 - url: "https://pub.dev" - source: hosted - version: "0.5.1" flutter_cache_manager: dependency: transitive description: @@ -673,10 +665,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -745,26 +737,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -800,7 +792,7 @@ packages: manager_api_new: dependency: "direct main" description: - path: manager_api_new + path: "../manager-app/manager_api_new" relative: true source: path version: "1.0.0" @@ -816,10 +808,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -832,10 +824,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mgrs_dart: dependency: transitive description: @@ -928,10 +920,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -992,42 +984,50 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "10.3.6" + version: "13.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "4.3.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.1" petitparser: dependency: transitive description: @@ -1285,18 +1285,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1341,10 +1341,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.6" timing: dependency: transitive description: @@ -1493,10 +1493,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" video_player: dependency: transitive description: @@ -1690,5 +1690,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index db26dec..8b6981f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,11 +40,11 @@ dependencies: carousel_slider: ^5.0.0 #flutter_svg_provider: ^1.0.3 photo_view: ^0.15.0 - intl: ^0.19.0 + intl: ^0.20.2 #audioplayers: ^2.0.0 just_audio: ^0.9.38 get: ^4.6.6 - permission_handler: ^10.4.5 #^11.3.1 + permission_handler: 12.0.1 #^11.3.1 diacritic: ^0.1.3 flutter_widget_from_html: ^0.15.2 webview_flutter: ^4.10.0 @@ -60,7 +60,7 @@ dependencies: sqflite: #not in web just_audio_cache: ^0.1.2 #not in web - flutter_beacon: ^0.5.1 #not in web + #flutter_beacon: ^0.5.1 #not in web flutter_staggered_grid_view: ^0.7.0 smooth_page_indicator: ^1.2.1 @@ -73,7 +73,7 @@ dependencies: url_launcher: ^6.3.1 manager_api_new: - path: manager_api_new + path: ../manager-app/manager_api_new # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 @@ -140,3 +140,6 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + +flutter_localizations: + sdk: flutter diff --git a/release/fortSaintHeribert_1_0_2.aab b/release/fortSaintHeribert_1_0_2.aab deleted file mode 100644 index 2999f52..0000000 Binary files a/release/fortSaintHeribert_1_0_2.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_3.aab b/release/fortSaintHeribert_1_0_3.aab deleted file mode 100644 index 4f01c4f..0000000 Binary files a/release/fortSaintHeribert_1_0_3.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_4.aab b/release/fortSaintHeribert_1_0_4.aab deleted file mode 100644 index fa00bb9..0000000 Binary files a/release/fortSaintHeribert_1_0_4.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_5.aab b/release/fortSaintHeribert_1_0_5.aab deleted file mode 100644 index 28e4b8f..0000000 Binary files a/release/fortSaintHeribert_1_0_5.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_6.aab b/release/fortSaintHeribert_1_0_6.aab deleted file mode 100644 index 313c730..0000000 Binary files a/release/fortSaintHeribert_1_0_6.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_7.aab b/release/fortSaintHeribert_1_0_7.aab deleted file mode 100644 index 9b12c9d..0000000 Binary files a/release/fortSaintHeribert_1_0_7.aab and /dev/null differ diff --git a/release/fortSaintHeribert_1_0_8.aab b/release/fortSaintHeribert_1_0_8.aab deleted file mode 100644 index 5fb5b3f..0000000 Binary files a/release/fortSaintHeribert_1_0_8.aab and /dev/null differ diff --git a/release/mdlf_2_0_0.aab b/release/mdlf_2_0_0.aab deleted file mode 100644 index 3dd7c50..0000000 Binary files a/release/mdlf_2_0_0.aab and /dev/null differ diff --git a/release/mdlf_2_0_1.aab b/release/mdlf_2_0_1.aab deleted file mode 100644 index bcbe037..0000000 Binary files a/release/mdlf_2_0_1.aab and /dev/null differ diff --git a/release/mymuseum_1_0_0.aab b/release/mymuseum_1_0_0.aab deleted file mode 100644 index 4b0b39a..0000000 Binary files a/release/mymuseum_1_0_0.aab and /dev/null differ diff --git a/release/mymuseum_1_0_1_6.aab b/release/mymuseum_1_0_1_6.aab deleted file mode 100644 index 62f232f..0000000 Binary files a/release/mymuseum_1_0_1_6.aab and /dev/null differ diff --git a/release/mymuseum_1_0_3.aab b/release/mymuseum_1_0_3.aab deleted file mode 100644 index 55a70ba..0000000 Binary files a/release/mymuseum_1_0_3.aab and /dev/null differ