diff --git a/.gitignore b/.gitignore
index cd4d4b3..64b652e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,52 +4,19 @@
*.log
*.pyc
*.swp
-.DS_Store
-.atom/
.buildlog/
.history
-.svn/
-
-# As packages are no longer pinned, we use a lockfile for testing locally.
-# When unpinning packages, Using lockfiles ensures that failures in PRs are
-# actually due to those PRs, not due to a package being updated.
-!/pubspec.lock
-
-# IntelliJ related
-*.iml
-*.ipr
-*.iws
-.idea/
-
-# Visual Studio Code related
-.classpath
-.project
-.settings/
-.vscode/*
-.ccls-cache
-
-# This file, on the master branch, should never exist or be checked-in.
-#
-# On a *final* release branch, that is, what will ship to stable or beta, the
-# file can be force added (git add --force) and checked-in in order to effectively
-# "pin" the engine artifact version so the flutter tool does not need to use git
-# to determine the engine artifacts.
-#
-# See https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md.
-/bin/internal/engine.version
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
-/bin/internal/engine.realm
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
-/dev/docs/api_docs.zip
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
@@ -65,11 +32,11 @@ analysis_benchmark.json
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
+.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
-.pub-cache/
.pub/
build/
flutter_*.png
@@ -83,11 +50,10 @@ unlinked_spec.ds
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
+**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
-local.properties
-**/.cxx/
# iOS/XCode related
**/ios/**/*.mode1v3
@@ -127,13 +93,11 @@ local.properties
**/xcuserdata/
# Windows
-**/windows/flutter/ephemeral/
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
-**/linux/flutter/ephemeral/
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
@@ -151,15 +115,6 @@ app.*.symbols
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
-!.vscode/settings.json
-
-# Monorepo
-.cipd
-.gclient
-.gclient_entries
-.python-version
-.gclient_previous_custom_vars
-.gclient_previous_sync_commits
# FVM Version Cache
.fvm/
\ No newline at end of file
diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml
new file mode 100644
index 0000000..6a7e7a7
--- /dev/null
+++ b/.idea/libraries/Dart_SDK.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml
new file mode 100644
index 0000000..2b96ac4
--- /dev/null
+++ b/.idea/libraries/KotlinJavaRuntime.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..b7f01cc
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml
new file mode 100644
index 0000000..aab7b5c
--- /dev/null
+++ b/.idea/runConfigurations/main_dart.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..5b3388c
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..0429863
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "minecraft-server-installer",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main/main.dart"
+ },
+ {
+ "name": "minecraft-server-installer (profile mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile",
+ "program": "lib/main/main.dart"
+ },
+ {
+ "name": "minecraft-server-installer (release mode)",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release",
+ "program": "lib/main/main.dart"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..146b1f5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "dart.flutterSdkPath": ".fvm/versions/3.29.3",
+ "dart.lineLength": 120
+}
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d29021..a610cc8 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -22,7 +22,8 @@ linter:
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
- # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+ prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+ prefer_const_constructors: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
diff --git a/lib/main.dart b/lib/main.dart
deleted file mode 100644
index 7b7f5b6..0000000
--- a/lib/main.dart
+++ /dev/null
@@ -1,122 +0,0 @@
-import 'package:flutter/material.dart';
-
-void main() {
- runApp(const MyApp());
-}
-
-class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- // This widget is the root of your application.
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- // This is the theme of your application.
- //
- // TRY THIS: Try running your application with "flutter run". You'll see
- // the application has a purple toolbar. Then, without quitting the app,
- // try changing the seedColor in the colorScheme below to Colors.green
- // and then invoke "hot reload" (save your changes or press the "hot
- // reload" button in a Flutter-supported IDE, or press "r" if you used
- // the command line to start the app).
- //
- // Notice that the counter didn't reset back to zero; the application
- // state is not lost during the reload. To reset the state, use hot
- // restart instead.
- //
- // This works for code too, not just values: Most code changes can be
- // tested with just a hot reload.
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
- ),
- home: const MyHomePage(title: 'Flutter Demo Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({super.key, required this.title});
-
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
- final String title;
-
- @override
- State createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State {
- int _counter = 0;
-
- void _incrementCounter() {
- setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
- _counter++;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
- return Scaffold(
- appBar: AppBar(
- // TRY THIS: Try changing the color here to a specific color (to
- // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
- // change color while the other colors stay the same.
- backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
- title: Text(widget.title),
- ),
- body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
- child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- //
- // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
- // action in the IDE, or press "p" in the console), to see the
- // wireframe for each widget.
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text('You have pushed the button this many times:'),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
- ), // This trailing comma makes auto-formatting nicer for build methods.
- );
- }
-}
diff --git a/lib/main/constants.dart b/lib/main/constants.dart
new file mode 100644
index 0000000..1226125
--- /dev/null
+++ b/lib/main/constants.dart
@@ -0,0 +1,4 @@
+abstract class Constants {
+ static const gameVersionListUrl = 'https://www.dropbox.com/s/mtz3moc9dpjtz7s/GameVersions.txt?dl=1';
+ static const serverFileName = 'server.jar';
+}
diff --git a/lib/main/framework/ui/basic_configuration_tab.dart b/lib/main/framework/ui/basic_configuration_tab.dart
new file mode 100644
index 0000000..8ce10fd
--- /dev/null
+++ b/lib/main/framework/ui/basic_configuration_tab.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:gap/gap.dart';
+import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
+import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_bloc.dart';
+import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
+import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_state.dart';
+import 'package:minecraft_server_installer/vanilla/framework/ui/game_version_dropdown.dart';
+
+class BasicConfigurationTab extends StatefulWidget {
+ const BasicConfigurationTab({super.key});
+
+ @override
+ State createState() => _BasicConfigurationTabState();
+}
+
+class _BasicConfigurationTabState extends State {
+ GameVersionViewModel? selectedGameVersion;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ const GameVersionDropdown(),
+ const Spacer(),
+ BlocConsumer(
+ listener: (_, __) {},
+ builder:
+ (context, state) => Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ if (state.isDownloading) Expanded(child: LinearProgressIndicator(value: state.downloadProgress)),
+ const Gap(32),
+ ElevatedButton.icon(
+ style: ElevatedButton.styleFrom(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
+ ),
+ onPressed: state.isGameVersionSelected ? _downloadServerFile : null,
+ icon: const Icon(Icons.download),
+ label: const Padding(
+ padding: EdgeInsets.symmetric(vertical: 12),
+ child: Text(Strings.buttonStartToInstall),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _downloadServerFile() {
+ context.read().add(VanillaServerFileDownloadedEvent());
+ }
+}
diff --git a/lib/main/framework/ui/minecraft_server_installer.dart b/lib/main/framework/ui/minecraft_server_installer.dart
new file mode 100644
index 0000000..190ba96
--- /dev/null
+++ b/lib/main/framework/ui/minecraft_server_installer.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.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/adapter/presentation/vanilla_state.dart';
+import 'package:minecraft_server_installer/vanilla/application/use_case/download_server_file_use_case.dart';
+import 'package:minecraft_server_installer/vanilla/application/use_case/get_game_version_list_use_case.dart';
+import 'package:minecraft_server_installer/vanilla/framework/api/vanilla_api_service_impl.dart';
+import 'package:minecraft_server_installer/vanilla/framework/storage/vanilla_file_storage_impl.dart';
+
+class MinecraftServerInstaller extends StatelessWidget {
+ const MinecraftServerInstaller({super.key});
+
+ Widget get _body =>
+ const Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab());
+
+ @override
+ Widget build(BuildContext context) {
+ final gameVersionApiService = VanillaApiServiceImpl();
+ final gameVersionFileStorage = VanillaFileStorageImpl();
+ final gameVersionRepository = VanillaRepositoryImpl(gameVersionApiService, gameVersionFileStorage);
+ final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository);
+ final downloadServerFileUseCase = DownloadServerFileUseCase(gameVersionRepository);
+
+ return MaterialApp(
+ title: 'Minecraft Server Installer',
+ theme: ThemeData.light().copyWith(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade900)),
+ home: MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create:
+ (context) =>
+ VanillaBloc(getGameVersionListUseCase, downloadServerFileUseCase)
+ ..add(VanillaGameVersionListLoadedEvent()),
+ ),
+ ],
+ child: Scaffold(
+ body: BlocConsumer(
+ listener: (_, __) {},
+ builder: (_, state) {
+ if (state.isLocked) {
+ return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body));
+ }
+
+ return _body;
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart
new file mode 100644
index 0000000..25acf60
--- /dev/null
+++ b/lib/main/framework/ui/strings.dart
@@ -0,0 +1,4 @@
+abstract class Strings {
+ static const fieldGameVersion = '遊戲版本';
+ static const buttonStartToInstall = '開始安裝';
+}
diff --git a/lib/main/main.dart b/lib/main/main.dart
new file mode 100644
index 0000000..b3aa558
--- /dev/null
+++ b/lib/main/main.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:minecraft_server_installer/main/framework/ui/minecraft_server_installer.dart';
+import 'package:window_manager/window_manager.dart';
+
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ await windowManager.ensureInitialized();
+
+ final windowOptions = const WindowOptions(
+ size: Size(400, 600),
+ minimumSize: Size(400, 600),
+ center: true,
+ backgroundColor: Colors.transparent,
+ skipTaskbar: false,
+ titleBarStyle: TitleBarStyle.hidden,
+ );
+ windowManager.waitUntilReadyToShow(windowOptions, () async {
+ await windowManager.show();
+ await windowManager.focus();
+ });
+
+ runApp(const MinecraftServerInstaller());
+}
diff --git a/lib/vanilla/adapter/gateway/vanilla_api_service.dart b/lib/vanilla/adapter/gateway/vanilla_api_service.dart
new file mode 100644
index 0000000..2c50673
--- /dev/null
+++ b/lib/vanilla/adapter/gateway/vanilla_api_service.dart
@@ -0,0 +1,10 @@
+import 'dart:typed_data';
+
+import 'package:minecraft_server_installer/vanilla/application/use_case/download_server_file_use_case.dart';
+import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
+
+abstract interface class VanillaApiService {
+ Future> fetchGameVersionList();
+
+ Future fetchServerFile(Uri url, {DownloadProgressCallback? onProgressChanged});
+}
diff --git a/lib/vanilla/adapter/gateway/vanilla_file_storage.dart b/lib/vanilla/adapter/gateway/vanilla_file_storage.dart
new file mode 100644
index 0000000..3217984
--- /dev/null
+++ b/lib/vanilla/adapter/gateway/vanilla_file_storage.dart
@@ -0,0 +1,5 @@
+import 'dart:typed_data';
+
+abstract interface class VanillaFileStorage {
+ Future saveFile(Uint8List fileBytes, String savePath);
+}
diff --git a/lib/vanilla/adapter/gateway/vanilla_repository_impl.dart b/lib/vanilla/adapter/gateway/vanilla_repository_impl.dart
new file mode 100644
index 0000000..3bd2dca
--- /dev/null
+++ b/lib/vanilla/adapter/gateway/vanilla_repository_impl.dart
@@ -0,0 +1,25 @@
+import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_api_service.dart';
+import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_file_storage.dart';
+import 'package:minecraft_server_installer/vanilla/application/repository/vanilla_repository.dart';
+import 'package:minecraft_server_installer/vanilla/application/use_case/download_server_file_use_case.dart';
+import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
+
+class VanillaRepositoryImpl implements VanillaRepository {
+ final VanillaApiService _gameVersionApiService;
+ final VanillaFileStorage _gameVersionFileStorage;
+
+ VanillaRepositoryImpl(this._gameVersionApiService, this._gameVersionFileStorage);
+
+ @override
+ Future> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
+
+ @override
+ Future downloadServerFile(
+ GameVersion version,
+ String savePath, {
+ DownloadProgressCallback? onProgressChanged,
+ }) async {
+ final fileBytes = await _gameVersionApiService.fetchServerFile(version.url, onProgressChanged: onProgressChanged);
+ await _gameVersionFileStorage.saveFile(fileBytes, savePath);
+ }
+}
diff --git a/lib/vanilla/adapter/presentation/game_version_view_model.dart b/lib/vanilla/adapter/presentation/game_version_view_model.dart
new file mode 100644
index 0000000..cb50d77
--- /dev/null
+++ b/lib/vanilla/adapter/presentation/game_version_view_model.dart
@@ -0,0 +1,18 @@
+import 'package:equatable/equatable.dart';
+import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
+
+class GameVersionViewModel with EquatableMixin {
+ final String name;
+ final Uri url;
+
+ const GameVersionViewModel({required this.name, required this.url});
+
+ GameVersionViewModel.from(GameVersion gameVersion) : name = gameVersion.name, url = gameVersion.url;
+
+ GameVersion toEntity() {
+ return GameVersion(name: name, url: url);
+ }
+
+ @override
+ List