diff --git a/assets/svg/bug.svg b/assets/svg/bug.svg new file mode 100644 index 0000000..cc7a7a7 --- /dev/null +++ b/assets/svg/bug.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/assets/svg/github.svg b/assets/svg/github.svg new file mode 100644 index 0000000..d4b3e03 --- /dev/null +++ b/assets/svg/github.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/assets/svg/send.svg b/assets/svg/send.svg new file mode 100644 index 0000000..b62c8a6 --- /dev/null +++ b/assets/svg/send.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/assets/svg/youtube.svg b/assets/svg/youtube.svg new file mode 100644 index 0000000..6539cc5 --- /dev/null +++ b/assets/svg/youtube.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/lib/main/constants.dart b/lib/main/constants.dart index f7345ff..96c0965 100644 --- a/lib/main/constants.dart +++ b/lib/main/constants.dart @@ -8,6 +8,10 @@ abstract class Constants { static const eulaFileContent = '#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\neula=true'; static const eulaUrl = 'https://account.mojang.com/documents/minecraft_eula'; + static const tutorialVideoUrl = 'https://www.youtube.com/watch?v=yNis5vcueQY'; + static const sourceCodeUrl = 'https://git.squidspirit.com/squid/minecraft-server-installer'; + static const bugReportUrl = 'https://github.com/an920107/minecraft-server-installer/issues/new'; + static const authorEmail = 'squid@squidspirit.com'; static final startScriptFileName = Platform.isWindows ? 'start.bat' : 'start.sh'; } diff --git a/lib/main/framework/ui/about_tab.dart b/lib/main/framework/ui/about_tab.dart new file mode 100644 index 0000000..e40f6d5 --- /dev/null +++ b/lib/main/framework/ui/about_tab.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gap/gap.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'; +import 'package:url_launcher/url_launcher.dart'; + +class AboutTab extends StatelessWidget { + const AboutTab({super.key}); + + @override + Widget build(BuildContext context) => SizedBox( + width: 460, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Image.asset('assets/img/mcsi_logo.png', width: 100, height: 100), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Constants.appName, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(fontWeight: FontWeight.w900, color: Colors.blueGrey.shade900), + ), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) => Text( + 'Version ${snapshot.data?.version ?? ''}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade700), + ), + ), + ], + ), + ], + ), + const Gap(32), + Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border(left: BorderSide(color: Colors.blueGrey.shade300, width: 6)), + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Icon(Icons.format_quote_rounded, color: Colors.grey.shade700), + ), + const Gap(8), + Text( + Strings.textSlogen, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.w500, color: Colors.grey.shade700), + ), + ], + ), + ), + const Gap(32), + Row( + children: [ + _actionButton( + onPressed: () => launchUrl(Uri.parse(Constants.tutorialVideoUrl)), + text: Strings.buttonTutorialVideo, + svgAssetName: 'assets/svg/youtube.svg', + ), + const Gap(12), + _actionButton( + onPressed: () => launchUrl(Uri.parse(Constants.bugReportUrl)), + text: Strings.buttonBugReport, + svgAssetName: 'assets/svg/bug.svg', + ), + const Gap(12), + _actionButton( + onPressed: () => launchUrl(Uri.parse('mailto:${Constants.authorEmail}')), + text: Strings.buttonContactAuthor, + svgAssetName: 'assets/svg/send.svg', + ), + const Gap(12), + _actionButton( + onPressed: () => launchUrl(Uri.parse(Constants.sourceCodeUrl)), + text: Strings.buttonSourceCode, + svgAssetName: 'assets/svg/github.svg', + ), + ], + ), + const Spacer(), + Text( + Strings.textCopyright, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade700), + ), + ], + ), + ); + + Widget _actionButton({ + required String text, + required String svgAssetName, + required void Function()? onPressed, + }) => + Builder( + builder: (context) => Expanded( + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blueGrey.shade50, width: 2), + ), + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16), + child: Column( + children: [ + SvgPicture.asset( + svgAssetName, + width: 32, + height: 32, + colorFilter: ColorFilter.mode(Colors.grey.shade800, BlendMode.srcIn), + ), + const Gap(12), + Text( + text, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w500, color: Colors.grey.shade700), + ), + ], + ), + ), + ), + ), + ), + ), + ); +} diff --git a/lib/main/framework/ui/basic_configuration_tab.dart b/lib/main/framework/ui/basic_configuration_tab.dart index b7b267e..716dabd 100644 --- a/lib/main/framework/ui/basic_configuration_tab.dart +++ b/lib/main/framework/ui/basic_configuration_tab.dart @@ -39,7 +39,7 @@ class BasicConfigurationTab extends StatelessWidget { readOnly: true, canRequestFocus: false, decoration: InputDecoration( - border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), label: const Text('${Strings.fieldPath} *'), ), ), @@ -50,9 +50,12 @@ class BasicConfigurationTab extends StatelessWidget { child: OutlinedButton( onPressed: () => _browseDirectory(context, initialPath: state.savePath), style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text(Strings.buttonBrowse), ), - child: const Text(Strings.buttonBrowse), ), ), ], @@ -72,7 +75,7 @@ class BasicConfigurationTab extends StatelessWidget { .add(InstallationConfigurationUpdatedEvent(isEulaAgreed: value ?? false)), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), )), ), IconButton( @@ -92,7 +95,7 @@ class BasicConfigurationTab extends StatelessWidget { context.read().add(InstallationConfigurationUpdatedEvent(isGuiEnabled: value)), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); @@ -106,7 +109,7 @@ class BasicConfigurationTab extends StatelessWidget { .add(InstallationConfigurationUpdatedEvent(isCustomRamSizeEnabled: value ?? false)), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); @@ -134,6 +137,7 @@ class BasicConfigurationTab extends StatelessWidget { ), ), ), + const Gap(16), Row( children: [ Expanded( @@ -143,7 +147,7 @@ class BasicConfigurationTab extends StatelessWidget { readOnly: true, decoration: InputDecoration( label: const Text('${Strings.fieldMinRamSize} (MB)'), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), ), ), @@ -155,7 +159,7 @@ class BasicConfigurationTab extends StatelessWidget { readOnly: true, decoration: InputDecoration( label: const Text('${Strings.fieldMaxRamSize} (MB)'), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), ), ), @@ -175,7 +179,7 @@ class BasicConfigurationTab extends StatelessWidget { Expanded(child: LinearProgressIndicator(value: state.downloadProgress.value)), const Gap(32), ElevatedButton.icon( - style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))), + style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), onPressed: context.watch().state.canStartToInstall ? () => _downloadServerFile(context) : null, icon: const Icon(Icons.download), diff --git a/lib/main/framework/ui/minecraft_server_installer.dart b/lib/main/framework/ui/minecraft_server_installer.dart index 944e569..227da66 100644 --- a/lib/main/framework/ui/minecraft_server_installer.dart +++ b/lib/main/framework/ui/minecraft_server_installer.dart @@ -9,6 +9,7 @@ import 'package:minecraft_server_installer/main/application/use_case/write_file_ 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/about_tab.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'; @@ -82,14 +83,38 @@ 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), - ), + builder: (context, state) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Column( + key: ValueKey('tab${state.toString()}'), + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey.shade300, width: 1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.title, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(fontWeight: FontWeight.w700, color: Colors.blueGrey.shade900), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(32), + child: _tabContent(state), + ), + ), + ], ), ), ); @@ -100,8 +125,9 @@ class MinecraftServerInstaller extends StatelessWidget { return const BasicConfigurationTab(); case NavigationItem.modConfiguration: case NavigationItem.serverProperties: - case NavigationItem.about: return const Placeholder(); + case NavigationItem.about: + return const AboutTab(); } } } diff --git a/lib/main/framework/ui/side_navigation_bar.dart b/lib/main/framework/ui/side_navigation_bar.dart index b75cefb..43cf183 100644 --- a/lib/main/framework/ui/side_navigation_bar.dart +++ b/lib/main/framework/ui/side_navigation_bar.dart @@ -15,16 +15,8 @@ class SideNavigationBar extends StatefulWidget { class _SideNavigationBarState extends State { bool _isExpanded = false; - PackageInfo? _packageInfo; - double get width => _isExpanded ? 360 : 80; - - @override - void initState() { - super.initState(); - PackageInfo.fromPlatform().then((packageInfo) => - WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _packageInfo = packageInfo))); - } + double get width => _isExpanded ? 340 : 80; @override Widget build(BuildContext context) => AnimatedContainer( @@ -48,9 +40,9 @@ class _SideNavigationBarState extends State { children: [ _animatedText( text: Constants.appName, - leading: SizedBox.square( - dimension: 36, - child: Image.asset('assets/img/mcsi_logo.png', width: 2048, height: 2048), + leading: Padding( + padding: const EdgeInsets.only(right: 4), + child: Image.asset('assets/img/mcsi_logo.png', width: 32, height: 32), ), padding: const EdgeInsets.only(left: 4), expandedKey: const ValueKey('expandedTitle'), @@ -87,12 +79,15 @@ class _SideNavigationBarState extends State { 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, + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) => _animatedText( + text: 'Version ${snapshot.data?.version ?? ''}', + padding: EdgeInsets.zero, + expandedKey: const ValueKey('expandedVersion'), + collapsedKey: const ValueKey('collapsedVersion'), + alignment: Alignment.bottomCenter, + ), ), ], ), @@ -173,7 +168,7 @@ class _SideNavigationBarState extends State { } } -extension _NavigationItemContent on NavigationItem { +extension NavigationItemContent on NavigationItem { String get title { switch (this) { case NavigationItem.basicConfiguration: diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart index 68da21d..f045224 100644 --- a/lib/main/framework/ui/strings.dart +++ b/lib/main/framework/ui/strings.dart @@ -8,6 +8,10 @@ abstract class Strings { static const fieldMaxRamSize = '最大 RAM 大小'; static const buttonStartToInstall = '開始安裝'; static const buttonBrowse = '瀏覽'; + static const buttonTutorialVideo = '教學影片'; + static const buttonBugReport = '問題回報'; + static const buttonContactAuthor = '聯絡作者'; + static const buttonSourceCode = '原始碼'; static const tabBasicConfiguration = '基本設定'; static const tabModConfiguration = '模組設定'; static const tabServerProperties = '伺服器選項'; @@ -15,4 +19,6 @@ abstract class Strings { static const tabInstallationProgress = '安裝進度'; static const tooltipEulaInfo = '點擊查看 EULA 條款'; static const dialogTitleSelectDirectory = '選擇安裝目錄'; + static const textSlogen = '讓 Minecraft 伺服器安裝變得更簡單!'; + static const textCopyright = 'Copyright © 2025 SquidSpirit'; } diff --git a/lib/main/main.dart b/lib/main/main.dart index 289987e..680850c 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(800, 600), - minimumSize: Size(800, 600), + size: Size(900, 600), + minimumSize: Size(900, 600), center: true, backgroundColor: Colors.transparent, skipTaskbar: false, diff --git a/lib/vanilla/framework/ui/game_version_dropdown.dart b/lib/vanilla/framework/ui/game_version_dropdown.dart index 7590b8c..ef1785e 100644 --- a/lib/vanilla/framework/ui/game_version_dropdown.dart +++ b/lib/vanilla/framework/ui/game_version_dropdown.dart @@ -17,6 +17,9 @@ class GameVersionDropdown extends StatelessWidget { enabled: state.gameVersions.isNotEmpty, requestFocusOnTap: false, expandedInsets: EdgeInsets.zero, + inputDecorationTheme: Theme.of(context) + .inputDecorationTheme + .copyWith(border: OutlineInputBorder(borderRadius: BorderRadius.circular(8))), label: const Text('${Strings.fieldGameVersion} *'), onSelected: (value) { if (value != null) { diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 32ffeca..ee0e496 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, 800, 600); + gtk_window_set_default_size(window, 900, 600); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/pubspec.lock b/pubspec.lock index 0ea9c0e..6715466 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -126,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -256,6 +272,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" plugin_platform_interface: dependency: transitive description: @@ -437,6 +469,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" + url: "https://pub.dev" + source: hosted + version: "1.1.17" vector_math: dependency: transitive description: @@ -477,6 +533,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 396905c..b200d46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: equatable: ^2.0.7 file_picker: ^10.2.0 flutter_bloc: ^9.1.1 + flutter_svg: ^2.2.0 gap: ^3.0.1 http: ^1.4.0 package_info_plus: ^8.3.0 @@ -71,6 +72,7 @@ flutter: # - images/a_dot_ham.jpeg assets: - assets/img/ + - assets/svg/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images