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:
parent
c6599e13c9
commit
538e677993
@ -40,7 +40,8 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"*/
|
||||
|
||||
android {
|
||||
namespace = "be.unov.mymuseum.fortsaintheribert"
|
||||
compileSdkVersion 35
|
||||
compileSdkVersion 36
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -70,6 +71,10 @@ android {
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
/*ndk {
|
||||
abiFilters "arm64-v8a"
|
||||
}*/
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.experimental.enable16kApk=true
|
||||
android.useNewNativePlugin=true
|
||||
@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
|
||||
@ -31,8 +31,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
|
||||
id "com.android.application" version "8.9.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
332
lib/Components/AssistantChatSheet.dart
Normal file
332
lib/Components/AssistantChatSheet.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -128,11 +128,13 @@ class _ScannerDialogState extends State<ScannerDialog> {
|
||||
VisitAppContext visitAppContext = widget.appContext!.getContext();
|
||||
|
||||
if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) {
|
||||
visitAppContext.statisticsService?.track(VisitEventType.qrScan, metadata: {'valid': false, 'sectionId': sectionId});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
visitAppContext.statisticsService?.track(VisitEventType.qrScan, sectionId: sectionId, metadata: {'valid': true});
|
||||
dynamic rawSection = visitAppContext.currentSections!.firstWhere((cs) => cs!['id'] == sectionId)!;
|
||||
Navigator.of(context).pop();
|
||||
Navigator.push(
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
// TODO // import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RequirementStateController extends GetxController {
|
||||
var bluetoothState = BluetoothState.stateOff.obs;
|
||||
var authorizationStatus = AuthorizationStatus.notDetermined.obs;
|
||||
var bluetoothState = false; //BluetoothState.stateOff.obs;
|
||||
var authorizationStatus = false; //AuthorizationStatus.notDetermined.obs;
|
||||
var locationService = false.obs;
|
||||
|
||||
var _startBroadcasting = false.obs;
|
||||
var _startScanning = false.obs;
|
||||
var _pauseScanning = false.obs;
|
||||
|
||||
bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
|
||||
/*bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
|
||||
bool get authorizationStatusOk =>
|
||||
authorizationStatus.value == AuthorizationStatus.allowed ||
|
||||
authorizationStatus.value == AuthorizationStatus.always;
|
||||
@ -22,7 +22,7 @@ class RequirementStateController extends GetxController {
|
||||
|
||||
updateAuthorizationStatus(AuthorizationStatus status) {
|
||||
authorizationStatus.value = status;
|
||||
}
|
||||
}*/
|
||||
|
||||
updateLocationService(bool flag) {
|
||||
locationService.value = flag;
|
||||
|
||||
56
lib/Models/AssistantResponse.dart
Normal file
56
lib/Models/AssistantResponse.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
|
||||
class VisitAppContext with ChangeNotifier {
|
||||
Client clientAPI = Client("https://api.mymuseum.be"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 // https://api.myinfomate.be // http://192.168.31.228:5000
|
||||
Client clientAPI = Client("http://192.168.31.228:5000"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 // https://api.myinfomate.be // http://192.168.31.228:5000
|
||||
|
||||
String? id = "";
|
||||
String? language = "";
|
||||
@ -27,6 +28,9 @@ class VisitAppContext with ChangeNotifier {
|
||||
|
||||
List<ResourceModel> audiosNotWorking = [];
|
||||
|
||||
ApplicationInstanceDTO? applicationInstanceDTO; // null = assistant non activé
|
||||
StatisticsService? statisticsService;
|
||||
|
||||
bool? isAdmin = false;
|
||||
bool? isAllLanguages = false;
|
||||
|
||||
|
||||
@ -3,10 +3,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
// TODO //import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
|
||||
@ -43,15 +44,15 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
|
||||
// Beacon specific
|
||||
final controller = Get.find<RequirementStateController>();
|
||||
StreamSubscription<BluetoothState>? _streamBluetooth;
|
||||
StreamSubscription<RangingResult>? _streamRanging;
|
||||
/*StreamSubscription<BluetoothState>? _streamBluetooth;
|
||||
StreamSubscription<RangingResult>? _streamRanging;*/
|
||||
/*final _regionBeacons = <Region, List<Beacon>>{};
|
||||
final _beacons = <Beacon>[];*/
|
||||
bool _isDialogShowing = false;
|
||||
DateTime? lastTimePopUpWasClosed;
|
||||
//bool _isArticleOpened = false;
|
||||
StreamSubscription? listener;
|
||||
final List<Region> regions = <Region>[];
|
||||
//final List<Region> regions = <Region>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -59,13 +60,13 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// iOS platform, at least set identifier and proximityUUID for region scanning
|
||||
regions.add(Region(
|
||||
/*regions.add(Region(
|
||||
identifier: 'MyMuseumB',
|
||||
proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825')
|
||||
);
|
||||
);*/
|
||||
} else {
|
||||
// Android platform, it can ranging out of beacon that filter all of Proximity UUID
|
||||
regions.add(Region(identifier: 'MyMuseumB'));
|
||||
//regions.add(Region(identifier: 'MyMuseumB'));
|
||||
}
|
||||
|
||||
super.initState();
|
||||
@ -86,16 +87,16 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
|
||||
listeningState() async {
|
||||
print('Listening to bluetooth state');
|
||||
_streamBluetooth = flutterBeacon
|
||||
/*_streamBluetooth = flutterBeacon
|
||||
.bluetoothStateChanged()
|
||||
.listen((BluetoothState state) async {
|
||||
controller.updateBluetoothState(state);
|
||||
await checkAllRequirements();
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
checkAllRequirements() async {
|
||||
final bluetoothState = await flutterBeacon.bluetoothState;
|
||||
/*final bluetoothState = await flutterBeacon.bluetoothState;
|
||||
controller.updateBluetoothState(bluetoothState);
|
||||
print('BLUETOOTH $bluetoothState');
|
||||
|
||||
@ -105,7 +106,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
|
||||
final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled;
|
||||
controller.updateLocationService(locationServiceEnabled);
|
||||
print('LOCATION SERVICE $locationServiceEnabled');
|
||||
print('LOCATION SERVICE $locationServiceEnabled');*/
|
||||
|
||||
var status = await Permission.bluetoothScan.status;
|
||||
|
||||
@ -123,7 +124,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
print(statuses[Permission.bluetoothConnect]);
|
||||
print(status);
|
||||
|
||||
if (controller.bluetoothEnabled &&
|
||||
/*if (controller.bluetoothEnabled &&
|
||||
controller.authorizationStatusOk &&
|
||||
controller.locationServiceEnabled) {
|
||||
print('STATE READY');
|
||||
@ -143,13 +144,13 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
} else {
|
||||
print('STATE NOT READY');
|
||||
controller.pauseScanning();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
initScanBeacon(VisitAppContext visitAppContext) async {
|
||||
|
||||
await flutterBeacon.initializeScanning;
|
||||
if (!controller.authorizationStatusOk ||
|
||||
//await flutterBeacon.initializeScanning;
|
||||
/*if (!controller.authorizationStatusOk ||
|
||||
!controller.locationServiceEnabled ||
|
||||
!controller.bluetoothEnabled) {
|
||||
print(
|
||||
@ -157,16 +158,16 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
'locationServiceEnabled=${controller.locationServiceEnabled}, '
|
||||
'bluetoothEnabled=${controller.bluetoothEnabled}');
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (_streamRanging != null) {
|
||||
/*if (_streamRanging != null) {
|
||||
if (_streamRanging!.isPaused) {
|
||||
_streamRanging?.resume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
_streamRanging =
|
||||
/*_streamRanging =
|
||||
flutterBeacon.ranging(regions).listen((RangingResult result) {
|
||||
//print(result);
|
||||
if (mounted) {
|
||||
@ -229,7 +230,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
//});
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
/*pauseScanBeacon() async {
|
||||
@ -259,14 +260,14 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
print('AppLifecycleState = $state');
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (_streamBluetooth != null) {
|
||||
/*if (_streamBluetooth != null) {
|
||||
if (_streamBluetooth!.isPaused) {
|
||||
_streamBluetooth?.resume();
|
||||
}
|
||||
}
|
||||
await checkAllRequirements();
|
||||
await checkAllRequirements();*/
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
_streamBluetooth?.pause();
|
||||
//_streamBluetooth?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,6 +353,39 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
body: Body(configuration: widget.configuration),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'assistant_config',
|
||||
backgroundColor: kMainColor1,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AssistantChatSheet(
|
||||
visitAppContext: visitAppContext,
|
||||
configurationId: widget.configuration.id,
|
||||
onNavigateToSection: (sectionId, sectionTitle) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => SectionPage(
|
||||
configuration: widget.configuration,
|
||||
rawSection: null,
|
||||
visitAppContextIn: visitAppContext,
|
||||
sectionId: sectionId,
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.chat_bubble_outline, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == visitAppContext.configuration!.id).isNotEmpty ? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
@ -360,7 +394,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
onTap: () async {
|
||||
bool isCancel = false;
|
||||
|
||||
if(!controller.authorizationStatusOk) {
|
||||
/*if(!controller.authorizationStatusOk) {
|
||||
//await handleOpenLocationSettings();
|
||||
|
||||
await showDialog(
|
||||
@ -469,12 +503,12 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
}
|
||||
if(!visitAppContext.isScanningBeacons) {
|
||||
print("Start Scan");
|
||||
print(_streamRanging);
|
||||
/*print(_streamRanging);
|
||||
if (_streamRanging != null) {
|
||||
_streamRanging?.resume();
|
||||
} else {
|
||||
await initScanBeacon(visitAppContext);
|
||||
}
|
||||
}*/
|
||||
|
||||
controller.startScanning();
|
||||
visitAppContext.isScanningBeacons = true;
|
||||
@ -486,7 +520,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
visitAppContext.isScanningBeacons = false;
|
||||
appContext.setContext(visitAppContext);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@ -512,7 +546,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
|
||||
handleOpenLocationSettings() async {
|
||||
if (Platform.isAndroid) {
|
||||
await flutterBeacon.openLocationSettings;
|
||||
//await flutterBeacon.openLocationSettings;
|
||||
} else if (Platform.isIOS) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
@ -537,7 +571,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindi
|
||||
handleOpenBluetooth() async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
await flutterBeacon.openBluetoothSettings;
|
||||
//await flutterBeacon.openBluetoothSettings;
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
@ -7,6 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/LanguageSelection.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
|
||||
@ -21,11 +21,11 @@ import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart';
|
||||
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'configurations_list.dart';
|
||||
|
||||
class HomePage3 extends StatefulWidget {
|
||||
const HomePage3({Key? key}) : super(key: key);
|
||||
@ -51,6 +51,102 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
_futureConfigurations = getConfigurationsCall(appContext);
|
||||
}
|
||||
|
||||
Widget _buildCard(BuildContext context, int index) {
|
||||
final lang = visitAppContext.language ?? "FR";
|
||||
final config = configurations[index];
|
||||
final titleEntry = config.title?.firstWhere(
|
||||
(t) => t.language == lang,
|
||||
orElse: () => config.title!.first,
|
||||
);
|
||||
final cleanedTitle = (titleEntry?.value ?? '').replaceAll('\n', ' ').replaceAll('<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
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
@ -60,56 +156,40 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: FutureBuilder(
|
||||
future: _futureConfigurations,//getConfigurationsCall(visitAppContext.clientAPI, appContext),
|
||||
future: _futureConfigurations,
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
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(
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey, Colors.lightGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
/*SizedBox(
|
||||
height: size.height * 0.35,
|
||||
width: size.width,
|
||||
child: Image.network(
|
||||
configurations[2].imageSource!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
),*/
|
||||
// Dark background
|
||||
const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()),
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
/*SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
collapsedHeight: 100.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
collapseMode: CollapseMode.none, // 👈 Optionnel pour éviter le fade
|
||||
background: Image.network(
|
||||
configurations[2].imageSource!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),*/
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
pinned: false,
|
||||
expandedHeight: 235.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
collapseMode: CollapseMode.pin, // 👈 Optionnel pour éviter le fade
|
||||
collapseMode: CollapseMode.pin,
|
||||
centerTitle: true,
|
||||
background: Container(
|
||||
padding: const EdgeInsets.only(bottom: 25.0),
|
||||
@ -120,10 +200,10 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
spreadRadius: 0.35,
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, -22),
|
||||
color: Colors.black38,
|
||||
spreadRadius: 0.5,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -135,12 +215,21 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.85,
|
||||
child: Image.network(
|
||||
if (configurations.isNotEmpty && configurations[0].imageSource != null)
|
||||
Image.network(
|
||||
configurations[0].imageSource!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
// Bottom gradient for title readability
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black54],
|
||||
stops: [0.5, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 35,
|
||||
@ -148,7 +237,7 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
child: SizedBox(
|
||||
width: 75,
|
||||
height: 75,
|
||||
child: LanguageSelection()
|
||||
child: LanguageSelection(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -156,240 +245,111 @@ class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
title: SizedBox(
|
||||
width: /*widget.isHomeButton ?*/ size.width * 1.0 /*: null*/,
|
||||
width: size.width * 1.0,
|
||||
height: 120,
|
||||
child: Center(
|
||||
child: HtmlWidget(
|
||||
configurations[0].title!.firstWhere((t) => t.language == "FR").value!,
|
||||
textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
|
||||
},
|
||||
),
|
||||
child: headerTitleEntry != null
|
||||
? HtmlWidget(
|
||||
headerTitleEntry.value!,
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'text-align': 'center',
|
||||
'font-family': 'Roboto',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
), // plus de FlexibleSpaceBar
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0, bottom: 20.0),
|
||||
sliver: SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childCount: configurations.length,
|
||||
itemBuilder: (context, index) {
|
||||
//return buildTile(configurations[index]);
|
||||
String cleanedTitle = configurations[index].title!.firstWhere((t) => t.language == "FR").value!.replaceAll('\n', ' ').replaceAll('<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)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/*SliverToBoxAdapter(
|
||||
child: Image.network(configurations[0].imageSource!), // ou NetworkImage
|
||||
),*/
|
||||
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0, right: 8.0, top: 8.0, bottom: 20.0),
|
||||
sliver: isMasonry
|
||||
? SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childCount: configurations.length,
|
||||
itemBuilder: (context, index) => SizedBox(
|
||||
height: 160 + (index % 3) * 50,
|
||||
child: _buildCard(context, index),
|
||||
),
|
||||
)
|
||||
: SliverGrid(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 0.82,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
_buildCard,
|
||||
childCount: configurations.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
/*FutureBuilder(
|
||||
future: getConfigurationsCall(visitAppContext.clientAPI, appContext),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
|
||||
|
||||
return /*RefreshIndicator (
|
||||
onRefresh: () {
|
||||
setState(() {});
|
||||
return Future(() => null);
|
||||
},
|
||||
color: kSecondColor,
|
||||
child:*/ ;/*ConfigurationsList(
|
||||
alreadyDownloaded: alreadyDownloaded,
|
||||
configurations: configurations,
|
||||
requestRefresh: () {
|
||||
setState(() {}); // For refresh
|
||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true)
|
||||
Positioned(
|
||||
bottom: 24,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'assistant_home',
|
||||
backgroundColor: kMainColor1,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AssistantChatSheet(
|
||||
visitAppContext: visitAppContext,
|
||||
onNavigateToSection: (configurationId, _) {
|
||||
final config = configurations
|
||||
.where((c) => c.id == configurationId)
|
||||
.firstOrNull;
|
||||
if (config != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ConfigurationPage(
|
||||
configuration: config,
|
||||
isAlreadyAllowed:
|
||||
visitAppContext.isScanBeaconAlreadyAllowed,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),*/
|
||||
//);
|
||||
}
|
||||
}
|
||||
),*/
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.chat_bubble_outline, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text(TranslationHelper.getFromLocale("noData", appContext.getContext()));
|
||||
} else {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: size.height * 0.15,
|
||||
child: const LoadingCommon()
|
||||
)
|
||||
child: SizedBox(
|
||||
height: size.height * 0.15,
|
||||
child: const LoadingCommon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
/*appBar: CustomAppBar(
|
||||
title: TranslationHelper.getFromLocale("visitTitle", appContext.getContext()),
|
||||
isHomeButton: false,
|
||||
),*/
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: FutureBuilder(
|
||||
future: getConfigurationsCall(appContext),
|
||||
builder: (context, AsyncSnapshot<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();
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
|
||||
isOnline = true; // Todo remove if not local test
|
||||
List<ConfigurationDTO>? configurations;
|
||||
configurations = List<ConfigurationDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations));
|
||||
alreadyDownloaded = configurations.map((c) => c.id).toList();
|
||||
print("GOT configurations from LOCAL");
|
||||
print(configurations.length);
|
||||
print(configurations);
|
||||
|
||||
if(!isOnline) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -449,6 +414,30 @@ class _HomePage3State extends State<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);
|
||||
}
|
||||
}
|
||||
@ -130,6 +130,11 @@ class _AgendaPage extends State<AgendaPage> {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print("${eventAgenda.name}");
|
||||
(Provider.of<AppContext>(context, listen: false).getContext() as VisitAppContext)
|
||||
.statisticsService?.track(
|
||||
VisitEventType.agendaEventTap,
|
||||
metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name},
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
|
||||
@ -30,6 +30,7 @@ class _PuzzlePage extends State<PuzzlePage> {
|
||||
|
||||
int allInPlaceCount = 0;
|
||||
bool isFinished = false;
|
||||
DateTime? _puzzleStartTime;
|
||||
GlobalKey _widgetKey = GlobalKey();
|
||||
Size? realWidgetSize;
|
||||
List<Widget> pieces = [];
|
||||
@ -42,6 +43,7 @@ class _PuzzlePage extends State<PuzzlePage> {
|
||||
puzzleDTO = widget.section;
|
||||
puzzleDTO.rows = puzzleDTO.rows ?? 3;
|
||||
puzzleDTO.cols = puzzleDTO.cols ?? 3;
|
||||
_puzzleStartTime = DateTime.now();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
@ -196,6 +198,11 @@ class _PuzzlePage extends State<PuzzlePage> {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
final duration = _puzzleStartTime != null ? DateTime.now().difference(_puzzleStartTime!).inSeconds : 0;
|
||||
visitAppContext.statisticsService?.track(
|
||||
VisitEventType.gameComplete,
|
||||
metadata: {'gameType': 'Puzzle', 'durationSeconds': duration},
|
||||
);
|
||||
TranslationAndResourceDTO? messageFin = puzzleDTO.messageFin != null && puzzleDTO.messageFin!.isNotEmpty ? puzzleDTO.messageFin!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null;
|
||||
|
||||
if(messageFin != null) {
|
||||
@ -69,6 +69,14 @@ class _GoogleMapViewState extends State<GoogleMapView> {
|
||||
mapContext.setSelectedPoint(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));
|
||||
}
|
||||
|
||||
@ -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 = value[index];
|
||||
var rawSectionData = rawSubSectionsData[index];
|
||||
(appContext.getContext() as VisitAppContext).statisticsService?.track(
|
||||
VisitEventType.menuItemTap,
|
||||
metadata: {'targetSectionId': section.id, 'menuItemTitle': section.title?.firstOrNull?.value},
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
SlideFromRightRoute(page: SectionPage(
|
||||
|
||||
@ -23,11 +23,12 @@ import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class QuizPage extends StatefulWidget {
|
||||
const QuizPage({Key? key, required this.visitAppContextIn, required this.quizDTO, required this.resourcesModel}) : super(key: key);
|
||||
const QuizPage({Key? key, required this.visitAppContextIn, required this.quizDTO, required this.resourcesModel, this.sectionId}) : super(key: key);
|
||||
|
||||
final QuizDTO quizDTO;
|
||||
final VisitAppContext visitAppContextIn;
|
||||
final List<ResourceModel?> resourcesModel;
|
||||
final String? sectionId;
|
||||
|
||||
@override
|
||||
State<QuizPage> createState() => _QuizPageState();
|
||||
@ -161,6 +162,11 @@ class _QuizPageState extends State<QuizPage> {
|
||||
}
|
||||
}
|
||||
log("goodResponses =" + goodResponses.toString());
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.quizComplete,
|
||||
sectionId: widget.sectionId,
|
||||
metadata: {'score': goodResponses, 'totalQuestions': widget.quizDTO.questions!.length},
|
||||
);
|
||||
List<TranslationAndResourceDTO> levelToShow = [];
|
||||
var test = goodResponses/widget.quizDTO.questions!.length;
|
||||
|
||||
|
||||
@ -20,12 +20,13 @@ import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Menu/menu_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/PDF/pdf_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Puzzle/puzzle_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Game/game_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Slider/slider_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Video/video_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Weather/weather_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Web/web_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -57,16 +58,34 @@ class _SectionPageState extends State<SectionPage> {
|
||||
List<Map<String, dynamic>>? icons;
|
||||
|
||||
String? mainAudioId;
|
||||
DateTime? _sectionOpenTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.visitAppContextIn.isContentCurrentlyShown = true;
|
||||
_sectionOpenTime = DateTime.now();
|
||||
super.initState();
|
||||
// Track SectionView (fire-and-forget)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.sectionView,
|
||||
sectionId: widget.sectionId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
visitAppContext.isContentCurrentlyShown = false;
|
||||
// Track SectionLeave with duration
|
||||
final duration = _sectionOpenTime != null
|
||||
? DateTime.now().difference(_sectionOpenTime!).inSeconds
|
||||
: null;
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.sectionLeave,
|
||||
sectionId: widget.sectionId,
|
||||
durationSeconds: duration,
|
||||
);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -124,6 +143,7 @@ class _SectionPageState extends State<SectionPage> {
|
||||
visitAppContextIn: widget.visitAppContextIn,
|
||||
quizDTO: quizDTO,
|
||||
resourcesModel: resourcesModel,
|
||||
sectionId: widget.sectionId,
|
||||
);
|
||||
case SectionType.Slider:
|
||||
SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!;
|
||||
|
||||
@ -15,7 +15,7 @@ class ApiService {
|
||||
try {
|
||||
List<ConfigurationDTO>? configurations;
|
||||
bool isOnline = await hasNetwork();
|
||||
if(isOnline) {
|
||||
if(true) { // TODO isOnline
|
||||
configurations = await client.configurationApi!.configurationGet(instanceId: visitAppContext?.instanceId);
|
||||
|
||||
if(configurations != null) {
|
||||
|
||||
53
lib/Services/assistantService.dart
Normal file
53
lib/Services/assistantService.dart
Normal 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();
|
||||
}
|
||||
52
lib/Services/statisticsService.dart
Normal file
52
lib/Services/statisticsService.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,15 @@ class Client {
|
||||
DeviceApi? _deviceApi;
|
||||
DeviceApi? get deviceApi => _deviceApi;
|
||||
|
||||
InstanceApi? _instanceApi;
|
||||
InstanceApi? get instanceApi => _instanceApi;
|
||||
|
||||
ApplicationInstanceApi? _applicationInstanceApi;
|
||||
ApplicationInstanceApi? get applicationInstanceApi => _applicationInstanceApi;
|
||||
|
||||
StatsApi? _statsApi;
|
||||
StatsApi? get statsApi => _statsApi;
|
||||
|
||||
Client(String path) {
|
||||
_apiClient = ApiClient(
|
||||
basePath: path); // "http://192.168.31.96"
|
||||
@ -35,5 +44,8 @@ class Client {
|
||||
_sectionApi = SectionApi(_apiClient);
|
||||
_resourceApi = ResourceApi(_apiClient);
|
||||
_deviceApi = DeviceApi(_apiClient);
|
||||
_instanceApi = InstanceApi(_apiClient);
|
||||
_applicationInstanceApi = ApplicationInstanceApi(_apiClient);
|
||||
_statsApi = StatsApi(_apiClient);
|
||||
}
|
||||
}
|
||||
145
lib/l10n/app_localizations.dart
Normal file
145
lib/l10n/app_localizations.dart
Normal 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, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<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.');
|
||||
}
|
||||
17
lib/l10n/app_localizations_en.dart
Normal file
17
lib/l10n/app_localizations_en.dart
Normal 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';
|
||||
}
|
||||
17
lib/l10n/app_localizations_fr.dart
Normal file
17
lib/l10n/app_localizations_fr.dart
Normal 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';
|
||||
}
|
||||
@ -3,20 +3,21 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
// TODO // import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Home/home.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Home/home_3.0.dart';
|
||||
import 'package:mymuseum_visitapp/l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'Helpers/DatabaseHelper.dart';
|
||||
import 'Models/visitContext.dart';
|
||||
import 'app_context.dart';
|
||||
import 'constants.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void main() async {
|
||||
|
||||
100
pubspec.lock
100
pubspec.lock
@ -173,10 +173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -197,10 +197,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -213,10 +213,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -293,10 +293,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -326,14 +326,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_beacon:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_beacon
|
||||
sha256: c93ee7a2e572a2dba31eb8db9f7a2c514b2b3e22584ea7d78d8cc854fdbded79
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -673,10 +665,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -745,26 +737,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "11.0.1"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -800,7 +792,7 @@ packages:
|
||||
manager_api_new:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: manager_api_new
|
||||
path: "../manager-app/manager_api_new"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
@ -816,10 +808,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -832,10 +824,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -928,10 +920,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -992,42 +984,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.5"
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.6"
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1285,18 +1285,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1341,10 +1341,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.6"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1493,10 +1493,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
video_player:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1690,5 +1690,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
11
pubspec.yaml
11
pubspec.yaml
@ -40,11 +40,11 @@ dependencies:
|
||||
carousel_slider: ^5.0.0
|
||||
#flutter_svg_provider: ^1.0.3
|
||||
photo_view: ^0.15.0
|
||||
intl: ^0.19.0
|
||||
intl: ^0.20.2
|
||||
#audioplayers: ^2.0.0
|
||||
just_audio: ^0.9.38
|
||||
get: ^4.6.6
|
||||
permission_handler: ^10.4.5 #^11.3.1
|
||||
permission_handler: 12.0.1 #^11.3.1
|
||||
diacritic: ^0.1.3
|
||||
flutter_widget_from_html: ^0.15.2
|
||||
webview_flutter: ^4.10.0
|
||||
@ -60,7 +60,7 @@ dependencies:
|
||||
|
||||
sqflite: #not in web
|
||||
just_audio_cache: ^0.1.2 #not in web
|
||||
flutter_beacon: ^0.5.1 #not in web
|
||||
#flutter_beacon: ^0.5.1 #not in web
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
|
||||
smooth_page_indicator: ^1.2.1
|
||||
@ -73,7 +73,7 @@ dependencies:
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
manager_api_new:
|
||||
path: manager_api_new
|
||||
path: ../manager-app/manager_api_new
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
@ -140,3 +140,6 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
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.
Loading…
x
Reference in New Issue
Block a user