From 8d89246b1a0f59cc966edfdaa0ce86f02448e364 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Fri, 11 Jul 2025 01:28:36 +0800 Subject: [PATCH] MCSI-3 feat: range slider --- .../presentation/installation_bloc.dart | 11 +++ .../presentation/installation_state.dart | 19 ++++- .../presentation/range_view_model.dart | 19 +++++ .../framework/ui/basic_configuration_tab.dart | 73 +++++++++++++++++++ lib/main/framework/ui/strings.dart | 3 + 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 lib/main/adapter/presentation/range_view_model.dart diff --git a/lib/main/adapter/presentation/installation_bloc.dart b/lib/main/adapter/presentation/installation_bloc.dart index a6c5701..f57dec1 100644 --- a/lib/main/adapter/presentation/installation_bloc.dart +++ b/lib/main/adapter/presentation/installation_bloc.dart @@ -3,6 +3,7 @@ import 'dart:io'; 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/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/main/application/use_case/download_file_use_case.dart'; import 'package:minecraft_server_installer/main/application/use_case/grant_file_permission_use_case.dart'; import 'package:minecraft_server_installer/main/application/use_case/write_file_use_case.dart'; @@ -59,10 +60,16 @@ class InstallationBloc extends Bloc { }); on((event, emit) { + if (event.customRamSize != null && !event.customRamSize!.isValid) { + return; + } + final newState = state.copyWith( gameVersion: event.gameVersion, savePath: event.savePath, isEulaAgreed: event.isEulaAgreed, + isCustomRamSizeEnabled: event.isCustomRamSizeEnabled, + customRamSize: event.customRamSize, ); emit(newState); }); @@ -83,10 +90,14 @@ class InstallationConfigurationUpdatedEvent extends InstallationEvent { final GameVersionViewModel? gameVersion; final String? savePath; final bool? isEulaAgreed; + final bool? isCustomRamSizeEnabled; + final RangeViewModel? customRamSize; InstallationConfigurationUpdatedEvent({ this.gameVersion, this.savePath, this.isEulaAgreed, + this.isCustomRamSizeEnabled, + this.customRamSize, }); } diff --git a/lib/main/adapter/presentation/installation_state.dart b/lib/main/adapter/presentation/installation_state.dart index 977b558..4a0a8c2 100644 --- a/lib/main/adapter/presentation/installation_state.dart +++ b/lib/main/adapter/presentation/installation_state.dart @@ -1,11 +1,16 @@ import 'package:equatable/equatable.dart'; import 'package:minecraft_server_installer/main/adapter/presentation/progress_view_model.dart'; +import 'package:minecraft_server_installer/main/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart'; class InstallationState with EquatableMixin { + static const _defaultRamSize = RangeViewModel(min: 2048, max: 2048); + final GameVersionViewModel? gameVersion; final String? savePath; final bool isEulaAgreed; + final bool isCustomRamSizeEnabled; + final RangeViewModel? _customRamSize; final ProgressViewModel downloadProgress; final bool isLocked; @@ -13,15 +18,19 @@ class InstallationState with EquatableMixin { required this.gameVersion, required this.savePath, required this.isEulaAgreed, + required this.isCustomRamSizeEnabled, + required RangeViewModel? customRamSize, required this.downloadProgress, required this.isLocked, - }); + }) : _customRamSize = customRamSize; const InstallationState.empty() : this( gameVersion: null, savePath: null, isEulaAgreed: false, + isCustomRamSizeEnabled: false, + customRamSize: _defaultRamSize, downloadProgress: const ProgressViewModel.zero(), isLocked: false, ); @@ -31,6 +40,8 @@ class InstallationState with EquatableMixin { gameVersion, savePath, isEulaAgreed, + isCustomRamSizeEnabled, + _customRamSize, downloadProgress, isLocked, ]; @@ -39,6 +50,8 @@ class InstallationState with EquatableMixin { GameVersionViewModel? gameVersion, String? savePath, bool? isEulaAgreed, + bool? isCustomRamSizeEnabled, + RangeViewModel? customRamSize, ProgressViewModel? downloadProgress, bool? isLocked, }) => @@ -46,10 +59,14 @@ class InstallationState with EquatableMixin { gameVersion: gameVersion ?? this.gameVersion, savePath: savePath ?? this.savePath, isEulaAgreed: isEulaAgreed ?? this.isEulaAgreed, + isCustomRamSizeEnabled: isCustomRamSizeEnabled ?? this.isCustomRamSizeEnabled, + customRamSize: customRamSize ?? _customRamSize, downloadProgress: downloadProgress ?? this.downloadProgress, isLocked: isLocked ?? this.isLocked, ); + RangeViewModel get ramSize => isCustomRamSizeEnabled ? _customRamSize ?? _defaultRamSize : _defaultRamSize; + bool get isGameVersionSelected => gameVersion != null; bool get isSavePathSelected => savePath != null && savePath!.isNotEmpty; diff --git a/lib/main/adapter/presentation/range_view_model.dart b/lib/main/adapter/presentation/range_view_model.dart new file mode 100644 index 0000000..2aa9a2e --- /dev/null +++ b/lib/main/adapter/presentation/range_view_model.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +class RangeViewModel with EquatableMixin { + final int min; + final int max; + + const RangeViewModel({ + required this.min, + required this.max, + }); + + @override + List get props => [ + min, + max, + ]; + + bool get isValid => min <= max; +} diff --git a/lib/main/framework/ui/basic_configuration_tab.dart b/lib/main/framework/ui/basic_configuration_tab.dart index 8069e03..f38736e 100644 --- a/lib/main/framework/ui/basic_configuration_tab.dart +++ b/lib/main/framework/ui/basic_configuration_tab.dart @@ -4,6 +4,7 @@ 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/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/main/constants.dart'; import 'package:minecraft_server_installer/main/framework/ui/strings.dart'; import 'package:minecraft_server_installer/vanilla/framework/ui/game_version_dropdown.dart'; @@ -20,6 +21,8 @@ class BasicConfigurationTab extends StatelessWidget { _pathBrowsingField, const Gap(16), _eulaCheckbox, + _enableCustomRamSizeCheckbox, + _customRamSizeControl, const Spacer(), _bottomControl, ], @@ -79,6 +82,76 @@ class BasicConfigurationTab extends StatelessWidget { ], ); + Widget get _enableCustomRamSizeCheckbox => BlocConsumer( + listener: (_, __) {}, + builder: (context, state) => CheckboxListTile( + title: const Text(Strings.fieldCustomRamSize), + value: state.isCustomRamSizeEnabled, + onChanged: (value) => context + .read() + .add(InstallationConfigurationUpdatedEvent(isCustomRamSizeEnabled: value ?? false)), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + ); + + Widget get _customRamSizeControl => BlocConsumer( + listener: (_, __) {}, + builder: (context, state) { + if (!state.isCustomRamSizeEnabled) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + RangeSlider( + values: RangeValues(state.ramSize.min.toDouble(), state.ramSize.max.toDouble()), + min: 512, + max: 16384, + divisions: (16384 - 512) ~/ 128, + labels: RangeLabels( + '${state.ramSize.min} MB', + '${state.ramSize.max} MB', + ), + onChanged: (values) => context.read().add( + InstallationConfigurationUpdatedEvent( + customRamSize: RangeViewModel(min: values.start.toInt(), max: values.end.toInt()), + ), + ), + ), + Row( + children: [ + Expanded( + child: TextField( + controller: TextEditingController(text: state.ramSize.min.toString()), + canRequestFocus: false, + readOnly: true, + decoration: InputDecoration( + label: const Text('${Strings.labelMinRamSize} (MB)'), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + ), + ), + ), + const Gap(16), + Expanded( + child: TextField( + controller: TextEditingController(text: state.ramSize.max.toString()), + canRequestFocus: false, + readOnly: true, + decoration: InputDecoration( + label: const Text('${Strings.labelMaxRamSize} (MB)'), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + ), + ), + ), + ], + ) + ], + ); + }, + ); + Widget get _bottomControl => BlocConsumer( listener: (_, __) {}, builder: (context, state) => Row( diff --git a/lib/main/framework/ui/strings.dart b/lib/main/framework/ui/strings.dart index 56292f6..8210f2d 100644 --- a/lib/main/framework/ui/strings.dart +++ b/lib/main/framework/ui/strings.dart @@ -2,8 +2,11 @@ abstract class Strings { static const fieldGameVersion = '遊戲版本'; static const fieldPath = '安裝路徑'; static const fieldEula = '我同意 EULA 條款'; + static const fieldCustomRamSize = '啟用自定義 RAM 大小'; static const buttonStartToInstall = '開始安裝'; static const buttonBrowse = '瀏覽'; + static const labelMinRamSize = '最小 RAM 大小'; + static const labelMaxRamSize = '最大 RAM 大小'; static const tooltipEulaInfo = '點擊查看 EULA 條款'; static const dialogTitleSelectDirectory = '選擇安裝目錄'; }