Compare commits

...

12 Commits

33 changed files with 754 additions and 231 deletions

49
.gitignore vendored
View File

@ -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/

19
.idea/libraries/Dart_SDK.xml generated Normal file
View File

@ -0,0 +1,19 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/async" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/collection" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/convert" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/core" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/developer" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/html" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/io" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/isolate" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/math" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/mirrors" />
<root url="file:///home/an920107/fvm/versions/3.29.3/bin/cache/dart-sdk/lib/typed_data" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

15
.idea/libraries/KotlinJavaRuntime.xml generated Normal file
View File

@ -0,0 +1,15 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime">
<CLASSES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect-sources.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-test-sources.jar!/" />
</SOURCES>
</library>
</component>

9
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/minecraft_server_installer.iml" filepath="$PROJECT_DIR$/minecraft_server_installer.iml" />
<module fileurl="file://$PROJECT_DIR$/android/minecraft_server_installer_android.iml" filepath="$PROJECT_DIR$/android/minecraft_server_installer_android.iml" />
</modules>
</component>
</project>

6
.idea/runConfigurations/main_dart.xml generated Normal file
View File

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method />
</configuration>
</component>

36
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="FileEditorManager">
<leaf>
<file leaf-file-name="main.dart" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/lib/main.dart">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="ToolWindowManager">
<editor active="true" />
<layout>
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
</layout>
</component>
<component name="ProjectView">
<navigator currentView="ProjectPane" proportions="" version="1">
</navigator>
<panes>
<pane id="ProjectPane">
<option name="show-excluded-files" value="false" />
</pane>
</panes>
</component>
<component name="PropertiesComponent">
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="dart.analysis.tool.window.force.activate" value="true" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
</project>

28
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
"dart.lineLength": 120
}

View File

@ -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

View File

@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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: <Widget>[
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.
);
}
}

4
lib/main/constants.dart Normal file
View File

@ -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';
}

View File

@ -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<BasicConfigurationTab> createState() => _BasicConfigurationTabState();
}
class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
GameVersionViewModel? selectedGameVersion;
@override
Widget build(BuildContext context) {
return Column(
children: [
const GameVersionDropdown(),
const Spacer(),
BlocConsumer<VanillaBloc, VanillaState>(
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<VanillaBloc>().add(VanillaServerFileDownloadedEvent());
}
}

View File

@ -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<VanillaBloc>(
create:
(context) =>
VanillaBloc(getGameVersionListUseCase, downloadServerFileUseCase)
..add(VanillaGameVersionListLoadedEvent()),
),
],
child: Scaffold(
body: BlocConsumer<VanillaBloc, VanillaState>(
listener: (_, __) {},
builder: (_, state) {
if (state.isLocked) {
return MouseRegion(cursor: SystemMouseCursors.forbidden, child: AbsorbPointer(child: _body));
}
return _body;
},
),
),
),
);
}
}

View File

@ -0,0 +1,4 @@
abstract class Strings {
static const fieldGameVersion = '遊戲版本';
static const buttonStartToInstall = '開始安裝';
}

23
lib/main/main.dart Normal file
View File

@ -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<void> 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());
}

View File

@ -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<List<GameVersion>> fetchGameVersionList();
Future<Uint8List> fetchServerFile(Uri url, {DownloadProgressCallback? onProgressChanged});
}

View File

@ -0,0 +1,5 @@
import 'dart:typed_data';
abstract interface class VanillaFileStorage {
Future<void> saveFile(Uint8List fileBytes, String savePath);
}

View File

@ -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<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
@override
Future<void> downloadServerFile(
GameVersion version,
String savePath, {
DownloadProgressCallback? onProgressChanged,
}) async {
final fileBytes = await _gameVersionApiService.fetchServerFile(version.url, onProgressChanged: onProgressChanged);
await _gameVersionFileStorage.saveFile(fileBytes, savePath);
}
}

View File

@ -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<Object?> get props => [name, url];
}

View File

@ -0,0 +1,74 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:minecraft_server_installer/main/constants.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/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:path/path.dart' as path;
class VanillaBloc extends Bloc<VanillaEvent, VanillaState> {
final GetGameVersionListUseCase _getGameVersionListUseCase;
final DownloadServerFileUseCase _downloadServerFileUseCase;
VanillaBloc(this._getGameVersionListUseCase, this._downloadServerFileUseCase) : super(const VanillaState.empty()) {
on<VanillaGameVersionListLoadedEvent>((_, emit) async {
try {
final gameVersions = await _getGameVersionListUseCase();
emit(
const VanillaState.empty().copyWith(
gameVersions: gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList(),
),
);
} on Exception {
emit(const VanillaState.empty());
}
});
on<VanillaGameVersionSelectedEvent>((event, emit) {
emit(state.copyWith(selectedGameVersion: event.gameVersion));
});
on<VanillaServerFileDownloadedEvent>((_, emit) async {
final gameVersion = state.selectedGameVersion;
if (gameVersion == null) {
return;
}
emit(state.copyWith(isLocked: true));
await _downloadServerFileUseCase(
gameVersion.toEntity(),
path.join('.', Constants.serverFileName),
onProgressChanged: (progress) => add(_VanillaDownloadProgressChangedEvent(progress)),
);
emit(state.copyWith(isLocked: false));
});
on<_VanillaDownloadProgressChangedEvent>((event, emit) {
if (event.progress < 0) {
emit(state.copyWith(downloadProgress: 0));
} else if (event.progress > 1) {
emit(state.copyWith(downloadProgress: 1));
} else {
emit(state.copyWith(downloadProgress: event.progress));
}
});
}
}
sealed class VanillaEvent {}
class VanillaGameVersionListLoadedEvent extends VanillaEvent {}
class VanillaGameVersionSelectedEvent extends VanillaEvent {
final GameVersionViewModel gameVersion;
VanillaGameVersionSelectedEvent(this.gameVersion);
}
class VanillaServerFileDownloadedEvent extends VanillaEvent {}
class _VanillaDownloadProgressChangedEvent extends VanillaEvent {
final double progress;
_VanillaDownloadProgressChangedEvent(this.progress);
}

View File

@ -0,0 +1,38 @@
import 'package:equatable/equatable.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
class VanillaState with EquatableMixin {
final bool isLocked;
final double downloadProgress;
final List<GameVersionViewModel> gameVersions;
final GameVersionViewModel? selectedGameVersion;
const VanillaState({
required this.isLocked,
required this.downloadProgress,
required this.gameVersions,
required this.selectedGameVersion,
});
const VanillaState.empty()
: this(isLocked: false, downloadProgress: 0, gameVersions: const [], selectedGameVersion: null);
@override
List<Object?> get props => [isLocked, downloadProgress, gameVersions, selectedGameVersion];
bool get isGameVersionSelected => selectedGameVersion != null;
bool get isDownloading => downloadProgress > 0 && downloadProgress < 1;
VanillaState copyWith({
bool? isLocked,
double? downloadProgress,
List<GameVersionViewModel>? gameVersions,
GameVersionViewModel? selectedGameVersion,
}) => VanillaState(
isLocked: isLocked ?? this.isLocked,
downloadProgress: downloadProgress ?? this.downloadProgress,
gameVersions: gameVersions ?? this.gameVersions,
selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion,
);
}

View File

@ -0,0 +1,8 @@
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 VanillaRepository {
Future<List<GameVersion>> getGameVersionList();
Future<void> downloadServerFile(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged});
}

View File

@ -0,0 +1,13 @@
import 'package:minecraft_server_installer/vanilla/application/repository/vanilla_repository.dart';
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
typedef DownloadProgressCallback = void Function(double progress);
class DownloadServerFileUseCase {
final VanillaRepository _gameVersionRepository;
DownloadServerFileUseCase(this._gameVersionRepository);
Future<void> call(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged}) =>
_gameVersionRepository.downloadServerFile(version, savePath, onProgressChanged: onProgressChanged);
}

View File

@ -0,0 +1,10 @@
import 'package:minecraft_server_installer/vanilla/application/repository/vanilla_repository.dart';
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
class GetGameVersionListUseCase {
final VanillaRepository _gameVersionRepository;
GetGameVersionListUseCase(this._gameVersionRepository);
Future<List<GameVersion>> call() => _gameVersionRepository.getGameVersionList();
}

View File

@ -0,0 +1,11 @@
import 'package:equatable/equatable.dart';
class GameVersion with EquatableMixin {
final String name;
final Uri url;
const GameVersion({required this.name, required this.url});
@override
List<Object?> get props => [name, url];
}

