Files
BadNote/lib/screens/app_shell.dart
Akiba So 198da00ecd
All checks were successful
CI / Windows build (push) Successful in 7m47s
feat: vault-aligned server v1 + UX polish
Redesign the optional FastAPI companion around vault files (manifest /
PUT/GET/DELETE + OCR jobs) instead of legacy strokes_json notes. Wire a
client Server settings panel for health/login. Polish shell UX: l10n for
settings/home/board, sticky-board empty state, and a narrow-screen
diagnostics FAB.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:04:48 +08:00

175 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../diagnostics/badnote_log.dart';
import '../diagnostics/diagnostic_chrome.dart';
import '../diagnostics/diagnostic_export.dart';
import '../l10n/app_localizations.dart';
import '../theme/app_theme.dart';
import 'board_screen.dart';
import 'home_screen.dart';
import 'search_screen.dart';
import 'settings_screen.dart';
/// Unified product shell — single chrome for library, board, search, settings.
class AppShell extends ConsumerStatefulWidget {
const AppShell({super.key});
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
int _index = 0;
final _diagKey = GlobalKey<DiagnosticChromeState>();
@override
void initState() {
super.initState();
BadNoteLog.instance.info(LogSubsystem.shell, 'shell_open');
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final wide = MediaQuery.sizeOf(context).width >= 900;
final destinations = [
_Dest(Icons.menu_book_outlined, Icons.menu_book, l.libraryTab),
_Dest(Icons.sticky_note_2_outlined, Icons.sticky_note_2, l.boardTab),
_Dest(Icons.search, Icons.search, l.search),
_Dest(Icons.tune, Icons.tune, l.settings),
];
final pages = const [
HomeScreen(embeddedInShell: true),
BoardScreen(),
SearchScreen(embeddedInShell: true),
SettingsScreen(embeddedInShell: true),
];
final body = DiagnosticChrome(
key: _diagKey,
child: pages[_index],
);
if (wide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _index,
onDestinationSelected: _select,
extended: MediaQuery.sizeOf(context).width >= 1200,
labelType: MediaQuery.sizeOf(context).width >= 1200
? NavigationRailLabelType.none
: NavigationRailLabelType.all,
leading: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 24),
child: Column(
children: [
Text(
'BadNote',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppTokens.copper,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
l.shellTagline,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppTokens.inkMuted,
),
),
],
),
),
trailing: Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DiagnosticToggleButton(
onToggle: () => _diagKey.currentState?.toggle(),
onExport: () => _diagKey.currentState?.exportPack(),
),
),
),
),
destinations: [
for (final d in destinations)
NavigationRailDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: Text(d.label),
),
],
),
VerticalDivider(width: 1, color: AppTokens.rule.withValues(alpha: 0.8)),
Expanded(child: body),
],
),
);
}
return Scaffold(
body: body,
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
NavigationBar(
selectedIndex: _index,
onDestinationSelected: _select,
destinations: [
for (final d in destinations)
NavigationDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: d.label,
),
],
),
],
),
floatingActionButton: FloatingActionButton.small(
heroTag: 'diag_fab',
tooltip: AppLocalizations.of(context).diagnosticsSection,
onPressed: () => _diagKey.currentState?.toggle(),
child: const Icon(Icons.bug_report_outlined),
),
);
}
void _select(int i) {
BadNoteLog.instance.info(LogSubsystem.shell, 'tab', fields: {'index': i});
setState(() => _index = i);
}
}
class _Dest {
const _Dest(this.icon, this.selectedIcon, this.label);
final IconData icon;
final IconData selectedIcon;
final String label;
}
/// Helper used by settings when not embedded — still export packs.
Future<void> exportDiagnosticPack(BuildContext context) async {
try {
final result = await DiagnosticExport.instance.exportPack();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).diagExported(result.bytes)),
duration: const Duration(seconds: 5),
),
);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).diagExportFail('$e'))),
);
}
}