import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:manager_api_new/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'; /// Types dont l'écran ne fait aucun appel réseau au rendu : ils savent s'afficher /// à partir du seul payload stocké en base locale. Les autres restent volontairement /// dehors — Web (webview), Weather (API météo) et Agenda (JSON distant) ne peuvent /// pas fonctionner sans réseau ; Parcours et Event appellent encore l'API au rendu ; /// Map attend un fond de carte hors ligne, sinon le visiteur ouvre une carte grise. const List offlineCapableSectionTypes = [ SectionType.Article, SectionType.Quiz, SectionType.Menu, SectionType.Slider, SectionType.Pdf, SectionType.Video, SectionType.Game, ]; /// Ce que le visiteur doit lire à la fin. `unreachable` et `incomplete` étaient /// tous deux invisibles : l'écran ne regardait jamais le résultat du `Future`, il /// déduisait son texte de `currentResourceNbr`, resté à -1 quand l'export échoue — /// donc « téléchargement en cours », pour toujours. enum DownloadOutcome { upToDate, complete, incomplete, unreachable } 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); /// Le téléchargement était lancé depuis `build()`, donc relancé à chaque rebuild /// du dialogue. Il démarre une fois, ici. Future? _downloadFuture; @override void didChangeDependencies() { super.didChangeDependencies(); _downloadFuture ??= download( Provider.of(context, listen: false).getContext()); } Future download(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), );*/ } } // 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!, language: 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 DownloadOutcome.unreachable; } 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); } Map localResourceDates = await readLocalResourceDates(); // Ressources dont le téléchargement a échoué. Vide = visite complète. final List failedResourceIds = []; // Sections dont le payload n'a pas pu être récupéré ou enregistré. final List failedSectionIds = []; var resourcesToDownload = exportConfigurationDTO.resources!.where((resource) => resource.type != ResourceType.ImageUrl && resource.type != ResourceType.VideoUrl && resource.type != ResourceType.JsonUrl && resource.url != null && isResourceOutdated(resource, fileList, localResourceDates[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, dateUpdate: resource.dateUpdate); try { await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); localResourceDates[resource.id!] = resource.dateUpdate; } catch (e) { print("We got an issue inserting image metadata ${resource.id}"); } currentResourceIndex.value++; } else { // D5 — un fichier raté ne disparaît plus dans un `print`. Une visite dont // des ressources manquent ne doit pas s'annoncer téléchargée : hors ligne, // le visiteur trouverait des images vides ou un audio muet sans comprendre // pourquoi, et sans rien pouvoir y faire. failedResourceIds.add(resource.id ?? '?'); debugPrint("[download] échec de la ressource ${resource.id} (${resource.url})"); } } // 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); try { resource.deleteSync(); } catch (e) { print("We got an issue deleting local file ${resource.path}"); } } 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) => offlineCapableSectionTypes.contains(s.type)).toList(); sectionsToKeep.sort((a,b) => a.order!.compareTo(b.order!)); int newOrder = 0; // Update local DB - Sections for(var section in sectionsToKeep) { section.order = newOrder; // Le payload typé n'est pas dans l'export : `ExportConfigurationDTO.sections` // est déserialisé en `SectionDTO`, qui ne porte que l'en-tête commun. C'est // `sectionGetDetail` qui renvoie le JSON complet du sous-type — le même que // celui dont `section_page` se sert en ligne. String? rawSectionJson; try { rawSectionJson = jsonEncode(await visitAppContext.clientAPI.sectionApi!.sectionGetDetail(section.id!)); } catch (e) { debugPrint("[download] détail indisponible pour la section ${section.id} : $e"); } // Une section sans son payload ne sait rien afficher hors ligne. On préfère // ne pas l'enregistrer et faire échouer la visite, plutôt que laisser le // visiteur ouvrir un article vide au milieu du site. if (rawSectionJson == null) { failedSectionIds.add(section.id ?? '?'); newOrder = newOrder + 1; continue; } try { await DatabaseHelper.instance.insert(DatabaseTableType.sections, ModelsHelper.sectionToMap(section, rawSectionJson)); } catch (e) { failedSectionIds.add(section.id ?? '?'); debugPrint("[download] échec d'insertion de la section ${section.id} : $e"); } // 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}"); } } } newOrder = newOrder + 1; } // Le bloc qui vivait ici parsait `section.data` type par type — et il était // commenté, donc une visite téléchargée n'embarquait que l'image de chaque // section : ni contenus d'articles, ni audios, ni icônes de carte. // // Il n'y a plus rien à redécouvrir : depuis le 2026-08-11 le serveur collecte // les ressources via `GetReferencedResourceIds()`, implémentée sur les 13 // sous-types, et les met toutes dans `resources`. On enregistre la charge. // Un nouveau type de section est couvert sans toucher à ce fichier. // // ⚠️ `insert` fait un UPDATE de toute la ligne quand l'id existe déjà : cette // boucle repasse sur les ressources que la boucle de téléchargement vient // d'enregistrer. Elle doit donc réécrire la date que celle-ci a posée, sinon // elle l'efface et le fichier redevient « sans date connue » à chaque visite. // Et pour une ressource dont le téléchargement a échoué, c'est l'ancienne date // locale qui est conservée — surtout pas celle du serveur, qui la déclarerait // à jour alors que le fichier est absent. for (var resource in exportConfigurationDTO.resources ?? []) { if (resource.id == null) continue; usedImageOrAudioIds.add(resource.id!); try { await DatabaseHelper.instance.insert( DatabaseTableType.resources, ResourceModel(id: resource.id, source: resource.url, type: resource.type, dateUpdate: localResourceDates[resource.id]).toMap(), ); } catch (e) { print("We got an issue inserting resource ${resource.id}"); } } 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}"); } } // `cleanLocalResources` reste désactivée : la table `resources` n'a pas de // colonne `configurationId`, elle est globale à toutes les visites. La purger // avec les ids d'une seule configuration effacerait les lignes des autres. // Les fichiers obsolètes, eux, sont bien supprimés plus haut — leur répertoire // est propre à la configuration. Et rien ne lit la colonne `path` pour le rendu : // `CachedCustomResource` retrouve le fichier en listant le répertoire. } // D5 — la visite n'est déclarée téléchargée que si elle l'est vraiment. Le // contenu déjà récupéré reste sur le device : relancer ne re-télécharge que // ce qui manque, `isResourceOutdated` voyant les fichiers présents. if (failedResourceIds.isNotEmpty || failedSectionIds.isNotEmpty) { debugPrint("[download] ${failedResourceIds.length} ressource(s) en échec : " "${failedResourceIds.join(', ')}"); debugPrint("[download] ${failedSectionIds.length} section(s) en échec : " "${failedSectionIds.join(', ')}"); return DownloadOutcome.incomplete; } } 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 DownloadOutcome.incomplete; } /*} 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 currentResourceNbr.value > 0 ? DownloadOutcome.complete : DownloadOutcome.upToDate; } @override Widget build(BuildContext context) { final appContext = Provider.of(context); // Pas de `Center` ici : le dialogue ne fixe plus la hauteur du contenu, et un // `Center` s'étirerait sur toute la hauteur disponible. return FutureBuilder( future: _downloadFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return _progress(appContext); } // Une exception non rattrapée dans `download` laissait le dialogue sur // « téléchargement en cours » elle aussi — le `Future` était en erreur, // personne ne regardait. if (snapshot.hasError) { debugPrint("[download] échec inattendu : ${snapshot.error}"); return _message(Icons.error_outline, Colors.redAccent, TranslationHelper.getFromLocale("downloadIncomplete", appContext.getContext())); } switch (snapshot.data!) { case DownloadOutcome.upToDate: return _label(TranslationHelper.getFromLocale("upToDate", appContext.getContext())); case DownloadOutcome.complete: return _label(TranslationHelper.getFromLocale("downloadFinish", appContext.getContext())); case DownloadOutcome.incomplete: return _message(Icons.cloud_off, Colors.orangeAccent, TranslationHelper.getFromLocale("downloadIncomplete", appContext.getContext())); case DownloadOutcome.unreachable: return _message(Icons.wifi_off, Colors.redAccent, TranslationHelper.getFromLocale("downloadFailed", appContext.getContext())); } }, ); } /// Le texte et la barre pendant le téléchargement. `currentResourceNbr` vaut -1 /// tant que l'export n'a pas répondu : on annonce le travail sans prétendre en /// connaître l'avancement. Widget _progress(AppContext appContext) { return ValueListenableBuilder( valueListenable: currentResourceNbr, builder: (context, valueNbr, _) => ValueListenableBuilder( valueListenable: currentResourceIndex, builder: (context, valueIndex, _) { final ratio = valueNbr > 0 ? valueIndex / valueNbr : null; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ _label(TranslationHelper.getFromLocale("downloadInProgress", appContext.getContext())), Padding( padding: const EdgeInsets.all(8.0), child: LinearProgressIndicator( value: ratio, semanticsLabel: 'Linear progress indicator', color: kMainColor0, ), ), if (ratio != null) Padding( padding: const EdgeInsets.all(8.0), child: Text( '${(ratio * 100).toStringAsFixed(0)}%', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w500), ), ), ], ); }, ), ); } /// Pas de `Center` autour : dans un `AlertDialog`, le contenu reçoit une hauteur /// maximale très large, et un `Center` la prend toute — d'où un dialogue haut /// comme l'écran pour deux lignes de texte. Une `Column` `min` s'ajuste au texte. Widget _label(String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Text(text, textAlign: TextAlign.center, style: const TextStyle(fontSize: 20)), ); } Widget _message(IconData icon, Color color, String text) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 34, color: color), const SizedBox(height: 10), Text(text, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)), ], ), ); } } /*class DownloadConfiguration { static Future> download(BuildContext buildContext, AppContext appContext, ConfigurationDTO configuration) async { VisitAppContext visitAppContext = (appContext.getContext() as VisitAppContext); bool isAllLanguages = false; List audiosNotWorking = []; 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), ); } } print("Start export configuration to store it locally"); ExportConfigurationDTO? exportConfigurationDTO; try{ 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); } if(exportConfigurationDTO != null) { // Delete all from local DB ref to this configurationId then add it all await DatabaseHelper.instance.insert(DatabaseTableType.configurations, ModelsHelper.configurationToMap(configuration)); List usedImageOrAudioIds = []; var currentLanguage = (appContext.getContext() as VisitAppContext).language; if(configuration.imageId != null) { usedImageOrAudioIds.add(configuration.imageId!); var imageData = exportConfigurationDTO.resources!.where((element) => element.id == configuration.imageId); if(imageData.isNotEmpty) { ResourceModel resourceModel = ResourceModel(id: imageData.first.id, source: configuration.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}"); } } } List? sections = exportConfigurationDTO.sections; if(sections!.isNotEmpty) { List sectionsInDB = await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, configuration.id!); List sectionsToKeep = sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quizz).toList(); // TODO: supporter tous les types de sections (Game, Menu, Map, PDF, Video, Slider, Web, Weather, Agenda) — actuellement limité à Article et Quiz 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 == currentLanguage); 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}"); } } cleanLocalResources(usedImageOrAudioIds, configuration); } } 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; } } /// Date de dernière mise à jour connue pour chaque ressource déjà sur le device. /// Une valeur nulle signifie « fichier téléchargé avant la v4 de la base locale, /// date inconnue » — pas « périmé ». Future> readLocalResourceDates() async { List> rows = await DatabaseHelper.instance.queryAllRows(DatabaseTableType.resources); return { for (var row in rows) row["id"] as String: DatabaseHelper.instance.getResourceFromDB(row).dateUpdate }; } bool isResourceOutdated(ResourceDTO resource, List fileList, DateTime? localDateUpdate) { var localFile = fileList.where((fileL) => fileL.uri.pathSegments.last.contains(resource.id!)); if (localFile.isEmpty) return true; // Fichier écrit avec une extension que la table ne savait pas déduire — c'était le // cas de tous les MP3 avant D3. Il est illisible, et sa date ne le dira jamais : // le serveur n'a pas changé, c'est le client qui l'avait mal enregistré. // // La date nulle borne la reprise à UN essai : sans elle, un type MIME réellement // absent de la table se re-téléchargerait à chaque visite pour se réécrire en // `.unknown`. On ne peut pas non plus déduire l'extension de l'URL en repli — une // URL Firebase n'en porte pas. if (localFile.first.uri.pathSegments.last.endsWith(".unknown") && localDateUpdate == null) return true; if (localDateUpdate == null || resource.dateUpdate == null) return false; return resource.dateUpdate!.isAfter(localDateUpdate); } String _getExtensionFromContentType(String contentType) { Map contentTypeToExtension = { "image/jpeg": "jpg", "image/jpg": "jpg", "image/png": "png", "image/gif": "gif", "audio/mpeg": "mp3", "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); if(audioData.isNotEmpty) { // TODO get all resources from API + store it in download directory of the app ResourceModel resourceModel = ResourceModel(id: audioData.first.id, source: "", /*data: audioData.first.data,*/ type: audioData.first.type); try { await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap()); return audiosNotWorking; } catch (e) { print("We got an issue inserting audio data ${audioData.first.id}"); resourceModel.label = audioData.first.label; print(resourceModel.label); // Update context with not working audios //VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext; audiosNotWorking.add(resourceModel); audiosNotWorking = audiosNotWorking.toSet().toList(); // Remove duplicates // appContext.setContext(visitAppContext); return audiosNotWorking; } } return audiosNotWorking; } void cleanLocalResources(List usedImageIds, ConfigurationDTO configuration) async { List> resourcesInDB = await DatabaseHelper.instance.queryAllRows(DatabaseTableType.resources); List resourcesModel = []; for(var resource in resourcesInDB) { resourcesModel.add(DatabaseHelper.instance.getResourceFromDB(resource)); } List resourceIdsToRemove = resourcesModel.map((r) => r.id).where((resourceId) => !usedImageIds.contains(resourceId)).toList(); for(var resourceIdToRemove in resourceIdsToRemove) { print("resource with id removed ________________ !!!!!"); print(resourceIdToRemove); try { await DatabaseHelper.instance.delete(resourceIdToRemove!, DatabaseTableType.resources); } catch (e) { print("We got an issue deleting resource id: ${resourceIdToRemove}"); } } }