MCSI-1 feat: download server file

This commit is contained in:
SquidSpirit 2025-06-29 14:58:29 +09:00
parent 91298fd13e
commit d040751262
14 changed files with 157 additions and 20 deletions

View File

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

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; 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/presentation/game_version_bloc.dart';
import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.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'; import 'package:minecraft_server_installer/vanila/framework/ui/game_version_dropdown.dart';
@ -14,6 +17,20 @@ class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Column(children: [GameVersionDropdown(onChanged: print)]); return Column(
children: [
const GameVersionDropdown(),
const Spacer(),
ElevatedButton.icon(
onPressed: context.watch<GameVersionBloc>().state.isGameVersionSelected ? _downloadServerFile : null,
icon: const Icon(Icons.download),
label: const Text(Strings.buttonStartToInstall),
),
],
);
}
void _downloadServerFile() {
context.read<GameVersionBloc>().add(VanilaServerFileDownloadedEvent());
} }
} }

View File

@ -3,8 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.dart'; import 'package:minecraft_server_installer/main/framework/ui/basic_configuration_tab.dart';
import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_repository_impl.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_bloc.dart';
import 'package:minecraft_server_installer/vanila/application/use_case/download_server_file_use_case.dart';
import 'package:minecraft_server_installer/vanila/application/use_case/get_game_version_list_use_case.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'; import 'package:minecraft_server_installer/vanila/framework/api/game_version_api_service_impl.dart';
import 'package:minecraft_server_installer/vanila/framework/storage/game_version_file_storage_impl.dart';
class MinecraftServerInstaller extends StatelessWidget { class MinecraftServerInstaller extends StatelessWidget {
const MinecraftServerInstaller({super.key}); const MinecraftServerInstaller({super.key});
@ -12,14 +14,20 @@ class MinecraftServerInstaller extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final gameVersionApiService = GameVersionApiServiceImpl(); final gameVersionApiService = GameVersionApiServiceImpl();
final gameVersionRepository = GameVersionRepositoryImpl(gameVersionApiService); final gameVersionFileStorage = GameVersionFileStorageImpl();
final gameVersionRepository = GameVersionRepositoryImpl(gameVersionApiService, gameVersionFileStorage);
final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository); final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository);
final downloadServerFileUseCase = DownloadServerFileUseCase(gameVersionRepository);
return MaterialApp( return MaterialApp(
title: 'Minecraft Server Installer', title: 'Minecraft Server Installer',
theme: ThemeData(primarySwatch: Colors.blue), theme: ThemeData(primarySwatch: Colors.blue),
home: MultiBlocProvider( home: MultiBlocProvider(
providers: [BlocProvider<GameVersionBloc>(create: (context) => GameVersionBloc(getGameVersionListUseCase))], providers: [
BlocProvider<GameVersionBloc>(
create: (context) => GameVersionBloc(getGameVersionListUseCase, downloadServerFileUseCase),
),
],
child: const Scaffold( child: const Scaffold(
body: Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab()), body: Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab()),
), ),

View File

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

View File

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

View File

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

View File

@ -1,12 +1,20 @@
import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_api_service.dart'; import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_api_service.dart';
import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_file_storage.dart';
import 'package:minecraft_server_installer/vanila/application/repository/game_version_repository.dart'; import 'package:minecraft_server_installer/vanila/application/repository/game_version_repository.dart';
import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart';
class GameVersionRepositoryImpl implements GameVersionRepository { class GameVersionRepositoryImpl implements GameVersionRepository {
final GameVersionApiService _gameVersionApiService; final GameVersionApiService _gameVersionApiService;
final GameVersionFileStorage _gameVersionFileStorage;
GameVersionRepositoryImpl(this._gameVersionApiService); GameVersionRepositoryImpl(this._gameVersionApiService, this._gameVersionFileStorage);
@override @override
Future<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList(); Future<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
@override
Future<void> downloadServerFile(GameVersion version, String savePath) async {
final fileBytes = await _gameVersionApiService.fetchServerFile(version.url);
await _gameVersionFileStorage.saveFile(fileBytes, savePath);
}
} }

View File

@ -1,22 +1,71 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; 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/adapter/presentation/game_version_view_model.dart';
import 'package:minecraft_server_installer/vanila/application/use_case/download_server_file_use_case.dart';
import 'package:minecraft_server_installer/vanila/application/use_case/get_game_version_list_use_case.dart'; import 'package:minecraft_server_installer/vanila/application/use_case/get_game_version_list_use_case.dart';
class GameVersionBloc extends Bloc<GameVersionEvent, List<GameVersionViewModel>> { class GameVersionBloc extends Bloc<VanilaEvent, VanilaState> {
final GetGameVersionListUseCase _getGameVersionListUseCase; final GetGameVersionListUseCase _getGameVersionListUseCase;
final DownloadServerFileUseCase _downloadServerFileUseCase;
GameVersionBloc(this._getGameVersionListUseCase) : super(const []) { GameVersionBloc(this._getGameVersionListUseCase, this._downloadServerFileUseCase) : super(const VanilaState.empty()) {
on<GameVersionLoadedEvent>((_, emit) async { on<VanilaGameVersionListLoadedEvent>((_, emit) async {
try { try {
final gameVersions = await _getGameVersionListUseCase(); final gameVersions = await _getGameVersionListUseCase();
emit(gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList()); emit(
VanilaState(
gameVersions: gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList(),
selectedGameVersion: null,
),
);
} on Exception { } on Exception {
emit(const []); emit(const VanilaState.empty());
} }
}); });
on<VanilaGameVersionSelectedEvent>((event, emit) {
emit(state.copyWith(selectedGameVersion: event.gameVersion));
});
on<VanilaServerFileDownloadedEvent>((_, emit) async {
final gameVersion = state.selectedGameVersion;
if (gameVersion == null) {
return;
}
await _downloadServerFileUseCase(gameVersion.toEntity(), './server.jar');
});
} }
} }
sealed class GameVersionEvent {} sealed class VanilaEvent {}
class GameVersionLoadedEvent extends GameVersionEvent {} class VanilaGameVersionListLoadedEvent extends VanilaEvent {}
class VanilaGameVersionSelectedEvent extends VanilaEvent {
final GameVersionViewModel gameVersion;
VanilaGameVersionSelectedEvent(this.gameVersion);
}
class VanilaServerFileDownloadedEvent extends VanilaEvent {}
class VanilaState with EquatableMixin {
final List<GameVersionViewModel> gameVersions;
final GameVersionViewModel? selectedGameVersion;
const VanilaState({required this.gameVersions, required this.selectedGameVersion});
const VanilaState.empty() : this(gameVersions: const [], selectedGameVersion: null);
@override
List<Object?> get props => [gameVersions, selectedGameVersion];
bool get isGameVersionSelected => selectedGameVersion != null;
VanilaState copyWith({List<GameVersionViewModel>? gameVersions, GameVersionViewModel? selectedGameVersion}) =>
VanilaState(
gameVersions: gameVersions ?? this.gameVersions,
selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion,
);
}

View File

@ -9,6 +9,10 @@ class GameVersionViewModel with EquatableMixin {
GameVersionViewModel.from(GameVersion gameVersion) : name = gameVersion.name, url = gameVersion.url; GameVersionViewModel.from(GameVersion gameVersion) : name = gameVersion.name, url = gameVersion.url;
GameVersion toEntity() {
return GameVersion(name: name, url: url);
}
@override @override
List<Object?> get props => [name, url]; List<Object?> get props => [name, url];
} }

View File

@ -2,4 +2,6 @@ import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dar
abstract interface class GameVersionRepository { abstract interface class GameVersionRepository {
Future<List<GameVersion>> getGameVersionList(); Future<List<GameVersion>> getGameVersionList();
Future<void> downloadServerFile(GameVersion version, String savePath);
} }

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 DownloadServerFileUseCase {
final GameVersionRepository _gameVersionRepository;
DownloadServerFileUseCase(this._gameVersionRepository);
Future<void> call(GameVersion version, String savePath) => _gameVersionRepository.downloadServerFile(version, savePath);
}

View File

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:http/http.dart' as http; 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/adapter/gateway/game_version_api_service.dart';
import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart'; import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart';
@ -17,4 +19,10 @@ class GameVersionApiServiceImpl implements GameVersionApiService {
return gameVersionList; return gameVersionList;
} }
@override
Future<Uint8List> fetchServerFile(Uri url) async {
final response = await http.get(url);
return response.bodyBytes;
}
} }

