diff --git a/assets/img/mcsi_logo.png b/assets/img/mcsi_logo.png new file mode 100644 index 0000000..d9fbc3f Binary files /dev/null and b/assets/img/mcsi_logo.png differ diff --git a/lib/main/constants.dart b/lib/main/constants.dart index 5c8610b..f7345ff 100644 --- a/lib/main/constants.dart +++ b/lib/main/constants.dart @@ -1,6 +1,7 @@ import 'dart:io'; abstract class Constants { + static const appName = 'Minecraft Server Installer'; static const gameVersionListUrl = 'https://www.dropbox.com/s/mtz3moc9dpjtz7s/GameVersions.txt?dl=1'; static const serverFileName = 'server.jar'; static const eulaFileName = 'eula.txt'; diff --git a/lib/main/framework/ui/minecraft_server_installer.dart b/lib/main/framework/ui/minecraft_server_installer.dart index 66ed43a..66eb691 100644 --- a/lib/main/framework/ui/minecraft_server_installer.dart +++ b/lib/main/framework/ui/minecraft_server_installer.dart @@ -5,9 +5,11 @@ import 'package:minecraft_server_installer/main/adapter/presentation/installatio 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'; +import 'package:minecraft_server_installer/main/constants.dart'; import 'package:minecraft_server_installer/main/framework/api/installation_api_service_impl.dart'; import 'package:minecraft_server_installer/main/framework/storage/installation_file_storage_impl.dart'; import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.dart'; +import 'package:minecraft_server_installer/main/framework/ui/side_navigation_bar.dart'; import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_repository_impl.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_bloc.dart'; import 'package:minecraft_server_installer/vanilla/application/use_case/get_game_version_list_use_case.dart'; @@ -16,8 +18,7 @@ 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.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab()); + Widget get _body => const Padding(padding: EdgeInsets.all(32), child: BasicConfigurationTab()); @override Widget build(BuildContext context) { @@ -34,7 +35,7 @@ class MinecraftServerInstaller extends StatelessWidget { final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository); return MaterialApp( - title: 'Minecraft Server Installer', + title: Constants.appName, theme: ThemeData( colorScheme: ColorScheme.fromSeed( brightness: Brightness.light, @@ -55,14 +56,21 @@ class MinecraftServerInstaller extends StatelessWidget { ), ], child: Scaffold( - body: Builder( - builder: (context) { - if (context.watch().state.isLocked) { - return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body)); - } + body: Row( + children: [ + const SideNavigationBar(), + Expanded( + child: Builder( + builder: (context) { + if (context.watch().state.isLocked) { + return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body)); + } - return _body; - }, + return _body; + }, + ), + ), + ], ), ), ), diff --git a/lib/main/framework/ui/side_navigation_bar.dart b/lib/main/framework/ui/side_navigation_bar.dart new file mode 100644 index 0000000..ccc6a63 --- /dev/null +++ b/lib/main/framework/ui/side_navigation_bar.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:minecraft_server_installer/main/constants.dart'; +import 'package:minecraft_server_installer/main/framework/ui/strings.dart'; + +class SideNavigationBar extends StatefulWidget { + const SideNavigationBar({super.key}); + + @override + State createState() => _SideNavigationBarState(); +} + +class _SideNavigationBarState extends State { + bool _isExpanded = false; + NavigationItem _selectedNavigationItem = NavigationItem.basicConfiguration; + + double get width => _isExpanded ? 360 : 80; + + @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: SizedBox.square( + dimension: 36, + 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 1.0.0', + 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, + ), + ), + Expanded( + child: Text( + text, + key: expandedKey, + style: style, + softWrap: false, + overflow: TextOverflow.visible, + ), + ), + ], + ) + : SizedBox.shrink(key: collapsedKey), + ), + ), + ), + ); + + Widget _navigationButton(NavigationItem navigationItem) { + 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), + 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), + ), + ], + ), + ), + ); + } +} + +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); + + final IconData iconData; + final String title; + + const NavigationItem({required this.iconData, required this.title}); +} diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart index 85205b0..4ca30b4 100644 --- a/lib/main/framework/ui/strings.dart +++ b/lib/main/framework/ui/strings.dart @@ -8,6 +8,11 @@ abstract class Strings { static const fieldMaxRamSize = '最大 RAM 大小'; static const buttonStartToInstall = '開始安裝'; static const buttonBrowse = '瀏覽'; + static const tabBasicConfiguration = '基本設定'; + static const tabModConfiguration = '模組設定'; + static const tabServerPropertiesConfiguration = '伺服器選項'; + static const tabAbout = '關於與說明'; + static const tabInstallationProgress = '安裝進度'; static const tooltipEulaInfo = '點擊查看 EULA 條款'; static const dialogTitleSelectDirectory = '選擇安裝目錄'; } diff --git a/lib/main/main.dart b/lib/main/main.dart index b3aa558..289987e 100644 --- a/lib/main/main.dart +++ b/lib/main/main.dart @@ -7,8 +7,8 @@ Future main() async { await windowManager.ensureInitialized(); final windowOptions = const WindowOptions( - size: Size(400, 600), - minimumSize: Size(400, 600), + size: Size(800, 600), + minimumSize: Size(800, 600), center: true, backgroundColor: Colors.transparent, skipTaskbar: false, diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index b1cd8f2..32ffeca 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -22,7 +22,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_decorated(window, FALSE); - gtk_window_set_default_size(window, 400, 600); + gtk_window_set_default_size(window, 800, 600); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/pubspec.yaml b/pubspec.yaml index b9b2101..c9535bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,6 +68,8 @@ flutter: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg + assets: + - assets/img/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images