From bdad1ae8b6986c2aca7cb7a634dbd4c05bf8ad92 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sun, 29 Jun 2025 04:59:49 +0800 Subject: [PATCH] MCSI-1 feat: version selection --- analysis_options.yaml | 3 +- .../framework/ui/basic_configuration_tab.dart | 19 +++++ .../ui/minecraft_server_installer.dart | 11 ++- lib/main/framework/ui/strings.dart | 3 + .../gateway/game_version_api_service.dart | 5 ++ .../gateway/game_version_repository_impl.dart | 12 ++++ .../presentation/game_version_bloc.dart | 22 ++++++ .../presentation/game_version_view_model.dart | 14 ++++ .../repository/game_version_repository.dart | 5 ++ .../get_game_version_list_use_case.dart | 10 +++ lib/vanila/domain/entity/game_version.dart | 11 +++ .../api/game_version_api_service_impl.dart | 20 ++++++ .../framework/ui/game_version_dropdown.dart | 64 +++++++++++++++++ pubspec.lock | 72 +++++++++++++++++++ pubspec.yaml | 3 + 15 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 lib/main/framework/ui/basic_configuration_tab.dart create mode 100644 lib/main/framework/ui/strings.dart create mode 100644 lib/vanila/adapter/gateway/game_version_api_service.dart create mode 100644 lib/vanila/adapter/gateway/game_version_repository_impl.dart create mode 100644 lib/vanila/adapter/presentation/game_version_bloc.dart create mode 100644 lib/vanila/adapter/presentation/game_version_view_model.dart create mode 100644 lib/vanila/application/repository/game_version_repository.dart create mode 100644 lib/vanila/application/use_case/get_game_version_list_use_case.dart create mode 100644 lib/vanila/domain/entity/game_version.dart create mode 100644 lib/vanila/framework/api/game_version_api_service_impl.dart create mode 100644 lib/vanila/framework/ui/game_version_dropdown.dart 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/framework/ui/basic_configuration_tab.dart b/lib/main/framework/ui/basic_configuration_tab.dart new file mode 100644 index 0000000..eece9cf --- /dev/null +++ b/lib/main/framework/ui/basic_configuration_tab.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.dart'; +import 'package:minecraft_server_installer/vanila/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 const Column(children: [GameVersionDropdown(onChanged: print)]); + } +} diff --git a/lib/main/framework/ui/minecraft_server_installer.dart b/lib/main/framework/ui/minecraft_server_installer.dart index 9435b8d..1e6f96f 100644 --- a/lib/main/framework/ui/minecraft_server_installer.dart +++ b/lib/main/framework/ui/minecraft_server_installer.dart @@ -1,10 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.dart'; class MinecraftServerInstaller extends StatelessWidget { const MinecraftServerInstaller({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return MaterialApp( + title: 'Minecraft Server Installer', + theme: ThemeData(primarySwatch: Colors.blue), + home: const Scaffold( + body: Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab()), + ), + ); } -} \ No newline at end of file +} diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart new file mode 100644 index 0000000..c7085f4 --- /dev/null +++ b/lib/main/framework/ui/strings.dart @@ -0,0 +1,3 @@ +abstract class Strings { + static const fieldGameVersion = '遊戲版本'; +} diff --git a/lib/vanila/adapter/gateway/game_version_api_service.dart b/lib/vanila/adapter/gateway/game_version_api_service.dart new file mode 100644 index 0000000..4fbe643 --- /dev/null +++ b/lib/vanila/adapter/gateway/game_version_api_service.dart @@ -0,0 +1,5 @@ +import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; + +abstract interface class GameVersionApiService { + Future> fetchGameVersionList(); +} diff --git a/lib/vanila/adapter/gateway/game_version_repository_impl.dart b/lib/vanila/adapter/gateway/game_version_repository_impl.dart new file mode 100644 index 0000000..758105a --- /dev/null +++ b/lib/vanila/adapter/gateway/game_version_repository_impl.dart @@ -0,0 +1,12 @@ +import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_api_service.dart'; +import 'package:minecraft_server_installer/vanila/application/repository/game_version_repository.dart'; +import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; + +class GameVersionRepositoryImpl implements GameVersionRepository { + final GameVersionApiService _gameVersionApiService; + + GameVersionRepositoryImpl(this._gameVersionApiService); + + @override + Future> getGameVersionList() => _gameVersionApiService.fetchGameVersionList(); +} diff --git a/lib/vanila/adapter/presentation/game_version_bloc.dart b/lib/vanila/adapter/presentation/game_version_bloc.dart new file mode 100644 index 0000000..f89019e --- /dev/null +++ b/lib/vanila/adapter/presentation/game_version_bloc.dart @@ -0,0 +1,22 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.dart'; +import 'package:minecraft_server_installer/vanila/application/use_case/get_game_version_list_use_case.dart'; + +class GameVersionBloc extends Bloc> { + final GetGameVersionListUseCase _getGameVersionListUseCase; + + GameVersionBloc(this._getGameVersionListUseCase) : super(const []) { + on((_, emit) async { + try { + final gameVersions = await _getGameVersionListUseCase(); + emit(gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList()); + } on Exception { + emit(const []); + } + }); + } +} + +sealed class GameVersionEvent {} + +class GameVersionLoadedEvent extends GameVersionEvent {} diff --git a/lib/vanila/adapter/presentation/game_version_view_model.dart b/lib/vanila/adapter/presentation/game_version_view_model.dart new file mode 100644 index 0000000..5f9607b --- /dev/null +++ b/lib/vanila/adapter/presentation/game_version_view_model.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; +import 'package:minecraft_server_installer/vanila/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; + + @override + List get props => [name, url]; +} diff --git a/lib/vanila/application/repository/game_version_repository.dart b/lib/vanila/application/repository/game_version_repository.dart new file mode 100644 index 0000000..836f45f --- /dev/null +++ b/lib/vanila/application/repository/game_version_repository.dart @@ -0,0 +1,5 @@ +import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; + +abstract interface class GameVersionRepository { + Future> getGameVersionList(); +} diff --git a/lib/vanila/application/use_case/get_game_version_list_use_case.dart b/lib/vanila/application/use_case/get_game_version_list_use_case.dart new file mode 100644 index 0000000..355bdab --- /dev/null +++ b/lib/vanila/application/use_case/get_game_version_list_use_case.dart @@ -0,0 +1,10 @@ +import 'package:minecraft_server_installer/vanila/application/repository/game_version_repository.dart'; +import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; + +class GetGameVersionListUseCase { + final GameVersionRepository _gameVersionRepository; + + GetGameVersionListUseCase(this._gameVersionRepository); + + Future> call() => _gameVersionRepository.getGameVersionList(); +} diff --git a/lib/vanila/domain/entity/game_version.dart b/lib/vanila/domain/entity/game_version.dart new file mode 100644 index 0000000..f1c6321 --- /dev/null +++ b/lib/vanila/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/vanila/framework/api/game_version_api_service_impl.dart b/lib/vanila/framework/api/game_version_api_service_impl.dart new file mode 100644 index 0000000..8762e79 --- /dev/null +++ b/lib/vanila/framework/api/game_version_api_service_impl.dart @@ -0,0 +1,20 @@ +import 'package:http/http.dart' as http; +import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_api_service.dart'; +import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; + +class GameVersionApiServiceImpl implements GameVersionApiService { + @override + Future> fetchGameVersionList() async { + final sourceUrl = Uri.parse('https://www.dropbox.com/s/mtz3moc9dpjtz7s/GameVersions.txt?dl=1'); + 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; + } +} diff --git a/lib/vanila/framework/ui/game_version_dropdown.dart b/lib/vanila/framework/ui/game_version_dropdown.dart new file mode 100644 index 0000000..48094b4 --- /dev/null +++ b/lib/vanila/framework/ui/game_version_dropdown.dart @@ -0,0 +1,64 @@ +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/vanila/adapter/gateway/game_version_repository_impl.dart'; +import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_bloc.dart'; +import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.dart'; +import 'package:minecraft_server_installer/vanila/application/use_case/get_game_version_list_use_case.dart'; +import 'package:minecraft_server_installer/vanila/framework/api/game_version_api_service_impl.dart'; + +class GameVersionDropdown extends StatelessWidget { + const GameVersionDropdown({super.key, required this.onChanged}); + + final void Function(GameVersionViewModel?) onChanged; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) { + final gameVersionApiService = GameVersionApiServiceImpl(); + final gameVersionRepository = GameVersionRepositoryImpl(gameVersionApiService); + final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository); + return GameVersionBloc(getGameVersionListUseCase); + }, + child: _GameVersionDropdown(key: key, onChanged: onChanged), + ); + } +} + +class _GameVersionDropdown extends StatefulWidget { + const _GameVersionDropdown({super.key, required this.onChanged}); + + final void Function(GameVersionViewModel?) onChanged; + + @override + State<_GameVersionDropdown> createState() => _GameVersionDropdownState(); +} + +class _GameVersionDropdownState extends State<_GameVersionDropdown> { + @override + void initState() { + super.initState(); + context.read().add(GameVersionLoadedEvent()); + } + + @override + Widget build(BuildContext context) => BlocConsumer>( + listener: (_, __) {}, + builder: + (_, gameVersions) => DropdownMenu( + enabled: gameVersions.isNotEmpty, + requestFocusOnTap: false, + expandedInsets: EdgeInsets.zero, + label: const Text(Strings.fieldGameVersion), + onSelected: widget.onChanged, + dropdownMenuEntries: + gameVersions + .map( + (gameVersion) => + DropdownMenuEntry(value: gameVersion, label: gameVersion.name), + ) + .toList(), + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index d993b91..f516c4f 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,22 @@ packages: description: flutter source: sdk version: "0.0.0" + 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" leak_tracker: dependency: transitive description: @@ -131,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +187,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + provider: + dependency: transitive + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" sky_engine: dependency: transitive description: flutter @@ -192,6 +248,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 +272,14 @@ 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" 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 fcd7c78..348e62e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,9 @@ 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 + http: ^1.4.0 dev_dependencies: flutter_test: