279 lines
8.4 KiB
Dart
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),
|
|
];
|
|
}
|