Files
2026-09-10 10:51:45 +02:00

279 lines
8.4 KiB
Dart

/// 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<AuthController>();
if (!auth.isLoggedIn) {
return IconButton(
tooltip: 'Sign in',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<bool>(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<SyncController>(),
builder: (context, _) {
final sync = context.read<SyncController>();
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<void>(
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<SyncController>(),
builder: (context, _) {
final sync = context.read<SyncController>();
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<SyncController>(),
builder: (context, _) {
final sync = context.read<SyncController>();
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<Widget> 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<void>(builder: (_) => const SettingsScreen()),
);
},
icon: const Icon(Icons.settings_outlined),
),
authAppBarAction(context),
];
}