Created
December 21, 2025 09:06
-
-
Save p32929/c407ce432f51f172f55560d25671efa7 to your computer and use it in GitHub Desktop.
A simple page to show user to update the app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'package:google_fonts/google_fonts.dart'; | |
| import 'package:package_info_plus/package_info_plus.dart'; | |
| import 'package:url_launcher/url_launcher.dart'; | |
| import '../../config/app_config.dart'; | |
| import '../../services/json_cache_service.dart'; | |
| // Design colors | |
| const _darkBg = Color(0xFF121212); | |
| const _cardBg = Color(0xFF1E1E1E); | |
| const _accentMint = Color(0xFF98D8C8); | |
| /// Updates page showing update information | |
| /// Automatically fetches update data from configured URL | |
| class UpdatesPage extends StatefulWidget { | |
| const UpdatesPage({super.key}); | |
| @override | |
| State<UpdatesPage> createState() => _UpdatesPageState(); | |
| } | |
| class _UpdatesPageState extends State<UpdatesPage> { | |
| PackageInfo? _packageInfo; | |
| Map<String, dynamic>? _updateData; | |
| bool _isLoading = true; | |
| String? _errorMessage; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _loadUpdateData(); | |
| } | |
| Future<void> _loadUpdateData() async { | |
| try { | |
| // Load package info and update data in parallel | |
| final results = await Future.wait([ | |
| PackageInfo.fromPlatform(), | |
| JsonCacheService.fetchWithCache( | |
| url: AppConfig.updateJsonUrl, | |
| cacheKey: 'update_data', | |
| ), | |
| ]); | |
| final packageInfo = results[0] as PackageInfo; | |
| final updateData = results[1] as Map<String, dynamic>; | |
| setState(() { | |
| _packageInfo = packageInfo; | |
| _updateData = updateData; | |
| _isLoading = false; | |
| }); | |
| } catch (e) { | |
| setState(() { | |
| _errorMessage = e.toString(); | |
| _isLoading = false; | |
| }); | |
| } | |
| } | |
| Future<void> _handleUpdate() async { | |
| if (_updateData == null) return; | |
| final url = _updateData!['url'] as String?; | |
| if (url != null) { | |
| final uri = Uri.parse(url); | |
| if (await canLaunchUrl(uri)) { | |
| await launchUrl(uri, mode: LaunchMode.externalApplication); | |
| } | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final isCancellable = _updateData?['cancellable'] as bool? ?? false; | |
| return PopScope( | |
| canPop: isCancellable, // Allow back if cancellable | |
| child: Scaffold( | |
| backgroundColor: _darkBg, | |
| appBar: AppBar( | |
| backgroundColor: _darkBg, | |
| elevation: 0, | |
| automaticallyImplyLeading: isCancellable, // Show back button if cancellable | |
| leading: isCancellable | |
| ? IconButton( | |
| icon: const Icon(Icons.close, color: Colors.white), | |
| onPressed: () => Navigator.of(context).pop(), | |
| ) | |
| : null, | |
| title: Text( | |
| isCancellable ? 'Update Available' : 'Update Required', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 24, | |
| fontWeight: FontWeight.w700, | |
| color: Colors.white, | |
| ), | |
| ), | |
| centerTitle: false, | |
| ), | |
| body: _isLoading | |
| ? _buildLoadingState() | |
| : _errorMessage != null | |
| ? _buildErrorState() | |
| : _buildContent(), | |
| ), | |
| ); | |
| } | |
| Widget _buildLoadingState() { | |
| return Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| CircularProgressIndicator(color: _accentMint), | |
| const SizedBox(height: 16), | |
| Text( | |
| 'Loading update information...', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 16, | |
| color: Colors.white.withValues(alpha: 0.7), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildErrorState() { | |
| return Center( | |
| child: Padding( | |
| padding: const EdgeInsets.all(24.0), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.error_outline, | |
| size: 64, | |
| color: Colors.white.withValues(alpha: 0.3), | |
| ), | |
| const SizedBox(height: 16), | |
| Text( | |
| 'Failed to load update information', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 20, | |
| fontWeight: FontWeight.w600, | |
| color: Colors.white.withValues(alpha: 0.7), | |
| ), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| _errorMessage ?? 'Unknown error', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 14, | |
| color: Colors.white.withValues(alpha: 0.5), | |
| ), | |
| textAlign: TextAlign.center, | |
| ), | |
| const SizedBox(height: 24), | |
| ElevatedButton.icon( | |
| onPressed: () { | |
| setState(() { | |
| _isLoading = true; | |
| _errorMessage = null; | |
| }); | |
| _loadUpdateData(); | |
| }, | |
| icon: const Icon(Icons.refresh), | |
| label: Text( | |
| 'Retry', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 16, | |
| fontWeight: FontWeight.w600, | |
| ), | |
| ), | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: _accentMint, | |
| foregroundColor: _darkBg, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildVersionUpgrade() { | |
| final currentVersion = _packageInfo?.version ?? '0.0.0'; | |
| final newVersion = _updateData?['version'] as String? ?? '0.0.0'; | |
| return Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), | |
| decoration: BoxDecoration( | |
| color: _cardBg, | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all( | |
| color: Colors.white.withValues(alpha: 0.1), | |
| width: 1, | |
| ), | |
| ), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| // Current Version (crossed out) | |
| Column( | |
| children: [ | |
| Text( | |
| 'v$currentVersion', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 22, | |
| fontWeight: FontWeight.w700, | |
| color: Colors.white.withValues(alpha: 0.4), | |
| decoration: TextDecoration.lineThrough, | |
| decorationColor: Colors.red.withValues(alpha: 0.6), | |
| decorationThickness: 2, | |
| ), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'Current', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 12, | |
| fontWeight: FontWeight.w600, | |
| color: Colors.white.withValues(alpha: 0.3), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(width: 24), | |
| // Arrow Icon | |
| Icon( | |
| Icons.arrow_forward, | |
| color: _accentMint, | |
| size: 32, | |
| ), | |
| const SizedBox(width: 24), | |
| // New Version (highlighted) | |
| Column( | |
| children: [ | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), | |
| decoration: BoxDecoration( | |
| gradient: LinearGradient( | |
| begin: Alignment.topLeft, | |
| end: Alignment.bottomRight, | |
| colors: [ | |
| _accentMint.withValues(alpha: 0.3), | |
| _accentMint.withValues(alpha: 0.1), | |
| ], | |
| ), | |
| borderRadius: BorderRadius.circular(8), | |
| border: Border.all( | |
| color: _accentMint.withValues(alpha: 0.5), | |
| width: 1.5, | |
| ), | |
| ), | |
| child: Text( | |
| 'v$newVersion', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 22, | |
| fontWeight: FontWeight.w800, | |
| color: _accentMint, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'New', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 12, | |
| fontWeight: FontWeight.w600, | |
| color: _accentMint, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildContent() { | |
| final changelogs = _updateData?['changelogs'] as List?; | |
| final hasChangelogs = changelogs != null && changelogs.isNotEmpty; | |
| final isCancellable = _updateData?['cancellable'] as bool? ?? false; | |
| return SafeArea( | |
| child: Padding( | |
| padding: const EdgeInsets.all(24), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| // Update Icon | |
| Center( | |
| child: Container( | |
| width: 120, | |
| height: 120, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| gradient: LinearGradient( | |
| begin: Alignment.topLeft, | |
| end: Alignment.bottomRight, | |
| colors: [ | |
| _accentMint.withValues(alpha: 0.3), | |
| _accentMint.withValues(alpha: 0.1), | |
| ], | |
| ), | |
| border: Border.all( | |
| color: _accentMint.withValues(alpha: 0.5), | |
| width: 2, | |
| ), | |
| ), | |
| child: Icon( | |
| Icons.system_update_alt, | |
| size: 64, | |
| color: _accentMint, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 32), | |
| // App Name | |
| Text( | |
| _packageInfo?.appName ?? 'App', | |
| textAlign: TextAlign.center, | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 28, | |
| fontWeight: FontWeight.w800, | |
| color: Colors.white, | |
| ), | |
| ), | |
| const SizedBox(height: 24), | |
| // Version Upgrade Animation | |
| _buildVersionUpgrade(), | |
| const SizedBox(height: 32), | |
| // Changelogs Section (only if available) | |
| if (hasChangelogs) ...[ | |
| Text( | |
| 'What\'s New', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 20, | |
| fontWeight: FontWeight.w700, | |
| color: Colors.white, | |
| ), | |
| ), | |
| const SizedBox(height: 12), | |
| Container( | |
| padding: const EdgeInsets.all(20), | |
| decoration: BoxDecoration( | |
| color: _cardBg, | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all( | |
| color: Colors.white.withValues(alpha: 0.1), | |
| width: 1, | |
| ), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: changelogs.map((changelog) { | |
| return Padding( | |
| padding: const EdgeInsets.only(bottom: 12), | |
| child: Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Container( | |
| margin: const EdgeInsets.only(top: 6), | |
| width: 6, | |
| height: 6, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: _accentMint, | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: Text( | |
| changelog.toString(), | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 16, | |
| fontWeight: FontWeight.w500, | |
| color: Colors.white.withValues(alpha: 0.8), | |
| height: 1.4, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| }).toList(), | |
| ), | |
| ), | |
| const SizedBox(height: 32), | |
| ], | |
| const Spacer(), | |
| // Update Button | |
| ElevatedButton.icon( | |
| onPressed: _handleUpdate, | |
| icon: const Icon(Icons.download, size: 24), | |
| label: Text( | |
| 'Update Now', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 20, | |
| fontWeight: FontWeight.w700, | |
| letterSpacing: 0.5, | |
| ), | |
| ), | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: _accentMint, | |
| foregroundColor: _darkBg, | |
| padding: const EdgeInsets.symmetric(vertical: 18), | |
| shape: RoundedRectangleBorder( | |
| borderRadius: BorderRadius.circular(16), | |
| ), | |
| elevation: 0, | |
| ), | |
| ), | |
| // Skip/Later Button (only if cancellable) | |
| if (isCancellable) ...[ | |
| const SizedBox(height: 12), | |
| TextButton( | |
| onPressed: () => Navigator.of(context).pop(), | |
| child: Text( | |
| 'Maybe Later', | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 16, | |
| fontWeight: FontWeight.w600, | |
| color: Colors.white.withValues(alpha: 0.5), | |
| ), | |
| ), | |
| ), | |
| ], | |
| const SizedBox(height: 16), | |
| // Info Text | |
| Text( | |
| isCancellable | |
| ? 'Update to access the latest features and improvements' | |
| : 'This update is required to continue using the app', | |
| textAlign: TextAlign.center, | |
| style: GoogleFonts.rajdhani( | |
| fontSize: 14, | |
| color: Colors.white.withValues(alpha: 0.4), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment