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 get props => [name, url]; +} diff --git a/lib/vanilla/adapter/presentation/vanilla_bloc.dart b/lib/vanilla/adapter/presentation/vanilla_bloc.dart new file mode 100644 index 0000000..23cb010 --- /dev/null +++ b/lib/vanilla/adapter/presentation/vanilla_bloc.dart @@ -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 { + final GetGameVersionListUseCase _getGameVersionListUseCase; + final DownloadServerFileUseCase _downloadServerFileUseCase; + + VanillaBloc(this._getGameVersionListUseCase, this._downloadServerFileUseCase) : super(const VanillaState.empty()) { + on((_, 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((event, emit) { + emit(state.copyWith(selectedGameVersion: event.gameVersion)); + }); + + on((_, 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); +} diff --git a/lib/vanilla/adapter/presentation/vanilla_state.dart b/lib/vanilla/adapter/presentation/vanilla_state.dart new file mode 100644 index 0000000..6ae3e2d --- /dev/null +++ b/lib/vanilla/adapter/presentation/vanilla_state.dart @@ -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 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 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? gameVersions, + GameVersionViewModel? selectedGameVersion, + }) => VanillaState( + isLocked: isLocked ?? this.isLocked, + downloadProgress: downloadProgress ?? this.downloadProgress, + gameVersions: gameVersions ?? this.gameVersions, + selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion, + ); +} diff --git a/lib/vanilla/application/repository/vanilla_repository.dart b/lib/vanilla/application/repository/vanilla_repository.dart new file mode 100644 index 0000000..f9c0bcb --- /dev/null +++ b/lib/vanilla/application/repository/vanilla_repository.dart @@ -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> getGameVersionList(); + + Future downloadServerFile(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged}); +} diff --git a/lib/vanilla/application/use_case/download_server_file_use_case.dart b/lib/vanilla/application/use_case/download_server_file_use_case.dart new file mode 100644 index 0000000..431ef72 --- /dev/null +++ b/lib/vanilla/application/use_case/download_server_file_use_case.dart @@ -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 call(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged}) => + _gameVersionRepository.downloadServerFile(version, savePath, onProgressChanged: onProgressChanged); +} diff --git a/lib/vanilla/application/use_case/get_game_version_list_use_case.dart b/lib/vanilla/application/use_case/get_game_version_list_use_case.dart new file mode 100644 index 0000000..3f4fcb5 --- /dev/null +++ b/lib/vanilla/application/use_case/get_game_version_list_use_case.dart @@ -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> call() => _gameVersionRepository.getGameVersionList(); +} diff --git a/lib/vanilla/domain/entity/game_version.dart b/lib/vanilla/domain/entity/game_version.dart new file mode 100644 index 0000000..f1c6321 --- /dev/null +++ b/lib/vanilla/domain/entity/game_version.dart @@ -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 get props => [name, url]; +} diff --git a/lib/vanilla/framework/api/vanilla_api_service_impl.dart b/lib/vanilla/framework/api/vanilla_api_service_impl.dart new file mode 100644 index 0000000..d633312 --- /dev/null +++ b/lib/vanilla/framework/api/vanilla_api_service_impl.dart @@ -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> 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 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(); + final bytes = []; + 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; + } +} diff --git a/lib/vanilla/framework/storage/vanilla_file_storage_impl.dart b/lib/vanilla/framework/storage/vanilla_file_storage_impl.dart new file mode 100644 index 0000000..921aa92 --- /dev/null +++ b/lib/vanilla/framework/storage/vanilla_file_storage_impl.dart @@ -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 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); + } +} diff --git a/lib/vanilla/framework/ui/game_version_dropdown.dart b/lib/vanilla/framework/ui/game_version_dropdown.dart new file mode 100644 index 0000000..2d9b9b7 --- /dev/null +++ b/lib/vanilla/framework/ui/game_version_dropdown.dart @@ -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( + 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().add(VanillaGameVersionSelectedEvent(value)); + } + }, + dropdownMenuEntries: + state.gameVersions + .map( + (gameVersion) => + DropdownMenuEntry(value: gameVersion, label: gameVersion.name), + ) + .toList(), + ), + ); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index ac27961..b1cd8f2 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -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(); diff --git a/minecraft_server_installer.iml b/minecraft_server_installer.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/minecraft_server_installer.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/pubspec.lock b/pubspec.lock index d993b91..d89613a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index e2dd2e9..a19fc57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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. diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 063aa55..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -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); - }); -}