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