feat: aggiungi controllo aggiornamenti con dialog all'avvio
All'avvio l'app chiama l'API GitHub releases per confrontare la versione installata con l'ultima disponibile (comparazione semantica major.minor.patch). Se esiste una versione più recente, mostra un AlertDialog con i pulsanti "Più tardi" e "Scarica" — quest'ultimo apre releases/latest nel browser. Il controllo è silenzioso in caso di assenza di rete o errori, e disabilitato su Flutter web (dart:io non disponibile). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import 'features/meal_planner/presentation/pages/meal_planner_page.dart';
|
|||||||
import 'features/converter/presentation/pages/converter_page.dart';
|
import 'features/converter/presentation/pages/converter_page.dart';
|
||||||
import 'features/shopping_list/presentation/pages/shopping_list_page.dart';
|
import 'features/shopping_list/presentation/pages/shopping_list_page.dart';
|
||||||
import 'features/guide/presentation/widgets/info_bottom_sheet.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 {
|
class BitePlanApp extends StatelessWidget {
|
||||||
const BitePlanApp({super.key});
|
const BitePlanApp({super.key});
|
||||||
@@ -31,6 +33,8 @@ class _MainScaffoldState extends State<_MainScaffold> {
|
|||||||
late final List<Widget> _pages;
|
late final List<Widget> _pages;
|
||||||
|
|
||||||
static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa'];
|
static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa'];
|
||||||
|
static const _releasesUrl =
|
||||||
|
'https://github.com/davide3011/biteplan/releases/latest';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -42,6 +46,38 @@ class _MainScaffoldState extends State<_MainScaffold> {
|
|||||||
const ConverterPage(),
|
const ConverterPage(),
|
||||||
const ShoppingListPage(),
|
const ShoppingListPage(),
|
||||||
];
|
];
|
||||||
|
UpdateService.checkUpdate().then((newVersion) {
|
||||||
|
if (newVersion != null && mounted) {
|
||||||
|
_showUpdateDialog(newVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUpdateDialog(String newVersion) {
|
||||||
|
showDialog<void>(
|
||||||
|
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
|
@override
|
||||||
|
|||||||
@@ -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<String?> 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<String, dynamic>;
|
||||||
|
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<int> parseVersion(String v) {
|
||||||
|
final parts = v.split('.');
|
||||||
|
return List.generate(
|
||||||
|
3, (i) => i < parts.length ? (int.tryParse(parts[i]) ?? 0) : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class UrlLauncherService {
|
||||||
|
static const _channel = MethodChannel('com.davide.biteplan/launcher');
|
||||||
|
|
||||||
|
static Future<void> launch(String url) async {
|
||||||
|
await _channel.invokeMethod<void>('launch', {'url': url});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user