MCSI-2 Browse installing directory #19
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
||||||
"dart.lineLength": 120
|
"dart.sdkPath": ".fvm/versions/3.29.3",
|
||||||
|
"[dart]": {
|
||||||
|
"editor.rulers": [120],
|
||||||
|
"editor.suggest.insertMode": "insert"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@
|
|||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
formatter:
|
||||||
|
page_width: 120
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
7
lib/main/adapter/gateway/installation_api_service.dart
Normal file
7
lib/main/adapter/gateway/installation_api_service.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart';
|
||||||
|
|
||||||
|
abstract interface class InstallationApiService {
|
||||||
|
Future<Uint8List> fetchRemoteFile(Uri url, {DownloadProgressCallback? onProgressChanged});
|
||||||
|
}
|
5
lib/main/adapter/gateway/installation_file_storage.dart
Normal file
5
lib/main/adapter/gateway/installation_file_storage.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
abstract interface class InstallationFileStorage {
|
||||||
|
Future<void> saveFile(Uint8List fileBytes, String path);
|
||||||
|
}
|
16
lib/main/adapter/gateway/installation_repository_impl.dart
Normal file
16
lib/main/adapter/gateway/installation_repository_impl.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:minecraft_server_installer/main/adapter/gateway/installation_api_service.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/gateway/installation_file_storage.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart';
|
||||||
|
|
||||||
|
class InstallationRepositoryImpl implements InstallationRepository {
|
||||||
|
final InstallationApiService _apiService;
|
||||||
|
final InstallationFileStorage _fileStorage;
|
||||||
|
|
||||||
|
InstallationRepositoryImpl(this._apiService, this._fileStorage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> downloadServerFile(Uri url, String path, {DownloadProgressCallback? onProgressChanged}) async {
|
||||||
|
final fileBytes = await _apiService.fetchRemoteFile(url, onProgressChanged: onProgressChanged);
|
||||||
|
await _fileStorage.saveFile(fileBytes, path);
|
||||||
|
}
|
||||||
|
}
|
72
lib/main/adapter/presentation/installation_bloc.dart
Normal file
72
lib/main/adapter/presentation/installation_bloc.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/progress_view_model.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/application/use_case/download_file_use_case.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/constants.dart';
|
||||||
|
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
|
||||||
|
InstallationBloc(DownloadFileUseCase downloadFileUseCase) : super(const InstallationState.empty()) {
|
||||||
|
on<InstallationStartedEvent>((_, emit) async {
|
||||||
|
if (!state.canStartToInstall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gameVersion = state.gameVersion!;
|
||||||
|
final savePath = state.savePath!;
|
||||||
|
|
||||||
|
emit(state.copyWith(isLocked: true, downloadProgress: const ProgressViewModel.start()));
|
||||||
|
|
||||||
|
await downloadFileUseCase(
|
||||||
|
gameVersion.url,
|
||||||
|
path.join(savePath, Constants.serverFileName),
|
||||||
|
onProgressChanged: (progressValue) => add(_InstallationProgressValueChangedEvent(progressValue)),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(isLocked: false, downloadProgress: const ProgressViewModel.complete()));
|
||||||
|
});
|
||||||
|
|
||||||
|
on<_InstallationProgressValueChangedEvent>((event, emit) {
|
||||||
|
ProgressViewModel newProgress;
|
||||||
|
|
||||||
|
if (event.progressValue < 0) {
|
||||||
|
newProgress = state.downloadProgress.copyWith(value: 0.0);
|
||||||
|
} else if (event.progressValue > 1) {
|
||||||
|
newProgress = state.downloadProgress.copyWith(value: 1.0);
|
||||||
|
} else {
|
||||||
|
newProgress = state.downloadProgress.copyWith(value: event.progressValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(downloadProgress: newProgress));
|
||||||
|
});
|
||||||
|
|
||||||
|
on<InstallationConfigurationUpdatedEvent>((event, emit) {
|
||||||
|
final newState = state.copyWith(
|
||||||
|
gameVersion: event.gameVersion,
|
||||||
|
savePath: event.savePath,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class InstallationEvent {}
|
||||||
|
|
||||||
|
class InstallationStartedEvent extends InstallationEvent {}
|
||||||
|
|
||||||
|
class _InstallationProgressValueChangedEvent extends InstallationEvent {
|
||||||
|
final double progressValue;
|
||||||
|
|
||||||
|
_InstallationProgressValueChangedEvent(this.progressValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstallationConfigurationUpdatedEvent extends InstallationEvent {
|
||||||
|
final GameVersionViewModel? gameVersion;
|
||||||
|
final String? savePath;
|
||||||
|
|
||||||
|
InstallationConfigurationUpdatedEvent({
|
||||||
|
this.gameVersion,
|
||||||
|
this.savePath,
|
||||||
|
});
|
||||||
|
}
|
52
lib/main/adapter/presentation/installation_state.dart
Normal file
52
lib/main/adapter/presentation/installation_state.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/progress_view_model.dart';
|
||||||
|
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||||
|
|
||||||
|
class InstallationState with EquatableMixin {
|
||||||
|
final GameVersionViewModel? gameVersion;
|
||||||
|
final String? savePath;
|
||||||
|
final ProgressViewModel downloadProgress;
|
||||||
|
final bool isLocked;
|
||||||
|
|
||||||
|
const InstallationState({
|
||||||
|
required this.gameVersion,
|
||||||
|
required this.savePath,
|
||||||
|
required this.downloadProgress,
|
||||||
|
required this.isLocked,
|
||||||
|
});
|
||||||
|
|
||||||
|
const InstallationState.empty()
|
||||||
|
: this(
|
||||||
|
gameVersion: null,
|
||||||
|
savePath: null,
|
||||||
|
downloadProgress: const ProgressViewModel.zero(),
|
||||||
|
isLocked: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
gameVersion,
|
||||||
|
savePath,
|
||||||
|
downloadProgress,
|
||||||
|
isLocked,
|
||||||
|
];
|
||||||
|
|
||||||
|
InstallationState copyWith({
|
||||||
|
GameVersionViewModel? gameVersion,
|
||||||
|
String? savePath,
|
||||||
|
ProgressViewModel? downloadProgress,
|
||||||
|
bool? isLocked,
|
||||||
|
}) =>
|
||||||
|
InstallationState(
|
||||||
|
gameVersion: gameVersion ?? this.gameVersion,
|
||||||
|
savePath: savePath ?? this.savePath,
|
||||||
|
downloadProgress: downloadProgress ?? this.downloadProgress,
|
||||||
|
isLocked: isLocked ?? this.isLocked,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get isGameVersionSelected => gameVersion != null;
|
||||||
|
|
||||||
|
bool get isSavePathSelected => savePath != null && savePath!.isNotEmpty;
|
||||||
|
|
||||||
|
bool get canStartToInstall => isGameVersionSelected && isSavePathSelected && !isLocked;
|
||||||
|
}
|
33
lib/main/adapter/presentation/progress_view_model.dart
Normal file
33
lib/main/adapter/presentation/progress_view_model.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class ProgressViewModel with EquatableMixin {
|
||||||
|
/// The value should between 0.0 and 1.0.
|
||||||
|
final double value;
|
||||||
|
final bool isInProgress;
|
||||||
|
|
||||||
|
const ProgressViewModel({
|
||||||
|
required this.value,
|
||||||
|
required this.isInProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProgressViewModel.zero() : this(value: 0.0, isInProgress: false);
|
||||||
|
|
||||||
|
const ProgressViewModel.start() : this(value: 0.0, isInProgress: true);
|
||||||
|
|
||||||
|
const ProgressViewModel.complete() : this(value: 1.0, isInProgress: false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
value,
|
||||||
|
isInProgress,
|
||||||
|
];
|
||||||
|
|
||||||
|
ProgressViewModel copyWith({
|
||||||
|
double? value,
|
||||||
|
bool? isInProgress,
|
||||||
|
}) =>
|
||||||
|
ProgressViewModel(
|
||||||
|
value: value ?? this.value,
|
||||||
|
isInProgress: isInProgress ?? this.isInProgress,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
typedef DownloadProgressCallback = void Function(double progress);
|
||||||
|
|
||||||
|
abstract interface class InstallationRepository {
|
||||||
|
Future<void> downloadServerFile(Uri url, String path, {DownloadProgressCallback? onProgressChanged});
|
||||||
|
}
|
10
lib/main/application/use_case/download_file_use_case.dart
Normal file
10
lib/main/application/use_case/download_file_use_case.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart';
|
||||||
|
|
||||||
|
class DownloadFileUseCase {
|
||||||
|
final InstallationRepository _installationRepository;
|
||||||
|
|
||||||
|
DownloadFileUseCase(this._installationRepository);
|
||||||
|
|
||||||
|
Future<void> call(Uri url, String path, {DownloadProgressCallback? onProgressChanged}) =>
|
||||||
|
_installationRepository.downloadServerFile(url, path, onProgressChanged: onProgressChanged);
|
||||||
|
}
|
40
lib/main/framework/api/installation_api_service_impl.dart
Normal file
40
lib/main/framework/api/installation_api_service_impl.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/gateway/installation_api_service.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart';
|
||||||
|
|
||||||
|
class InstallationApiServiceImpl implements InstallationApiService {
|
||||||
|
@override
|
||||||
|
Future<Uint8List> fetchRemoteFile(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<Uint8List>();
|
||||||
|
final bytes = <int>[];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/gateway/installation_file_storage.dart';
|
||||||
|
|
||||||
|
class InstallationFileStorageImpl implements InstallationFileStorage {
|
||||||
|
@override
|
||||||
|
Future<void> saveFile(Uint8List fileBytes, String path) async {
|
||||||
|
final file = File(path);
|
||||||
|
|
||||||
|
if (!await file.parent.exists()) {
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.create();
|
||||||
|
await file.writeAsBytes(fileBytes, flush: true);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/framework/ui/path_browsing_field.dart';
|
||||||
import 'package:minecraft_server_installer/main/framework/ui/strings.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/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';
|
import 'package:minecraft_server_installer/vanilla/framework/ui/game_version_dropdown.dart';
|
||||||
|
|
||||||
class BasicConfigurationTab extends StatefulWidget {
|
class BasicConfigurationTab extends StatefulWidget {
|
||||||
@ -18,38 +19,38 @@ class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
|
|||||||
GameVersionViewModel? selectedGameVersion;
|
GameVersionViewModel? selectedGameVersion;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Column(
|
||||||
return Column(
|
children: [
|
||||||
children: [
|
const GameVersionDropdown(),
|
||||||
const GameVersionDropdown(),
|
const Gap(16),
|
||||||
const Spacer(),
|
const PathBrowsingField(),
|
||||||
BlocConsumer<VanillaBloc, VanillaState>(
|
const Spacer(),
|
||||||
listener: (_, __) {},
|
_bottomControl,
|
||||||
builder:
|
],
|
||||||
(context, state) => Row(
|
);
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
Widget get _bottomControl => BlocConsumer<InstallationBloc, InstallationState>(
|
||||||
if (state.isDownloading) Expanded(child: LinearProgressIndicator(value: state.downloadProgress)),
|
listener: (_, __) {},
|
||||||
const Gap(32),
|
builder: (_, state) => Row(
|
||||||
ElevatedButton.icon(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
style: ElevatedButton.styleFrom(
|
children: [
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
if (state.downloadProgress.isInProgress)
|
||||||
),
|
Expanded(child: LinearProgressIndicator(value: state.downloadProgress.value)),
|
||||||
onPressed: state.isGameVersionSelected ? _downloadServerFile : null,
|
const Gap(32),
|
||||||
icon: const Icon(Icons.download),
|
ElevatedButton.icon(
|
||||||
label: const Padding(
|
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))),
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
onPressed: context.watch<InstallationBloc>().state.canStartToInstall ? _downloadServerFile : null,
|
||||||
child: Text(Strings.buttonStartToInstall),
|
icon: const Icon(Icons.download),
|
||||||
),
|
label: const Padding(
|
||||||
),
|
padding: EdgeInsets.symmetric(vertical: 12),
|
||||||
],
|
child: Text(Strings.buttonStartToInstall),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _downloadServerFile() {
|
void _downloadServerFile() {
|
||||||
context.read<VanillaBloc>().add(VanillaServerFileDownloadedEvent());
|
context.read<InstallationBloc>().add((InstallationStartedEvent()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/gateway/installation_repository_impl.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/application/use_case/download_file_use_case.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/framework/api/installation_api_service_impl.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/framework/storage/installation_file_storage_impl.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/vanilla/adapter/gateway/vanilla_repository_impl.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_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/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/api/vanilla_api_service_impl.dart';
|
||||||
import 'package:minecraft_server_installer/vanilla/framework/storage/vanilla_file_storage_impl.dart';
|
|
||||||
|
|
||||||
class MinecraftServerInstaller extends StatelessWidget {
|
class MinecraftServerInstaller extends StatelessWidget {
|
||||||
const MinecraftServerInstaller({super.key});
|
const MinecraftServerInstaller({super.key});
|
||||||
@ -17,26 +20,28 @@ class MinecraftServerInstaller extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final installationApiService = InstallationApiServiceImpl();
|
||||||
|
final installationFileStorage = InstallationFileStorageImpl();
|
||||||
|
final installationRepository = InstallationRepositoryImpl(installationApiService, installationFileStorage);
|
||||||
|
|
||||||
final gameVersionApiService = VanillaApiServiceImpl();
|
final gameVersionApiService = VanillaApiServiceImpl();
|
||||||
final gameVersionFileStorage = VanillaFileStorageImpl();
|
final gameVersionRepository = VanillaRepositoryImpl(gameVersionApiService);
|
||||||
final gameVersionRepository = VanillaRepositoryImpl(gameVersionApiService, gameVersionFileStorage);
|
|
||||||
|
final downloadFileUseCase = DownloadFileUseCase(installationRepository);
|
||||||
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.light().copyWith(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade900)),
|
theme: ThemeData.light().copyWith(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade900)),
|
||||||
home: MultiBlocProvider(
|
home: MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
BlocProvider(create: (_) => InstallationBloc(downloadFileUseCase)),
|
||||||
BlocProvider<VanillaBloc>(
|
BlocProvider<VanillaBloc>(
|
||||||
create:
|
create: (_) => VanillaBloc(getGameVersionListUseCase)..add(VanillaGameVersionListLoadedEvent()),
|
||||||
(context) =>
|
|
||||||
VanillaBloc(getGameVersionListUseCase, downloadServerFileUseCase)
|
|
||||||
..add(VanillaGameVersionListLoadedEvent()),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: BlocConsumer<VanillaBloc, VanillaState>(
|
body: BlocConsumer<InstallationBloc, InstallationState>(
|
||||||
listener: (_, __) {},
|
listener: (_, __) {},
|
||||||
builder: (_, state) {
|
builder: (_, state) {
|
||||||
if (state.isLocked) {
|
if (state.isLocked) {
|
||||||
|
75
lib/main/framework/ui/path_browsing_field.dart
Normal file
75
lib/main/framework/ui/path_browsing_field.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
|
||||||
|
|
||||||
|
class PathBrowsingField extends StatefulWidget {
|
||||||
|
const PathBrowsingField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PathBrowsingField> createState() => _PathBrowsingFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PathBrowsingFieldState extends State<PathBrowsingField> {
|
||||||
|
final _textEditingController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_textEditingController.text = context.read<InstallationBloc>().state.savePath ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => BlocConsumer<InstallationBloc, InstallationState>(
|
||||||
|
listener: (_, state) {
|
||||||
|
if (state.savePath != null) {
|
||||||
|
_textEditingController.text = state.savePath!;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (_, __) => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _textEditingController,
|
||||||
|
readOnly: true,
|
||||||
|
canRequestFocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
|
label: const Text('${Strings.fieldPath} *'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: _browseDirectory,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
|
),
|
||||||
|
child: const Text(Strings.buttonBrowse),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> _browseDirectory() async {
|
||||||
|
final directory = await FilePicker.platform.getDirectoryPath(
|
||||||
|
dialogTitle: Strings.dialogTitleSelectDirectory,
|
||||||
|
initialDirectory: _textEditingController.text.isNotEmpty ? _textEditingController.text : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted || directory == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.read<InstallationBloc>().add(InstallationConfigurationUpdatedEvent(
|
||||||
|
savePath: directory,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
abstract class Strings {
|
abstract class Strings {
|
||||||
static const fieldGameVersion = '遊戲版本';
|
static const fieldGameVersion = '遊戲版本';
|
||||||
|
static const fieldPath = '安裝路徑';
|
||||||
static const buttonStartToInstall = '開始安裝';
|
static const buttonStartToInstall = '開始安裝';
|
||||||
|
static const buttonBrowse = '瀏覽';
|
||||||
|
static const dialogTitleSelectDirectory = '選擇安裝目錄';
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
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';
|
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
|
||||||
|
|
||||||
abstract interface class VanillaApiService {
|
abstract interface class VanillaApiService {
|
||||||
Future<List<GameVersion>> fetchGameVersionList();
|
Future<List<GameVersion>> fetchGameVersionList();
|
||||||
|
|
||||||
Future<Uint8List> fetchServerFile(Uri url, {DownloadProgressCallback? onProgressChanged});
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
abstract interface class VanillaFileStorage {
|
|
||||||
Future<void> saveFile(Uint8List fileBytes, String savePath);
|
|
||||||
}
|
|
@ -1,25 +1,12 @@
|
|||||||
import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_api_service.dart';
|
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/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';
|
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
|
||||||
|
|
||||||
class VanillaRepositoryImpl implements VanillaRepository {
|
class VanillaRepositoryImpl implements VanillaRepository {
|
||||||
final VanillaApiService _gameVersionApiService;
|
final VanillaApiService _gameVersionApiService;
|
||||||
final VanillaFileStorage _gameVersionFileStorage;
|
|
||||||
|
|
||||||
VanillaRepositoryImpl(this._gameVersionApiService, this._gameVersionFileStorage);
|
VanillaRepositoryImpl(this._gameVersionApiService);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
|
Future<List<GameVersion>> getGameVersionList() => _gameVersionApiService.fetchGameVersionList();
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> downloadServerFile(
|
|
||||||
GameVersion version,
|
|
||||||
String savePath, {
|
|
||||||
DownloadProgressCallback? onProgressChanged,
|
|
||||||
}) async {
|
|
||||||
final fileBytes = await _gameVersionApiService.fetchServerFile(version.url, onProgressChanged: onProgressChanged);
|
|
||||||
await _gameVersionFileStorage.saveFile(fileBytes, savePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,16 @@ class GameVersionViewModel with EquatableMixin {
|
|||||||
final String name;
|
final String name;
|
||||||
final Uri url;
|
final Uri url;
|
||||||
|
|
||||||
const GameVersionViewModel({required this.name, required this.url});
|
const GameVersionViewModel({
|
||||||
|
required this.name,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
GameVersionViewModel.from(GameVersion gameVersion) : name = gameVersion.name, url = gameVersion.url;
|
GameVersionViewModel.fromEntity(GameVersion gameVersion)
|
||||||
|
: name = gameVersion.name,
|
||||||
|
url = gameVersion.url;
|
||||||
|
|
||||||
GameVersion toEntity() {
|
GameVersion toEntity() => GameVersion(name: name, url: url);
|
||||||
return GameVersion(name: name, url: url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [name, url];
|
List<Object?> get props => [name, url];
|
||||||
|
@ -1,57 +1,22 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
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/game_version_view_model.dart';
|
||||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_state.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/application/use_case/get_game_version_list_use_case.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
class VanillaBloc extends Bloc<VanillaEvent, VanillaState> {
|
class VanillaBloc extends Bloc<VanillaEvent, VanillaState> {
|
||||||
final GetGameVersionListUseCase _getGameVersionListUseCase;
|
VanillaBloc(GetGameVersionListUseCase getGameVersionListUseCase) : super(const VanillaState.empty()) {
|
||||||
final DownloadServerFileUseCase _downloadServerFileUseCase;
|
|
||||||
|
|
||||||
VanillaBloc(this._getGameVersionListUseCase, this._downloadServerFileUseCase) : super(const VanillaState.empty()) {
|
|
||||||
on<VanillaGameVersionListLoadedEvent>((_, emit) async {
|
on<VanillaGameVersionListLoadedEvent>((_, emit) async {
|
||||||
try {
|
try {
|
||||||
final gameVersions = await _getGameVersionListUseCase();
|
final gameVersions = await getGameVersionListUseCase();
|
||||||
emit(
|
emit(
|
||||||
const VanillaState.empty().copyWith(
|
const VanillaState.empty().copyWith(
|
||||||
gameVersions: gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList(),
|
gameVersions: gameVersions.map((entity) => GameVersionViewModel.fromEntity(entity)).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on Exception {
|
} on Exception {
|
||||||
emit(const VanillaState.empty());
|
emit(const VanillaState.empty());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
on<VanillaGameVersionSelectedEvent>((event, emit) {
|
|
||||||
emit(state.copyWith(selectedGameVersion: event.gameVersion));
|
|
||||||
});
|
|
||||||
|
|
||||||
on<VanillaServerFileDownloadedEvent>((_, 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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,11 +29,3 @@ class VanillaGameVersionSelectedEvent extends VanillaEvent {
|
|||||||
|
|
||||||
VanillaGameVersionSelectedEvent(this.gameVersion);
|
VanillaGameVersionSelectedEvent(this.gameVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
class VanillaServerFileDownloadedEvent extends VanillaEvent {}
|
|
||||||
|
|
||||||
class _VanillaDownloadProgressChangedEvent extends VanillaEvent {
|
|
||||||
final double progress;
|
|
||||||
|
|
||||||
_VanillaDownloadProgressChangedEvent(this.progress);
|
|
||||||
}
|
|
||||||
|
@ -2,37 +2,29 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||||
|
|
||||||
class VanillaState with EquatableMixin {
|
class VanillaState with EquatableMixin {
|
||||||
final bool isLocked;
|
|
||||||
final double downloadProgress;
|
|
||||||
final List<GameVersionViewModel> gameVersions;
|
final List<GameVersionViewModel> gameVersions;
|
||||||
final GameVersionViewModel? selectedGameVersion;
|
|
||||||
|
|
||||||
const VanillaState({
|
const VanillaState({
|
||||||
required this.isLocked,
|
|
||||||
required this.downloadProgress,
|
|
||||||
required this.gameVersions,
|
required this.gameVersions,
|
||||||
required this.selectedGameVersion,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const VanillaState.empty()
|
const VanillaState.empty()
|
||||||
: this(isLocked: false, downloadProgress: 0, gameVersions: const [], selectedGameVersion: null);
|
: this(
|
||||||
|
gameVersions: const [],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [isLocked, downloadProgress, gameVersions, selectedGameVersion];
|
List<Object?> get props => [
|
||||||
|
gameVersions,
|
||||||
bool get isGameVersionSelected => selectedGameVersion != null;
|
];
|
||||||
|
|
||||||
bool get isDownloading => downloadProgress > 0 && downloadProgress < 1;
|
|
||||||
|
|
||||||
VanillaState copyWith({
|
VanillaState copyWith({
|
||||||
bool? isLocked,
|
bool? isLocked,
|
||||||
double? downloadProgress,
|
double? downloadProgress,
|
||||||
List<GameVersionViewModel>? gameVersions,
|
List<GameVersionViewModel>? gameVersions,
|
||||||
GameVersionViewModel? selectedGameVersion,
|
GameVersionViewModel? selectedGameVersion,
|
||||||
}) => VanillaState(
|
}) =>
|
||||||
isLocked: isLocked ?? this.isLocked,
|
VanillaState(
|
||||||
downloadProgress: downloadProgress ?? this.downloadProgress,
|
gameVersions: gameVersions ?? this.gameVersions,
|
||||||
gameVersions: gameVersions ?? this.gameVersions,
|
);
|
||||||
selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
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';
|
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
|
||||||
|
|
||||||
abstract interface class VanillaRepository {
|
abstract interface class VanillaRepository {
|
||||||
Future<List<GameVersion>> getGameVersionList();
|
Future<List<GameVersion>> getGameVersionList();
|
||||||
|
|
||||||
Future<void> downloadServerFile(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged});
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
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<void> call(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged}) =>
|
|
||||||
_gameVersionRepository.downloadServerFile(version, savePath, onProgressChanged: onProgressChanged);
|
|
||||||
}
|
|
@ -4,7 +4,10 @@ class GameVersion with EquatableMixin {
|
|||||||
final String name;
|
final String name;
|
||||||
final Uri url;
|
final Uri url;
|
||||||
|
|
||||||
const GameVersion({required this.name, required this.url});
|
const GameVersion({
|
||||||
|
required this.name,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [name, url];
|
List<Object?> get props => [name, url];
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:minecraft_server_installer/main/constants.dart';
|
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/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';
|
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
|
||||||
|
|
||||||
class VanillaApiServiceImpl implements VanillaApiService {
|
class VanillaApiServiceImpl implements VanillaApiService {
|
||||||
@ -22,36 +20,4 @@ class VanillaApiServiceImpl implements VanillaApiService {
|
|||||||
|
|
||||||
return gameVersionList;
|
return gameVersionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Uint8List> 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<Uint8List>();
|
|
||||||
final bytes = <int>[];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
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<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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:minecraft_server_installer/main/adapter/presentation/installation_bloc.dart';
|
||||||
import 'package:minecraft_server_installer/main/framework/ui/strings.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/vanilla_bloc.dart';
|
||||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||||
@ -10,26 +11,24 @@ class GameVersionDropdown extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => BlocConsumer<VanillaBloc, VanillaState>(
|
Widget build(BuildContext context) => BlocConsumer<VanillaBloc, VanillaState>(
|
||||||
listener: (_, __) {},
|
listener: (_, __) {},
|
||||||
builder:
|
builder: (_, state) => DropdownMenu<GameVersionViewModel>(
|
||||||
(_, state) => DropdownMenu(
|
initialSelection: context.read<InstallationBloc>().state.gameVersion,
|
||||||
initialSelection: state.selectedGameVersion,
|
|
||||||
enabled: state.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: (value) {
|
onSelected: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<VanillaBloc>().add(VanillaGameVersionSelectedEvent(value));
|
context.read<InstallationBloc>().add(InstallationConfigurationUpdatedEvent(gameVersion: value));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dropdownMenuEntries:
|
dropdownMenuEntries: state.gameVersions
|
||||||
state.gameVersions
|
.map((gameVersion) => DropdownMenuEntry<GameVersionViewModel>(
|
||||||
.map(
|
value: gameVersion,
|
||||||
(gameVersion) =>
|
label: gameVersion.name,
|
||||||
DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),
|
))
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
49
pubspec.lock
49
pubspec.lock
@ -49,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -73,6 +81,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.2"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.2.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -94,11 +118,24 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.28"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
gap:
|
gap:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -344,6 +381,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.0"
|
||||||
window_manager:
|
window_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -353,5 +398,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.2 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.27.0"
|
||||||
|
@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ">=3.6.0 <4.0.0"
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
@ -35,6 +35,7 @@ dependencies:
|
|||||||
# 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
|
equatable: ^2.0.7
|
||||||
|
file_picker: ^10.2.0
|
||||||
flutter_bloc: ^9.1.1
|
flutter_bloc: ^9.1.1
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
http: ^1.4.0
|
http: ^1.4.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user