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
}

View File

@ -1,4 +1,7 @@
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/framework/ui/game_version_dropdown.dart';
@ -14,6 +17,20 @@ class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
@override
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/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/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/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 {
const MinecraftServerInstaller({super.key});
@ -12,14 +14,20 @@ class MinecraftServerInstaller extends StatelessWidget {
@override
Widget build(BuildContext context) {
final gameVersionApiService = GameVersionApiServiceImpl();
final gameVersionRepository = GameVersionRepositoryImpl(gameVersionApiService);
final gameVersionFileStorage = GameVersionFileStorageImpl();
final gameVersionRepository = GameVersionRepositoryImpl(gameVersionApiService, gameVersionFileStorage);
final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository);
final downloadServerFileUseCase = DownloadServerFileUseCase(gameVersionRepository);
return MaterialApp(
title: 'Minecraft Server Installer',
theme: ThemeData(primarySwatch: Colors.blue),
home: MultiBlocProvider(
providers: [BlocProvider<GameVersionBloc>(create: (context) => GameVersionBloc(getGameVersionListUseCase))],
providers: [
BlocProvider<GameVersionBloc>(
create: (context) => GameVersionBloc(getGameVersionListUseCase, downloadServerFileUseCase),
),
],
child: const Scaffold(
body: Padding(padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: BasicConfigurationTab()),
),

View File

@ -1,3 +1,4 @@
abstract class Strings {
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';
abstract interface class GameVersionApiService {
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_file_storage.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;
final GameVersionFileStorage _gameVersionFileStorage;
GameVersionRepositoryImpl(this._gameVersionApiService);
GameVersionRepositoryImpl(this._gameVersionApiService, this._gameVersionFileStorage);
@override
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: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';
class GameVersionBloc extends Bloc<GameVersionEvent, List<GameVersionViewModel>> {
class GameVersionBloc extends Bloc<VanilaEvent, VanilaState> {
final GetGameVersionListUseCase _getGameVersionListUseCase;
final DownloadServerFileUseCase _downloadServerFileUseCase;
GameVersionBloc(this._getGameVersionListUseCase) : super(const []) {
on<GameVersionLoadedEvent>((_, emit) async {
GameVersionBloc(this._getGameVersionListUseCase, this._downloadServerFileUseCase) : super(const VanilaState.empty()) {
on<VanilaGameVersionListLoadedEvent>((_, emit) async {
try {
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 {
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;
GameVersion toEntity() {
return GameVersion(name: name, url: url);
}
@override
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 {
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:minecraft_server_installer/vanila/adapter/gateway/game_version_api_service.dart';
import 'package:minecraft_server_installer/vanila/domain/entity/game_version.dart';
@ -17,4 +19,10 @@ class GameVersionApiServiceImpl implements GameVersionApiService {
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';
class GameVersionDropdown extends StatefulWidget {
const GameVersionDropdown({super.key, required this.onChanged});
final void Function(GameVersionViewModel?) onChanged;
const GameVersionDropdown({super.key});
@override
State<GameVersionDropdown> createState() => _GameVersionDropdownState();
@ -17,21 +15,25 @@ class _GameVersionDropdownState extends State<GameVersionDropdown> {
@override
void initState() {
super.initState();
context.read<GameVersionBloc>().add(GameVersionLoadedEvent());
context.read<GameVersionBloc>().add(VanilaGameVersionListLoadedEvent());
}
@override
Widget build(BuildContext context) => BlocConsumer<GameVersionBloc, List<GameVersionViewModel>>(
Widget build(BuildContext context) => BlocConsumer<GameVersionBloc, VanilaState>(
listener: (_, __) {},
builder:
(_, gameVersions) => DropdownMenu(
enabled: gameVersions.isNotEmpty,
(_, state) => DropdownMenu(
enabled: state.gameVersions.isNotEmpty,
requestFocusOnTap: false,
expandedInsets: EdgeInsets.zero,
label: const Text(Strings.fieldGameVersion),
onSelected: widget.onChanged,
onSelected: (value) {
if (value != null) {
context.read<GameVersionBloc>().add(VanilaGameVersionSelectedEvent(value));
}
},
dropdownMenuEntries:
gameVersions
state.gameVersions
.map(
(gameVersion) =>
DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),