/// Shared helpers to open Settings from app bars. library; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../providers/sync_provider.dart'; import '../screens/account_screen.dart'; import '../screens/login_screen.dart'; import '../screens/settings_screen.dart'; /// Account / sign-in control for app bars. /// /// Sync progress is subscribed in a nested widget so progress ticks do not /// rebuild the whole screen (Overview charts, Look ahead, etc.). Widget authAppBarAction(BuildContext context) => const _AuthAppBarAction(); class _AuthAppBarAction extends StatelessWidget { const _AuthAppBarAction(); @override Widget build(BuildContext context) { final auth = context.watch(); if (!auth.isLoggedIn) { return IconButton( tooltip: 'Sign in', onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const LoginScreen()), ); }, icon: const Icon(Icons.person_outline), ); } return _SyncAwareAccountMenu(auth: auth); } } class _SyncAwareAccountMenu extends StatelessWidget { const _SyncAwareAccountMenu({required this.auth}); final AuthController auth; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; // MenuAnchor (not PopupMenuButton): no modal black barrier, and the menu // stays anchored under this trailing control instead of jumping left. return MenuAnchor( consumeOutsideTap: true, alignmentOffset: const Offset(0, 4), style: MenuStyle( alignment: AlignmentDirectional.bottomEnd, backgroundColor: WidgetStatePropertyAll(colors.surfaceContainerHigh), surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent), elevation: const WidgetStatePropertyAll(6), shadowColor: const WidgetStatePropertyAll(Colors.black38), shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(vertical: 8), ), ), builder: (context, controller, child) { return ListenableBuilder( listenable: context.read(), builder: (context, _) { final sync = context.read(); final tooltip = sync.isSyncing ? 'Syncing… ${(sync.syncProgress * 100).round()}%' : auth.displayName; return IconButton( tooltip: tooltip, onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: child ?? const SizedBox.shrink(), ); }, ); }, menuChildren: [ if (auth.email case final email?) Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: Text( email, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colors.onSurfaceVariant, ), ), ), const _SyncProgressMenuEntry(), MenuItemButton( leadingIcon: const Icon(Icons.person_outline), onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const AccountScreen(), ), ); }, child: const Text('Account'), ), MenuItemButton( leadingIcon: const Icon(Icons.logout), onPressed: () async { await auth.logout(); if (!context.mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); }, child: const Text('Sign out'), ), ], child: _AccountAvatar(auth: auth), ); } } /// Avatar + optional sync ring; watches sync without rebuilding the menu shell. class _AccountAvatar extends StatelessWidget { const _AccountAvatar({required this.auth}); final AuthController auth; @override Widget build(BuildContext context) { return ListenableBuilder( listenable: context.read(), builder: (context, _) { final sync = context.read(); final showRing = sync.showProgressRing; final progress = sync.syncProgress; final success = sync.progressSuccess; final ringColor = success ? const Color(0xFF16A34A) : Theme.of(context).colorScheme.primary; return SizedBox( width: 32, height: 32, child: Stack( alignment: Alignment.center, children: [ CircleAvatar( radius: 14, backgroundColor: Theme.of(context).colorScheme.primaryContainer, child: Text( auth.initials, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context) .colorScheme .onPrimaryContainer, ), ), ), if (showRing) SizedBox( width: 32, height: 32, child: CircularProgressIndicator( value: progress <= 0 ? null : progress.clamp(0.02, 1.0), strokeWidth: 2.5, color: ringColor, backgroundColor: Theme.of(context) .colorScheme .outlineVariant .withValues(alpha: 0.45), ), ), ], ), ); }, ); } } /// Sync row shown only while a sync is active / finishing. class _SyncProgressMenuEntry extends StatelessWidget { const _SyncProgressMenuEntry(); @override Widget build(BuildContext context) { return ListenableBuilder( listenable: context.read(), builder: (context, _) { final sync = context.read(); if (!sync.isSyncing && !sync.showProgressRing) { return const SizedBox.shrink(); } final theme = Theme.of(context); final colors = theme.colorScheme; final pct = (sync.syncProgress * 100).round().clamp(0, 100); final detail = sync.statusDetail.trim(); if (sync.progressSuccess) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon( Icons.check_circle_outline, color: colors.primary, size: 20, ), const SizedBox(width: 12), Text('Sync complete', style: theme.textTheme.bodyMedium), ], ), ); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( value: sync.syncProgress <= 0 ? null : sync.syncProgress.clamp(0.02, 1.0), strokeWidth: 2.5, ), ), const SizedBox(width: 12), Expanded( child: Text( detail.isEmpty ? 'Sync in progress ($pct%)' : detail, style: theme.textTheme.bodyMedium, ), ), ], ), ); }, ); } } List bankAppBarActions({ required BuildContext context, VoidCallback? onRefresh, bool refreshing = false, }) { return [ if (onRefresh != null) IconButton( tooltip: 'Refresh', onPressed: refreshing ? null : onRefresh, icon: const Icon(Icons.refresh), ), IconButton( tooltip: 'Settings', onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); }, icon: const Icon(Icons.settings_outlined), ), authAppBarAction(context), ]; }