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.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.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
formatter:
|
||||
page_width: 120
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# 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:
|
27
lib/main/adapter/presentation/installation_bloc.dart
Normal file
27
lib/main/adapter/presentation/installation_bloc.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart';
|
||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||
|
||||
class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
|
||||
InstallationBloc() : super(const InstallationState.empty()) {
|
||||
on<InstallationConfigurationUpdatedEvent>((event, emit) {
|
||||
final newState = state.copyWith(
|
||||
gameVersion: event.gameVersion,
|
||||
savePath: event.savePath,
|
||||
);
|
||||
emit(newState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallationEvent {}
|
||||
|
||||
class InstallationConfigurationUpdatedEvent extends InstallationEvent {
|
||||
final GameVersionViewModel? gameVersion;
|
||||
final String? savePath;
|
||||
|
||||
InstallationConfigurationUpdatedEvent({
|
||||
this.gameVersion,
|
||||
this.savePath,
|
||||
});
|
||||
}
|
35
lib/main/adapter/presentation/installation_state.dart
Normal file
35
lib/main/adapter/presentation/installation_state.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||
|
||||
class InstallationState with EquatableMixin {
|
||||
final GameVersionViewModel? gameVersion;
|
||||
final String? savePath;
|
||||
|
||||
const InstallationState({
|
||||
this.gameVersion,
|
||||
this.savePath,
|
||||
});
|
||||
|
||||
const InstallationState.empty()
|
||||
: this(
|
||||
gameVersion: null,
|
||||
savePath: null,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
gameVersion,
|
||||
savePath,
|
||||
];
|
||||
|
||||
InstallationState copyWith({
|
||||
GameVersionViewModel? gameVersion,
|
||||
String? savePath,
|
||||
}) =>
|
||||
InstallationState(
|
||||
gameVersion: gameVersion ?? this.gameVersion,
|
||||
savePath: savePath ?? this.savePath,
|
||||
);
|
||||
|
||||
bool get canStartToInstall => gameVersion != null && savePath != null && savePath!.isNotEmpty;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.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/vanilla/adapter/presentation/vanilla_bloc.dart';
|
||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||
@ -18,36 +19,35 @@ class _BasicConfigurationTabState extends State<BasicConfigurationTab> {
|
||||
GameVersionViewModel? selectedGameVersion;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const GameVersionDropdown(),
|
||||
const Spacer(),
|
||||
BlocConsumer<VanillaBloc, VanillaState>(
|
||||
listener: (_, __) {},
|
||||
builder:
|
||||
(context, state) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (state.isDownloading) Expanded(child: LinearProgressIndicator(value: state.downloadProgress)),
|
||||
const Gap(32),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
onPressed: state.isGameVersionSelected ? _downloadServerFile : null,
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(Strings.buttonStartToInstall),
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [
|
||||
const GameVersionDropdown(),
|
||||
const Gap(16),
|
||||
const PathBrowsingField(),
|
||||
const Spacer(),
|
||||
_bottomControl,
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _bottomControl => BlocConsumer<VanillaBloc, VanillaState>(
|
||||
listener: (_, __) {},
|
||||
builder: (context, state) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (state.isDownloading) Expanded(child: LinearProgressIndicator(value: state.downloadProgress)),
|
||||
const Gap(32),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))),
|
||||
onPressed: state.isGameVersionSelected ? _downloadServerFile : null,
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(Strings.buttonStartToInstall),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
void _downloadServerFile() {
|
||||
context.read<VanillaBloc>().add(VanillaServerFileDownloadedEvent('.'));
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.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/basic_configuration_tab.dart';
|
||||
import 'package:minecraft_server_installer/vanilla/adapter/gateway/vanilla_repository_impl.dart';
|
||||
import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_bloc.dart';
|
||||
@ -29,11 +30,10 @@ class MinecraftServerInstaller extends StatelessWidget {
|
||||
home: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<VanillaBloc>(
|
||||
create:
|
||||
(context) =>
|
||||
VanillaBloc(getGameVersionListUseCase, downloadServerFileUseCase)
|
||||
..add(VanillaGameVersionListLoadedEvent()),
|
||||
create: (_) => VanillaBloc(getGameVersionListUseCase, downloadServerFileUseCase)
|
||||
..add(VanillaGameVersionListLoadedEvent()),
|
||||
),
|
||||
BlocProvider(create: (_) => InstallationBloc())
|
||||
],
|
||||
child: Scaffold(
|
||||
body: BlocConsumer<VanillaBloc, VanillaState>(
|
||||
|
67
lib/main/framework/ui/path_browsing_field.dart
Normal file
67
lib/main/framework/ui/path_browsing_field.dart
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
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,
|
||||
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 {
|
||||
static const fieldGameVersion = '遊戲版本';
|
||||
static const fieldPath = '安裝路徑';
|
||||
static const buttonStartToInstall = '開始安裝';
|
||||
static const buttonBrowse = '瀏覽';
|
||||
static const dialogTitleSelectDirectory = '選擇安裝目錄';
|
||||
}
|
||||
|
@ -19,7 +19,10 @@ class VanillaRepositoryImpl implements VanillaRepository {
|
||||
String savePath, {
|
||||
DownloadProgressCallback? onProgressChanged,
|
||||
}) async {
|
||||
final fileBytes = await _gameVersionApiService.fetchServerFile(version.url, onProgressChanged: onProgressChanged);
|
||||
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 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() {
|
||||
return GameVersion(name: name, url: url);
|
||||
}
|
||||
GameVersion toEntity() => GameVersion(name: name, url: url);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, url];
|
||||
|
@ -16,7 +16,7 @@ class VanillaBloc extends Bloc<VanillaEvent, VanillaState> {
|
||||
final gameVersions = await _getGameVersionListUseCase();
|
||||
emit(
|
||||
const VanillaState.empty().copyWith(
|
||||
gameVersions: gameVersions.map((entity) => GameVersionViewModel.from(entity)).toList(),
|
||||
gameVersions: gameVersions.map((entity) => GameVersionViewModel.fromEntity(entity)).toList(),
|
||||
),
|
||||
);
|
||||
} on Exception {
|
||||
|
@ -15,10 +15,20 @@ class VanillaState with EquatableMixin {
|
||||
});
|
||||
|
||||
const VanillaState.empty()
|
||||
: this(isLocked: false, downloadProgress: 0, gameVersions: const [], selectedGameVersion: null);
|
||||
: this(
|
||||
isLocked: false,
|
||||
downloadProgress: 0,
|
||||
gameVersions: const [],
|
||||
selectedGameVersion: null,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isLocked, downloadProgress, gameVersions, selectedGameVersion];
|
||||
List<Object?> get props => [
|
||||
isLocked,
|
||||
downloadProgress,
|
||||
gameVersions,
|
||||
selectedGameVersion,
|
||||
];
|
||||
|
||||
bool get isGameVersionSelected => selectedGameVersion != null;
|
||||
|
||||
@ -29,10 +39,11 @@ class VanillaState with EquatableMixin {
|
||||
double? downloadProgress,
|
||||
List<GameVersionViewModel>? gameVersions,
|
||||
GameVersionViewModel? selectedGameVersion,
|
||||
}) => VanillaState(
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
downloadProgress: downloadProgress ?? this.downloadProgress,
|
||||
gameVersions: gameVersions ?? this.gameVersions,
|
||||
selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion,
|
||||
);
|
||||
}) =>
|
||||
VanillaState(
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
downloadProgress: downloadProgress ?? this.downloadProgress,
|
||||
gameVersions: gameVersions ?? this.gameVersions,
|
||||
selectedGameVersion: selectedGameVersion ?? this.selectedGameVersion,
|
||||
);
|
||||
}
|
||||
|
@ -4,5 +4,9 @@ import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.da
|
||||
abstract interface class VanillaRepository {
|
||||
Future<List<GameVersion>> getGameVersionList();
|
||||
|
||||
Future<void> downloadServerFile(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged});
|
||||
Future<void> downloadServerFile(
|
||||
GameVersion version,
|
||||
String savePath, {
|
||||
DownloadProgressCallback? onProgressChanged,
|
||||
});
|
||||
}
|
||||
|
@ -8,6 +8,14 @@ class DownloadServerFileUseCase {
|
||||
|
||||
DownloadServerFileUseCase(this._gameVersionRepository);
|
||||
|
||||
Future<void> call(GameVersion version, String savePath, {DownloadProgressCallback? onProgressChanged}) =>
|
||||
_gameVersionRepository.downloadServerFile(version, savePath, onProgressChanged: onProgressChanged);
|
||||
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 Uri url;
|
||||
|
||||
const GameVersion({required this.name, required this.url});
|
||||
const GameVersion({
|
||||
required this.name,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, url];
|
||||
|
@ -10,9 +10,8 @@ class GameVersionDropdown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocConsumer<VanillaBloc, VanillaState>(
|
||||
listener: (_, __) {},
|
||||
builder:
|
||||
(_, state) => DropdownMenu(
|
||||
listener: (_, __) {},
|
||||
builder: (_, state) => DropdownMenu(
|
||||
initialSelection: state.selectedGameVersion,
|
||||
enabled: state.gameVersions.isNotEmpty,
|
||||
requestFocusOnTap: false,
|
||||
@ -23,13 +22,12 @@ class GameVersionDropdown extends StatelessWidget {
|
||||
context.read<VanillaBloc>().add(VanillaGameVersionSelectedEvent(value));
|
||||
}
|
||||
},
|
||||
dropdownMenuEntries:
|
||||
state.gameVersions
|
||||
.map(
|
||||
(gameVersion) =>
|
||||
DropdownMenuEntry<GameVersionViewModel>(value: gameVersion, label: gameVersion.name),
|
||||
)
|
||||
.toList(),
|
||||
dropdownMenuEntries: state.gameVersions
|
||||
.map((gameVersion) => DropdownMenuEntry<GameVersionViewModel>(
|
||||
value: gameVersion,
|
||||
label: gameVersion.name,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
49
pubspec.lock
49
pubspec.lock
@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -73,6 +81,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -94,11 +118,24 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
gap:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -344,6 +381,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -353,5 +398,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
sdks:
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
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
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
sdk: ">=3.6.0 <4.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@ -35,6 +35,7 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
equatable: ^2.0.7
|
||||
file_picker: ^10.2.0
|
||||
flutter_bloc: ^9.1.1
|
||||
gap: ^3.0.1
|
||||
http: ^1.4.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user