View File

@ -0,0 +1,57 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:minecraft_server_installer/main/constants.dart';
import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_api_service.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 VanillaApiServiceImpl implements VanillaApiService {
@override
Future<List<GameVersion>> fetchGameVersionList() async {
final sourceUrl = Uri.parse(Constants.gameVersionListUrl);
final response = await http.get(sourceUrl);
final rawGameVersionList = response.body.split('\n');
final gameVersionList =
rawGameVersionList.map((line) => line.split(' ')).where((parts) => parts.length == 2).map((parts) {
final [name, url] = parts;
return GameVersion(name: name, url: Uri.parse(url));
}).toList();
return gameVersionList;
}
@override
Future<Uint8List> fetchServerFile(Uri url, {DownloadProgressCallback? onProgressChanged}) async {
final client = http.Client();
final request = http.Request('GET', url);
final response = await client.send(request);
final contentLength = response.contentLength;
final completer = Completer<Uint8List>();
final bytes = <int>[];
var receivedBytes = 0;
response.stream.listen(
(chunk) {
bytes.addAll(chunk);
receivedBytes += chunk.length;
if (onProgressChanged != null && contentLength != null) {
onProgressChanged(receivedBytes / contentLength);
}
},
onDone: () {
if (onProgressChanged != null) {
onProgressChanged(1);
}
completer.complete(Uint8List.fromList(bytes));
},
onError: completer.completeError,
cancelOnError: true,
);
return completer.future;
}
}

View File

@ -0,0 +1,18 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_file_storage.dart';
class VanillaFileStorageImpl implements VanillaFileStorage {
@override
Future<void> saveFile(Uint8List fileBytes, String savePath) async {
final file = File(savePath);
if (!await file.parent.exists()) {
await file.parent.create(recursive: true);
}
await file.create();
await file.writeAsBytes(fileBytes, flush: true);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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';
class GameVersionDropdown extends StatelessWidget {
const GameVersionDropdown({super.key});
@override
Widget build(BuildContext context) => BlocConsumer<VanillaBloc, VanillaState>(
listener: (_, __) {},
builder:
(_, state) => DropdownMenu(
initialSelection: state.selectedGameVersion,
enabled: state.gameVersions.isNotEmpty,
requestFocusOnTap: false,
expandedInsets: EdgeInsets.zero,
label: const Text('${Strings.fieldGameVersion} *'),
onSelected: (value) {
if (value != null) {
context.read<VanillaBloc>().add(VanillaGameVersionSelectedEvent(value));
}
},
dropdownMenuEntries:
state.gameVersions
.map(
(gameVersion) =>
DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),
)
.toList(),
),
);
}

View File

@ -20,34 +20,9 @@ static void my_application_activate(GApplication* application) {
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "minecraft_server_installer");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "minecraft_server_installer");
}
gtk_window_set_decorated(window, FALSE);
gtk_window_set_default_size(window, 1280, 720);
gtk_window_set_default_size(window, 400, 600);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.12.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
boolean_selector:
dependency: transitive
description:
@ -49,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async:
dependency: transitive
description:
@ -62,6 +78,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38
url: "https://pub.dev"
source: hosted
version: "9.1.1"
flutter_lints:
dependency: "direct dev"
description:
@ -75,6 +99,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
gap:
dependency: "direct main"
description:
name: gap
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
url: "https://pub.dev"
source: hosted
version: "3.0.1"
http:
dependency: "direct main"
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@ -131,14 +187,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
sky_engine:
dependency: transitive
description: flutter
@ -192,6 +312,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
@ -208,6 +336,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "51d50168ab267d344b975b15390426b1243600d436770d3f13de67e55b05ec16"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
sdks:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@ -1,8 +1,8 @@
name: minecraft_server_installer
description: "A new Flutter project."
description: "A tool that makes installing a Minecraft Server easier."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
@ -34,6 +34,12 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
equatable: ^2.0.7
flutter_bloc: ^9.1.1
gap: ^3.0.1
http: ^1.4.0
path: ^1.9.1
window_manager: ^0.5.0
dev_dependencies:
flutter_test:
@ -51,7 +57,6 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.

View File

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:minecraft_server_installer/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}