diff --git a/lib/screens/servers_screen.dart b/lib/screens/servers_screen.dart index 9f1663e..f6c7fb5 100644 --- a/lib/screens/servers_screen.dart +++ b/lib/screens/servers_screen.dart @@ -78,13 +78,35 @@ class _ServersScreenState extends State { ], ), ); - if (go == true) { - final ok = await UpdateService(widget.api.baseUrl).apply(info); - if (mounted && !ok) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Impossible d\'ouvrir le téléchargement.')), - ); - } + if (go != true || !mounted) return; + + final progress = ValueNotifier(0); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: const Text('Téléchargement…'), + content: ValueListenableBuilder( + valueListenable: progress, + builder: (_, p, __) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator(value: p), + const SizedBox(height: 10), + Text(p == null ? '' : '${(p * 100).round()} %'), + ], + ), + ), + ), + ); + + final ok = await UpdateService(widget.api.baseUrl) + .apply(info, onProgress: (p) => progress.value = p); + if (mounted) Navigator.of(context, rootNavigator: true).pop(); // ferme la progression + if (mounted && !ok) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Mise à jour impossible.')), + ); } } diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index c8906df..e8ddd20 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -1,12 +1,15 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:open_filex/open_filex.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; -/// Vérifie s'il existe une version plus récente (API `/version`) et, si oui, -/// ouvre l'URL de l'APK (le navigateur télécharge, l'utilisateur installe — -/// robuste, sans dépendance de packaging fragile). +/// Détecte une version plus récente (`/version`) et l'installe **in-app** : +/// télécharge l'APK dans le stockage de l'app puis lance l'installeur système +/// (un seul tap « Installer »). Fallback navigateur si l'install directe échoue. class UpdateService { UpdateService(this._baseUrl); final Future _baseUrl; @@ -31,7 +34,39 @@ class UpdateService { return null; } - Future apply(UpdateInfo info) async { + /// Télécharge l'APK puis lance l'installeur. `onProgress` : 0.0 → 1.0 + /// (ou null si la taille est inconnue). Renvoie true si l'installeur s'ouvre. + Future apply(UpdateInfo info, {void Function(double?)? onProgress}) async { + try { + final dir = await getExternalStorageDirectory() ?? await getTemporaryDirectory(); + final file = File('${dir.path}/gamemanager-update.apk'); + + final req = http.Request('GET', Uri.parse(info.apkUrl)); + final resp = await req.send().timeout(const Duration(seconds: 30)); + if (resp.statusCode != 200) return _fallback(info); + + final total = resp.contentLength ?? 0; + var received = 0; + final sink = file.openWrite(); + await for (final chunk in resp.stream) { + sink.add(chunk); + received += chunk.length; + onProgress?.call(total > 0 ? received / total : null); + } + await sink.close(); + + final result = await OpenFilex.open( + file.path, + type: 'application/vnd.android.package-archive', + ); + if (result.type == ResultType.done) return true; + return _fallback(info); // l'installeur n'a pas pris → navigateur + } catch (_) { + return _fallback(info); + } + } + + Future _fallback(UpdateInfo info) async { final uri = Uri.parse(info.apkUrl); if (!await canLaunchUrl(uri)) return false; return launchUrl(uri, mode: LaunchMode.externalApplication); diff --git a/pubspec.yaml b/pubspec.yaml index b2ce81a..4cadd9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: shared_preferences: ^2.3.2 package_info_plus: ^8.1.1 url_launcher: ^6.3.1 + path_provider: ^2.1.4 + open_filex: ^4.5.0 cupertino_icons: ^1.0.8 dev_dependencies: diff --git a/tool/patch_android.py b/tool/patch_android.py index dac93e5..f05681c 100644 --- a/tool/patch_android.py +++ b/tool/patch_android.py @@ -62,14 +62,22 @@ if "signingConfigs {" not in s: r"(\n\s*buildTypes\s*\{)", '\n signingConfigs {\n' ' release {\n' - ' keyAlias keystoreProperties["keyAlias"]\n' - ' keyPassword keystoreProperties["keyPassword"]\n' - ' storeFile file(keystoreProperties["storeFile"])\n' - ' storePassword keystoreProperties["storePassword"]\n' + ' keyAlias = keystoreProperties["keyAlias"]\n' + ' keyPassword = keystoreProperties["keyPassword"]\n' + ' storeFile = file(keystoreProperties["storeFile"])\n' + ' storePassword = keystoreProperties["storePassword"]\n' ' }\n }\n\\1', s, count=1, ) +# Flutter 3.24 génère `signingConfig = signingConfigs.debug` (AVEC `=`). On gère +# les deux formes pour ne jamais laisser le build release signé en clé debug +# (sinon signature différente à chaque run CI → « conflit » à l'install de MAJ). +before = s +s = s.replace("signingConfig = signingConfigs.debug", "signingConfig = signingConfigs.release") s = s.replace("signingConfig signingConfigs.debug", "signingConfig signingConfigs.release") +if s == before: + raise SystemExit("patch_android: ERREUR — ligne 'signingConfig ... debug' introuvable, " + "signature release NON appliquée (build annulé)") open(gr, "w").write(s) -print("patch_android: OK") +print("patch_android: OK (signature release appliquee)")