MCSI-7 Tab and navigation #26

Merged
squid merged 4 commits from MCSI-7_tab_and_navigation into main 2025-07-11 12:39:05 +08:00
6 changed files with 114 additions and 19 deletions
Showing only changes of commit b4e42b93c5 - Show all commits

View File

@ -0,0 +1,22 @@
import 'package:flutter_bloc/flutter_bloc.dart';
class NavigationBloc extends Bloc<NavigationEvent, NavigationItem> {
NavigationBloc() : super(NavigationItem.basicConfiguration) {
on<NavigationChangedEvent>((event, emit) => emit(event.item));
}
}
sealed class NavigationEvent {}
class NavigationChangedEvent extends NavigationEvent {
final NavigationItem item;
NavigationChangedEvent(this.item);
}
enum NavigationItem {
basicConfiguration,
modConfiguration,
serverProperties,
about,
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/gateway/installation_repository_impl.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.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/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/grant_file_permission_use_case.dart';
import 'package:minecraft_server_installer/main/application/use_case/write_file_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 { class MinecraftServerInstaller extends StatelessWidget {
const MinecraftServerInstaller({super.key}); const MinecraftServerInstaller({super.key});
Widget get _body => const Padding(padding: EdgeInsets.all(32), child: BasicConfigurationTab());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final installationApiService = InstallationApiServiceImpl(); final installationApiService = InstallationApiServiceImpl();
@ -44,6 +43,7 @@ class MinecraftServerInstaller extends StatelessWidget {
), ),
home: MultiBlocProvider( home: MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => NavigationBloc()),
BlocProvider( BlocProvider(
create: (_) => InstallationBloc( create: (_) => InstallationBloc(
downloadFileUseCase, downloadFileUseCase,
@ -63,7 +63,10 @@ class MinecraftServerInstaller extends StatelessWidget {
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (context.watch<InstallationBloc>().state.isLocked) { if (context.watch<InstallationBloc>().state.isLocked) {
return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body)); return MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: AbsorbPointer(child: _body),
);
} }
return _body; return _body;
@ -76,4 +79,29 @@ class MinecraftServerInstaller extends StatelessWidget {
), ),
); );
} }
Widget get _body => BlocConsumer<NavigationBloc, NavigationItem>(
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();
}
}
} }

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.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/constants.dart';
import 'package:minecraft_server_installer/main/framework/ui/strings.dart'; import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
import 'package:package_info_plus/package_info_plus.dart';
class SideNavigationBar extends StatefulWidget { class SideNavigationBar extends StatefulWidget {
const SideNavigationBar({super.key}); const SideNavigationBar({super.key});
@ -12,10 +15,17 @@ class SideNavigationBar extends StatefulWidget {
class _SideNavigationBarState extends State<SideNavigationBar> { class _SideNavigationBarState extends State<SideNavigationBar> {
bool _isExpanded = false; bool _isExpanded = false;
NavigationItem _selectedNavigationItem = NavigationItem.basicConfiguration; PackageInfo? _packageInfo;
double get width => _isExpanded ? 360 : 80; double get width => _isExpanded ? 360 : 80;
@override
void initState() {
super.initState();
PackageInfo.fromPlatform().then((packageInfo) =>
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _packageInfo = packageInfo)));
}
@override @override
Widget build(BuildContext context) => AnimatedContainer( Widget build(BuildContext context) => AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@ -78,7 +88,8 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
_navigationButton(NavigationItem.about), _navigationButton(NavigationItem.about),
const Spacer(), const Spacer(),
_animatedText( _animatedText(
text: 'Version 1.0.0', text: 'Version ${_packageInfo?.version ?? ''}',
padding: EdgeInsets.zero,
expandedKey: const ValueKey('expandedVersion'), expandedKey: const ValueKey('expandedVersion'),
collapsedKey: const ValueKey('collapsedVersion'), collapsedKey: const ValueKey('collapsedVersion'),
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
@ -116,7 +127,7 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
child: leading, child: leading,
), ),
), ),
Expanded( Flexible(
child: Text( child: Text(
text, text,
key: expandedKey, key: expandedKey,
@ -134,14 +145,15 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
); );
Widget _navigationButton(NavigationItem navigationItem) { Widget _navigationButton(NavigationItem navigationItem) {
final isSelected = _selectedNavigationItem == navigationItem; final selectedNavigationItem = context.watch<NavigationBloc>().state;
final isSelected = selectedNavigationItem == navigationItem;
final color = isSelected ? Colors.blue.shade900 : Colors.blueGrey.shade600; final color = isSelected ? Colors.blue.shade900 : Colors.blueGrey.shade600;
return Material( return Material(
color: isSelected ? Colors.blueGrey.shade100 : Colors.transparent, color: isSelected ? Colors.blueGrey.shade100 : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: () => setState(() => _selectedNavigationItem = navigationItem), onTap: () => context.read<NavigationBloc>().add(NavigationChangedEvent(navigationItem)),
child: Row( child: Row(
children: [ children: [
Padding( Padding(
@ -161,14 +173,30 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
} }
} }
enum NavigationItem { extension _NavigationItemContent on NavigationItem {
basicConfiguration(iconData: Icons.dashboard_rounded, title: Strings.tabBasicConfiguration), String get title {
modConfiguration(iconData: Icons.extension, title: Strings.tabModConfiguration), switch (this) {
serverProperties(iconData: Icons.settings, title: Strings.tabServerPropertiesConfiguration), case NavigationItem.basicConfiguration:
about(iconData: Icons.info, title: Strings.tabAbout); return Strings.tabBasicConfiguration;
case NavigationItem.modConfiguration:
return Strings.tabModConfiguration;
case NavigationItem.serverProperties:
return Strings.tabServerProperties;
case NavigationItem.about:
return Strings.tabAbout;
}
}
final IconData iconData; IconData get iconData {
final String title; switch (this) {
case NavigationItem.basicConfiguration:
const NavigationItem({required this.iconData, required this.title}); return Icons.dashboard_rounded;
case NavigationItem.modConfiguration:
return Icons.extension;
case NavigationItem.serverProperties:
return Icons.settings;
case NavigationItem.about:
return Icons.info;
}
}
} }

View File

@ -10,7 +10,7 @@ abstract class Strings {
static const buttonBrowse = '瀏覽'; static const buttonBrowse = '瀏覽';
static const tabBasicConfiguration = '基本設定'; static const tabBasicConfiguration = '基本設定';
static const tabModConfiguration = '模組設定'; static const tabModConfiguration = '模組設定';
static const tabServerPropertiesConfiguration = '伺服器選項'; static const tabServerProperties = '伺服器選項';
static const tabAbout = '關於與說明'; static const tabAbout = '關於與說明';
static const tabInstallationProgress = '安裝進度'; static const tabInstallationProgress = '安裝進度';
static const tooltipEulaInfo = '點擊查看 EULA 條款'; static const tooltipEulaInfo = '點擊查看 EULA 條款';

View File

@ -232,6 +232,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" 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: path:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ">=3.6.0 <4.0.0" sdk: ">=3.6.0 <4.0.0"
@ -39,6 +39,7 @@ dependencies:
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
gap: ^3.0.1 gap: ^3.0.1
http: ^1.4.0 http: ^1.4.0
package_info_plus: ^8.3.0
path: ^1.9.1 path: ^1.9.1
url_launcher: ^6.3.1 url_launcher: ^6.3.1
window_manager: ^0.5.0 window_manager: ^0.5.0