Add multi-account support and update project docs

- Add SavedAccount/AccountStore and rework ServerProvider around a list
  of saved accounts: switch/remove/add, swipe-to-cycle on the avatar
  button, per-account browsing prefs (sort/view/filters/cache), and a
  session-generation guard so a stale fetch from an abandoned account
  can't land in the newly active one
- Migrate existing single-account installs into the new schema
  automatically so upgrading users stay logged in
- Settings gains an Accounts section; LoginView gains an "add account"
  mode pushed on top of the main shell instead of replacing it
- Give every Scrollbar in the app a thicker, draggable, Android-style
  thumb via a shared ScrollbarTheme instead of the default hairline
- Splash screen: use the monochrome app icon (tinted per theme) and a
  smaller spinner
- Update CLAUDE.md and .claude/context/*.md to reflect the accumulated
  multi-account, sharing, and UI changes across this session, and add
  a standing instruction to keep these docs current going forward

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 11:21:36 -04:00
co-authored by Claude Sonnet 5
parent 5299ac5c2c
commit 1fbdc1f8d0
15 changed files with 1121 additions and 222 deletions
+145
View File
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/app_tab.dart';
import '../models/saved_account.dart';
import '../providers/server_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/frosted_glass_container.dart';
import '../widgets/seek_bar_painter.dart';
import 'login_view.dart';
class AccountView extends StatelessWidget {
const AccountView({super.key});
@@ -177,6 +179,17 @@ class AccountView extends StatelessWidget {
),
const SizedBox(height: 24),
// Accounts Section
Text(
'Accounts',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
const _AccountsCard(),
const SizedBox(height: 24),
// Server Credentials Section
Text(
'Server Connection Info',
@@ -465,6 +478,138 @@ class AccountView extends StatelessWidget {
}
}
/// Lists every saved account, letting the user switch to an inactive one,
/// remove any of them, or add another via [LoginView] pushed in "add
/// account" mode.
class _AccountsCard extends StatelessWidget {
const _AccountsCard();
Future<void> _confirmRemove(
BuildContext context,
ServerProvider provider,
SavedAccount account,
) async {
final isActive = account.id == provider.activeAccountId;
final host = Uri.tryParse(account.serverUrl)?.host ?? account.serverUrl;
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Remove Account'),
content: Text(
isActive && provider.accounts.length > 1
? 'Remove ${account.username} ($host)? Another saved account will become active.'
: 'Remove ${account.username} ($host)? You can add it again later.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Cancel'),
),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(dialogContext).colorScheme.error,
),
onPressed: () => Navigator.pop(dialogContext, true),
child: const Text('Remove'),
),
],
);
},
);
if (confirmed == true) {
await provider.removeAccount(account.id);
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<ServerProvider>();
final accounts = provider.accounts;
return Card(
child: Column(
children: [
for (final account in accounts) ...[
_AccountRow(
account: account,
isActive: account.id == provider.activeAccountId,
onTap: account.id == provider.activeAccountId
? null
: () => provider.switchAccount(account.id),
onRemove: () => _confirmRemove(context, provider, account),
),
const Divider(height: 1, indent: 16, endIndent: 16),
],
ListTile(
leading: const Icon(Icons.add_circle_outline_rounded),
title: const Text('Add account'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const LoginView(isAddingAccount: true),
),
);
},
),
],
),
);
}
}
class _AccountRow extends StatelessWidget {
final SavedAccount account;
final bool isActive;
final VoidCallback? onTap;
final VoidCallback onRemove;
const _AccountRow({
required this.account,
required this.isActive,
required this.onTap,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final host = Uri.tryParse(account.serverUrl)?.host ?? account.serverUrl;
final initial = account.username.isNotEmpty
? account.username[0].toUpperCase()
: '?';
return ListTile(
leading: CircleAvatar(
backgroundColor: colorScheme.primary,
child: Text(
initial,
style: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
title: Text(account.username),
subtitle: Text(host),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isActive)
Icon(Icons.check_circle_rounded, color: colorScheme.primary),
IconButton(
icon: const Icon(Icons.delete_outline_rounded),
tooltip: 'Remove account',
onPressed: onRemove,
),
],
),
onTap: onTap,
);
}
}
class _BottomBarAppearanceCard extends StatefulWidget {
const _BottomBarAppearanceCard();