Compare commits
No commits in common. "3bc9ecacdc1d950bc29df75eddf7fd854923f75e" and "9e5a2eb8158a62de87f00483dbadee3ffc9710ac" have entirely different histories.
3bc9ecacdc
...
9e5a2eb815
@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
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/installation_state.dart';
|
||||||
import 'package:minecraft_server_installer/main/adapter/presentation/progress_view_model.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/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/grant_file_permission_use_case.dart';
|
||||||
import 'package:minecraft_server_installer/main/application/use_case/write_file_use_case.dart';
|
import 'package:minecraft_server_installer/main/application/use_case/write_file_use_case.dart';
|
||||||
@ -34,8 +33,9 @@ class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final startScriptFilePath = path.join(savePath, Constants.startScriptFileName);
|
final startScriptFilePath = path.join(savePath, Constants.startScriptFileName);
|
||||||
final startScriptContent =
|
final startScriptContent = Platform.isWindows
|
||||||
'java -Xmx${state.ramSize.max}M -Xms${state.ramSize.min}M -jar ${Platform.isWindows ? '.${Constants.serverFileName}\r\n' : './${Constants.serverFileName}\n'}';
|
? 'java -jar .\\${Constants.serverFileName}\r\n'
|
||||||
|
: 'java -jar ./${Constants.serverFileName}\n';
|
||||||
await writeFileUseCase(startScriptFilePath, startScriptContent);
|
await writeFileUseCase(startScriptFilePath, startScriptContent);
|
||||||
await grantFilePermissionUseCase(startScriptFilePath);
|
await grantFilePermissionUseCase(startScriptFilePath);
|
||||||
|
|
||||||
@ -59,16 +59,10 @@ class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
on<InstallationConfigurationUpdatedEvent>((event, emit) {
|
on<InstallationConfigurationUpdatedEvent>((event, emit) {
|
||||||
if (event.customRamSize != null && !event.customRamSize!.isValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final newState = state.copyWith(
|
final newState = state.copyWith(
|
||||||
gameVersion: event.gameVersion,
|
gameVersion: event.gameVersion,
|
||||||
savePath: event.savePath,
|
savePath: event.savePath,
|
||||||
isEulaAgreed: event.isEulaAgreed,
|
isEulaAgreed: event.isEulaAgreed,
|
||||||
isCustomRamSizeEnabled: event.isCustomRamSizeEnabled,
|
|
||||||
customRamSize: event.customRamSize,
|
|
||||||
);
|
);
|
||||||
emit(newState);
|
emit(newState);
|
||||||
});
|
});
|
||||||
@ -89,14 +83,10 @@ class InstallationConfigurationUpdatedEvent extends InstallationEvent {
|
|||||||
final GameVersionViewModel? gameVersion;
|
final GameVersionViewModel? gameVersion;
|
||||||
final String? savePath;
|
final String? savePath;
|
||||||
final bool? isEulaAgreed;
|
final bool? isEulaAgreed;
|
||||||
final bool? isCustomRamSizeEnabled;
|
|
||||||
final RangeViewModel? customRamSize;
|
|
||||||
|
|
||||||
InstallationConfigurationUpdatedEvent({
|
InstallationConfigurationUpdatedEvent({
|
||||||
this.gameVersion,
|
this.gameVersion,
|
||||||
this.savePath,
|
this.savePath,
|
||||||
this.isEulaAgreed,
|
this.isEulaAgreed,
|
||||||
this.isCustomRamSizeEnabled,
|
|
||||||
this.customRamSize,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
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/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';
|
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart';
|
||||||
|
|
||||||
class InstallationState with EquatableMixin {
|
class InstallationState with EquatableMixin {
|
||||||
static const _defaultRamSize = RangeViewModel(min: 2048, max: 2048);
|
|
||||||
|
|
||||||
final GameVersionViewModel? gameVersion;
|
final GameVersionViewModel? gameVersion;
|
||||||
final String? savePath;
|
final String? savePath;
|
||||||
final bool isEulaAgreed;
|
final bool isEulaAgreed;
|
||||||
final bool isCustomRamSizeEnabled;
|
|
||||||
final RangeViewModel? _customRamSize;
|
|
||||||
final ProgressViewModel downloadProgress;
|
final ProgressViewModel downloadProgress;
|
||||||
final bool isLocked;
|
final bool isLocked;
|
||||||
|
|
||||||
@ -18,19 +13,15 @@ class InstallationState with EquatableMixin {
|
|||||||
required this.gameVersion,
|
required this.gameVersion,
|
||||||
required this.savePath,
|
required this.savePath,
|
||||||
required this.isEulaAgreed,
|
required this.isEulaAgreed,
|
||||||
required this.isCustomRamSizeEnabled,
|
|
||||||
required RangeViewModel? customRamSize,
|
|
||||||
required this.downloadProgress,
|
required this.downloadProgress,
|
||||||
required this.isLocked,
|
required this.isLocked,
|
||||||
}) : _customRamSize = customRamSize;
|
});
|
||||||
|
|
||||||
const InstallationState.empty()
|
const InstallationState.empty()
|
||||||
: this(
|
: this(
|
||||||
gameVersion: null,
|
gameVersion: null,
|
||||||
savePath: null,
|
savePath: null,
|
||||||
isEulaAgreed: false,
|
isEulaAgreed: false,
|
||||||
isCustomRamSizeEnabled: false,
|
|
||||||
customRamSize: _defaultRamSize,
|
|
||||||
downloadProgress: const ProgressViewModel.zero(),
|
downloadProgress: const ProgressViewModel.zero(),
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
);
|
);
|
||||||
@ -40,8 +31,6 @@ class InstallationState with EquatableMixin {
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
savePath,
|
savePath,
|
||||||
isEulaAgreed,
|
isEulaAgreed,
|
||||||
isCustomRamSizeEnabled,
|
|
||||||
_customRamSize,
|
|
||||||
downloadProgress,
|
downloadProgress,
|
||||||
isLocked,
|
isLocked,
|
||||||
];
|
];
|
||||||
@ -50,8 +39,6 @@ class InstallationState with EquatableMixin {
|
|||||||
GameVersionViewModel? gameVersion,
|
GameVersionViewModel? gameVersion,
|
||||||
String? savePath,
|
String? savePath,
|
||||||
bool? isEulaAgreed,
|
bool? isEulaAgreed,
|
||||||
bool? isCustomRamSizeEnabled,
|
|
||||||
RangeViewModel? customRamSize,
|
|
||||||
ProgressViewModel? downloadProgress,
|
ProgressViewModel? downloadProgress,
|
||||||
bool? isLocked,
|
bool? isLocked,
|
||||||
}) =>
|
}) =>
|
||||||
@ -59,14 +46,10 @@ class InstallationState with EquatableMixin {
|
|||||||
gameVersion: gameVersion ?? this.gameVersion,
|
gameVersion: gameVersion ?? this.gameVersion,
|
||||||
savePath: savePath ?? this.savePath,
|
savePath: savePath ?? this.savePath,
|
||||||
isEulaAgreed: isEulaAgreed ?? this.isEulaAgreed,
|
isEulaAgreed: isEulaAgreed ?? this.isEulaAgreed,
|
||||||
isCustomRamSizeEnabled: isCustomRamSizeEnabled ?? this.isCustomRamSizeEnabled,
|
|
||||||
customRamSize: customRamSize ?? _customRamSize,
|
|
||||||
downloadProgress: downloadProgress ?? this.downloadProgress,
|
downloadProgress: downloadProgress ?? this.downloadProgress,
|
||||||
isLocked: isLocked ?? this.isLocked,
|
isLocked: isLocked ?? this.isLocked,
|
||||||
);
|
);
|
||||||
|
|
||||||
RangeViewModel get ramSize => isCustomRamSizeEnabled ? _customRamSize ?? _defaultRamSize : _defaultRamSize;
|
|
||||||
|
|
||||||
bool get isGameVersionSelected => gameVersion != null;
|
bool get isGameVersionSelected => gameVersion != null;
|
||||||
|
|
||||||
bool get isSavePathSelected => savePath != null && savePath!.isNotEmpty;
|
bool get isSavePathSelected => savePath != null && savePath!.isNotEmpty;
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
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<Object?> get props => [
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
];
|
|
||||||
|
|
||||||
bool get isValid => min <= max;
|
|
||||||
}
|
|
@ -1,11 +1,10 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
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_bloc.dart';
|
||||||
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.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/constants.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/framework/ui/game_version_dropdown.dart';
|
import 'package:minecraft_server_installer/vanilla/framework/ui/game_version_dropdown.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@ -18,46 +17,14 @@ class BasicConfigurationTab extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const GameVersionDropdown(),
|
const GameVersionDropdown(),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
_pathBrowsingField,
|
const PathBrowsingField(),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
_eulaCheckbox,
|
_eulaCheckbox,
|
||||||
_enableCustomRamSizeCheckbox,
|
|
||||||
_customRamSizeControl,
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_bottomControl,
|
_bottomControl,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget get _pathBrowsingField => BlocConsumer<InstallationBloc, InstallationState>(
|
|
||||||
listener: (_, __) {},
|
|
||||||
builder: (context, state) => Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: TextEditingController(text: state.savePath ?? ''),
|
|
||||||
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(context, initialPath: state.savePath),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
|
||||||
),
|
|
||||||
child: const Text(Strings.buttonBrowse),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _eulaCheckbox => Row(
|
Widget get _eulaCheckbox => Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -82,76 +49,6 @@ class BasicConfigurationTab extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget get _enableCustomRamSizeCheckbox => BlocConsumer<InstallationBloc, InstallationState>(
|
|
||||||
listener: (_, __) {},
|
|
||||||
builder: (context, state) => CheckboxListTile(
|
|
||||||
title: const Text(Strings.fieldCustomRamSize),
|
|
||||||
value: state.isCustomRamSizeEnabled,
|
|
||||||
onChanged: (value) => context
|
|
||||||
.read<InstallationBloc>()
|
|
||||||
.add(InstallationConfigurationUpdatedEvent(isCustomRamSizeEnabled: value ?? false)),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _customRamSizeControl => BlocConsumer<InstallationBloc, InstallationState>(
|
|
||||||
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<InstallationBloc>().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<InstallationBloc, InstallationState>(
|
Widget get _bottomControl => BlocConsumer<InstallationBloc, InstallationState>(
|
||||||
listener: (_, __) {},
|
listener: (_, __) {},
|
||||||
builder: (context, state) => Row(
|
builder: (context, state) => Row(
|
||||||
@ -174,22 +71,6 @@ class BasicConfigurationTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _browseDirectory(BuildContext context, {String? initialPath}) async {
|
|
||||||
final hasInitialPath = initialPath?.isNotEmpty ?? false;
|
|
||||||
final directory = await FilePicker.platform.getDirectoryPath(
|
|
||||||
dialogTitle: Strings.dialogTitleSelectDirectory,
|
|
||||||
initialDirectory: hasInitialPath ? initialPath : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context.mounted || directory == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<InstallationBloc>().add(InstallationConfigurationUpdatedEvent(
|
|
||||||
savePath: directory,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _downloadServerFile(BuildContext context) {
|
void _downloadServerFile(BuildContext context) {
|
||||||
context.read<InstallationBloc>().add((InstallationStartedEvent()));
|
context.read<InstallationBloc>().add((InstallationStartedEvent()));
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,8 @@ abstract class Strings {
|
|||||||
static const fieldGameVersion = '遊戲版本';
|
static const fieldGameVersion = '遊戲版本';
|
||||||
static const fieldPath = '安裝路徑';
|
static const fieldPath = '安裝路徑';
|
||||||
static const fieldEula = '我同意 EULA 條款';
|
static const fieldEula = '我同意 EULA 條款';
|
||||||
static const fieldCustomRamSize = '啟用自定義 RAM 大小';
|
|
||||||
static const buttonStartToInstall = '開始安裝';
|
static const buttonStartToInstall = '開始安裝';
|
||||||
static const buttonBrowse = '瀏覽';
|
static const buttonBrowse = '瀏覽';
|
||||||
static const labelMinRamSize = '最小 RAM 大小';
|
|
||||||
static const labelMaxRamSize = '最大 RAM 大小';
|
|
||||||
static const tooltipEulaInfo = '點擊查看 EULA 條款';
|
static const tooltipEulaInfo = '點擊查看 EULA 條款';
|
||||||
static const dialogTitleSelectDirectory = '選擇安裝目錄';
|
static const dialogTitleSelectDirectory = '選擇安裝目錄';
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user