Ce commit boucle le travail en cours sur la branche (assistant lunettes Meta, passage du lecteur audio flottant a un onglet, ajustements scanner / liste de configurations / telechargement) et y ajoute le badge de version. Le badge, en bas de la feuille Parametres, affiche « flavor . version . commit ». Un APK pose sur une tablette du terrain n'etait rattachable a aucun commit precis : la version du pubspec ne bougeait pas d'un build a l'autre et rien n'indiquait le flavor reellement installe. kGitSha suit le meme schema que kApiBaseUrl, injecte par --dart-define, et kFlavor expose le flavor deja calcule. package_info_plus etait deja une dependance transitive ; il devient direct, puisqu'il est desormais importe. /!\ Un --dart-define modifie n'est PAS pris en compte sans `flutter clean` sur ce projet : verifie a la sentinelle, le SHA restait absent de libapp.so tant que le cache Dart n'etait pas vide. Un build de release destine au terrain doit donc toujours passer par un clean, sinon le badge affiche le SHA du build precedent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
476 lines
17 KiB
Dart
476 lines
17 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:beacon_scanner/beacon_scanner.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';
|
|
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
|
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
|
import 'package:mymuseum_visitapp/Screens/ConfigurationPage/beaconArticleFound.dart';
|
|
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart';
|
|
import 'package:mymuseum_visitapp/Services/pushNotificationService.dart';
|
|
import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart';
|
|
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
|
import 'package:mymuseum_visitapp/app_context.dart';
|
|
import 'package:mymuseum_visitapp/constants.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'components/body.dart';
|
|
|
|
class ConfigurationPage extends StatefulWidget {
|
|
const ConfigurationPage({Key? key,required this.configuration, required this.isAlreadyAllowed, this.openSectionId}) : super(key: key);
|
|
|
|
final ConfigurationDTO configuration;
|
|
final bool isAlreadyAllowed;
|
|
|
|
/// Section à ouvrir dès que la visite est chargée. Renseignée par le scan d'un
|
|
/// QR code appartenant à une autre visite : le visiteur a désigné un point
|
|
/// précis, l'amener sur la liste lui demanderait de le retrouver lui-même.
|
|
final String? openSectionId;
|
|
|
|
@override
|
|
State<ConfigurationPage> createState() => _ConfigurationPageState();
|
|
}
|
|
|
|
class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindingObserver {
|
|
//ConfigurationDTO? configuration;
|
|
|
|
int timeBetweenBeaconPopUp = 20000; // 20 sec
|
|
int meterToBeacon = 100; // 15 meters
|
|
bool modeDebugBeacon = false;
|
|
|
|
// Beacon specific
|
|
final controller = Get.find<RequirementStateController>();
|
|
StreamSubscription<ScanResult>? _streamRanging;
|
|
/*final _regionBeacons = <Region, List<Beacon>>{};
|
|
final _beacons = <Beacon>[];*/
|
|
bool _isDialogShowing = false;
|
|
DateTime? lastTimePopUpWasClosed;
|
|
//bool _isArticleOpened = false;
|
|
StreamSubscription? listener;
|
|
//final List<Region> regions = <Region>[];
|
|
late final VoidCallback _notificationListener;
|
|
|
|
@override
|
|
void initState() {
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
if (Platform.isIOS) {
|
|
// iOS platform, at least set identifier and proximityUUID for region scanning
|
|
/*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'));
|
|
}
|
|
|
|
super.initState();
|
|
|
|
if(widget.isAlreadyAllowed) {
|
|
listeningState();
|
|
}
|
|
|
|
_notificationListener = () {
|
|
final message = PushNotificationService.tappedMessage.value;
|
|
if (message == null) return;
|
|
if (!mounted) return;
|
|
final title = message.notification?.title ?? '';
|
|
final body = message.notification?.body ?? '';
|
|
ScaffoldMessenger.of(context).showMaterialBanner(
|
|
MaterialBanner(
|
|
content: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (title.isNotEmpty) Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
if (body.isNotEmpty) Text(body),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => ScaffoldMessenger.of(context).hideCurrentMaterialBanner(),
|
|
child: const Text('OK'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
};
|
|
PushNotificationService.tappedMessage.addListener(_notificationListener);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
|
VisitAppContext visitAppContext = appContext.getContext();
|
|
visitAppContext.configuration = widget.configuration;
|
|
visitAppContext.sectionIds = widget.configuration.sectionIds;
|
|
appContext.setContext(visitAppContext);
|
|
|
|
listener = controller.startStream.listen((flag) async {
|
|
if (flag == true && mounted) {
|
|
final ctx = Provider.of<AppContext>(context, listen: false).getContext();
|
|
await initScanBeacon(ctx);
|
|
}
|
|
});
|
|
});
|
|
//listeningState();
|
|
}
|
|
|
|
listeningState() async {
|
|
await BeaconScanner.instance.initialize(true);
|
|
}
|
|
|
|
checkAllRequirements() async {
|
|
/*final bluetoothState = await flutterBeacon.bluetoothState;
|
|
controller.updateBluetoothState(bluetoothState);
|
|
print('BLUETOOTH $bluetoothState');
|
|
|
|
final authorizationStatus = await flutterBeacon.authorizationStatus;
|
|
controller.updateAuthorizationStatus(authorizationStatus);
|
|
print('AUTHORIZATION $authorizationStatus');
|
|
|
|
final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled;
|
|
controller.updateLocationService(locationServiceEnabled);
|
|
print('LOCATION SERVICE $locationServiceEnabled');*/
|
|
|
|
var status = await Permission.bluetoothScan.status;
|
|
|
|
if (status.isDenied) {
|
|
print("IS DENIIIED");
|
|
// We didn't ask for permission yet or the permission has been denied before but not permanently.
|
|
}
|
|
|
|
// You can request multiple permissions at once.
|
|
Map<Permission, PermissionStatus> statuses = await [
|
|
Permission.bluetoothScan,
|
|
Permission.bluetoothConnect,
|
|
].request();
|
|
print(statuses[Permission.bluetoothScan]);
|
|
print(statuses[Permission.bluetoothConnect]);
|
|
print(status);
|
|
|
|
/*if (controller.bluetoothEnabled &&
|
|
controller.authorizationStatusOk &&
|
|
controller.locationServiceEnabled) {
|
|
print('STATE READY');
|
|
//if (currentIndex == 0) {
|
|
print('SCANNING');
|
|
//controller.startScanning();
|
|
|
|
/*controller.startStream.listen((flag) {
|
|
if (flag == true) {
|
|
initScanBeacon();
|
|
}
|
|
});*/
|
|
/*} else {
|
|
print('BROADCASTING');
|
|
controller.startBroadcasting();
|
|
}*/
|
|
} else {
|
|
print('STATE NOT READY');
|
|
controller.pauseScanning();
|
|
}*/
|
|
}
|
|
|
|
initScanBeacon(VisitAppContext visitAppContext) async {
|
|
if (_streamRanging != null && !_streamRanging!.isPaused) return;
|
|
|
|
if (_streamRanging != null && _streamRanging!.isPaused) {
|
|
_streamRanging!.resume();
|
|
return;
|
|
}
|
|
|
|
final regions = <Region>[
|
|
if (Platform.isIOS)
|
|
Region(
|
|
identifier: 'MyMuseumB',
|
|
beaconId: IBeaconId(proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825'),
|
|
)
|
|
else
|
|
Region(identifier: 'MyMuseumB'),
|
|
];
|
|
|
|
_streamRanging = BeaconScanner.instance.ranging(regions).listen((ScanResult result) {
|
|
if (!mounted || result.beacons.isEmpty) return;
|
|
|
|
final beaconSections = visitAppContext.beaconSections;
|
|
if (beaconSections == null) return;
|
|
|
|
var matches = beaconSections.where((bs) =>
|
|
result.beacons.any((b) =>
|
|
b.id.minorId == bs!.minorBeaconId && b.accuracy < meterToBeacon
|
|
)
|
|
);
|
|
|
|
if (matches.isNotEmpty) {
|
|
matches = matches.where((bs) => bs!.configurationId == widget.configuration.id);
|
|
}
|
|
|
|
if (matches.isNotEmpty && !modeDebugBeacon) {
|
|
matches = matches.where((bs) =>
|
|
!visitAppContext.readSections.any((ra) => ra.id == bs!.sectionId)
|
|
);
|
|
}
|
|
|
|
if (matches.isEmpty) return;
|
|
|
|
final milliLastTime = lastTimePopUpWasClosed?.millisecondsSinceEpoch ?? 0;
|
|
final cooldownOk = (DateTime.now().millisecondsSinceEpoch - milliLastTime) > timeBetweenBeaconPopUp;
|
|
|
|
if (!_isDialogShowing && !visitAppContext.isContentCurrentlyShown && cooldownOk && visitAppContext.isScanningBeacons) {
|
|
final sorted = matches.toList()..sort((a, b) => a!.orderInConfig!.compareTo(b!.orderInConfig!));
|
|
_onBeaconFound(visitAppContext, sorted.first);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*pauseScanBeacon() async {
|
|
_streamRanging?.pause();
|
|
if (_beacons.isNotEmpty) {
|
|
setState(() {
|
|
_beacons.clear();
|
|
});
|
|
}
|
|
}*/
|
|
|
|
/*int _compareParameters(Beacon a, Beacon b) {
|
|
int compare = a.proximityUUID.compareTo(b.proximityUUID);
|
|
|
|
if (compare == 0) {
|
|
compare = a.major.compareTo(b.major);
|
|
}
|
|
|
|
if (compare == 0) {
|
|
compare = a.minor.compareTo(b.minor);
|
|
}
|
|
|
|
return compare;
|
|
}*/
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
|
print('AppLifecycleState = $state');
|
|
if (state == AppLifecycleState.resumed) {
|
|
/*if (_streamBluetooth != null) {
|
|
if (_streamBluetooth!.isPaused) {
|
|
_streamBluetooth?.resume();
|
|
}
|
|
}
|
|
await checkAllRequirements();*/
|
|
} else if (state == AppLifecycleState.paused) {
|
|
//_streamBluetooth?.pause();
|
|
}
|
|
}
|
|
|
|
void _onBeaconFound(VisitAppContext visitAppContext, BeaconSection? beaconSection) {
|
|
_isDialogShowing = true;
|
|
PushNotificationService.showBeaconDiscoveryNotification(
|
|
title: TranslationHelper.getFromLocale('beaconFound', visitAppContext),
|
|
body: TranslationHelper.getFromLocale('beaconFoundBody', visitAppContext),
|
|
);
|
|
showDialog(
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) => AlertDialog(
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(10.0))
|
|
),
|
|
content: BeaconArticleFound(beaconSection: beaconSection),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text(TranslationHelper.getFromLocale("close", visitAppContext), style: const TextStyle(color: kSecondGrey)),
|
|
onPressed: () {
|
|
_isDialogShowing = false; // set it `false` since dialog is closed
|
|
Navigator.of(context).pop();
|
|
lastTimePopUpWasClosed = DateTime.now();
|
|
},
|
|
),
|
|
TextButton(
|
|
child: Text(TranslationHelper.getFromLocale("open", visitAppContext), style: const TextStyle(color: kSecondGrey)),
|
|
onPressed: () {
|
|
_isDialogShowing = false; // set it `false` since dialog is closed
|
|
Navigator.of(context).pop();
|
|
//visitAppContext.isArticleCurrentlyShown = true;
|
|
lastTimePopUpWasClosed = DateTime.now();
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => SectionPage(
|
|
configuration: widget.configuration,
|
|
rawSection: null,
|
|
visitAppContextIn: visitAppContext,
|
|
sectionId: beaconSection!.sectionId!,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
],
|
|
actionsAlignment: MainAxisAlignment.spaceAround,
|
|
contentPadding: EdgeInsets.zero,
|
|
), context: context
|
|
);
|
|
/*Future.delayed(const Duration(seconds: 6), () {
|
|
Navigator.pop(context); //pop dialog
|
|
_isDialogShowing = false;
|
|
});*/
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
PushNotificationService.tappedMessage.removeListener(_notificationListener);
|
|
listener?.cancel();
|
|
_streamRanging?.cancel();
|
|
controller.pauseScanning();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final appContext = Provider.of<AppContext>(context);
|
|
VisitAppContext visitAppContext = appContext.getContext();
|
|
|
|
return PopScope(
|
|
canPop: true,
|
|
child: Scaffold(
|
|
/*appBar: CustomAppBar(
|
|
title: TranslationHelper.get(configuration!.title, visitAppContext),
|
|
isHomeButton: true,
|
|
),*/
|
|
backgroundColor: kBackgroundGrey,
|
|
body: Body(configuration: widget.configuration, openSectionId: widget.openSectionId),
|
|
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: () {
|
|
AssistantChatSheet.show(
|
|
context,
|
|
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 == widget.configuration.id).isNotEmpty ? Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
|
child: InkWell(
|
|
onTap: () async {
|
|
if (!visitAppContext.isScanningBeacons) {
|
|
await [
|
|
Permission.bluetoothScan,
|
|
Permission.bluetoothConnect,
|
|
Permission.location,
|
|
].request();
|
|
controller.startScanning();
|
|
visitAppContext.isScanningBeacons = true;
|
|
visitAppContext.isScanBeaconAlreadyAllowed = true;
|
|
appContext.setContext(visitAppContext);
|
|
} else {
|
|
_streamRanging?.pause();
|
|
controller.pauseScanning();
|
|
visitAppContext.isScanningBeacons = false;
|
|
appContext.setContext(visitAppContext);
|
|
}
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: visitAppContext.isScanningBeacons ? kMainColor1 : Colors.grey,
|
|
),
|
|
height: 75.0,
|
|
width: 65.0,
|
|
child: const Icon(Icons.my_location, color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
) : const SizedBox(),
|
|
Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: ScannerBouton(appContext: appContext),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
handleOpenLocationSettings() async {
|
|
if (Platform.isAndroid) {
|
|
//await flutterBeacon.openLocationSettings;
|
|
} else if (Platform.isIOS) {
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: Text('Location Services Off'),
|
|
content: Text(
|
|
'Please enable Location Services on Settings > Privacy > Location Services.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text('OK'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
handleOpenBluetooth() async {
|
|
if (Platform.isAndroid) {
|
|
try {
|
|
//await flutterBeacon.openBluetoothSettings;
|
|
} on PlatformException catch (e) {
|
|
print(e);
|
|
}
|
|
} else if (Platform.isIOS) {
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: Text('Bluetooth is Off'),
|
|
content: Text('Please enable Bluetooth on Settings > Bluetooth.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text('OK'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
} |