import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide; import '../core/constants/app_sections.dart'; import '../core/theme/theme_controller.dart'; import '../core/permissions/permission_manager.dart'; class AppShell extends StatelessWidget { const AppShell({ super.key, required this.child, required this.currentLocation, }); final Widget child; final String currentLocation; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 960; final manager = PermissionScope.of(context); final filteredPages = [ for (final section in appSections) for (final page in section.pages) if (manager.can(page.path, PermissionAction.view)) page, ]; final pages = filteredPages.isEmpty ? allAppPages : filteredPages; if (isWide) { return Scaffold( appBar: AppBar( title: const Text('Superport v2'), actions: [ IconButton( tooltip: '로그아웃', icon: const Icon(lucide.LucideIcons.logOut), onPressed: () => context.go(loginRoutePath), ), ], ), body: Row( children: [ _NavigationRail(currentLocation: currentLocation, pages: pages), const VerticalDivider(width: 1), Expanded(child: child), ], ), ); } return Scaffold( appBar: AppBar( title: const Text('Superport v2'), actions: [ IconButton( tooltip: '로그아웃', icon: const Icon(lucide.LucideIcons.logOut), onPressed: () => context.go(loginRoutePath), ), ], ), drawer: Drawer( child: SafeArea( child: _NavigationList( currentLocation: currentLocation, onTap: (path) { Navigator.of(context).pop(); context.go(path); }, pages: pages, ), ), ), body: child, ); }, ); } } class _NavigationRail extends StatelessWidget { const _NavigationRail({required this.currentLocation, required this.pages}); final String currentLocation; final List pages; @override Widget build(BuildContext context) { final selectedIndex = _selectedIndex(currentLocation, pages); final theme = Theme.of(context); final colorScheme = theme.colorScheme; final themeController = ThemeControllerScope.of(context); final currentThemeMode = themeController.mode; return Container( width: 104, decoration: BoxDecoration( border: Border(right: BorderSide(color: colorScheme.outlineVariant)), ), child: Column( children: [ const SizedBox(height: 24), const FlutterLogo(size: 48), const SizedBox(height: 24), Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: pages.length, itemBuilder: (context, index) { final page = pages[index]; final isSelected = index == selectedIndex; final textStyle = theme.textTheme.labelSmall?.copyWith( color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Material( color: isSelected ? colorScheme.primary.withValues(alpha: 0.12) : Colors.transparent, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { if (page.path != currentLocation) { context.go(page.path); } }, child: Padding( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 8, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( page.icon, size: 22, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), const SizedBox(height: 6), Text( page.label, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: textStyle, ), ], ), ), ), ), ); }, ), ), Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), child: _ThemeMenuButton( mode: currentThemeMode, onChanged: themeController.update, ), ), ], ), ); } } class _NavigationList extends StatelessWidget { const _NavigationList({ required this.currentLocation, required this.onTap, required this.pages, }); final String currentLocation; final ValueChanged onTap; final List pages; @override Widget build(BuildContext context) { final selectedIndex = _selectedIndex(currentLocation, pages); final themeController = ThemeControllerScope.of(context); return ListView.builder( itemCount: pages.length + 1, itemBuilder: (context, index) { if (index == pages.length) { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), child: _ThemeMenuButton( mode: themeController.mode, onChanged: (mode) { themeController.update(mode); Navigator.of(context).maybePop(); }, ), ); } final page = pages[index]; final selected = index == selectedIndex; return ListTile( leading: Icon(page.icon), title: Text(page.label), subtitle: Text( page.summary, maxLines: 1, overflow: TextOverflow.ellipsis, ), selected: selected, selectedColor: Theme.of(context).colorScheme.primary, onTap: () => onTap(page.path), ); }, ); } } class _ThemeMenuButton extends StatelessWidget { const _ThemeMenuButton({required this.mode, required this.onChanged}); final ThemeMode mode; final ValueChanged onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final label = _label(mode); final icon = _icon(mode); return PopupMenuButton( tooltip: '테마 변경', onSelected: onChanged, itemBuilder: (context) => ThemeMode.values .map( (value) => PopupMenuItem( value: value, child: Row( children: [ Icon(_icon(value), size: 18), const SizedBox(width: 8), Text(_label(value)), ], ), ), ) .toList(), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: colorScheme.outlineVariant), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 18), const SizedBox(width: 8), Text('테마 · $label', style: theme.textTheme.labelSmall), ], ), ), ); } static String _label(ThemeMode mode) { switch (mode) { case ThemeMode.system: return '시스템'; case ThemeMode.light: return '라이트'; case ThemeMode.dark: return '다크'; } } static IconData _icon(ThemeMode mode) { switch (mode) { case ThemeMode.system: return lucide.LucideIcons.monitorCog; case ThemeMode.light: return lucide.LucideIcons.sun; case ThemeMode.dark: return lucide.LucideIcons.moon; } } } int _selectedIndex(String location, List pages) { final normalized = location.toLowerCase(); final exact = pages.indexWhere( (page) => normalized == page.path.toLowerCase(), ); if (exact != -1) { return exact; } final prefix = pages.indexWhere( (page) => normalized.startsWith(page.path.toLowerCase()), ); return prefix == -1 ? 0 : prefix; }