look ahead
This commit is contained in:
+178
-145
@@ -10,120 +10,150 @@ import '../screens/account_screen.dart';
|
||||
import '../screens/login_screen.dart';
|
||||
import '../screens/settings_screen.dart';
|
||||
|
||||
Widget authAppBarAction(BuildContext context) {
|
||||
final auth = context.watch<AuthController>();
|
||||
final sync = context.watch<SyncController>();
|
||||
final showRing = sync.showProgressRing;
|
||||
final progress = sync.syncProgress;
|
||||
final success = sync.progressSuccess;
|
||||
final syncActive = sync.isSyncing || showRing;
|
||||
/// 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();
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
final ringColor = success
|
||||
? const Color(0xFF16A34A)
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
class _AuthAppBarAction extends StatelessWidget {
|
||||
const _AuthAppBarAction();
|
||||
|
||||
return PopupMenuButton<_AccountMenuAction>(
|
||||
tooltip: sync.isSyncing
|
||||
? 'Syncing… ${(progress * 100).round()}%'
|
||||
: auth.displayName,
|
||||
offset: const Offset(0, 48),
|
||||
onSelected: (action) async {
|
||||
switch (action) {
|
||||
case _AccountMenuAction.account:
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const AccountScreen()),
|
||||
);
|
||||
case _AccountMenuAction.logout:
|
||||
await auth.logout();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
final email = auth.email;
|
||||
// Capture whether sync was active when the menu opened; the tile
|
||||
// itself keeps listening so the % can update while the menu is open.
|
||||
final showSyncRow = syncActive;
|
||||
return [
|
||||
if (email != null)
|
||||
PopupMenuItem<_AccountMenuAction>(
|
||||
enabled: false,
|
||||
child: Text(
|
||||
email,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
if (showSyncRow)
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
enabled: false,
|
||||
child: _SyncProgressMenuTile(),
|
||||
),
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
value: _AccountMenuAction.account,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.person_outline),
|
||||
title: Text('Account'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
value: _AccountMenuAction.logout,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text('Sign out'),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
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),
|
||||
@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) {
|
||||
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 syncActive = sync.isSyncing || showRing;
|
||||
final ringColor = success
|
||||
? const Color(0xFF16A34A)
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
|
||||
return PopupMenuButton<_AccountMenuAction>(
|
||||
tooltip: sync.isSyncing
|
||||
? 'Syncing… ${(progress * 100).round()}%'
|
||||
: auth.displayName,
|
||||
offset: const Offset(0, 48),
|
||||
onSelected: (action) async {
|
||||
switch (action) {
|
||||
case _AccountMenuAction.account:
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const AccountScreen(),
|
||||
),
|
||||
);
|
||||
case _AccountMenuAction.logout:
|
||||
await auth.logout();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
final email = auth.email;
|
||||
final showSyncRow = syncActive;
|
||||
return [
|
||||
if (email != null)
|
||||
PopupMenuItem<_AccountMenuAction>(
|
||||
enabled: false,
|
||||
child: Text(
|
||||
email,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showSyncRow)
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
enabled: false,
|
||||
child: _SyncProgressMenuTile(),
|
||||
),
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
value: _AccountMenuAction.account,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.person_outline),
|
||||
title: Text('Account'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<_AccountMenuAction>(
|
||||
value: _AccountMenuAction.logout,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text('Sign out'),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
tooltip: 'Sign in',
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<bool>(builder: (_) => const LoginScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.person_outline),
|
||||
);
|
||||
}
|
||||
|
||||
/// Live sync status row inside the account popup (watches [SyncController]).
|
||||
@@ -132,46 +162,49 @@ class _SyncProgressMenuTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sync = context.watch<SyncController>();
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
final pct = (sync.syncProgress * 100).round().clamp(0, 100);
|
||||
final detail = sync.statusDetail.trim();
|
||||
return ListenableBuilder(
|
||||
listenable: context.read<SyncController>(),
|
||||
builder: (context, _) {
|
||||
final sync = context.read<SyncController>();
|
||||
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 ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.check_circle_outline, color: colors.primary),
|
||||
title: Text(
|
||||
'Sync complete',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurface,
|
||||
if (sync.progressSuccess) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.check_circle_outline, color: colors.primary),
|
||||
title: Text(
|
||||
'Sync complete',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
value: sync.syncProgress <= 0
|
||||
? null
|
||||
: sync.syncProgress.clamp(0.02, 1.0),
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
value: sync.syncProgress <= 0
|
||||
? null
|
||||
: sync.syncProgress.clamp(0.02, 1.0),
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
detail.isEmpty ? 'Sync in progress ($pct%)' : detail,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: detail.isEmpty
|
||||
? null
|
||||
: Text('Sync in progress ($pct%)'),
|
||||
title: Text(
|
||||
detail.isEmpty ? 'Sync in progress ($pct%)' : detail,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: detail.isEmpty ? null : Text('Sync in progress ($pct%)'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user