MCSI-1 feat: version selection

This commit is contained in:
SquidSpirit 2025-06-29 04:59:49 +08:00
parent ecee7ac16f
commit e8ad6592f5
16 changed files with 274 additions and 3 deletions

View File

@ -22,7 +22,8 @@ linter:
# producing the lint. # producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # 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 # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -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<BasicConfigurationTab> createState() => _BasicConfigurationTabState();
}
class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
GameVersionViewModel? selectedGameVersion;
@override
Widget build(BuildContext context) {
return const Column(children: [GameVersionDropdown(onChanged: print)]);
}
}

View File

@ -1,10 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.dart';
class MinecraftServerInstaller extends StatelessWidget { class MinecraftServerInstaller extends StatelessWidget {
const MinecraftServerInstaller({super.key}); const MinecraftServerInstaller({super.key});
@override @override
Widget build(BuildContext context) { 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()),
),
);
} }
} }

View File

@ -0,0 +1,3 @@
abstract class Strings {
static const fieldGameVersion = '遊戲版本';
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:minecraft_server_installer/main/framework/ui/minecraft_server_installer.dart'; import 'package:minecraft_server_installer/main/framework/ui/minecraft_server_installer.dart';
import 'package:minecraft_server_installer/vanila/framework/api/game_version_api_service_impl.dart';
void main() { void main() {
final apiService = GameVersionApiServiceImpl();
apiService.fetchGameVersionList();
runApp(const MinecraftServerInstaller()); runApp(const MinecraftServerInstaller());
} }

View File

@ -0,0 +1,5 @@
import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart';
abstract interface class GameVersionApiService {
Future<List<GameVersion>> fetchGameVersionList();
}

View File

@ -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<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
}

View File

@ -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<GameVersionEvent, List<GameVersionViewModel>> {
final GetGameVersionListUseCase _getGameVersionListUseCase;
GameVersionBloc(this._getGameVersionListUseCase) : super(const []) {
on<GameVersionLoadedEvent>((_, 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 {}

View File

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

View File

@ -0,0 +1,5 @@
import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart';
abstract interface class GameVersionRepository {
Future<List<GameVersion>> getGameVersionList();
}

View File

@ -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<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,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<List<GameVersion>> 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;
}
}

View File

@ -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<GameVersionBloc>(
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<GameVersionBloc>().add(GameVersionLoadedEvent());
}
@override
Widget build(BuildContext context) => BlocConsumer<GameVersionBloc, List<GameVersionViewModel>>(
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<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),
)
.toList(),
),
);
}

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -49,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -62,6 +78,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -75,6 +99,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -131,6 +171,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -139,6 +187,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
provider:
dependency: transitive
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -192,6 +248,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -208,6 +272,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks: sdks:
dart: ">=3.7.2 <4.0.0" dart: ">=3.7.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.18.0-18.0.pre.54"

View File

@ -34,6 +34,9 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
equatable: ^2.0.7
flutter_bloc: ^9.1.1
http: ^1.4.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: