import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import 'settings_provider.dart'; const _currentVersion = 1; final updateCheckProvider = FutureProvider((ref) async { final settings = ref.watch(settingsProvider).valueOrNull; if (settings == null) return null; final baseUrl = settings.workoutPlayerUrl; // Update server runs alongside workout-player final host = Uri.parse(baseUrl).host; try { final response = await http.get( Uri.parse('http://$host:9091/api/mirror-os/version'), ).timeout(const Duration(seconds: 5)); if (response.statusCode != 200) return null; final data = jsonDecode(response.body) as Map; final latestVersion = data['version'] as int? ?? 0; final downloadUrl = data['downloadUrl'] as String?; final changelog = data['changelog'] as String?; if (latestVersion > _currentVersion && downloadUrl != null) { return UpdateInfo( currentVersion: _currentVersion, latestVersion: latestVersion, downloadUrl: downloadUrl, changelog: changelog, ); } return null; } catch (_) { return null; } }); class UpdateInfo { final int currentVersion; final int latestVersion; final String downloadUrl; final String? changelog; const UpdateInfo({ required this.currentVersion, required this.latestVersion, required this.downloadUrl, this.changelog, }); } class UpdateBanner extends ConsumerWidget { const UpdateBanner({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final update = ref.watch(updateCheckProvider).valueOrNull; if (update == null) return const SizedBox.shrink(); return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E3A5F), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF60A5FA), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.system_update, color: Color(0xFF60A5FA), size: 20), const SizedBox(width: 8), Text( 'Update available (v${update.latestVersion})', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: Colors.white, ), ), ], ), if (update.changelog != null) ...[ const SizedBox(height: 8), Text( update.changelog!, style: const TextStyle(fontSize: 13, color: Colors.white70), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 12), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: () => _downloadUpdate(context, update.downloadUrl), icon: const Icon(Icons.download, size: 18), label: const Text('Download & Install'), ), ), ], ), ); } void _downloadUpdate(BuildContext context, String url) async { final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Open in browser: $url')), ); } } } }