diff --git a/lib/app.dart b/lib/app.dart index 48844a4..ea3bf9a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,8 @@ import 'features/meal_planner/presentation/pages/meal_planner_page.dart'; import 'features/converter/presentation/pages/converter_page.dart'; import 'features/shopping_list/presentation/pages/shopping_list_page.dart'; import 'features/guide/presentation/widgets/info_bottom_sheet.dart'; +import 'shared/services/update_service.dart'; +import 'shared/services/url_launcher_service.dart'; class BitePlanApp extends StatelessWidget { const BitePlanApp({super.key}); @@ -31,6 +33,8 @@ class _MainScaffoldState extends State<_MainScaffold> { late final List _pages; static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa']; + static const _releasesUrl = + 'https://github.com/davide3011/biteplan/releases/latest'; @override void initState() { @@ -42,6 +46,38 @@ class _MainScaffoldState extends State<_MainScaffold> { const ConverterPage(), const ShoppingListPage(), ]; + UpdateService.checkUpdate().then((newVersion) { + if (newVersion != null && mounted) { + _showUpdateDialog(newVersion); + } + }); + } + + void _showUpdateDialog(String newVersion) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.system_update_outlined, size: 32), + title: const Text('Aggiornamento disponibile'), + content: Text( + 'È disponibile la versione $newVersion di BitePlan.\n' + 'Vuoi scaricare il nuovo APK?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Più tardi'), + ), + FilledButton( + onPressed: () { + Navigator.of(ctx).pop(); + UrlLauncherService.launch(_releasesUrl); + }, + child: const Text('Scarica'), + ), + ], + ), + ); } @override diff --git a/lib/shared/services/update_service.dart b/lib/shared/services/update_service.dart new file mode 100644 index 0000000..c27974b --- /dev/null +++ b/lib/shared/services/update_service.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; + +import '../../core/constants/app_constants.dart'; + +class UpdateService { + static const _apiUrl = + 'https://api.github.com/repos/davide3011/biteplan/releases/latest'; + + /// Returns the latest version string if it's newer than the current version, + /// or null if already up to date or if the check fails. + static Future checkUpdate() async { + if (kIsWeb) return null; + try { + final client = HttpClient() + ..connectionTimeout = const Duration(seconds: 5); + final request = await client + .getUrl(Uri.parse(_apiUrl)) + .timeout(const Duration(seconds: 5)); + request.headers + ..set('Accept', 'application/vnd.github+json') + ..set('User-Agent', 'biteplan/$kAppVersion'); + final response = + await request.close().timeout(const Duration(seconds: 5)); + final body = await response.transform(utf8.decoder).join(); + client.close(); + if (response.statusCode != 200) return null; + final data = jsonDecode(body) as Map; + final tag = (data['tag_name'] as String).replaceFirst(RegExp(r'^v'), ''); + return isNewer(tag, kAppVersion) ? tag : null; + } catch (_) { + return null; + } + } + + @visibleForTesting + static bool isNewer(String remote, String local) { + final r = parseVersion(remote); + final l = parseVersion(local); + for (var i = 0; i < 3; i++) { + if (r[i] > l[i]) return true; + if (r[i] < l[i]) return false; + } + return false; + } + + @visibleForTesting + static List parseVersion(String v) { + final parts = v.split('.'); + return List.generate( + 3, (i) => i < parts.length ? (int.tryParse(parts[i]) ?? 0) : 0); + } +} diff --git a/lib/shared/services/url_launcher_service.dart b/lib/shared/services/url_launcher_service.dart new file mode 100644 index 0000000..d78d6b1 --- /dev/null +++ b/lib/shared/services/url_launcher_service.dart @@ -0,0 +1,9 @@ +import 'package:flutter/services.dart'; + +class UrlLauncherService { + static const _channel = MethodChannel('com.davide.biteplan/launcher'); + + static Future launch(String url) async { + await _channel.invokeMethod('launch', {'url': url}); + } +}