import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; /// Images définies dans le manager (Applications → Mobile) gardées sur le device. /// /// Elles sont réclamées avant que quoi que ce soit d'autre existe : le splash /// s'affiche pendant le boot, donc avant le premier appel API, avant le contexte /// et avant le `Provider`. Et hors ligne, il n'y a plus d'URL à interroger du /// tout. Un cache fichier, relu à froid, est la seule chose qui réponde aux deux. /// /// Volontairement à part du répertoire d'une visite (`localPath//`) : /// ces images appartiennent à l'instance, pas à une configuration, et doivent /// survivre à la suppression d'une visite téléchargée. class InstanceImages { static const _loaderSlot = 'loader'; static const _mainSlot = 'main'; /// Loader tournant des écrans de chargement et du splash. static File? loader; /// Image principale de l'app, utilisée comme logo de démarrage. static File? main; static Future? _restoring; /// Relit ce qu'un lancement précédent a mis en cache. Idempotent : le splash et /// les écrans de chargement l'appellent sans avoir à se coordonner. static Future restore() => _restoring ??= _restore(); static Future _restore() async { loader = await _cachedFile(_loaderSlot); main = await _cachedFile(_mainSlot); } /// Aligne le cache sur ce que le manager expose aujourd'hui. Best-effort : un /// échec réseau laisse en place l'image du lancement précédent. static Future refresh({String? loaderUrl, String? mainImageUrl}) async { await restore(); loader = await _sync(_loaderSlot, loaderUrl); main = await _sync(_mainSlot, mainImageUrl); } static Future _directory() async { try { final base = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); if (base == null) return null; final dir = Directory('${base.path}/instance_images'); if (!dir.existsSync()) dir.createSync(recursive: true); return dir; } catch (e) { debugPrint('[instanceImages] répertoire indisponible : $e'); return null; } } static Future _cachedFile(String slot) async { final dir = await _directory(); if (dir == null) return null; final file = File('${dir.path}/$slot'); return file.existsSync() ? file : null; } /// L'URL source est écrite à côté du fichier : c'est elle qui dit si l'image a /// changé dans le manager. Sans elle, il faudrait re-télécharger à chaque /// démarrage — ou ne jamais voir un changement. static Future _sync(String slot, String? url) async { final dir = await _directory(); if (dir == null) return null; final file = File('${dir.path}/$slot'); final sourceFile = File('${dir.path}/$slot.url'); if (url == null) { // Image retirée côté manager : le device ne doit pas continuer à servir // l'ancienne indéfiniment. try { if (file.existsSync()) file.deleteSync(); if (sourceFile.existsSync()) sourceFile.deleteSync(); } catch (_) {} return null; } final knownSource = sourceFile.existsSync() ? sourceFile.readAsStringSync() : null; if (file.existsSync() && knownSource == url) return file; try { final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10)); if (response.statusCode != 200) { debugPrint('[instanceImages] $slot : HTTP ${response.statusCode}'); return file.existsSync() ? file : null; } await file.writeAsBytes(response.bodyBytes); await sourceFile.writeAsString(url); return file; } catch (e) { debugPrint('[instanceImages] $slot non téléchargée : $e'); return file.existsSync() ? file : null; } } }