View File

@ -0,0 +1,18 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:minecraft_server_installer/vanila/adapter/gateway/game_version_file_storage.dart';
class GameVersionFileStorageImpl implements GameVersionFileStorage {
@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

@ -5,9 +5,7 @@ import 'package:minecraft_server_installer/vanila/adapter/presentation/game_vers
import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.dart'; import 'package:minecraft_server_installer/vanila/adapter/presentation/game_version_view_model.dart';
class GameVersionDropdown extends StatefulWidget { class GameVersionDropdown extends StatefulWidget {
const GameVersionDropdown({super.key, required this.onChanged}); const GameVersionDropdown({super.key});
final void Function(GameVersionViewModel?) onChanged;
@override @override
State<GameVersionDropdown> createState() => _GameVersionDropdownState(); State<GameVersionDropdown> createState() => _GameVersionDropdownState();
@ -17,21 +15,25 @@ class _GameVersionDropdownState extends State<GameVersionDropdown> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
context.read<GameVersionBloc>().add(GameVersionLoadedEvent()); context.read<GameVersionBloc>().add(VanilaGameVersionListLoadedEvent());
} }
@override @override
Widget build(BuildContext context) => BlocConsumer<GameVersionBloc, List<GameVersionViewModel>>( Widget build(BuildContext context) => BlocConsumer<GameVersionBloc, VanilaState>(
listener: (_, __) {}, listener: (_, __) {},
builder: builder:
(_, gameVersions) => DropdownMenu( (_, state) => DropdownMenu(
enabled: gameVersions.isNotEmpty, enabled: state.gameVersions.isNotEmpty,
requestFocusOnTap: false, requestFocusOnTap: false,
expandedInsets: EdgeInsets.zero, expandedInsets: EdgeInsets.zero,
label: const Text(Strings.fieldGameVersion), label: const Text(Strings.fieldGameVersion),
onSelected: widget.onChanged, onSelected: (value) {
if (value != null) {
context.read<GameVersionBloc>().add(VanilaGameVersionSelectedEvent(value));
}
},
dropdownMenuEntries: dropdownMenuEntries:
gameVersions state.gameVersions
.map( .map(
(gameVersion) => (gameVersion) =>
DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name), DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),