From b4e42b93c52434f5c124ec03f4b0c9877b1df594 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Fri, 11 Jul 2025 12:27:47 +0800 Subject: [PATCH] MCSI-7 feat: implement NavigationBloc and integrate with side navigation bar --- .../adapter/presentation/navigation_bloc.dart | 22 ++++++++ .../ui/minecraft_server_installer.dart | 34 ++++++++++- .../framework/ui/side_navigation_bar.dart | 56 ++++++++++++++----- lib/main/framework/ui/strings.dart | 2 +- pubspec.lock | 16 ++++++ pubspec.yaml | 3 +- 6 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 lib/main/adapter/presentation/navigation_bloc.dart diff --git a/lib/main/adapter/presentation/navigation_bloc.dart b/lib/main/adapter/presentation/navigation_bloc.dart new file mode 100644 index 0000000..4aa0cde --- /dev/null +++ b/lib/main/adapter/presentation/navigation_bloc.dart @@ -0,0 +1,22 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NavigationBloc extends Bloc { + NavigationBloc() : super(NavigationItem.basicConfiguration) { + on((event, emit) => emit(event.item)); + } +} + +sealed class NavigationEvent {} + +class NavigationChangedEvent extends NavigationEvent { + final NavigationItem item; + + NavigationChangedEvent(this.item); +} + +enum NavigationItem { + basicConfiguration, + modConfiguration, + serverProperties, + about, +} diff --git a/lib/main/framework/ui/minecraft_server_installer.dart b/lib/main/framework/ui/minecraft_server_installer.dart index 66eb691..944e569 100644 --- a/lib/main/framework/ui/minecraft_server_installer.dart +++ b/lib/main/framework/ui/minecraft_server_installer.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:minecraft_server_installer/main/adapter/gateway/installation_repository_impl.dart'; import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.dart'; +import 'package:minecraft_server_installer/main/adapter/presentation/navigation_bloc.dart'; import 'package:minecraft_server_installer/main/application/use_case/download_file_use_case.dart'; import 'package:minecraft_server_installer/main/application/use_case/grant_file_permission_use_case.dart'; import 'package:minecraft_server_installer/main/application/use_case/write_file_use_case.dart'; @@ -18,8 +19,6 @@ import 'package:minecraft_server_installer/vanilla/framework/api/vanilla_api_ser class MinecraftServerInstaller extends StatelessWidget { const MinecraftServerInstaller({super.key}); - Widget get _body => const Padding(padding: EdgeInsets.all(32), child: BasicConfigurationTab()); - @override Widget build(BuildContext context) { final installationApiService = InstallationApiServiceImpl(); @@ -44,6 +43,7 @@ class MinecraftServerInstaller extends StatelessWidget { ), home: MultiBlocProvider( providers: [ + BlocProvider(create: (_) => NavigationBloc()), BlocProvider( create: (_) => InstallationBloc( downloadFileUseCase, @@ -63,7 +63,10 @@ class MinecraftServerInstaller extends StatelessWidget { child: Builder( builder: (context) { if (context.watch().state.isLocked) { - return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body)); + return MouseRegion( + cursor: SystemMouseCursors.forbidden, + child: AbsorbPointer(child: _body), + ); } return _body; @@ -76,4 +79,29 @@ class MinecraftServerInstaller extends StatelessWidget { ), ); } + + Widget get _body => BlocConsumer( + listener: (_, __) {}, + builder: (_, state) => Padding( + padding: const EdgeInsets.all(32), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: SizedBox( + key: ValueKey('tab${state.toString()}'), + child: _tabContent(state), + ), + ), + ), + ); + + Widget _tabContent(NavigationItem navigationItem) { + switch (navigationItem) { + case NavigationItem.basicConfiguration: + return const BasicConfigurationTab(); + case NavigationItem.modConfiguration: + case NavigationItem.serverProperties: + case NavigationItem.about: + return const Placeholder(); + } + } } diff --git a/lib/main/framework/ui/side_navigation_bar.dart b/lib/main/framework/ui/side_navigation_bar.dart index ccc6a63..b75cefb 100644 --- a/lib/main/framework/ui/side_navigation_bar.dart +++ b/lib/main/framework/ui/side_navigation_bar.dart @@ -1,7 +1,10 @@ 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}); @@ -12,10 +15,17 @@ class SideNavigationBar extends StatefulWidget { class _SideNavigationBarState extends State { bool _isExpanded = false; - NavigationItem _selectedNavigationItem = NavigationItem.basicConfiguration; + PackageInfo? _packageInfo; double get width => _isExpanded ? 360 : 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), @@ -78,7 +88,8 @@ class _SideNavigationBarState extends State { _navigationButton(NavigationItem.about), const Spacer(), _animatedText( - text: 'Version 1.0.0', + text: 'Version ${_packageInfo?.version ?? ''}', + padding: EdgeInsets.zero, expandedKey: const ValueKey('expandedVersion'), collapsedKey: const ValueKey('collapsedVersion'), alignment: Alignment.bottomCenter, @@ -116,7 +127,7 @@ class _SideNavigationBarState extends State { child: leading, ), ), - Expanded( + Flexible( child: Text( text, key: expandedKey, @@ -134,14 +145,15 @@ class _SideNavigationBarState extends State { ); Widget _navigationButton(NavigationItem navigationItem) { - final isSelected = _selectedNavigationItem == 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: () => setState(() => _selectedNavigationItem = navigationItem), + onTap: () => context.read().add(NavigationChangedEvent(navigationItem)), child: Row( children: [ Padding( @@ -161,14 +173,30 @@ class _SideNavigationBarState extends State { } } -enum NavigationItem { - basicConfiguration(iconData: Icons.dashboard_rounded, title: Strings.tabBasicConfiguration), - modConfiguration(iconData: Icons.extension, title: Strings.tabModConfiguration), - serverProperties(iconData: Icons.settings, title: Strings.tabServerPropertiesConfiguration), - about(iconData: Icons.info, title: Strings.tabAbout); +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; + } + } - final IconData iconData; - final String title; - - const NavigationItem({required this.iconData, required this.title}); + 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; + } + } } diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart index 4ca30b4..68da21d 100644 --- a/lib/main/framework/ui/strings.dart +++ b/lib/main/framework/ui/strings.dart @@ -10,7 +10,7 @@ abstract class Strings { static const buttonBrowse = '瀏覽'; static const tabBasicConfiguration = '基本設定'; static const tabModConfiguration = '模組設定'; - static const tabServerPropertiesConfiguration = '伺服器選項'; + static const tabServerProperties = '伺服器選項'; static const tabAbout = '關於與說明'; static const tabInstallationProgress = '安裝進度'; static const tooltipEulaInfo = '點擊查看 EULA 條款'; diff --git a/pubspec.lock b/pubspec.lock index f159f56..0ea9c0e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -232,6 +232,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.dev" + source: hosted + version: "3.2.0" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c9535bb..396905c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: "6.0.0-pre.1" environment: sdk: ">=3.6.0 <4.0.0" @@ -39,6 +39,7 @@ dependencies: flutter_bloc: ^9.1.1 gap: ^3.0.1 http: ^1.4.0 + package_info_plus: ^8.3.0 path: ^1.9.1 url_launcher: ^6.3.1 window_manager: ^0.5.0