2026-08-05 17:55:27 +08:00
|
|
|
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,
|
2026-08-05 19:04:48 +08:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-08-05 17:55:27 +08:00
|
|
|
],
|
|
|
|
|
),
|
2026-08-05 19:04:48 +08:00
|
|
|
floatingActionButton: FloatingActionButton.small(
|
|
|
|
|
heroTag: 'diag_fab',
|
|
|
|
|
tooltip: AppLocalizations.of(context).diagnosticsSection,
|
|
|
|
|
onPressed: () => _diagKey.currentState?.toggle(),
|
|
|
|
|
child: const Icon(Icons.bug_report_outlined),
|
|
|
|
|
),
|
2026-08-05 17:55:27 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2026-08-05 19:04:48 +08:00
|
|
|
content: Text(AppLocalizations.of(context).diagExported(result.bytes)),
|
2026-08-05 17:55:27 +08:00
|
|
|
duration: const Duration(seconds: 5),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
2026-08-05 19:04:48 +08:00
|
|
|
SnackBar(content: Text(AppLocalizations.of(context).diagExportFail('$e'))),
|
2026-08-05 17:55:27 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|