diff --git a/lib/Components/AdminPopup.dart b/lib/Components/AdminPopup.dart index 627973a..08bc865 100644 --- a/lib/Components/AdminPopup.dart +++ b/lib/Components/AdminPopup.dart @@ -21,7 +21,7 @@ class AdminPopup extends StatefulWidget { class _AdminPopupState extends State { final TextEditingController _controller = TextEditingController(); bool isPasswordOk = false; - String password = "FORT2023!"; + String password = "ADMIN2024!"; //FORT2023! VisitAppContext? visitAppContext; @override diff --git a/lib/Components/BuilderImageSlider.dart b/lib/Components/BuilderImageSlider.dart index 0224aa6..db69e28 100644 --- a/lib/Components/BuilderImageSlider.dart +++ b/lib/Components/BuilderImageSlider.dart @@ -36,7 +36,7 @@ class BuilderImageSlider extends StatelessWidget { //width: size.width * 0.95, child: ClipRRect( borderRadius: BorderRadius.circular(15.0), - child: Image.memory(base64Decode(i.data!))/*PhotoView( + child: Image.memory(base64Decode(i.path!))/*PhotoView( imageProvider: Image.memory(base64Decode(i!.data!)).image, minScale: PhotoViewComputedScale.contained * 0.8, maxScale: PhotoViewComputedScale.contained * 3.0, diff --git a/lib/Components/CustomAppBar.dart b/lib/Components/CustomAppBar.dart index 7aa5f30..deec23f 100644 --- a/lib/Components/CustomAppBar.dart +++ b/lib/Components/CustomAppBar.dart @@ -28,6 +28,7 @@ class _CustomAppBarState extends State { Widget build(BuildContext context) { final appContext = Provider.of(context); VisitAppContext visitAppContext = appContext.getContext(); + Size size = MediaQuery.of(context).size; //final notchInset = MediaQuery.of(context).padding; return AppBar( @@ -45,7 +46,17 @@ class _CustomAppBarState extends State { ), context: context ); }, - child: HtmlWidget(widget.title, textStyle: TextStyle(color: Colors.white),), + child: SizedBox( + width: widget.isHomeButton ? size.width * 0.8 : null, + child: HtmlWidget( + widget.title, + textStyle: const TextStyle(color: Colors.white), + customStylesBuilder: (element) + { + return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2",}; + } + ), + ), ), centerTitle: true, leading: widget.isHomeButton ? IconButton( @@ -73,7 +84,7 @@ class _CustomAppBarState extends State { }, child: SizedBox( width: 50, - child: visitAppContext.isMaximizeTextSize ? const Icon(Icons.text_fields) : const Icon(Icons.format_size) + child: visitAppContext.isMaximizeTextSize ? const Icon(Icons.text_fields, color: Colors.white) : const Icon(Icons.format_size, color: Colors.white) ), ), Padding( @@ -97,8 +108,10 @@ class _CustomAppBarState extends State { Color(0xFF7633B8), Color(0xFF6528B6), Color(0xFF6025B6)*/ + Color(0xFFf6b3c4), kMainColor1, kMainColor2, + ], ), ), diff --git a/lib/Components/ShowImagePopup.dart b/lib/Components/ShowImagePopup.dart index 34b8c00..b6fba39 100644 --- a/lib/Components/ShowImagePopup.dart +++ b/lib/Components/ShowImagePopup.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; @@ -10,7 +11,7 @@ import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:photo_view/photo_view.dart'; -void showImagePopup(ContentDTO contentDTO, ResourceModel resourceModel, AppContext appContext, BuildContext context, Size size) { +void showImagePopup(ContentDTO contentDTO, ResourceModel resourceModel, AppContext appContext, BuildContext context, Size size, File? resourceModelFile) { showDialog( builder: (BuildContext context) => AlertDialog( shape: const RoundedRectangleBorder( @@ -33,8 +34,8 @@ void showImagePopup(ContentDTO contentDTO, ResourceModel resourceModel, AppConte child: Padding( padding: const EdgeInsets.only(left:8.0, right: 8.0, bottom: 8.0, top: 8.0), child: PhotoView( - imageProvider: (appContext.getContext() as VisitAppContext).configuration!.isOffline! ? - Image.memory(base64Decode(resourceModel.data!)).image : + imageProvider: (appContext.getContext() as VisitAppContext).configuration!.isOffline! && resourceModelFile != null ? + Image.file(resourceModelFile).image : // GET FROM FILE Image.network(resourceModel.source!).image, minScale: PhotoViewComputedScale.contained * 1.0, maxScale: PhotoViewComputedScale.contained * 3.0, diff --git a/lib/Components/SliderImages.dart b/lib/Components/SliderImages.dart index d96603f..3c8f195 100644 --- a/lib/Components/SliderImages.dart +++ b/lib/Components/SliderImages.dart @@ -5,6 +5,7 @@ import 'package:manager_api/api.dart'; import 'package:mymuseum_visitapp/Components/ShowImagePopup.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/Services/apiService.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:provider/provider.dart'; @@ -69,45 +70,54 @@ class _SliderImagesWidget extends State { return Builder( builder: (BuildContext context) { //print(widget.imagesDTO[currentIndex-1]); - return Padding( - padding: const EdgeInsets.only(top: 5.0), - child: InkWell( - onTap: () { - showImagePopup(widget.contentsDTO[currentIndex.value-1]!, i!, appContext, context, size); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(15.0), - child: visitAppContext.configuration!.isOffline! ? - Image.memory(base64Decode(i!.data!)) : - Image.network( - i!.source!, - loadingBuilder: (BuildContext context, Widget child, - ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) { - return child; - } - return Center( - child: CircularProgressIndicator( - color: kMainColor1, - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, + return FutureBuilder( + future: ApiService.getResource(appContext, visitAppContext.configuration!, i!.id!), + builder: (context, AsyncSnapshot snapshot) { + return Padding( + padding: const EdgeInsets.only(top: 5.0), + child: InkWell( + onTap: () { + showImagePopup(widget.contentsDTO[currentIndex.value-1]!, i, appContext, context, size, snapshot.data); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(15.0), + child: visitAppContext.configuration!.isOffline! ? + snapshot.data != null ? + Image.file( + snapshot.data!, + fit: BoxFit.cover, + ) : null : + Image.network( + i.source!, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + color: kMainColor1, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ) /*PhotoView( + imageProvider: Image.memory(base64Decode(i!.data!)).image, + minScale: PhotoViewComputedScale.contained * 0.8, + maxScale: PhotoViewComputedScale.contained * 3.0, + backgroundDecoration: BoxDecoration( + color: Colors.transparent, + /*shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(15.0),*/ ), - ); - }, - ) /*PhotoView( - imageProvider: Image.memory(base64Decode(i!.data!)).image, - minScale: PhotoViewComputedScale.contained * 0.8, - maxScale: PhotoViewComputedScale.contained * 3.0, - backgroundDecoration: BoxDecoration( - color: Colors.transparent, - /*shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(15.0),*/ + )*/, ), - )*/, - ), - ), + ), + ); + } ); }, ); diff --git a/lib/Helpers/DatabaseHelper.dart b/lib/Helpers/DatabaseHelper.dart index fde0c6f..46c518e 100644 --- a/lib/Helpers/DatabaseHelper.dart +++ b/lib/Helpers/DatabaseHelper.dart @@ -27,6 +27,7 @@ class DatabaseHelper { static const columnLabel = 'label'; static const columnId = 'id'; static const columnInstanceId = 'instanceId'; + static const columnPath = 'path'; static const columnData = 'data'; static const columnType = 'type'; static const columnDateCreation = 'dateCreation'; @@ -204,7 +205,7 @@ class DatabaseHelper { db.execute(''' CREATE TABLE $resourcesTable ( $columnId TEXT NOT NULL PRIMARY KEY, - $columnData TEXT NOT NULL, + $columnPath TEXT NOT NULL, $columnSource TEXT NOT NULL, $columnType INT NOT NULL ) @@ -406,9 +407,10 @@ class DatabaseHelper { } ResourceModel getResourceFromDB(dynamic element) { + // Here retrieve file from path ? return ResourceModel( id: element["id"], - data: element["data"], + path: element["path"], source: element["source"], type: ResourceType.values[element["type"]] ); diff --git a/lib/Helpers/translationHelper.dart b/lib/Helpers/translationHelper.dart index 7d3baf2..91d3a1a 100644 --- a/lib/Helpers/translationHelper.dart +++ b/lib/Helpers/translationHelper.dart @@ -1,6 +1,5 @@ import 'package:manager_api/api.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; -import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/translations.dart'; class TranslationHelper { diff --git a/lib/Models/resourceModel.dart b/lib/Models/resourceModel.dart index 6538753..6342b2a 100644 --- a/lib/Models/resourceModel.dart +++ b/lib/Models/resourceModel.dart @@ -3,17 +3,17 @@ import 'package:manager_api/api.dart'; class ResourceModel { String? id = ""; - String? data = ""; + String? path = ""; String? source = ""; String? label = ""; ResourceType? type; - ResourceModel({this.id, this.data, this.source, this.type}); + ResourceModel({this.id, this.path, this.source, this.type}); Map toMap() { return { 'id': id, - 'data': data, + 'path': path, 'source': source, 'type': type?.value }; @@ -22,7 +22,7 @@ class ResourceModel { factory ResourceModel.fromJson(Map json) { return ResourceModel( id: json['id'] as String, - data: json['data'] as String, + path: json['path'] as String, source: json['source'] as String, type: json['type'] as ResourceType ); @@ -30,6 +30,6 @@ class ResourceModel { @override String toString() { - return 'ResourceModel{id: $id, type: $type, source: $source, data: $data, label: $label}'; + return 'ResourceModel{id: $id, type: $type, source: $source, path: $path, label: $label}'; } } \ No newline at end of file diff --git a/lib/Models/visitContext.dart b/lib/Models/visitContext.dart index 07239fd..1e290de 100644 --- a/lib/Models/visitContext.dart +++ b/lib/Models/visitContext.dart @@ -3,13 +3,15 @@ import 'package:manager_api/api.dart'; import 'package:mymuseum_visitapp/Models/articleRead.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.myinfomate.be"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 -class VisitAppContext with ChangeNotifier{ String? id = ""; String? language = ""; String? instanceId = "63514fd67ed8c735aaa4b8f2"; // 63514fd67ed8c735aaa4b8f2 MyInfoMate test instance -- Fort de Saint-Héribert Mymuseum instance id : 633ee379d9405f32f166f047 // 63514fd67ed8c735aaa4b8f1 Mymuseum test - List? configurations; ConfigurationDTO? configuration; List? sectionIds; // Use to valid QR code found @@ -26,6 +28,8 @@ class VisitAppContext with ChangeNotifier{ bool? isAdmin = false; bool? isAllLanguages = false; + String? localPath; + VisitAppContext({this.language, this.id, this.configuration, this.isAdmin, this.isAllLanguages, this.instanceId}); Map toMap() { diff --git a/lib/Screens/Article/article_page.dart b/lib/Screens/Article/article_page.dart index dfbd322..4df4b2f 100644 --- a/lib/Screens/Article/article_page.dart +++ b/lib/Screens/Article/article_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -18,6 +19,7 @@ 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 'package:path_provider/path_provider.dart'; import 'audio_player_floating.dart'; @@ -37,8 +39,8 @@ class _ArticlePageState extends State { List resourcesModel = []; ResourceModel? audioResourceModel; final GlobalKey _scaffoldKey = GlobalKey(); - late Uint8List audiobytes; - VisitAppContext? visitAppContext; + late File audioFile; + late VisitAppContext visitAppContext; @override void initState() { @@ -48,7 +50,7 @@ class _ArticlePageState extends State { @override void dispose() { - visitAppContext!.isContentCurrentlyShown = false; + visitAppContext.isContentCurrentlyShown = false; super.dispose(); } @@ -63,12 +65,12 @@ class _ArticlePageState extends State { return Scaffold( key: _scaffoldKey, appBar: CustomAppBar( - title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext!) : "", + title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "", isHomeButton: false, isTextSizeButton: true, ), body: FutureBuilder( - future: getArticle(appContext, appContext.clientAPI, widget.articleId, false), + future: getArticle(appContext, visitAppContext.clientAPI, widget.articleId, false), builder: (context, AsyncSnapshot snapshot) { if(articleDTO != null && sectionDTO != null) { if(size.height > size.width) { @@ -89,7 +91,7 @@ class _ArticlePageState extends State { ], ); } else { - return Container( + return SizedBox( height: size.height, width: size.width, child: Column( @@ -120,11 +122,11 @@ class _ArticlePageState extends State { } ), floatingActionButton: FutureBuilder( - future: getArticle(appContext, appContext.clientAPI, widget.articleId, true), + future: getArticle(appContext, visitAppContext.clientAPI, widget.articleId, true), builder: (context, AsyncSnapshot snapshot) { return Padding( padding: EdgeInsets.only(right: 0, top: 0), //size.height*0.1 - child: audioResourceModel != null && audioResourceModel!.source != null ? AudioPlayerFloatingContainer(file: null, audioBytes: null, resourceURl: audioResourceModel!.source!, isAuto: articleDTO!.isReadAudioAuto!) : null, + child: audioResourceModel != null && audioResourceModel!.source != null ? AudioPlayerFloatingContainer(file: audioFile, resourceURl: "", isAuto: articleDTO!.isReadAudioAuto!) : null, ); } ), @@ -212,6 +214,10 @@ class _ArticlePageState extends State { child: HtmlWidget( TranslationHelper.get(articleDTO!.content, appContext.getContext()), textStyle: TextStyle(fontSize: (appContext.getContext() as VisitAppContext).isMaximizeTextSize ? kArticleContentBiggerSize : kArticleContentSize), + customStylesBuilder: (element) + { + return {'font-family': "Roboto"}; + } //textAlign: TextAlign.left, ), ), @@ -245,7 +251,7 @@ class _ArticlePageState extends State { child: HtmlWidget( TranslationHelper.get(articleDTO!.content, appContext.getContext()), //textAlign: TextAlign.left, - textStyle: TextStyle(fontSize: (appContext.getContext() as VisitAppContext).isMaximizeTextSize ? kArticleContentBiggerSize : kArticleContentSize) + textStyle: TextStyle(fontSize: (appContext.getContext() as VisitAppContext).isMaximizeTextSize ? kArticleContentBiggerSize : kArticleContentSize, fontFamily: "Arial"), ), ), ) @@ -270,9 +276,9 @@ class _ArticlePageState extends State { try { SectionRead articleRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch); await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, articleRead.toMap()); - visitAppContext!.readSections.add(articleRead); + visitAppContext.readSections.add(articleRead); - appContext.setContext(visitAppContext!); + appContext.setContext(visitAppContext); } catch (e) { print("DATABASE ERROR ARTICLEREAD"); print(e); @@ -289,9 +295,9 @@ class _ArticlePageState extends State { try { SectionRead articleRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch); await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, articleRead.toMap()); - visitAppContext!.readSections.add(articleRead); + visitAppContext.readSections.add(articleRead); - appContext.setContext(visitAppContext!); + appContext.setContext(visitAppContext); } catch (e) { print("DATABASE ERROR ARTICLEREAD"); print(e); @@ -311,32 +317,50 @@ class _ArticlePageState extends State { { try{ // OFFLINE + List> ressourceTest = await DatabaseHelper .instance.queryWithColumnId( DatabaseTableType.resources, audioIdArticle.first.value!); + + if (ressourceTest.isNotEmpty) { + audioResourceModel = DatabaseHelper.instance.getResourceFromDB(ressourceTest.first); + print(audioResourceModel!.id); + if(audioResourceModel!.path != null) { + audioFile = File(audioResourceModel!.path!); + } + } else { + print("EMPTY resourcesModel - first"); + } + + /*List> ressourceTest = await DatabaseHelper + .instance.queryWithColumnId( + DatabaseTableType.resources, audioIdArticle.first.value!); if (ressourceTest.isNotEmpty) { audioResourceModel = DatabaseHelper.instance.getResourceFromDB(ressourceTest.first); print(audioResourceModel!.id); - Uint8List base64String = base64Decode(audioResourceModel!.data!); + Uint8List base64String = base64Decode(audioResourceModel!.path!); // TODO get from file audiobytes = base64String; } else { print("EMPTY resourcesModel - first"); - } + }*/ } catch(e) { print("Error in audio loading: " + e.toString()); } } else { + // TODO Get file instead.. if exist ResourceDTO? resourceDTO = await client.resourceApi!.resourceGetDetail(audioIdArticle.first.value!); - // ONLINE - ResourceModel? resourceAudioOnline = await ApiService.downloadAudio(client, audioIdArticle.first.value!); - if(resourceAudioOnline != null) { - resourceAudioOnline.source = resourceDTO!.url; + if(resourceDTO != null && resourceDTO.url != null) { + // ONLINE + //ResourceModel? resourceAudioOnline = await ApiService.downloadAudio(client, resourceDTO.url!, resourceDTO.id!); + ResourceModel resourceAudioOnline = ResourceModel(); + resourceAudioOnline.source = resourceDTO.url; audioResourceModel = resourceAudioOnline; - Uint8List base64String = base64Decode(resourceAudioOnline.data!); - audiobytes = base64String; + + /*Uint8List base64String = base64Decode(resourceAudioOnline.path!); // GET FROM FILE + audiobytes = base64String;*/ } else { print("EMPTY resourcesModel online - audio"); } diff --git a/lib/Screens/Article/audio_player_floating.dart b/lib/Screens/Article/audio_player_floating.dart index d1898a0..4cf271a 100644 --- a/lib/Screens/Article/audio_player_floating.dart +++ b/lib/Screens/Article/audio_player_floating.dart @@ -11,10 +11,9 @@ import 'package:just_audio_cache/just_audio_cache.dart'; class AudioPlayerFloatingContainer extends StatefulWidget { - const AudioPlayerFloatingContainer({Key? key, required this.file, required this.audioBytes, required this.resourceURl, required this.isAuto}) : super(key: key); + const AudioPlayerFloatingContainer({Key? key, required this.file, required this.resourceURl, required this.isAuto}) : super(key: key); final File? file; - final Uint8List? audioBytes; final String resourceURl; final bool isAuto; @@ -36,10 +35,6 @@ class _AudioPlayerFloatingContainerState extends State { List alreadyDownloaded = []; VisitAppContext? visitAppContext; + bool isDialogOpen = false; + @override void initState() { configurations = widget.configurations; @@ -130,22 +132,21 @@ class _ConfigurationsListState extends State { width: size.width * 0.3, child: FutureBuilder( future: ApiService.getResource( - appContext, configurations[index].imageId!), + appContext, configurations[index], configurations[index].imageId!), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { - return snapshot.data != null - ? ClipRRect( + return ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), bottomLeft: Radius.circular(20)), - child: snapshot.data.data != null - ? Image.memory( - base64Decode( - snapshot.data.data!), - fit: BoxFit.cover) - : Image.network( + child: snapshot.data != null + ? Image.file( + snapshot.data!, + fit: BoxFit.cover, + ): + Image.network( configurations[index] .imageSource!, fit: BoxFit.cover, @@ -174,8 +175,7 @@ class _ConfigurationsListState extends State { ); }, ), - ) - : const Text(""); + ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(TranslationHelper.getFromLocale( @@ -270,7 +270,7 @@ class _ConfigurationsListState extends State { : TranslationHelper.getFromLocale( "downloadPromptUpdate", appContext.getContext()), - style: const TextStyle(color: kMainColor), + style: const TextStyle(color: kSecondGrey), textAlign: TextAlign.center), ), const SizedBox( @@ -279,7 +279,7 @@ class _ConfigurationsListState extends State { Text( TranslationHelper.getFromLocale( "downloadLanguage", appContext.getContext()), - style: const TextStyle(color: kMainColor), + style: const TextStyle(color: kSecondGrey), textAlign: TextAlign.center), const SizedBox( height: 25, @@ -293,7 +293,7 @@ class _ConfigurationsListState extends State { child: Text( TranslationHelper.getFromLocale( "close", appContext.getContext()), - style: TextStyle(color: kMainColor)), + style: TextStyle(color: kSecondGrey)), onPressed: () { isCancel = true; Navigator.of(context).pop(); @@ -303,7 +303,7 @@ class _ConfigurationsListState extends State { child: Text( TranslationHelper.getFromLocale( "download", appContext.getContext()), - style: TextStyle(color: kMainColor)), + style: TextStyle(color: kSecondGrey)), onPressed: () async { Navigator.of(context).pop(); }, @@ -316,7 +316,7 @@ class _ConfigurationsListState extends State { //} if (!isCancel) { - String loadingText = TranslationHelper.getFromLocale( + /*String loadingText = TranslationHelper.getFromLocale( "downloadConfiguration", appContext.getContext()); showDialog( barrierDismissible: false, @@ -338,18 +338,40 @@ class _ConfigurationsListState extends State { ), ), ); - }); + });*/ - var audiosNotWorking = await DownloadConfiguration.download(buildContext, appContext, configuration); + // DIALOG HERE + if(!isDialogOpen) { + isDialogOpen = true; - print("C'EST FINI. - nombre d'audios qui ne fonctionnent pas"); - print(audiosNotWorking.length); + var result = await showDialog( + builder: (BuildContext dialogContext) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)) + ), + content: Container( + width: 400, + height: 200, + child: DownloadConfigurationWidget(configuration: configuration), + ), + actions: [], + ), context: context + ); + isDialogOpen = false; + } else { + print("ALREADY OPEN LAAAA"); + } - Navigator.of(context).pop(); + //var audiosNotWorking = await DownloadConfiguration.download(buildContext, appContext, configuration); + + /*print("C'EST FINI. - nombre d'audios qui ne fonctionnent pas"); + print(audiosNotWorking.length);*/ + + //Navigator.of(context).pop(); VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext; visitAppContext.audiosNotWorking = []; - visitAppContext.audiosNotWorking = audiosNotWorking; + //visitAppContext.audiosNotWorking = audiosNotWorking; appContext.setContext(visitAppContext); } } diff --git a/lib/Screens/Home/home.dart b/lib/Screens/Home/home.dart index 293e014..3e8a4d5 100644 --- a/lib/Screens/Home/home.dart +++ b/lib/Screens/Home/home.dart @@ -36,7 +36,7 @@ class _HomePageState extends State with WidgetsBindingObserver { List configurations = []; List alreadyDownloaded = []; - VisitAppContext? visitAppContext; + late VisitAppContext visitAppContext; @override Widget build(BuildContext context) { @@ -54,7 +54,7 @@ class _HomePageState extends State with WidgetsBindingObserver { width: size.width, height: size.height, child: FutureBuilder( - future: getConfigurationsCall(appContext.clientAPI, appContext), + future: getConfigurationsCall(visitAppContext.clientAPI, appContext), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { configurations = List.from(snapshot.data).where((configuration) => configuration.isMobile!).toList(); diff --git a/lib/Screens/Quizz/quizz_page.dart b/lib/Screens/Quizz/quizz_page.dart index f88a1f3..3688e62 100644 --- a/lib/Screens/Quizz/quizz_page.dart +++ b/lib/Screens/Quizz/quizz_page.dart @@ -40,7 +40,7 @@ class _QuizzPageState extends State { ResourceModel? audioResourceModel; final GlobalKey _scaffoldKey = GlobalKey(); late Uint8List audiobytes; - VisitAppContext? visitAppContext; + late VisitAppContext visitAppContext; QuizzDTO? quizzDTO; List _questionsSubDTO = []; @@ -67,7 +67,7 @@ class _QuizzPageState extends State { @override void dispose() { - visitAppContext!.isContentCurrentlyShown = false; + visitAppContext.isContentCurrentlyShown = false; currentIndex = 1; //_controllerCenter!.dispose(); if(quizzDTO != null) { @@ -94,7 +94,7 @@ class _QuizzPageState extends State { body: OrientationBuilder( builder: (context, orientation) { return FutureBuilder( - future: getQuizz(appContext, appContext.clientAPI, widget.sectionId), // MAYBE MOVE THAT TO PARENT .. + future: getQuizz(appContext, visitAppContext.clientAPI, widget.sectionId), // MAYBE MOVE THAT TO PARENT .. builder: (context, AsyncSnapshot snapshot) { if(quizzDTO != null && sectionDTO != null) { diff --git a/lib/Screens/Visit/beaconArticleFound.dart b/lib/Screens/Visit/beaconArticleFound.dart index beb6a29..b50e715 100644 --- a/lib/Screens/Visit/beaconArticleFound.dart +++ b/lib/Screens/Visit/beaconArticleFound.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; @@ -18,6 +19,7 @@ import 'package:mymuseum_visitapp/client.dart'; import 'package:mymuseum_visitapp/constants.dart'; import 'package:photo_view/photo_view.dart'; import 'package:provider/provider.dart'; +import 'package:path_provider/path_provider.dart'; class BeaconArticleFound extends StatefulWidget { const BeaconArticleFound({Key? key, required this.beaconSection}) : super(key: key); @@ -29,7 +31,7 @@ class BeaconArticleFound extends StatefulWidget { } class _BeaconArticleFoundState extends State { - VisitAppContext? visitAppContext; + late VisitAppContext visitAppContext; SectionDTO? sectionFound; @override @@ -39,15 +41,15 @@ class _BeaconArticleFoundState extends State { visitAppContext = appContext.getContext(); - if(widget.beaconSection != null && visitAppContext!.currentSections != null && visitAppContext!.currentSections!.isNotEmpty) + if(widget.beaconSection != null && visitAppContext.currentSections != null && visitAppContext.currentSections!.isNotEmpty) { - var testSection = visitAppContext!.currentSections!.where((section) => section!.id.toString() == widget.beaconSection!.sectionId.toString()).toList(); + var testSection = visitAppContext.currentSections!.where((section) => section!.id.toString() == widget.beaconSection!.sectionId.toString()).toList(); sectionFound = testSection.isNotEmpty ? testSection.first : null; } return FutureBuilder( - future: getSectionImage(appContext, appContext.clientAPI, sectionFound), - builder: (context, AsyncSnapshot snapshot) { + future: getSectionImage(appContext, visitAppContext.clientAPI, sectionFound), + builder: (context, AsyncSnapshot snapshot) { return SizedBox( height: size.height *0.4, width: size.width *0.9, @@ -56,7 +58,7 @@ class _BeaconArticleFoundState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if(snapshot.data != null && ((visitAppContext!.configuration!.isOffline! && snapshot.data!.data != null) || (visitAppContext!.configuration!.isOffline! && snapshot.data!.source != null))) + if(snapshot.data != null && ((visitAppContext.configuration!.isOffline! && snapshot.data!.path != null) || (visitAppContext.configuration!.isOffline! && snapshot.data!.source != null))) SizedBox( height: size.height * 0.25, width: size.width * 0.75, @@ -65,30 +67,31 @@ class _BeaconArticleFoundState extends State { padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(20)), - child: visitAppContext!.configuration!.isOffline! ? - Image.memory( - base64Decode(snapshot.data!.data!), - fit: BoxFit.cover - ) : - Image.network( - snapshot.data!.source!, - fit: BoxFit.cover, - loadingBuilder: (BuildContext context, Widget child, - ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) { - return child; - } - return Center( - child: CircularProgressIndicator( - color: kMainColor1, - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ); - }, - ), + child: visitAppContext.configuration!.isOffline! ? + snapshot.data != null ? + Image.file( + snapshot.data!, + fit: BoxFit.cover, + ) : null : + Image.network( + snapshot.data!.source!, + fit: BoxFit.cover, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + color: kMainColor1, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), ), ), ), @@ -120,7 +123,7 @@ class _BeaconArticleFoundState extends State { ); } - Future getSectionImage(AppContext appContext, Client clientAPI, SectionDTO? sectionFound) async { + Future getSectionImage(AppContext appContext, Client clientAPI, SectionDTO? sectionFound) async { if(sectionFound == null) { return null; } @@ -128,7 +131,16 @@ class _BeaconArticleFoundState extends State { if(isConfigOffline) { List> resource = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, sectionFound.imageId!); if(resource.isNotEmpty) { - return DatabaseHelper.instance.getResourceFromDB(resource.first); + Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); + String localPath = appDocumentsDirectory!.path; + Directory configurationDirectory = Directory('$localPath/${sectionFound.configurationId}'); + List fileList = configurationDirectory.listSync(); + + if(fileList.any((fileL) => fileL.uri.pathSegments.last.contains(sectionFound.imageId!))) { + File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(sectionFound.imageId!)).path); + return file; + } + return null; } else { print("EMPTY resourcesModel - getSectionImage"); return null; diff --git a/lib/Screens/Visit/components/body.dart b/lib/Screens/Visit/components/body.dart index 77f2ffa..20e295d 100644 --- a/lib/Screens/Visit/components/body.dart +++ b/lib/Screens/Visit/components/body.dart @@ -163,7 +163,7 @@ class _BodyState extends State { else { // ONLINE - List? sectionsDownloaded = await ApiService.getAllSections(appContext.clientAPI, visitAppContext.configuration!.id!); + List? sectionsDownloaded = await ApiService.getAllSections(visitAppContext.clientAPI, visitAppContext.configuration!.id!); //print(sectionsDownloaded); if(sectionsDownloaded!.isNotEmpty) { sections = sectionsDownloaded.where((s) => s.type == SectionType.Article || s.type == SectionType.Quizz).toList(); // TODO Support more than Article and Quizz section type diff --git a/lib/Screens/Visit/components/section_card.dart b/lib/Screens/Visit/components/section_card.dart index 0bc7c04..b6054a1 100644 --- a/lib/Screens/Visit/components/section_card.dart +++ b/lib/Screens/Visit/components/section_card.dart @@ -82,15 +82,15 @@ class SectionCard extends StatelessWidget { // image is square but we add extra 20 + 20 padding thats why width is 200 width: size.width*0.5, child: FutureBuilder( - future: ApiService.getResource(appContext, sectionDTO.imageId!), + future: ApiService.getResource(appContext, (appContext.getContext() as VisitAppContext).configuration!, sectionDTO.imageId!), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { - return snapshot.data != null ? ClipRRect( + return ClipRRect( borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20)), - child: isOffline ? - Image.memory( - base64Decode(snapshot.data!.data!), - fit: BoxFit.cover + child: isOffline && snapshot.data != null ? + Image.file( + snapshot.data!, + fit: BoxFit.cover, ) : Image.network( sectionDTO.imageSource!, @@ -111,7 +111,7 @@ class SectionCard extends StatelessWidget { ); }, ), - ) : const Text(""); + ); } else if (snapshot.connectionState == ConnectionState.none) { return Text(TranslationHelper.getFromLocale("noData", appContext.getContext())); } else { diff --git a/lib/Screens/Visit/visit.dart b/lib/Screens/Visit/visit.dart index 449fc66..38968d3 100644 --- a/lib/Screens/Visit/visit.dart +++ b/lib/Screens/Visit/visit.dart @@ -272,7 +272,7 @@ class _VisitPageState extends State with WidgetsBindingObserver { content: BeaconArticleFound(beaconSection: beaconSection), actions: [ TextButton( - child: Text(TranslationHelper.getFromLocale("close", visitAppContext), style: TextStyle(color: kMainColor)), + 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(); @@ -280,7 +280,7 @@ class _VisitPageState extends State with WidgetsBindingObserver { }, ), TextButton( - child: Text(TranslationHelper.getFromLocale("open", visitAppContext), style: TextStyle(color: kMainColor)), + 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(); @@ -364,147 +364,146 @@ class _VisitPageState extends State with WidgetsBindingObserver { alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.only(right: 90, bottom: 1), - child: SizedBox( - height: 75.0, - width: 65.0, - child: FittedBox( - child: FloatingActionButton( - heroTag: "beacon", - onPressed: () async { - bool isCancel = false; + child: InkWell( + onTap: () async { + bool isCancel = false; - if(!controller.authorizationStatusOk) { - //await handleOpenLocationSettings(); + if(!controller.authorizationStatusOk) { + //await handleOpenLocationSettings(); - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - return AlertDialog( - backgroundColor: Colors.white, - content: Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: SizedBox( - height: 215, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.my_location, color: kMainColor), - Padding( - padding: const EdgeInsets.all(10.0), - child: Text(TranslationHelper.getFromLocale("locationWarning", visitAppContext), style: const TextStyle(color: kMainColor), textAlign: TextAlign.center), - ), - ], + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + return AlertDialog( + backgroundColor: Colors.white, + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: SizedBox( + height: 215, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.my_location, color: kSecondGrey), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text(TranslationHelper.getFromLocale("locationWarning", visitAppContext), style: const TextStyle(color: kSecondGrey), textAlign: TextAlign.center), ), - ), + ], ), - actions: [ - TextButton( - child: Text(TranslationHelper.getFromLocale("close", visitAppContext), style: TextStyle(color: kMainColor)), - onPressed: () { - isCancel = true; - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(TranslationHelper.getFromLocale("ok", visitAppContext), style: TextStyle(color: kMainColor)), - onPressed: () async { - Navigator.of(context).pop(); - }, - ) - ], - actionsAlignment: MainAxisAlignment.spaceAround, - contentPadding: EdgeInsets.zero, - ); - }); + ), + ), + actions: [ + TextButton( + child: Text(TranslationHelper.getFromLocale("close", visitAppContext), style: TextStyle(color: kSecondGrey)), + onPressed: () { + isCancel = true; + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(TranslationHelper.getFromLocale("ok", visitAppContext), style: TextStyle(color: kSecondGrey)), + onPressed: () async { + Navigator.of(context).pop(); + }, + ) + ], + actionsAlignment: MainAxisAlignment.spaceAround, + contentPadding: EdgeInsets.zero, + ); + }); - if(!isCancel) { - if(Platform.isIOS) { - Map statuses0 = await [ - Permission.bluetooth, - ].request(); + if(!isCancel) { + if(Platform.isIOS) { + Map statuses0 = await [ + Permission.bluetooth, + ].request(); - Map statuses1 = await [ - Permission.bluetoothScan, - ].request(); + Map statuses1 = await [ + Permission.bluetoothScan, + ].request(); - Map statuses2 = await [ - Permission.bluetoothConnect, - ].request(); + Map statuses2 = await [ + Permission.bluetoothConnect, + ].request(); - Map statuses3 = await [ - Permission.locationWhenInUse, - ].request(); + Map statuses3 = await [ + Permission.locationWhenInUse, + ].request(); - Map statuses4 = await [ - Permission.location, - ].request(); + Map statuses4 = await [ + Permission.location, + ].request(); - Map statuses5 = await [ - Permission.locationAlways, - ].request(); + Map statuses5 = await [ + Permission.locationAlways, + ].request(); - print(statuses0[Permission.bluetooth]); - print(statuses1[Permission.bluetoothScan]); - print(statuses2[Permission.bluetoothConnect]); - print(statuses3[Permission.locationWhenInUse]); - print(statuses4[Permission.location]); - print(statuses5[Permission.locationAlways]); + print(statuses0[Permission.bluetooth]); + print(statuses1[Permission.bluetoothScan]); + print(statuses2[Permission.bluetoothConnect]); + print(statuses3[Permission.locationWhenInUse]); + print(statuses4[Permission.location]); + print(statuses5[Permission.locationAlways]); - } else { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.location, - ].request(); + } else { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ].request(); - print(statuses[Permission.bluetoothScan]); - print(statuses[Permission.bluetoothConnect]); - print(statuses[Permission.location]); - print(statuses[Permission.locationWhenInUse]); + print(statuses[Permission.bluetoothScan]); + print(statuses[Permission.bluetoothConnect]); + print(statuses[Permission.location]); + print(statuses[Permission.locationWhenInUse]); - var status = await Permission.bluetoothScan.status; - print(status); - } - await listeningState(); - } + var status = await Permission.bluetoothScan.status; + print(status); + } + await listeningState(); + } + } + + if(!isCancel) { + if(!controller.bluetoothEnabled) { + await handleOpenBluetooth(); + } + + if(!controller.locationServiceEnabled) { + await handleOpenLocationSettings(); + } + if(!visitAppContext.isScanningBeacons) { + print("Start Scan"); + print(_streamRanging); + if (_streamRanging != null) { + _streamRanging?.resume(); + } else { + await initScanBeacon(visitAppContext); } - if(!isCancel) { - if(!controller.bluetoothEnabled) { - await handleOpenBluetooth(); - } - - if(!controller.locationServiceEnabled) { - await handleOpenLocationSettings(); - } - if(!visitAppContext.isScanningBeacons) { - print("Start Scan"); - print(_streamRanging); - if (_streamRanging != null) { - _streamRanging?.resume(); - } else { - await initScanBeacon(visitAppContext); - } - - controller.startScanning(); - visitAppContext.isScanningBeacons = true; - visitAppContext.isScanBeaconAlreadyAllowed = true; - appContext.setContext(visitAppContext); - } else { - print("Pause Scan"); - controller.pauseScanning(); // PAUSE OR DISPOSE ? - visitAppContext.isScanningBeacons = false; - appContext.setContext(visitAppContext); - } - } - }, - tooltip: 'Beacon', - backgroundColor: visitAppContext.isScanningBeacons ? kMainColor1 : Colors.grey, - child: const Icon(Icons.my_location), + controller.startScanning(); + visitAppContext.isScanningBeacons = true; + visitAppContext.isScanBeaconAlreadyAllowed = true; + appContext.setContext(visitAppContext); + } else { + print("Pause Scan"); + controller.pauseScanning(); // PAUSE OR DISPOSE ? + 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), ), ), ), diff --git a/lib/Services/apiService.dart b/lib/Services/apiService.dart index e759288..c6bdf1b 100644 --- a/lib/Services/apiService.dart +++ b/lib/Services/apiService.dart @@ -8,6 +8,7 @@ import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/client.dart'; +import 'package:path_provider/path_provider.dart'; class ApiService { static Future?> getConfigurations(Client client, VisitAppContext? visitAppContext) async { @@ -117,7 +118,7 @@ class ApiService { await for(dynamic d in response) { _downloadData.addAll(d); } //print("AFTER"); final base64Str = base64.encode(_downloadData); - ResourceModel resourceModel = ResourceModel(id: contentDTO.resourceId, source: contentDTO.resourceUrl, data: base64Str, type: ResourceType.Image); + ResourceModel resourceModel = ResourceModel(id: contentDTO.resourceId, source: contentDTO.resourceUrl, path: base64Str, type: ResourceType.Image); return resourceModel; } @@ -132,7 +133,7 @@ class ApiService { } else { bool isOnline = await hasNetwork(); if(isOnline) { - ResourceModel? resourceModel = await downloadAudio(client, audioId); + ResourceModel? resourceModel = await downloadAudio(client, audioId, resourceTest.first[0]); if(resourceModel != null) { await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); } @@ -148,33 +149,43 @@ class ApiService { } } - static Future downloadAudio(Client client, String audioId) async { - var url = "https://api.myinfomate.be/api/Resource/"+audioId; // TO TEST TODO UPDATE ROUTE + static Future downloadAudio(Client client, String resourceUrl, String audioId) async { + //var url = "https://api.myinfomate.be/api/Resource/"+audioId; // TO TEST TODO UPDATE ROUTE HttpClient client2 = HttpClient(); var _downloadData = []; - final HttpClientRequest request = await client2.getUrl(Uri.parse(url)); + final HttpClientRequest request = await client2.getUrl(Uri.parse(resourceUrl)); HttpClientResponse response = await request.close(); await for(dynamic d in response) { _downloadData.addAll(d); } final base64Str = base64.encode(_downloadData); - ResourceModel resourceModel = ResourceModel(id: audioId, data: base64Str, type: ResourceType.Audio, source: ""); + ResourceModel resourceModel = ResourceModel(id: audioId, path: base64Str, type: ResourceType.Audio, source: ""); return resourceModel; } - static Future getResource(AppContext appContext, String imageId) async { + static Future getResource(AppContext appContext, ConfigurationDTO configurationDTO, String imageId) async { if((appContext.getContext() as VisitAppContext).configuration == null || (appContext.getContext() as VisitAppContext).configuration!.isOffline!) { + Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); + String localPath = appDocumentsDirectory!.path; + Directory configurationDirectory = Directory('$localPath/${configurationDTO.id}'); + List fileList = configurationDirectory.listSync(); + + if(fileList.any((fileL) => fileL.uri.pathSegments.last.contains(imageId))) { + File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(imageId)).path); + return file; + } // OFFLINE - List> resourceTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, imageId); + /*List> resourceTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, imageId); if(resourceTest.isNotEmpty) { return DatabaseHelper.instance.getResourceFromDB(resourceTest.first); } else { return null; - } - } else - { - // ONLINE - return ResourceModel(); // To mock + }*/ + + } + + // ONLINE + return null; } static Future exportConfiguration(Client client, String configurationId, String? language) async { diff --git a/lib/Services/downloadConfiguration.dart b/lib/Services/downloadConfiguration.dart index 6dc3122..1ac90cd 100644 --- a/lib/Services/downloadConfiguration.dart +++ b/lib/Services/downloadConfiguration.dart @@ -1,17 +1,329 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:manager_api/api.dart'; import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart'; import 'package:mymuseum_visitapp/Helpers/modelsHelper.dart'; +import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Services/apiService.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 'package:http/http.dart' as http; +import 'package:fluttertoast/fluttertoast.dart'; -class DownloadConfiguration { +class DownloadConfigurationWidget extends StatefulWidget { + DownloadConfigurationWidget({Key? key, required this.configuration}) : super(key: key); + final ConfigurationDTO configuration; + + @override + State createState() => _DownloadConfigurationWidgetState(); +} + +class _DownloadConfigurationWidgetState extends State { + ValueNotifier currentResourceIndex = ValueNotifier(0); + ValueNotifier currentResourceNbr = ValueNotifier(-1); + bool isAlreadyDownloading = false; + //OtaEvent? currentEvent; + + Future download(BuildContext buildContext, VisitAppContext visitAppContext) async { + bool isAllLanguages = true; + + if(visitAppContext.isAllLanguages != null) { + isAllLanguages = visitAppContext.isAllLanguages!; + if(isAllLanguages) { + print("Admin here and all download all audios !"); + /*ScaffoldMessenger.of(buildContext).showSnackBar( + const SnackBar( + content: Text("Tous les audios vont être téléchargés"), + backgroundColor: kMainColor), + );*/ + + + } + } + + if(!isAlreadyDownloading) { + isAlreadyDownloading = true; + + // HERE CHECK VERSION APK + if(true) { + Map statuses = await [ + Permission.requestInstallPackages, + ].request(); + //tryOtaUpdate(); + } + + ExportConfigurationDTO? exportConfigurationDTO; + try{ + // Retrieve all url from resource to download (get all resource from configuration en somme) + exportConfigurationDTO = await visitAppContext.clientAPI.configurationApi!.configurationExport(widget.configuration.id!, isAllLanguages ? null : visitAppContext.language); // tabletAppContext.configuration!.id! // 65c5f0ee4c030e63ce16bff5 TODO Remove + } catch(e) { + print("Erreur lors du téléchargement de la configuration et de ses ressources !"); + print(e); + return false; + } + + exportConfigurationDTO.resources!.forEach((element) { + print(element.id); + print(element.label); + }); + + if(exportConfigurationDTO.resources != null && exportConfigurationDTO.resources!.isNotEmpty) { + Map statuses = await [ + Permission.storage, + ].request(); + //if(statuses[Permission.storage] == PermissionStatus.granted) { + try{ + try { + Directory directory = Directory('${visitAppContext.localPath}'); + List allConfigurations = directory.listSync(); + Directory configurationDirectory = Directory('${visitAppContext.localPath}/${widget.configuration.id}'); + if(!allConfigurations.any((configurationDirectory) => configurationDirectory.uri.pathSegments.any((element) => element == widget.configuration.id))) { + // create directory + print("Trying to create directory"); + configurationDirectory.createSync(recursive: true); + print('Répertoire créé avec succès.'); + } + } catch(e) { + print("Listing failed, so try to create directory"); + Directory configurationDirectory = Directory('${visitAppContext.localPath}/${widget.configuration.id}'); + + configurationDirectory.createSync(recursive: true); + print('Répertoire créé avec succès.'); + } + + Directory configurationDirectory = Directory('${visitAppContext.localPath}/${widget.configuration.id}'); + List fileList = configurationDirectory.listSync(); + + for (var file in fileList) { + print(file.uri.pathSegments.last); + } + + var resourcesToDownload = exportConfigurationDTO.resources!.where((resource) => resource.type != ResourceType.ImageUrl && resource.type != ResourceType.VideoUrl && resource.type != ResourceType.JsonUrl && resource.url != null && !fileList.any((fileL) => fileL.uri.pathSegments.last.contains(resource.id!))); + + currentResourceNbr.value = resourcesToDownload.length; + + // foreach ou on va tout télécharger - avec un joli etape 0 / length - on peut rendre tout lent on s'en fou ça ne ce fait qu'une fois + for (var resource in resourcesToDownload) { + String? filePath = await downloadResource(visitAppContext, widget.configuration, resource, visitAppContext.localPath!); + + if (filePath != null) + { + // Insert in database + ResourceModel resourceModel = ResourceModel(id: resource.id, source: resource.url, path: filePath, type: resource.type); + try { + await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); + } catch (e) { + print("We got an issue inserting image metadata ${resource.id}"); + } + + currentResourceIndex.value++; + } else { + print("NOT SUCCESSS"); + } + } + + // Delete others that are no more used + var resourceToDelete = fileList.where((fileL) => + !exportConfigurationDTO!.resources!.any((resource) => + resource.id != null && fileL.uri.pathSegments.last.contains(resource.id!))); + + for (var resource in resourceToDelete) { + print("resource to DELETE"); + print(resource.path); + // resource.deleteSync(); + // Preserve call to firebase // TODO uncomment if needed + } + + await DatabaseHelper.instance.insert(DatabaseTableType.configurations, ModelsHelper.configurationToMap(widget.configuration)); + + List? sections = exportConfigurationDTO.sections; + List usedImageOrAudioIds = []; + + if(sections!.isNotEmpty) { + + List sectionsInDB = await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, widget.configuration.id!); + List sectionsToKeep = sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quizz).toList(); // TODO handle other type of section (for now, Article and Quizz) + + sectionsToKeep.sort((a,b) => a.order!.compareTo(b.order!)); + int newOrder = 0; + // Update local DB - Sections + for(var section in sectionsToKeep) { + section.order = newOrder; + try { + await DatabaseHelper.instance.insert(DatabaseTableType.sections, ModelsHelper.sectionToMap(section)); + } catch (e) { + print("We got an issue inserting section data ${section.id}"); + } + + // Download section image + if(section.imageId != null) { + usedImageOrAudioIds.add(section.imageId!); + var imageData = exportConfigurationDTO.resources!.where((element) => element.id == section.imageId); + if(imageData.isNotEmpty) { + // TODO get all resources from API + store it in download directory of the app + ResourceModel resourceModel = ResourceModel(id: imageData.first.id, source: section.imageSource, /*data: imageData.first.data,*/ type: imageData.first.type); + try { + await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); + } catch (e) { + print("We got an issue inserting image data ${imageData.first.id}"); + } + } + } + + // Download all images.. + ArticleDTO? articleDTO = ArticleDTO.fromJson(jsonDecode(section.data!)); + + if(articleDTO != null) { + for(var image in articleDTO.contents!) { + usedImageOrAudioIds.add(image.resourceId!); + /*var imageData = exportConfigurationDTO.resources!.where((element) => element.id == image.resourceId); + if(imageData.isNotEmpty) { + // TODO get all resources from API + store it in download directory of the app + ResourceModel resourceModel = ResourceModel(id: imageData.first.id, source: image.resourceUrl, /*data: imageData.first.data,*/ type: imageData.first.type); + try { + await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); + } catch (e) { + print("We got an issue inserting image data ${imageData.first.id}"); + } + }*/ + } + var audioIdsArticle = isAllLanguages ? articleDTO.audioIds! : articleDTO.audioIds!.where((audioId) => audioId.language == visitAppContext.language); + for(var audioId in audioIdsArticle) { + if(audioId.value != null) { + usedImageOrAudioIds.add(audioId.value!); + //audiosNotWorking = await importAudio(visitAppContext, exportConfigurationDTO, audioId.value!, audiosNotWorking); + } + } + } + newOrder = newOrder + 1; + } + + List sectionIdsToRemove = sectionsInDB.map((s) => s.id).where((sectionId) => !sectionsToKeep.map((sk) => sk.id).contains(sectionId)).toList(); + + for(var sectionIdToRemove in sectionIdsToRemove) { + print("section with id removed"); + print(sectionIdToRemove); + try { + await DatabaseHelper.instance.delete(sectionIdToRemove!, DatabaseTableType.sections); + } catch (e) { + print("We got an issue deleting section id: ${sectionIdToRemove}"); + } + } + + // TODO CLEAN AND REMOVE FILES ! + //cleanLocalResources(usedImageOrAudioIds, widget.configuration); + } + + } catch(e) { + print("ERRORRRR"); + print(e); + + if(statuses[Permission.storage] != PermissionStatus.granted) { + Fluttertoast.showToast( + msg: "PermissionStatus not granted, issue may be linked to that. Please check os version (if less than 13, real issue).", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.redAccent, + textColor: Colors.white, + fontSize: 16.0 + ); + } + return false; + } + /*} else { + print("PermissionStatus.granted NOT GRANTED"); + Fluttertoast.showToast( + msg: "PermissionStatus not granted", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.redAccent, + textColor: Colors.white, + fontSize: 16.0 + ); + return false; + }*/ + } + } + + return true; + } + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(context); + VisitAppContext visitAppContext = appContext.getContext(); + Size size = MediaQuery.of(context).size; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + ValueListenableBuilder( + valueListenable: currentResourceNbr, + builder: (context, valueNbr, _) { + return ValueListenableBuilder( + valueListenable: currentResourceIndex, + builder: (context, valueIndex, _) { + return valueNbr != -1 && valueNbr != 0 ? Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${valueIndex.toString()}/${valueNbr.toString()}", + style: TextStyle(fontSize: 45, fontWeight: FontWeight.w500), + ), + ), + ) : SizedBox(height: 0,width: 0); + } + ); + } + ), + FutureBuilder(future: download(context, visitAppContext), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + // Loader ou indicateur de chargement pendant la vérification + Color primaryColor = kMainColor1; + return Center(child: CircularProgressIndicator(color: primaryColor)); + } else { + return ValueListenableBuilder( + valueListenable: currentResourceNbr, + builder: (context, valueNbr, _) { + return ValueListenableBuilder( + valueListenable: currentResourceIndex, + builder: (context, valueIndex, _) { + return Center( + child: Text( + valueIndex == valueNbr && valueNbr != -1 ? valueNbr == 0 ? + TranslationHelper.getFromLocale( + "upToDate", + appContext.getContext()) : TranslationHelper.getFromLocale( + "downloadFinish", + appContext.getContext()) : TranslationHelper.getFromLocale( + "downloadInProgress", + appContext.getContext()), + style: const TextStyle(fontSize: 20), + ), + ); + } + ); + } + ); + } + }), + Spacer() + ], + ); + } +} + + +/*class DownloadConfiguration { static Future> download(BuildContext buildContext, AppContext appContext, ConfigurationDTO configuration) async { VisitAppContext visitAppContext = (appContext.getContext() as VisitAppContext); @@ -35,7 +347,7 @@ class DownloadConfiguration { ExportConfigurationDTO? exportConfigurationDTO; try{ - exportConfigurationDTO = await ApiService.exportConfiguration(appContext.clientAPI, configuration.id!, isAllLanguages ? null : visitAppContext.language!); + exportConfigurationDTO = await ApiService.exportConfiguration(visitAppContext.clientAPI, configuration.id!, isAllLanguages ? null : visitAppContext.language!); } catch(e) { print("Erreur lors du téléchargement de la visite"); print(e); @@ -144,6 +456,56 @@ class DownloadConfiguration { return audiosNotWorking; } } +*/ +Future downloadResource(VisitAppContext visitAppContext, ConfigurationDTO configurationDTO, ResourceDTO resourceDTO, String localPath) async { + try { + // Téléchargement de la ressource depuis l'URL + http.Response response = await http.get(Uri.parse(resourceDTO.url!)); + + if (response.statusCode == 200) { + // Vérification de l'en-tête Content-Type + String contentType = response.headers["content-type"] ?? ""; + + // Déduction de l'extension en fonction du Content-Type + String extension = _getExtensionFromContentType(contentType); + + print("LOCAL PATTH"); + print(localPath); + + File file = File('$localPath/${configurationDTO.id}/${resourceDTO.id}.$extension'); + + // Écriture du contenu téléchargé dans le fichier local + await file.writeAsBytes(response.bodyBytes); + + return file.path; + } else { + print("Échec du téléchargement de la ressource - ${response.statusCode}"); + return null; + } + } catch (e) { + print("Erreur lors du téléchargement de la ressource !"); + print(e); + return null; + } +} + +String _getExtensionFromContentType(String contentType) { + Map contentTypeToExtension = { + "image/jpeg": "jpg", + "image/jpg": "jpg", + "image/png": "png", + "image/gif": "gif", + "audio/mp3": "mp3", + "video/mp4": "mp4", + "video/webm": "webm", + "video/avi": "avi", + "video/quicktime": "mov", + "application/pdf": "pdf", + "application/json": "json" + }; + + return contentTypeToExtension[contentType] ?? "unknown"; +} Future> importAudio(VisitAppContext visitAppContext, ExportConfigurationDTO exportConfigurationDTO, String audioId, List audiosNotWorking) async { var audioData = exportConfigurationDTO.resources!.where((element) => element.id == audioId); @@ -192,3 +554,6 @@ void cleanLocalResources(List usedImageIds, ConfigurationDTO configurati } } + + + diff --git a/lib/app_context.dart b/lib/app_context.dart index 73dbd48..80c8d96 100644 --- a/lib/app_context.dart +++ b/lib/app_context.dart @@ -5,8 +5,7 @@ import 'Models/visitContext.dart'; class AppContext with ChangeNotifier { - VisitAppContext _visitContext; - Client clientAPI = Client("https://api.myinfomate.be"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089 + VisitAppContext? _visitContext; AppContext(this._visitContext); diff --git a/lib/main.dart b/lib/main.dart index fe1e978..c14ac3b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,12 +15,16 @@ import 'app_context.dart'; import 'constants.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path_provider/path_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); String initialRoute; VisitAppContext? localContext = await DatabaseHelper.instance.getData(DatabaseTableType.main); + Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); + String localPath = appDocumentsDirectory!.path; + if(localContext != null) { print("we've got an local db !"); print(localContext); @@ -38,6 +42,9 @@ void main() async { print("NO LOCAL DB !"); } + localContext.localPath = localPath; + print("Local path $localPath"); + initialRoute = '/home'; final MyApp myApp = MyApp( @@ -104,6 +111,9 @@ class _MyAppState extends State { //fontFamily: "Vollkorn", textTheme: TextTheme(bodyLarge: TextStyle(color: widget.visitAppContext.configuration != null ? widget.visitAppContext.configuration!.primaryColor != null ? Color(int.parse(widget.visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kMainColor1 : kMainColor1)), visualDensity: VisualDensity.adaptivePlatformDensity, + appBarTheme: const AppBarTheme( + iconTheme: IconThemeData(color: Colors.white), // Change the color here + ), ), routes: { '/home': (context) => const HomePage(), diff --git a/lib/translations.dart b/lib/translations.dart index 019e178..4b0a4e8 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -22,7 +22,10 @@ List translations = [ "downloadLanguage": "Sélectionner la langue de la visite", "ok": "OK", "responses": "Réponses", - "restart": "Recommencer" + "restart": "Recommencer", + "downloadInProgress": "Téléchargement en cours", + "downloadFinish": "Téléchargement terminé", + "upToDate": "Tout est à jour" }), Translation(language: "EN", data: { "visitTitle": "List of tours", @@ -45,7 +48,10 @@ List translations = [ "downloadLanguage": "Select the tour language", "ok": "OK", "responses": "Answers", - "restart": "Restart" + "restart": "Restart", + "downloadInProgress": "Download in progress", + "downloadFinish": "Download complete", + "upToDate": "Up to date" }), Translation(language: "DE", data: { "visitTitle": "Liste der Touren", @@ -68,7 +74,10 @@ List translations = [ "downloadLanguage": "Wählen Sie die Sprache des Besuchs aus", "ok": "OK", "responses": "Antworten", - "restart": "Neu starten" + "restart": "Neu starten", + "downloadInProgress": "Download läuft", + "downloadFinish": "Download abgeschlossen", + "upToDate": "Alles ist auf dem neuesten Stand" }), Translation(language: "NL", data: { "visitTitle": "Lijst met rondleidingen", @@ -91,7 +100,10 @@ List translations = [ "downloadLanguage": "Selecteer de taal van de tour", "ok": "OK", "responses": "Antwoorden", - "restart": "Herstarten" + "restart": "Herstarten", + "downloadInProgress": "Download bezig", + "downloadFinish": "Download voltooid", + "upToDate": "Alles is up-to-date" }), Translation(language: "IT", data: { "visitTitle": "Elenco dei tour", @@ -114,7 +126,10 @@ List translations = [ "downloadLanguage": "Seleziona la lingua del tour", "ok": "OK", "responses": "Risposte", - "restart": "Ricomincia" + "restart": "Ricomincia", + "downloadInProgress": "Download in corso", + "downloadFinish": "Download completato", + "upToDate": "Tutto è aggiornato" }), Translation(language: "ES", data: { "visitTitle": "Lista de recorridos", @@ -137,7 +152,10 @@ List translations = [ "downloadLanguage": "Selecciona el idioma del tour", "ok": "Ok", "responses": "Respuestas", - "restart": "Reanudar" + "restart": "Reanudar", + "downloadInProgress": "Descarga en curso", + "downloadFinish": "Descarga completada", + "upToDate": "Todo está al día" }), Translation(language: "PL", data: { "visitTitle": "Lista wycieczek", @@ -160,7 +178,10 @@ List translations = [ "downloadLanguage": "Wybierz język wycieczki", "ok": "OK", "responses": "Odpowiedzi", - "restart": "Uruchom ponownie" + "restart": "Uruchom ponownie", + "downloadInProgress": "Pobieranie w toku", + "downloadFinish": "Pobieranie zakończone", + "upToDate": "Wszystko jest aktualne" }), Translation(language: "CN", data: { "visitTitle": "旅游清单", @@ -183,7 +204,10 @@ List translations = [ "downloadLanguage": "选择游览语言", "ok": "好的", "responses": "答案", - "restart": "重新开始" + "restart": "重新开始", + "downloadInProgress": "下载中", + "downloadFinish": "下载完成", + "upToDate": "已是最新" }), Translation(language: "UK", data: { "visitTitle": "Список турів", @@ -206,7 +230,10 @@ List translations = [ "downloadLanguage": "Виберіть мову туру", "ok": "В порядку", "responses": "Відповіді", - "restart": "Перезапустіть" + "restart": "Перезапустіть", + "downloadInProgress": "Завантаження триває", + "downloadFinish": "Завантаження завершено", + "upToDate": "Все актуально" }), Translation(language: "AR", data: { "visitTitle": "قائمة الجولات", @@ -229,6 +256,9 @@ List translations = [ "downloadLanguage": "حدد لغة الجولة", "ok": "نعم", "responses": "الإجابات", - "restart": "إعادة تشغيل" + "restart": "إعادة تشغيل", + "downloadInProgress": "جارٍ التنزيل", + "downloadFinish": "اكتمل التنزيل", + "upToDate": "كل شيء محدث" }), ]; \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index ae28089..4eb07d8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -378,6 +378,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.1" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847" + url: "https://pub.dev" + source: hosted + version: "8.2.6" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 85d3222..e493f90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: openapi_generator_annotations: ^5.0.2 sqflite: provider: ^6.1.2 + fluttertoast: #carousel_slider: ^4.2.1 #flutter_svg_provider: ^1.0.3 photo_view: ^0.15.0