import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; import 'package:minecraft_server_installer/main/adapter/presentation/navigation_bloc.dart'; import 'package:minecraft_server_installer/main/constants.dart'; import 'package:minecraft_server_installer/main/framework/ui/strings.dart'; import 'package:package_info_plus/package_info_plus.dart'; class SideNavigationBar extends StatefulWidget { const SideNavigationBar({super.key}); @override State createState() => _SideNavigationBarState(); } class _SideNavigationBarState extends State { bool _isExpanded = false; PackageInfo? _packageInfo; double get width => _isExpanded ? 340 : 80; @override void initState() { super.initState(); PackageInfo.fromPlatform().then((packageInfo) => WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _packageInfo = packageInfo))); } @override Widget build(BuildContext context) => AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: width, padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: const BorderRadius.only( topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), boxShadow: [ const BoxShadow(color: Colors.black26, offset: Offset(0, 0), blurRadius: 8), ]), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _animatedText( text: Constants.appName, leading: Padding( padding: const EdgeInsets.only(right: 4), child: SizedBox.square( dimension: 32, child: Image.asset('assets/img/mcsi_logo.png', width: 2048, height: 2048), ), ), padding: const EdgeInsets.only(left: 4), expandedKey: const ValueKey('expandedTitle'), collapsedKey: const ValueKey('collapsedTitle'), style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.w900, color: Colors.blueGrey.shade900), ), Material( color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: InkWell( key: ValueKey(_isExpanded), borderRadius: BorderRadius.circular(8), onTap: _toggleIsExpanded, child: Padding( padding: const EdgeInsets.all(8), child: Icon(_isExpanded ? Icons.menu_open : Icons.menu, color: Colors.blueGrey.shade600), ), ), ), ), ], ), const Gap(32), _navigationButton(NavigationItem.basicConfiguration), const Gap(8), _navigationButton(NavigationItem.modConfiguration), const Gap(8), _navigationButton(NavigationItem.serverProperties), const Gap(8), _navigationButton(NavigationItem.about), const Spacer(), _animatedText( text: 'Version ${_packageInfo?.version ?? ''}', padding: EdgeInsets.zero, expandedKey: const ValueKey('expandedVersion'), collapsedKey: const ValueKey('collapsedVersion'), alignment: Alignment.bottomCenter, ), ], ), ); void _toggleIsExpanded() => setState(() => _isExpanded = !_isExpanded); Widget _animatedText({ required String text, required Key expandedKey, required Key collapsedKey, EdgeInsetsGeometry padding = const EdgeInsets.only(left: 8), TextStyle? style, AlignmentGeometry alignment = Alignment.centerLeft, Widget? leading, }) => Expanded( child: ClipRect( child: Container( alignment: alignment, padding: padding, child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: _isExpanded ? Row( mainAxisSize: MainAxisSize.min, children: [ if (leading != null) Flexible( child: Padding( padding: const EdgeInsets.only(right: 8), child: leading, ), ), Flexible( child: Text( text, key: expandedKey, style: style, softWrap: false, overflow: TextOverflow.visible, ), ), ], ) : SizedBox.shrink(key: collapsedKey), ), ), ), ); Widget _navigationButton(NavigationItem navigationItem) { final selectedNavigationItem = context.watch().state; final isSelected = selectedNavigationItem == navigationItem; final color = isSelected ? Colors.blue.shade900 : Colors.blueGrey.shade600; return Material( color: isSelected ? Colors.blueGrey.shade100 : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => context.read().add(NavigationChangedEvent(navigationItem)), child: Row( children: [ Padding( padding: const EdgeInsets.all(8), child: Icon(navigationItem.iconData, color: color), ), _animatedText( text: navigationItem.title, expandedKey: ValueKey('expanded${navigationItem.toString()}'), collapsedKey: ValueKey('collapsed${navigationItem.toString()}'), style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500, color: color), ), ], ), ), ); } } extension _NavigationItemContent on NavigationItem { String get title { switch (this) { case NavigationItem.basicConfiguration: return Strings.tabBasicConfiguration; case NavigationItem.modConfiguration: return Strings.tabModConfiguration; case NavigationItem.serverProperties: return Strings.tabServerProperties; case NavigationItem.about: return Strings.tabAbout; } } IconData get iconData { switch (this) { case NavigationItem.basicConfiguration: return Icons.dashboard_rounded; case NavigationItem.modConfiguration: return Icons.extension; case NavigationItem.serverProperties: return Icons.settings; case NavigationItem.about: return Icons.info; } } }