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 createState() => _AppShellState(); } class _AppShellState extends ConsumerState { int _index = 0; final _diagKey = GlobalKey(); @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: NavigationBar( selectedIndex: _index, onDestinationSelected: _select, destinations: [ for (final d in destinations) NavigationDestination( icon: Icon(d.icon), selectedIcon: Icon(d.selectedIcon), label: d.label, ), ], ), ); } 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 exportDiagnosticPack(BuildContext context) async { try { final result = await DiagnosticExport.instance.exportPack(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('诊断包: ${result.zipPath}'), duration: const Duration(seconds: 5), ), ); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('导出失败: $e')), ); } }