import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import '../models/settings.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/pin_lock_view_model.dart'; import '../viewmodels/settings_view_model.dart'; class SettingsScreenView extends StatefulWidget { const SettingsScreenView({super.key}); @override State createState() => _SettingsScreenViewState(); } class _SettingsScreenViewState extends State { String _appVersion = ''; int _versionTaps = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().ensureLoaded(); context.read().ensureLoaded(); _loadVersion(); }); } Future currentVersion() async { final info = await PackageInfo.fromPlatform(); debugPrint("Current app version: ${info.version}"); return info.version; } void _onVersionTap() { _versionTaps++; if (_versionTaps >= 7) { _versionTaps = 0; context.push('/dev'); } } Future _loadVersion() async { final version = await currentVersion(); if (!mounted) return; setState(() { _appVersion = version; }); } Future _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) { final controller = TextEditingController(); return showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), content: SizedBox( width: 320, child: TextField( controller: controller, autofocus: true, obscureText: true, keyboardType: TextInputType.number, maxLength: 4, decoration: InputDecoration(hintText: hint), ), ), actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 12), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), const SizedBox(width: 12), FilledButton( onPressed: () => Navigator.pop(context, controller.text), child: const Text('Confirm'), ), ], ), ); } void _showError(String message) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } Future _enablePin() async { final pin = await _promptPin('Set PIN Code', hint: '4 digits'); if (pin == null) return; if (pin.length != 4) { _showError('PIN must be exactly 4 digits'); return; } final pinLockViewModel = context.read(); final success = await pinLockViewModel.setPin(pin); if (!success) { _showError(pinLockViewModel.errorMessage ?? 'Could not set PIN.'); return; } pinLockViewModel.setPinRequired(true); await context.read().updatePinRequired(true); } Future _changePin() async { final current = await _promptPin('Enter Current PIN'); if (current == null) return; final newPin = await _promptPin('Enter New PIN', hint: '4 digits'); if (newPin == null) return; if (newPin.length != 4) { _showError('PIN must be exactly 4 digits'); return; } final pinLockViewModel = context.read(); final success = await pinLockViewModel.changePin( currentPin: current, newPin: newPin, ); if (!success) { _showError(pinLockViewModel.errorMessage ?? 'Could not change PIN.'); } } Future _disablePin() async { final current = await _promptPin('Enter Current PIN to Disable'); if (current == null) return; final pinLockViewModel = context.read(); final success = await pinLockViewModel.disablePin(current); if (!success) { _showError(pinLockViewModel.errorMessage ?? 'Could not disable PIN.'); return; } pinLockViewModel.setPinRequired(false); await context.read().updatePinRequired(false); } @override Widget build(BuildContext context) { final settingsViewModel = context.watch(); final pinLockViewModel = context.watch(); return Scaffold( appBar: AppBar( title: Row( children: [ IconButton( onPressed: () => context.go('/bar'), icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 5), const Text('Settings'), ], ), ), body: Builder( builder: (context) { final isLoading = settingsViewModel.isLoading || pinLockViewModel.isLoading; final hasLoaded = settingsViewModel.hasLoaded && pinLockViewModel.hasLoaded; if (isLoading && !hasLoaded) { return const Center( child: CircularProgressIndicator(strokeWidth: 2.5), ); } final settings = settingsViewModel.settings; return ListView( padding: const EdgeInsets.symmetric(vertical: 12), children: [ _SettingsSection( title: 'Security', children: [ _SettingsSwitchTile( icon: Icons.lock_outline_rounded, title: 'PIN Required', value: settings.pinRequired, onChanged: (value) { if (value) { _enablePin(); } else { _disablePin(); } }, ), if (settings.pinRequired) _SettingsTile( icon: Icons.pin_rounded, title: 'Change PIN', onTap: _changePin, ), ], ), _SettingsSection( title: 'Appearance', children: [ _SettingsTile( icon: Icons.brightness_6_rounded, title: 'Theme', subtitle: switch (settings.themeMode) { AppThemeMode.system => 'System', AppThemeMode.light => 'Light', AppThemeMode.dark => 'Dark', AppThemeMode.ugly => 'Ugly', }, onTap: () async { final selected = await showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: AppThemeMode.values.map((mode) { return RadioListTile( value: mode, groupValue: settings.themeMode, title: Text(switch (mode) { AppThemeMode.system => 'System', AppThemeMode.light => 'Light', AppThemeMode.dark => 'Dark', AppThemeMode.ugly => 'Ugly', }), onChanged: (value) => Navigator.pop(context, value), ); }).toList(), ), ), ); if (selected != null) { await settingsViewModel.updateThemeMode(selected); } }, ), ], ), if (Platform.isAndroid) _SettingsSection( title: 'Updates', children: [ _SettingsTile( icon: Icons.system_update_alt_rounded, title: settingsViewModel.checkingForUpdates ? 'Checking for updates...' : 'Check for updates', onTap: settingsViewModel.checkingForUpdates ? null : _checkForUpdates, ), ], ), const SizedBox(height: 40), GestureDetector( onTap: _onVersionTap, child: Center( child: Text( 'Version $_appVersion', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.5), ), ), ), ), const SizedBox(height: 20), ], ); }, ), ); } Future _checkForUpdates() async { if (!Platform.isAndroid) return; final vm = context.read(); try { final update = await vm.checkForUpdates(); if (!mounted) return; if (update == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('You are already on the latest version.'), ), ); return; } _showUpdateDialog(update); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Update check failed: $e'))); } } void _showUpdateDialog(UpdateInfo update) { showDialog( context: context, barrierDismissible: !update.mandatory, builder: (context) => AlertDialog( title: const Text("Update available"), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Version ${update.version} is available."), const SizedBox(height: 12), Text(update.notes), ], ), actions: [ if (!update.mandatory) TextButton( onPressed: () => Navigator.pop(context), child: const Text("Later"), ), FilledButton( onPressed: () async { Navigator.pop(context); try { await context.read().installUpdate( update, onProgress: (progress) { debugPrint( "Download ${(progress * 100).toStringAsFixed(0)}%", ); }, ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text("Update failed: $e"))); } }, child: const Text("Update"), ), ], ), ); } } class _SettingsSection extends StatelessWidget { final String title; final List children; const _SettingsSection({required this.title, required this.children}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 0.5, color: scheme.onSurface.withValues(alpha: 0.5), ), ), const SizedBox(height: 8), Container( decoration: BoxDecoration( color: scheme.onSurface.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(14), border: Border.all( color: scheme.onSurface.withValues(alpha: 0.06), ), ), clipBehavior: Clip.antiAlias, child: Column(children: children), ), ], ), ); } } class _SettingsTile extends StatelessWidget { final IconData icon; final String title; final String? subtitle; final VoidCallback? onTap; const _SettingsTile({ required this.icon, required this.title, this.subtitle, this.onTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon( icon, size: 22, color: scheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(width: 14), Expanded( child: Text( title, style: TextStyle( fontWeight: FontWeight.w600, color: scheme.onSurface, ), ), ), if (subtitle != null) ...[ Text(subtitle!, style: Theme.of(context).textTheme.bodySmall), const SizedBox(width: 4), ], if (onTap != null) Icon( Icons.chevron_right_rounded, color: scheme.onSurface.withValues(alpha: 0.3), ), ], ), ), ), ); } } class _SettingsSwitchTile extends StatelessWidget { final IconData icon; final String title; final bool value; final ValueChanged onChanged; const _SettingsSwitchTile({ required this.icon, required this.title, required this.value, required this.onChanged, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Row( children: [ Icon(icon, size: 22, color: scheme.onSurface.withValues(alpha: 0.7)), const SizedBox(width: 14), Expanded( child: Text( title, style: TextStyle( fontWeight: FontWeight.w600, color: scheme.onSurface, ), ), ), Switch(value: value, onChanged: onChanged), ], ), ); } }