Update gradle, remove bluetooth beacon (gralde update etc..) + ai assistant (wip) + stats + layout update + clean repo (remove old release) + link to unique backend generation managerapp (TO BE TESTED)

This commit is contained in:
Thomas Fransolet 2026-03-13 15:15:18 +01:00
parent c6599e13c9
commit 538e677993
44 changed files with 1133 additions and 358 deletions

View File

@ -40,7 +40,8 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"*/
android { android {
namespace = "be.unov.mymuseum.fortsaintheribert" namespace = "be.unov.mymuseum.fortsaintheribert"
compileSdkVersion 35 compileSdkVersion 36
ndkVersion "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -70,6 +71,10 @@ android {
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true multiDexEnabled true
/*ndk {
abiFilters "arm64-v8a"
}*/
} }
signingConfigs { signingConfigs {

View File

@ -1,3 +1,5 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.experimental.enable16kApk=true
android.useNewNativePlugin=true

View File

@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@ -31,8 +31,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false id "com.android.application" version "8.9.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false
} }
include ":app" include ":app"

View File

@ -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<AssistantChatSheet> createState() => _AssistantChatSheetState();
}
class _AssistantChatSheetState extends State<AssistantChatSheet> {
late AssistantService _assistantService;
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
final List<Widget> _bubbles = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_assistantService = AssistantService(visitAppContext: widget.visitAppContext);
}
Future<void> _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),
),
],
),
),
],
),
);
}
}

View File

@ -128,11 +128,13 @@ class _ScannerDialogState extends State<ScannerDialog> {
VisitAppContext visitAppContext = widget.appContext!.getContext(); VisitAppContext visitAppContext = widget.appContext!.getContext();
if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) { if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) {
visitAppContext.statisticsService?.track(VisitEventType.qrScan, metadata: {'valid': false, 'sectionId': sectionId});
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2), SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2),
); );
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
visitAppContext.statisticsService?.track(VisitEventType.qrScan, sectionId: sectionId, metadata: {'valid': true});
dynamic rawSection = visitAppContext.currentSections!.firstWhere((cs) => cs!['id'] == sectionId)!; dynamic rawSection = visitAppContext.currentSections!.firstWhere((cs) => cs!['id'] == sectionId)!;
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.push( Navigator.push(

View File

@ -1,16 +1,16 @@
import 'package:flutter_beacon/flutter_beacon.dart'; // TODO // import 'package:flutter_beacon/flutter_beacon.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class RequirementStateController extends GetxController { class RequirementStateController extends GetxController {
var bluetoothState = BluetoothState.stateOff.obs; var bluetoothState = false; //BluetoothState.stateOff.obs;
var authorizationStatus = AuthorizationStatus.notDetermined.obs; var authorizationStatus = false; //AuthorizationStatus.notDetermined.obs;
var locationService = false.obs; var locationService = false.obs;
var _startBroadcasting = false.obs; var _startBroadcasting = false.obs;
var _startScanning = false.obs; var _startScanning = false.obs;
var _pauseScanning = false.obs; var _pauseScanning = false.obs;
bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn; /*bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
bool get authorizationStatusOk => bool get authorizationStatusOk =>
authorizationStatus.value == AuthorizationStatus.allowed || authorizationStatus.value == AuthorizationStatus.allowed ||
authorizationStatus.value == AuthorizationStatus.always; authorizationStatus.value == AuthorizationStatus.always;
@ -22,7 +22,7 @@ class RequirementStateController extends GetxController {
updateAuthorizationStatus(AuthorizationStatus status) { updateAuthorizationStatus(AuthorizationStatus status) {
authorizationStatus.value = status; authorizationStatus.value = status;
} }*/
updateLocationService(bool flag) { updateLocationService(bool flag) {
locationService.value = flag; locationService.value = flag;

View File

@ -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<String, dynamic> 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<String, dynamic> json) =>
AssistantNavigationAction(
sectionId: json['sectionId'] as String? ?? '',
sectionTitle: json['sectionTitle'] as String? ?? '',
sectionType: json['sectionType'] as String? ?? '',
);
}
class AssistantResponse {
final String reply;
final List<AiCard>? cards;
final AssistantNavigationAction? navigation;
const AssistantResponse({
required this.reply,
this.cards,
this.navigation,
});
factory AssistantResponse.fromJson(Map<String, dynamic> json) =>
AssistantResponse(
reply: json['reply'] as String? ?? '',
cards: (json['cards'] as List<dynamic>?)
?.map((e) => AiCard.fromJson(e as Map<String, dynamic>))
.toList(),
navigation: json['navigation'] != null
? AssistantNavigationAction.fromJson(
json['navigation'] as Map<String, dynamic>)
: null,
);
}

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:mymuseum_visitapp/Models/articleRead.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/beaconSection.dart';
import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.dart';
import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/client.dart';
class VisitAppContext with ChangeNotifier { 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? id = "";
String? language = ""; String? language = "";
@ -27,6 +28,9 @@ class VisitAppContext with ChangeNotifier {
List<ResourceModel> audiosNotWorking = []; List<ResourceModel> audiosNotWorking = [];
ApplicationInstanceDTO? applicationInstanceDTO; // null = assistant non activé
StatisticsService? statisticsService;
bool? isAdmin = false; bool? isAdmin = false;
bool? isAllLanguages = false; bool? isAllLanguages = false;

View File

@ -3,10 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:manager_api_new/api.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/CustomAppBar.dart';
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart'; import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
@ -43,15 +44,15 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
// Beacon specific // Beacon specific
final controller = Get.find<RequirementStateController>(); final controller = Get.find<RequirementStateController>();
StreamSubscription<BluetoothState>? _streamBluetooth; /*StreamSubscription<BluetoothState>? _streamBluetooth;
StreamSubscription<RangingResult>? _streamRanging; StreamSubscription<RangingResult>? _streamRanging;*/
/*final _regionBeacons = <Region, List<Beacon>>{}; /*final _regionBeacons = <Region, List<Beacon>>{};
final _beacons = <Beacon>[];*/ final _beacons = <Beacon>[];*/
bool _isDialogShowing = false; bool _isDialogShowing = false;
DateTime? lastTimePopUpWasClosed; DateTime? lastTimePopUpWasClosed;
//bool _isArticleOpened = false; //bool _isArticleOpened = false;
StreamSubscription? listener; StreamSubscription? listener;
final List<Region> regions = <Region>[]; //final List<Region> regions = <Region>[];
@override @override
void initState() { void initState() {
@ -59,13 +60,13 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
if (Platform.isIOS) { if (Platform.isIOS) {
// iOS platform, at least set identifier and proximityUUID for region scanning // iOS platform, at least set identifier and proximityUUID for region scanning
regions.add(Region( /*regions.add(Region(
identifier: 'MyMuseumB', identifier: 'MyMuseumB',
proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825') proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825')
); );*/
} else { } else {
// Android platform, it can ranging out of beacon that filter all of Proximity UUID // 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(); super.initState();
@ -86,16 +87,16 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
listeningState() async { listeningState() async {
print('Listening to bluetooth state'); print('Listening to bluetooth state');
_streamBluetooth = flutterBeacon /*_streamBluetooth = flutterBeacon
.bluetoothStateChanged() .bluetoothStateChanged()
.listen((BluetoothState state) async { .listen((BluetoothState state) async {
controller.updateBluetoothState(state); controller.updateBluetoothState(state);
await checkAllRequirements(); await checkAllRequirements();
}); });*/
} }
checkAllRequirements() async { checkAllRequirements() async {
final bluetoothState = await flutterBeacon.bluetoothState; /*final bluetoothState = await flutterBeacon.bluetoothState;
controller.updateBluetoothState(bluetoothState); controller.updateBluetoothState(bluetoothState);
print('BLUETOOTH $bluetoothState'); print('BLUETOOTH $bluetoothState');
@ -105,7 +106,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled; final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled;
controller.updateLocationService(locationServiceEnabled); controller.updateLocationService(locationServiceEnabled);
print('LOCATION SERVICE $locationServiceEnabled'); print('LOCATION SERVICE $locationServiceEnabled');*/
var status = await Permission.bluetoothScan.status; var status = await Permission.bluetoothScan.status;
@ -123,7 +124,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
print(statuses[Permission.bluetoothConnect]); print(statuses[Permission.bluetoothConnect]);
print(status); print(status);
if (controller.bluetoothEnabled && /*if (controller.bluetoothEnabled &&
controller.authorizationStatusOk && controller.authorizationStatusOk &&
controller.locationServiceEnabled) { controller.locationServiceEnabled) {
print('STATE READY'); print('STATE READY');
@ -143,13 +144,13 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
} else { } else {
print('STATE NOT READY'); print('STATE NOT READY');
controller.pauseScanning(); controller.pauseScanning();
} }*/
} }
initScanBeacon(VisitAppContext visitAppContext) async { initScanBeacon(VisitAppContext visitAppContext) async {
await flutterBeacon.initializeScanning; //await flutterBeacon.initializeScanning;
if (!controller.authorizationStatusOk || /*if (!controller.authorizationStatusOk ||
!controller.locationServiceEnabled || !controller.locationServiceEnabled ||
!controller.bluetoothEnabled) { !controller.bluetoothEnabled) {
print( print(
@ -157,16 +158,16 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
'locationServiceEnabled=${controller.locationServiceEnabled}, ' 'locationServiceEnabled=${controller.locationServiceEnabled}, '
'bluetoothEnabled=${controller.bluetoothEnabled}'); 'bluetoothEnabled=${controller.bluetoothEnabled}');
return; return;
} }*/
if (_streamRanging != null) { /*if (_streamRanging != null) {
if (_streamRanging!.isPaused) { if (_streamRanging!.isPaused) {
_streamRanging?.resume(); _streamRanging?.resume();
return; return;
} }
} }*/
_streamRanging = /*_streamRanging =
flutterBeacon.ranging(regions).listen((RangingResult result) { flutterBeacon.ranging(regions).listen((RangingResult result) {
//print(result); //print(result);
if (mounted) { if (mounted) {
@ -229,7 +230,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
//}); //});
} }
}); });
*/
} }
/*pauseScanBeacon() async { /*pauseScanBeacon() async {
@ -259,14 +260,14 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
void didChangeAppLifecycleState(AppLifecycleState state) async { void didChangeAppLifecycleState(AppLifecycleState state) async {
print('AppLifecycleState = $state'); print('AppLifecycleState = $state');
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
if (_streamBluetooth != null) { /*if (_streamBluetooth != null) {
if (_streamBluetooth!.isPaused) { if (_streamBluetooth!.isPaused) {
_streamBluetooth?.resume(); _streamBluetooth?.resume();
} }
} }
await checkAllRequirements(); await checkAllRequirements();*/
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
_streamBluetooth?.pause(); //_streamBluetooth?.pause();
} }
} }
@ -352,6 +353,39 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
body: Body(configuration: widget.configuration), body: Body(configuration: widget.configuration),
floatingActionButton: Stack( floatingActionButton: Stack(
children: [ 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( visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == visitAppContext.configuration!.id).isNotEmpty ? Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Padding( child: Padding(
@ -360,7 +394,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
onTap: () async { onTap: () async {
bool isCancel = false; bool isCancel = false;
if(!controller.authorizationStatusOk) { /*if(!controller.authorizationStatusOk) {
//await handleOpenLocationSettings(); //await handleOpenLocationSettings();
await showDialog( await showDialog(
@ -469,12 +503,12 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
} }
if(!visitAppContext.isScanningBeacons) { if(!visitAppContext.isScanningBeacons) {
print("Start Scan"); print("Start Scan");
print(_streamRanging); /*print(_streamRanging);
if (_streamRanging != null) { if (_streamRanging != null) {
_streamRanging?.resume(); _streamRanging?.resume();
} else { } else {
await initScanBeacon(visitAppContext); await initScanBeacon(visitAppContext);
} }*/
controller.startScanning(); controller.startScanning();
visitAppContext.isScanningBeacons = true; visitAppContext.isScanningBeacons = true;
@ -486,7 +520,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
visitAppContext.isScanningBeacons = false; visitAppContext.isScanningBeacons = false;
appContext.setContext(visitAppContext); appContext.setContext(visitAppContext);
} }
} }*/
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -512,7 +546,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
handleOpenLocationSettings() async { handleOpenLocationSettings() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
await flutterBeacon.openLocationSettings; //await flutterBeacon.openLocationSettings;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
await showDialog( await showDialog(
context: context, context: context,
@ -537,7 +571,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
handleOpenBluetooth() async { handleOpenBluetooth() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
try { try {
await flutterBeacon.openBluetoothSettings; //await flutterBeacon.openBluetoothSettings;
} on PlatformException catch (e) { } on PlatformException catch (e) {
print(e); print(e);
} }

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.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_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:manager_api_new/api.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/CustomAppBar.dart';
import 'package:mymuseum_visitapp/Components/LanguageSelection.dart'; import 'package:mymuseum_visitapp/Components/LanguageSelection.dart';
import 'package:mymuseum_visitapp/Components/ScannerBouton.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/Screens/ConfigurationPage/configuration_page.dart';
import 'package:mymuseum_visitapp/Services/apiService.dart'; import 'package:mymuseum_visitapp/Services/apiService.dart';
import 'package:mymuseum_visitapp/Services/downloadConfiguration.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/app_context.dart';
import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/client.dart';
import 'package:mymuseum_visitapp/constants.dart'; import 'package:mymuseum_visitapp/constants.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'configurations_list.dart';
class HomePage3 extends StatefulWidget { class HomePage3 extends StatefulWidget {
const HomePage3({Key? key}) : super(key: key); const HomePage3({Key? key}) : super(key: key);
@ -51,6 +51,102 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
_futureConfigurations = getConfigurationsCall(appContext); _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('<br>', ' ');
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size; Size size = MediaQuery.of(context).size;
@ -60,56 +156,40 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
body: FutureBuilder( body: FutureBuilder(
future: _futureConfigurations,//getConfigurationsCall(visitAppContext.clientAPI, appContext), future: _futureConfigurations,
builder: (context, AsyncSnapshot<dynamic> snapshot) { builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
configurations = List<ConfigurationDTO>.from(snapshot.data).where((configuration) => configuration.isMobile!).toList(); configurations = List<ConfigurationDTO>.from(snapshot.data)
.where((c) => c.isMobile!)
.toList();
//String cleanedTitle = configurations[0].title.replaceAll('\n', ' ').replaceAll('<br>', ' '); // 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( return Stack(
children: [ children: [
Container( // Dark background
decoration: const BoxDecoration( const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()),
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,
)
),*/
SafeArea( SafeArea(
top: false, top: false,
bottom: false, bottom: false,
child: CustomScrollView( child: CustomScrollView(
slivers: [ 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( SliverAppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
pinned: false, pinned: false,
expandedHeight: 235.0, expandedHeight: 235.0,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin, // 👈 Optionnel pour éviter le fade collapseMode: CollapseMode.pin,
centerTitle: true, centerTitle: true,
background: Container( background: Container(
padding: const EdgeInsets.only(bottom: 25.0), padding: const EdgeInsets.only(bottom: 25.0),
@ -120,10 +200,10 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black26, color: Colors.black38,
spreadRadius: 0.35, spreadRadius: 0.5,
blurRadius: 2, blurRadius: 8,
offset: Offset(0, -22), offset: Offset(0, 4),
), ),
], ],
), ),
@ -135,12 +215,21 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Opacity( if (configurations.isNotEmpty && configurations[0].imageSource != null)
opacity: 0.85, Image.network(
child: Image.network(
configurations[0].imageSource!, configurations[0].imageSource!,
fit: BoxFit.cover, 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( Positioned(
top: 35, top: 35,
@ -148,7 +237,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
child: SizedBox( child: SizedBox(
width: 75, width: 75,
height: 75, height: 75,
child: LanguageSelection() child: LanguageSelection(),
), ),
), ),
], ],
@ -156,240 +245,111 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
), ),
), ),
title: SizedBox( title: SizedBox(
width: /*widget.isHomeButton ?*/ size.width * 1.0 /*: null*/, width: size.width * 1.0,
height: 120, height: 120,
child: Center( child: Center(
child: HtmlWidget( child: headerTitleEntry != null
configurations[0].title!.firstWhere((t) => t.language == "FR").value!, ? HtmlWidget(
textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20), headerTitleEntry.value!,
customStylesBuilder: (element) textStyle: const TextStyle(
{ color: Colors.white,
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"}; 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('<br>', ' ');
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)
),
)
],
),
),
),
),
);
},
), ),
), ),
SliverPadding(
/*SliverToBoxAdapter( padding: const EdgeInsets.only(
child: Image.network(configurations[0].imageSource!), // ou NetworkImage 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( if (visitAppContext.applicationInstanceDTO?.isAssistant == true)
future: getConfigurationsCall(visitAppContext.clientAPI, appContext), Positioned(
builder: (context, AsyncSnapshot<dynamic> snapshot) { bottom: 24,
if (snapshot.connectionState == ConnectionState.done) { right: 16,
child: FloatingActionButton(
heroTag: 'assistant_home',
return /*RefreshIndicator ( backgroundColor: kMainColor1,
onRefresh: () { onPressed: () {
setState(() {}); showModalBottomSheet(
return Future(() => null); context: context,
}, isScrollControlled: true,
color: kSecondColor, backgroundColor: Colors.transparent,
child:*/ ;/*ConfigurationsList( builder: (_) => AssistantChatSheet(
alreadyDownloaded: alreadyDownloaded, visitAppContext: visitAppContext,
configurations: configurations, onNavigateToSection: (configurationId, _) {
requestRefresh: () { final config = configurations
setState(() {}); // For refresh .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) { } else if (snapshot.connectionState == ConnectionState.none) {
return Text(TranslationHelper.getFromLocale("noData", appContext.getContext())); return Text(TranslationHelper.getFromLocale("noData", appContext.getContext()));
} else { } else {
return Center( return Center(
child: SizedBox( child: SizedBox(
height: size.height * 0.15, height: size.height * 0.15,
child: const LoadingCommon() 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<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
configurations = List<ConfigurationDTO>.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<HomePage3> with WidgetsBindingObserver {
bool isOnline = await hasNetwork(); bool isOnline = await hasNetwork();
VisitAppContext visitAppContext = appContext.getContext(); VisitAppContext visitAppContext = appContext.getContext();
isOnline = true; // Todo remove if not local test
List<ConfigurationDTO>? configurations; List<ConfigurationDTO>? configurations;
configurations = List<ConfigurationDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations)); configurations = List<ConfigurationDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations));
alreadyDownloaded = configurations.map((c) => c.id).toList(); alreadyDownloaded = configurations.map((c) => c.id).toList();
print("GOT configurations from LOCAL");
print(configurations.length);
print(configurations);
if(!isOnline) { if(!isOnline) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -449,6 +414,30 @@ class _HomePage3State extends State<HomePage3> 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); return await ApiService.getConfigurations(visitAppContext.clientAPI, visitAppContext);
} }
} }

View File

@ -130,6 +130,11 @@ class _AgendaPage extends State<AgendaPage> {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
print("${eventAgenda.name}"); print("${eventAgenda.name}");
(Provider.of<AppContext>(context, listen: false).getContext() as VisitAppContext)
.statisticsService?.track(
VisitEventType.agendaEventTap,
metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name},
);
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {

View File

@ -30,6 +30,7 @@ class _PuzzlePage extends State<PuzzlePage> {
int allInPlaceCount = 0; int allInPlaceCount = 0;
bool isFinished = false; bool isFinished = false;
DateTime? _puzzleStartTime;
GlobalKey _widgetKey = GlobalKey(); GlobalKey _widgetKey = GlobalKey();
Size? realWidgetSize; Size? realWidgetSize;
List<Widget> pieces = []; List<Widget> pieces = [];
@ -42,6 +43,7 @@ class _PuzzlePage extends State<PuzzlePage> {
puzzleDTO = widget.section; puzzleDTO = widget.section;
puzzleDTO.rows = puzzleDTO.rows ?? 3; puzzleDTO.rows = puzzleDTO.rows ?? 3;
puzzleDTO.cols = puzzleDTO.cols ?? 3; puzzleDTO.cols = puzzleDTO.cols ?? 3;
_puzzleStartTime = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
Size size = MediaQuery.of(context).size; Size size = MediaQuery.of(context).size;
@ -196,6 +198,11 @@ class _PuzzlePage extends State<PuzzlePage> {
Size size = MediaQuery.of(context).size; Size size = MediaQuery.of(context).size;
final appContext = Provider.of<AppContext>(context, listen: false); final appContext = Provider.of<AppContext>(context, listen: false);
VisitAppContext visitAppContext = appContext.getContext(); 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; TranslationAndResourceDTO? messageFin = puzzleDTO.messageFin != null && puzzleDTO.messageFin!.isNotEmpty ? puzzleDTO.messageFin!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null;
if(messageFin != null) { if(messageFin != null) {

View File

@ -69,6 +69,14 @@ class _GoogleMapViewState extends State<GoogleMapView> {
mapContext.setSelectedPoint(point); mapContext.setSelectedPoint(point);
//mapContext.setSelectedPointForNavigate(point); //mapContext.setSelectedPointForNavigate(point);
//}); //});
(Provider.of<AppContext>(context, listen: false).getContext() as VisitAppContext)
.statisticsService?.track(
VisitEventType.mapPoiTap,
metadata: {
'geoPointId': point.id,
'geoPointTitle': parse(textSansHTML.body!.text).documentElement!.text,
},
);
}, },
infoWindow: InfoWindow.noText)); infoWindow: InfoWindow.noText));
} }

View File

@ -206,6 +206,10 @@ class _MenuPageState extends State<MenuPage> {
//SectionDTO? section = await (appContext.getContext() as TabletAppContext).clientAPI!.sectionApi!.sectionGetDetail(menuDTO.sections![index].id!); //SectionDTO? section = await (appContext.getContext() as TabletAppContext).clientAPI!.sectionApi!.sectionGetDetail(menuDTO.sections![index].id!);
SectionDTO section = value[index]; SectionDTO section = value[index];
var rawSectionData = rawSubSectionsData[index]; var rawSectionData = rawSubSectionsData[index];
(appContext.getContext() as VisitAppContext).statisticsService?.track(
VisitEventType.menuItemTap,
metadata: {'targetSectionId': section.id, 'menuItemTitle': section.title?.firstOrNull?.value},
);
Navigator.push( Navigator.push(
context, context,
SlideFromRightRoute(page: SectionPage( SlideFromRightRoute(page: SectionPage(

View File

@ -23,11 +23,12 @@ import 'package:mymuseum_visitapp/constants.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class QuizPage extends StatefulWidget { 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 QuizDTO quizDTO;
final VisitAppContext visitAppContextIn; final VisitAppContext visitAppContextIn;
final List<ResourceModel?> resourcesModel; final List<ResourceModel?> resourcesModel;
final String? sectionId;
@override @override
State<QuizPage> createState() => _QuizPageState(); State<QuizPage> createState() => _QuizPageState();
@ -161,6 +162,11 @@ class _QuizPageState extends State<QuizPage> {
} }
} }
log("goodResponses =" + goodResponses.toString()); log("goodResponses =" + goodResponses.toString());
widget.visitAppContextIn.statisticsService?.track(
VisitEventType.quizComplete,
sectionId: widget.sectionId,
metadata: {'score': goodResponses, 'totalQuestions': widget.quizDTO.questions!.length},
);
List<TranslationAndResourceDTO> levelToShow = []; List<TranslationAndResourceDTO> levelToShow = [];
var test = goodResponses/widget.quizDTO.questions!.length; var test = goodResponses/widget.quizDTO.questions!.length;

View File

@ -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/Map/map_page.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Menu/menu_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/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/Quiz/quizz_page.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Slider/slider_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/Video/video_page.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Weather/weather_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/Screens/Sections/Web/web_page.dart';
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/app_context.dart';
import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/client.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -57,16 +58,34 @@ class _SectionPageState extends State<SectionPage> {
List<Map<String, dynamic>>? icons; List<Map<String, dynamic>>? icons;
String? mainAudioId; String? mainAudioId;
DateTime? _sectionOpenTime;
@override @override
void initState() { void initState() {
widget.visitAppContextIn.isContentCurrentlyShown = true; widget.visitAppContextIn.isContentCurrentlyShown = true;
_sectionOpenTime = DateTime.now();
super.initState(); super.initState();
// Track SectionView (fire-and-forget)
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.visitAppContextIn.statisticsService?.track(
VisitEventType.sectionView,
sectionId: widget.sectionId,
);
});
} }
@override @override
void dispose() { void dispose() {
visitAppContext.isContentCurrentlyShown = false; 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(); super.dispose();
} }
@ -124,6 +143,7 @@ class _SectionPageState extends State<SectionPage> {
visitAppContextIn: widget.visitAppContextIn, visitAppContextIn: widget.visitAppContextIn,
quizDTO: quizDTO, quizDTO: quizDTO,
resourcesModel: resourcesModel, resourcesModel: resourcesModel,
sectionId: widget.sectionId,
); );
case SectionType.Slider: case SectionType.Slider:
SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!; SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!;

View File

@ -15,7 +15,7 @@ class ApiService {
try { try {
List<ConfigurationDTO>? configurations; List<ConfigurationDTO>? configurations;
bool isOnline = await hasNetwork(); bool isOnline = await hasNetwork();
if(isOnline) { if(true) { // TODO isOnline
configurations = await client.configurationApi!.configurationGet(instanceId: visitAppContext?.instanceId); configurations = await client.configurationApi!.configurationGet(instanceId: visitAppContext?.instanceId);
if(configurations != null) { if(configurations != null) {

View File

@ -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<String, dynamic> toJson() => {'role': role, 'content': content};
}
class AssistantService {
final VisitAppContext visitAppContext;
final List<AiChatMessage> history = [];
AssistantService({required this.visitAppContext});
Future<AssistantResponse> 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<String, dynamic>;
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();
}

View File

@ -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<int>.generate(16, (_) => rand.nextInt(256));
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
Future<void> track(
String eventType, {
String? sectionId,
int? durationSeconds,
Map<String, dynamic>? 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
}
}
}

View File

@ -25,6 +25,15 @@ class Client {
DeviceApi? _deviceApi; DeviceApi? _deviceApi;
DeviceApi? get 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) { Client(String path) {
_apiClient = ApiClient( _apiClient = ApiClient(
basePath: path); // "http://192.168.31.96" basePath: path); // "http://192.168.31.96"
@ -35,5 +44,8 @@ class Client {
_sectionApi = SectionApi(_apiClient); _sectionApi = SectionApi(_apiClient);
_resourceApi = ResourceApi(_apiClient); _resourceApi = ResourceApi(_apiClient);
_deviceApi = DeviceApi(_apiClient); _deviceApi = DeviceApi(_apiClient);
_instanceApi = InstanceApi(_apiClient);
_applicationInstanceApi = ApplicationInstanceApi(_apiClient);
_statsApi = StatsApi(_apiClient);
} }
} }

View File

@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['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.');
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -3,20 +3,21 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:get/get.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
import 'package:mymuseum_visitapp/Models/articleRead.dart'; import 'package:mymuseum_visitapp/Models/articleRead.dart';
import 'package:mymuseum_visitapp/Screens/Home/home.dart'; import 'package:mymuseum_visitapp/Screens/Home/home.dart';
import 'package:mymuseum_visitapp/Screens/Home/home_3.0.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 'package:provider/provider.dart';
import 'Helpers/DatabaseHelper.dart'; import 'Helpers/DatabaseHelper.dart';
import 'Models/visitContext.dart'; import 'Models/visitContext.dart';
import 'app_context.dart'; import 'app_context.dart';
import 'constants.dart'; import 'constants.dart';
import 'package:flutter_localizations/flutter_localizations.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'; import 'package:path_provider/path_provider.dart';
void main() async { void main() async {

View File

@ -173,10 +173,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -197,10 +197,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@ -213,10 +213,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.0" version: "1.19.1"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -293,10 +293,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -326,14 +326,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@ -673,10 +665,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -745,26 +737,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.7" version: "11.0.1"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.8" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -800,7 +792,7 @@ packages:
manager_api_new: manager_api_new:
dependency: "direct main" dependency: "direct main"
description: description:
path: manager_api_new path: "../manager-app/manager_api_new"
relative: true relative: true
source: path source: path
version: "1.0.0" version: "1.0.0"
@ -816,10 +808,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -832,10 +824,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
mgrs_dart: mgrs_dart:
dependency: transitive dependency: transitive
description: description:
@ -928,10 +920,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@ -992,42 +984,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.4.5" version: "12.0.1"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.6" version: "13.0.1"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.12.0" version: "4.3.0"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_windows name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3" version: "0.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -1285,18 +1285,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.12.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@ -1341,10 +1341,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.6"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1493,10 +1493,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
video_player: video_player:
dependency: transitive dependency: transitive
description: description:
@ -1690,5 +1690,5 @@ packages:
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=3.5.0 <4.0.0" dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.24.0"

View File

@ -40,11 +40,11 @@ dependencies:
carousel_slider: ^5.0.0 carousel_slider: ^5.0.0
#flutter_svg_provider: ^1.0.3 #flutter_svg_provider: ^1.0.3
photo_view: ^0.15.0 photo_view: ^0.15.0
intl: ^0.19.0 intl: ^0.20.2
#audioplayers: ^2.0.0 #audioplayers: ^2.0.0
just_audio: ^0.9.38 just_audio: ^0.9.38
get: ^4.6.6 get: ^4.6.6
permission_handler: ^10.4.5 #^11.3.1 permission_handler: 12.0.1 #^11.3.1
diacritic: ^0.1.3 diacritic: ^0.1.3
flutter_widget_from_html: ^0.15.2 flutter_widget_from_html: ^0.15.2
webview_flutter: ^4.10.0 webview_flutter: ^4.10.0
@ -60,7 +60,7 @@ dependencies:
sqflite: #not in web sqflite: #not in web
just_audio_cache: ^0.1.2 #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 flutter_staggered_grid_view: ^0.7.0
smooth_page_indicator: ^1.2.1 smooth_page_indicator: ^1.2.1
@ -73,7 +73,7 @@ dependencies:
url_launcher: ^6.3.1 url_launcher: ^6.3.1
manager_api_new: manager_api_new:
path: manager_api_new path: ../manager-app/manager_api_new
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
@ -140,3 +140,6 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages
flutter_localizations:
sdk: flutter

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.