Compare commits

..

6 Commits

28 changed files with 741 additions and 71 deletions

View File

@ -3,22 +3,22 @@ import 'package:minecraft_server_installer/main/adapter/gateway/installation_fil
import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart'; import 'package:minecraft_server_installer/main/application/repository/installation_repository.dart';
class InstallationRepositoryImpl implements InstallationRepository { class InstallationRepositoryImpl implements InstallationRepository {
final InstallationApiService _apiService; final InstallationApiService _installationApiService;
final InstallationFileStorage _fileStorage; final InstallationFileStorage _installationFileStorage;
InstallationRepositoryImpl(this._apiService, this._fileStorage); InstallationRepositoryImpl(this._installationApiService, this._installationFileStorage);
@override @override
Future<void> downloadFile(Uri url, String path, {DownloadProgressCallback? onProgressChanged}) async { Future<void> downloadFile(Uri url, String path, {DownloadProgressCallback? onProgressChanged}) async {
final fileBytes = await _apiService.fetchRemoteFile(url, onProgressChanged: onProgressChanged); final fileBytes = await _installationApiService.fetchRemoteFile(url, onProgressChanged: onProgressChanged);
await _fileStorage.saveFile(fileBytes, path); await _installationFileStorage.saveFile(fileBytes, path);
} }
@override @override
Future<void> writeFile(String path, String content) => _fileStorage.writeFile(path, content); Future<void> writeFile(String path, String content) => _installationFileStorage.writeFile(path, content);
@override @override
Future<void> grantFileExecutePermission(String path) { Future<void> grantFileExecutePermission(String path) {
return _fileStorage.grantFileExecutePermission(path); return _installationFileStorage.grantFileExecutePermission(path);
} }
} }

View File

@ -1,45 +1,32 @@
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/presenter/installation_state.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/progress_view_model.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/progress_view_model.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/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/install_server_use_case.dart';
import 'package:minecraft_server_installer/main/application/use_case/grant_file_permission_use_case.dart'; import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_view_model.dart';
import 'package:minecraft_server_installer/main/application/use_case/write_file_use_case.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/game_version_view_model.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> { class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
InstallationBloc( InstallationBloc(
DownloadFileUseCase downloadFileUseCase, InstallServerUseCase installServerUseCase,
WriteFileUseCase writeFileUseCase,
GrantFilePermissionUseCase grantFilePermissionUseCase,
) : super(const InstallationState.empty()) { ) : super(const InstallationState.empty()) {
on<InstallationStartedEvent>((_, emit) async { on<InstallationStartedEvent>((event, emit) async {
if (!state.canStartToInstall) { if (!state.canStartToInstall) {
return; return;
} }
final gameVersion = state.gameVersion!;
final savePath = state.savePath!;
emit(state.copyWith(isLocked: true, downloadProgress: const ProgressViewModel.start())); emit(state.copyWith(isLocked: true, downloadProgress: const ProgressViewModel.start()));
await downloadFileUseCase( await installServerUseCase(
gameVersion.url, gameVersion: state.gameVersion!.toEntity(),
path.join(savePath, Constants.serverFileName), savePath: state.savePath!,
maxRam: state.ramSize.max,
minRam: state.ramSize.min,
isGuiEnabled: state.isGuiEnabled,
serverProperties: event.serverProperties.toEntity(),
onProgressChanged: (progressValue) => add(_InstallationProgressValueChangedEvent(progressValue)), onProgressChanged: (progressValue) => add(_InstallationProgressValueChangedEvent(progressValue)),
); );
final startScriptFilePath = path.join(savePath, Constants.startScriptFileName);
final serverFilePath = path.join('.', Constants.serverFileName);
final startScriptContent =
'java -Xmx${state.ramSize.max}M -Xms${state.ramSize.min}M -jar $serverFilePath ${state.isGuiEnabled ? '' : 'nogui'}';
await writeFileUseCase(startScriptFilePath, startScriptContent);
await grantFilePermissionUseCase(startScriptFilePath);
await writeFileUseCase(path.join(savePath, Constants.eulaFileName), Constants.eulaFileContent);
emit(state.copyWith(isLocked: false, downloadProgress: const ProgressViewModel.complete())); emit(state.copyWith(isLocked: false, downloadProgress: const ProgressViewModel.complete()));
}); });
@ -77,7 +64,11 @@ class InstallationBloc extends Bloc<InstallationEvent, InstallationState> {
sealed class InstallationEvent {} sealed class InstallationEvent {}
class InstallationStartedEvent extends InstallationEvent {} class InstallationStartedEvent extends InstallationEvent {
final ServerPropertiesViewModel serverProperties;
InstallationStartedEvent(this.serverProperties);
}
class _InstallationProgressValueChangedEvent extends InstallationEvent { class _InstallationProgressValueChangedEvent extends InstallationEvent {
final double progressValue; final double progressValue;

View File

@ -1,7 +1,7 @@
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/presenter/progress_view_model.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/range_view_model.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/game_version_view_model.dart';
class InstallationState with EquatableMixin { class InstallationState with EquatableMixin {
static const _defaultRamSize = RangeViewModel(min: 2048, max: 2048); static const _defaultRamSize = RangeViewModel(min: 2048, max: 2048);

View File

@ -0,0 +1,53 @@
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';
import 'package:minecraft_server_installer/main/constants.dart';
import 'package:minecraft_server_installer/properties/application/use_case/write_server_properties_use_case.dart';
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
import 'package:minecraft_server_installer/vanilla/domain/entity/game_version.dart';
import 'package:path/path.dart' as path;
class InstallServerUseCase {
final DownloadFileUseCase _downloadFileUseCase;
final WriteFileUseCase _writeFileUseCase;
final GrantFilePermissionUseCase _grantFilePermissionUseCase;
final WriteServerPropertiesUseCase _writeServerPropertiesUseCase;
InstallServerUseCase(
this._downloadFileUseCase,
this._writeFileUseCase,
this._grantFilePermissionUseCase,
this._writeServerPropertiesUseCase,
);
Future<void> call({
required GameVersion gameVersion,
required String savePath,
required int maxRam,
required int minRam,
required bool isGuiEnabled,
required ServerProperties serverProperties,
required void Function(double) onProgressChanged,
}) async {
// 1. Download server file
await _downloadFileUseCase(
gameVersion.url,
path.join(savePath, Constants.serverFileName),
onProgressChanged: onProgressChanged,
);
// 2. Write start script
final startScriptFilePath = path.join(savePath, Constants.startScriptFileName);
final serverFilePath = path.join('.', Constants.serverFileName);
final startScriptContent = 'java -Xmx${maxRam}M -Xms${minRam}M -jar $serverFilePath ${isGuiEnabled ? '' : 'nogui'}';
await _writeFileUseCase(startScriptFilePath, startScriptContent);
await _grantFilePermissionUseCase(startScriptFilePath);
// 3. Write EULA file
final eulaFilePath = path.join(savePath, Constants.eulaFileName);
await _writeFileUseCase(eulaFilePath, Constants.eulaFileContent);
// 4. Write server.properties file
await _writeServerPropertiesUseCase(serverProperties, savePath);
}
}

View File

@ -2,11 +2,12 @@ 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/presenter/installation_bloc.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/installation_state.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/installation_state.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/range_view_model.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/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/strings.dart'; import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_bloc.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';
@ -209,6 +210,6 @@ class BasicConfigurationTab extends StatelessWidget {
} }
void _downloadServerFile(BuildContext context) { void _downloadServerFile(BuildContext context) {
context.read<InstallationBloc>().add((InstallationStartedEvent())); context.read<InstallationBloc>().add((InstallationStartedEvent(context.read<ServerPropertiesBloc>().state)));
} }
} }

View File

@ -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:minecraft_server_installer/main/adapter/gateway/installation_repository_impl.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/presenter/installation_bloc.dart';
import 'package:minecraft_server_installer/main/adapter/presentation/navigation_bloc.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/navigation_bloc.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/install_server_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';
import 'package:minecraft_server_installer/main/constants.dart'; import 'package:minecraft_server_installer/main/constants.dart';
import 'package:minecraft_server_installer/main/framework/api/installation_api_service_impl.dart'; import 'package:minecraft_server_installer/main/framework/api/installation_api_service_impl.dart';
@ -12,8 +13,13 @@ import 'package:minecraft_server_installer/main/framework/storage/installation_f
import 'package:minecraft_server_installer/main/framework/ui/about_tab.dart'; import 'package:minecraft_server_installer/main/framework/ui/about_tab.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/main/framework/ui/side_navigation_bar.dart'; import 'package:minecraft_server_installer/main/framework/ui/side_navigation_bar.dart';
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_repository_impl.dart';
import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_bloc.dart';
import 'package:minecraft_server_installer/properties/application/use_case/write_server_properties_use_case.dart';
import 'package:minecraft_server_installer/properties/framework/storage/server_properties_file_storage_impl.dart';
import 'package:minecraft_server_installer/properties/framework/ui/server_properties_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/presenter/vanilla_bloc.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';
@ -22,17 +28,27 @@ class MinecraftServerInstaller extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final gameVersionApiService = VanillaApiServiceImpl();
final gameVersionRepository = VanillaRepositoryImpl(gameVersionApiService);
final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository);
final serverPropertiesFileStorage = ServerPropertiesFileStorageImpl();
final serverPropertiesRepository = ServerPropertiesRepositoryImpl(serverPropertiesFileStorage);
final writeServerPropertiesUseCase = WriteServerPropertiesUseCase(serverPropertiesRepository);
final installationApiService = InstallationApiServiceImpl(); final installationApiService = InstallationApiServiceImpl();
final installationFileStorage = InstallationFileStorageImpl(); final installationFileStorage = InstallationFileStorageImpl();
final installationRepository = InstallationRepositoryImpl(installationApiService, installationFileStorage); final installationRepository = InstallationRepositoryImpl(installationApiService, installationFileStorage);
final gameVersionApiService = VanillaApiServiceImpl();
final gameVersionRepository = VanillaRepositoryImpl(gameVersionApiService);
final downloadFileUseCase = DownloadFileUseCase(installationRepository); final downloadFileUseCase = DownloadFileUseCase(installationRepository);
final writeFileUseCase = WriteFileUseCase(installationRepository); final writeFileUseCase = WriteFileUseCase(installationRepository);
final grantFilePermissionUseCase = GrantFilePermissionUseCase(installationRepository); final grantFilePermissionUseCase = GrantFilePermissionUseCase(installationRepository);
final getGameVersionListUseCase = GetGameVersionListUseCase(gameVersionRepository);
final installServerUseCase = InstallServerUseCase(
downloadFileUseCase,
writeFileUseCase,
grantFilePermissionUseCase,
writeServerPropertiesUseCase,
);
return MaterialApp( return MaterialApp(
title: Constants.appName, title: Constants.appName,
@ -45,16 +61,13 @@ class MinecraftServerInstaller extends StatelessWidget {
home: MultiBlocProvider( home: MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => NavigationBloc()), BlocProvider(create: (_) => NavigationBloc()),
BlocProvider( BlocProvider(create: (_) => ServerPropertiesBloc()),
create: (_) => InstallationBloc(
downloadFileUseCase,
writeFileUseCase,
grantFilePermissionUseCase,
),
),
BlocProvider<VanillaBloc>( BlocProvider<VanillaBloc>(
create: (_) => VanillaBloc(getGameVersionListUseCase)..add(VanillaGameVersionListLoadedEvent()), create: (_) => VanillaBloc(getGameVersionListUseCase)..add(VanillaGameVersionListLoadedEvent()),
), ),
BlocProvider(
create: (_) => InstallationBloc(installServerUseCase),
),
], ],
child: Scaffold( child: Scaffold(
body: Row( body: Row(
@ -111,21 +124,24 @@ class MinecraftServerInstaller extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: _tabContent(state), child: state.tabContent,
), ),
), ),
], ],
), ),
), ),
); );
}
Widget _tabContent(NavigationItem navigationItem) { extension _NavigationItemExtension on NavigationItem {
switch (navigationItem) { Widget get tabContent {
switch (this) {
case NavigationItem.basicConfiguration: case NavigationItem.basicConfiguration:
return const BasicConfigurationTab(); return const BasicConfigurationTab();
case NavigationItem.modConfiguration: case NavigationItem.modConfiguration:
case NavigationItem.serverProperties:
return const Placeholder(); return const Placeholder();
case NavigationItem.serverProperties:
return const ServerPropertiesTab();
case NavigationItem.about: case NavigationItem.about:
return const AboutTab(); return const AboutTab();
} }

View File

@ -1,7 +1,7 @@
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/navigation_bloc.dart'; import 'package:minecraft_server_installer/main/adapter/presenter/navigation_bloc.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/strings.dart'; import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -49,7 +49,7 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
collapsedKey: const ValueKey('collapsedTitle'), collapsedKey: const ValueKey('collapsedTitle'),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium .titleSmall
?.copyWith(fontWeight: FontWeight.w900, color: Colors.blueGrey.shade900), ?.copyWith(fontWeight: FontWeight.w900, color: Colors.blueGrey.shade900),
), ),
Material( Material(
@ -83,6 +83,7 @@ class _SideNavigationBarState extends State<SideNavigationBar> {
future: PackageInfo.fromPlatform(), future: PackageInfo.fromPlatform(),
builder: (context, snapshot) => _animatedText( builder: (context, snapshot) => _animatedText(
text: 'Version ${snapshot.data?.version ?? ''}', text: 'Version ${snapshot.data?.version ?? ''}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade700),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
expandedKey: const ValueKey('expandedVersion'), expandedKey: const ValueKey('expandedVersion'),
collapsedKey: const ValueKey('collapsedVersion'), collapsedKey: const ValueKey('collapsedVersion'),

View File

@ -6,6 +6,15 @@ abstract class Strings {
static const fieldCustomRamSize = '啟用自定義 RAM 大小'; static const fieldCustomRamSize = '啟用自定義 RAM 大小';
static const fieldMinRamSize = '最小 RAM 大小'; static const fieldMinRamSize = '最小 RAM 大小';
static const fieldMaxRamSize = '最大 RAM 大小'; static const fieldMaxRamSize = '最大 RAM 大小';
static const fieldServerPort = '伺服器連接埠';
static const fieldMaxPlayers = '玩家人數上限';
static const fieldSpawnProtection = '重生點保護';
static const fieldViewDistance = '最大視野距離';
static const fieldPvp = '玩家間傷害';
static const fieldGameMode = '預設遊戲模式';
static const fieldDifficulty = '遊戲難度';
static const fieldEnableCommandBlock = '啟用指令方塊';
static const fieldMotd = '伺服器描述';
static const buttonStartToInstall = '開始安裝'; static const buttonStartToInstall = '開始安裝';
static const buttonBrowse = '瀏覽'; static const buttonBrowse = '瀏覽';
static const buttonTutorialVideo = '教學影片'; static const buttonTutorialVideo = '教學影片';
@ -18,7 +27,20 @@ abstract class Strings {
static const tabAbout = '關於與說明'; static const tabAbout = '關於與說明';
static const tabInstallationProgress = '安裝進度'; static const tabInstallationProgress = '安裝進度';
static const tooltipEulaInfo = '點擊查看 EULA 條款'; static const tooltipEulaInfo = '點擊查看 EULA 條款';
static const tooltipResetToDefault = '重置為預設值';
static const tooltipRestoreChanges = '取消變更';
static const tooltipApplyChanges = '套用變更';
static const dialogTitleSelectDirectory = '選擇安裝目錄'; static const dialogTitleSelectDirectory = '選擇安裝目錄';
static const gamemodeSurvival = '生存';
static const gamemodeCreative = '創造';
static const gamemodeAdventure = '冒險';
static const gamemodeSpectator = '觀察者';
static const difficultyPeaceful = '和平';
static const difficultyEasy = '簡單';
static const difficultyNormal = '普通';
static const difficultyHard = '困難';
static const textEnable = '啟用';
static const textDisable = '禁用';
static const textSlogen = '讓 Minecraft 伺服器安裝變得更簡單!'; static const textSlogen = '讓 Minecraft 伺服器安裝變得更簡單!';
static const textCopyright = 'Copyright © 2025 SquidSpirit'; static const textCopyright = 'Copyright © 2025 SquidSpirit';
} }

View File

@ -0,0 +1,86 @@
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
import 'package:minecraft_server_installer/properties/domain/enum/difficulty.dart';
import 'package:minecraft_server_installer/properties/domain/enum/game_mode.dart';
class ServerPropertiesDto {
final int serverPort;
final int maxPlayers;
final int spawnProtection;
final int viewDistance;
final bool pvp;
final String gameMode;
final String difficulty;
final bool enableCommandBlock;
final bool onlineMode;
final String motd;
const ServerPropertiesDto({
required this.serverPort,
required this.maxPlayers,
required this.spawnProtection,
required this.viewDistance,
required this.pvp,
required this.gameMode,
required this.difficulty,
required this.enableCommandBlock,
required this.onlineMode,
required this.motd,
});
ServerPropertiesDto.fromEntity(ServerProperties serverProperties)
: this(
serverPort: serverProperties.serverPort,
maxPlayers: serverProperties.maxPlayers,
spawnProtection: serverProperties.spawnProtection,
viewDistance: serverProperties.viewDistance,
pvp: serverProperties.pvp,
gameMode: serverProperties.gameMode.value,
difficulty: serverProperties.difficulty.value,
enableCommandBlock: serverProperties.enableCommandBlock,
onlineMode: serverProperties.onlineMode,
motd: serverProperties.motd,
);
Map<String, String> toStringMap() => {
'server-port': serverPort.toString(),
'max-players': maxPlayers.toString(),
'spawn-protection': spawnProtection.toString(),
'view-distance': viewDistance.toString(),
'pvp': pvp.toString(),
'gamemode': gameMode,
'difficulty': difficulty,
'enable-command-block': enableCommandBlock.toString(),
'online-mode': onlineMode.toString(),
'motd': motd,
};
}
extension _GameModeExtension on GameMode {
String get value {
switch (this) {
case GameMode.survival:
return 'survival';
case GameMode.creative:
return 'creative';
case GameMode.adventure:
return 'adventure';
case GameMode.spectator:
return 'spectator';
}
}
}
extension _DifficultyExtension on Difficulty {
String get value {
switch (this) {
case Difficulty.peaceful:
return 'peaceful';
case Difficulty.easy:
return 'easy';
case Difficulty.normal:
return 'normal';
case Difficulty.hard:
return 'hard';
}
}
}

View File

@ -0,0 +1,5 @@
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_dto.dart';
abstract interface class ServerPropertiesFileStorage {
Future<void> writeServerProperties(ServerPropertiesDto serverPropertiesDto, String savePath);
}

View File

@ -0,0 +1,17 @@
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_dto.dart';
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_file_storage.dart';
import 'package:minecraft_server_installer/properties/application/repository/server_properties_repository.dart';
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
class ServerPropertiesRepositoryImpl implements ServerPropertiesRepository {
final ServerPropertiesFileStorage _serverPropertiesFileStorage;
ServerPropertiesRepositoryImpl(this._serverPropertiesFileStorage);
@override
Future<void> writeServerProperties(ServerProperties serverProperties, String savePath) =>
_serverPropertiesFileStorage.writeServerProperties(
ServerPropertiesDto.fromEntity(serverProperties),
savePath,
);
}

View File

@ -0,0 +1,51 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_view_model.dart';
import 'package:minecraft_server_installer/properties/domain/enum/difficulty.dart';
import 'package:minecraft_server_installer/properties/domain/enum/game_mode.dart';
class ServerPropertiesBloc extends Bloc<ServerPropertiesEvent, ServerPropertiesViewModel> {
ServerPropertiesBloc() : super(ServerPropertiesViewModel.defaultValue) {
on<ServerPropertiesUpdatedEvent>((event, emit) => emit(
state.copyWith(
serverPort: event.serverPort ?? state.serverPort,
maxPlayers: event.maxPlayers ?? state.maxPlayers,
spawnProtection: event.spawnProtection ?? state.spawnProtection,
viewDistance: event.viewDistance ?? state.viewDistance,
pvp: event.pvp ?? state.pvp,
gameMode: event.gameMode ?? state.gameMode,
difficulty: event.difficulty ?? state.difficulty,
enableCommandBlock: event.enableCommandBlock ?? state.enableCommandBlock,
onlineMode: event.onlineMode ?? state.onlineMode,
motd: event.motd ?? state.motd,
),
));
}
}
sealed class ServerPropertiesEvent {}
class ServerPropertiesUpdatedEvent extends ServerPropertiesEvent {
final int? serverPort;
final int? maxPlayers;
final int? spawnProtection;
final int? viewDistance;
final bool? pvp;
final GameMode? gameMode;
final Difficulty? difficulty;
final bool? enableCommandBlock;
final bool? onlineMode;
final String? motd;
ServerPropertiesUpdatedEvent({
this.serverPort,
this.maxPlayers,
this.spawnProtection,
this.viewDistance,
this.pvp,
this.gameMode,
this.difficulty,
this.enableCommandBlock,
this.onlineMode,
this.motd,
});
}

View File

@ -0,0 +1,100 @@
import 'package:equatable/equatable.dart';
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
import 'package:minecraft_server_installer/properties/domain/enum/difficulty.dart';
import 'package:minecraft_server_installer/properties/domain/enum/game_mode.dart';
class ServerPropertiesViewModel with EquatableMixin {
final int serverPort;
final int maxPlayers;
final int spawnProtection;
final int viewDistance;
final bool pvp;
final GameMode gameMode;
final Difficulty difficulty;
final bool enableCommandBlock;
final bool onlineMode;
final String motd;
const ServerPropertiesViewModel({
required this.serverPort,
required this.maxPlayers,
required this.spawnProtection,
required this.viewDistance,
required this.pvp,
required this.gameMode,
required this.difficulty,
required this.enableCommandBlock,
required this.onlineMode,
required this.motd,
});
static const defaultValue = ServerPropertiesViewModel(
serverPort: 25565,
maxPlayers: 20,
spawnProtection: 16,
viewDistance: 10,
pvp: true,
gameMode: GameMode.survival,
difficulty: Difficulty.normal,
enableCommandBlock: false,
onlineMode: true,
motd: 'A Minecraft Server',
);
ServerProperties toEntity() => ServerProperties(
serverPort: serverPort,
maxPlayers: maxPlayers,
spawnProtection: spawnProtection,
viewDistance: viewDistance,
pvp: pvp,
gameMode: gameMode,
difficulty: difficulty,
enableCommandBlock: enableCommandBlock,
onlineMode: onlineMode,
motd: motd,
);
ServerPropertiesViewModel copyWith({
int? serverPort,
int? maxPlayers,
int? spawnProtection,
int? viewDistance,
bool? pvp,
GameMode? gameMode,
Difficulty? difficulty,
bool? enableCommandBlock,
bool? onlineMode,
String? motd,
}) =>
ServerPropertiesViewModel(
serverPort: serverPort ?? this.serverPort,
maxPlayers: maxPlayers ?? this.maxPlayers,
spawnProtection: spawnProtection ?? this.spawnProtection,
viewDistance: viewDistance ?? this.viewDistance,
pvp: pvp ?? this.pvp,
gameMode: gameMode ?? this.gameMode,
difficulty: difficulty ?? this.difficulty,
enableCommandBlock: enableCommandBlock ?? this.enableCommandBlock,
onlineMode: onlineMode ?? this.onlineMode,
motd: motd ?? this.motd,
);
@override
List<Object?> get props => [
serverPort,
maxPlayers,
spawnProtection,
viewDistance,
pvp,
gameMode,
difficulty,
enableCommandBlock,
onlineMode,
motd,
];
bool get isServerPortValid => serverPort > 0 && serverPort <= 65535;
bool get isMaxPlayersValid => maxPlayers > 0;
bool get isSpawnProtectionValid => spawnProtection >= 0;
bool get isViewDistanceValid => viewDistance > 0;
}

View File

@ -0,0 +1,5 @@
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
abstract interface class ServerPropertiesRepository {
Future<void> writeServerProperties(ServerProperties serverProperties, String savePath);
}

View File

@ -0,0 +1,11 @@
import 'package:minecraft_server_installer/properties/application/repository/server_properties_repository.dart';
import 'package:minecraft_server_installer/properties/domain/entity/server_properties.dart';
class WriteServerPropertiesUseCase {
final ServerPropertiesRepository _serverPropertiesRepository;
WriteServerPropertiesUseCase(this._serverPropertiesRepository);
Future<void> call(ServerProperties serverProperties, String savePath) =>
_serverPropertiesRepository.writeServerProperties(serverProperties, savePath);
}

View File

@ -0,0 +1,43 @@
import 'package:equatable/equatable.dart';
import 'package:minecraft_server_installer/properties/domain/enum/difficulty.dart';
import 'package:minecraft_server_installer/properties/domain/enum/game_mode.dart';
class ServerProperties with EquatableMixin {
final int serverPort;
final int maxPlayers;
final int spawnProtection;
final int viewDistance;
final bool pvp;
final GameMode gameMode;
final Difficulty difficulty;
final bool enableCommandBlock;
final bool onlineMode;
final String motd;
const ServerProperties({
required this.serverPort,
required this.maxPlayers,
required this.spawnProtection,
required this.viewDistance,
required this.pvp,
required this.gameMode,
required this.difficulty,
required this.enableCommandBlock,
required this.onlineMode,
required this.motd,
});
@override
List<Object?> get props => [
serverPort,
maxPlayers,
spawnProtection,
viewDistance,
pvp,
gameMode,
difficulty,
enableCommandBlock,
onlineMode,
motd,
];
}

View File

@ -0,0 +1,6 @@
enum Difficulty {
peaceful,
easy,
normal,
hard,
}

View File

@ -0,0 +1,6 @@
enum GameMode {
survival,
creative,
adventure,
spectator,
}

View File

@ -0,0 +1,15 @@
import 'dart:io';
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_dto.dart';
import 'package:minecraft_server_installer/properties/adapter/gateway/server_properties_file_storage.dart';
class ServerPropertiesFileStorageImpl implements ServerPropertiesFileStorage {
@override
Future<void> writeServerProperties(ServerPropertiesDto serverPropertiesDto, String savePath) async {
File file = File('$savePath/server.properties');
await file.create(recursive: true);
final propertiesMap = serverPropertiesDto.toStringMap();
await file.writeAsString(propertiesMap.entries.map((e) => '${e.key}=${e.value}').join('\n'));
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:minecraft_server_installer/main/framework/ui/strings.dart';
class InlineTextField extends StatefulWidget {
const InlineTextField({
super.key,
required this.labelText,
required this.defaultValue,
required this.value,
required this.onChanged,
});
final String labelText;
final String defaultValue;
final String value;
final void Function(String) onChanged;
@override
State<InlineTextField> createState() => _InlineTextFieldState();
}
class _InlineTextFieldState extends State<InlineTextField> {
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
bool _isEditing = false;
@override
void initState() {
super.initState();
_textEditingController.text = widget.value;
}
@override
Widget build(BuildContext context) => Row(
children: [
Expanded(
child: TextField(
controller: _textEditingController,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: widget.labelText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
onChanged: (newValue) {
if (newValue != widget.value) {
setState(() => _isEditing = true);
}
},
),
),
if (_isEditing) ...[
const Gap(8),
IconButton.filledTonal(
tooltip: Strings.tooltipRestoreChanges,
onPressed: () {
_focusNode.unfocus();
_textEditingController.text = widget.value;
setState(() => _isEditing = false);
},
icon: const Icon(Icons.cancel_outlined)),
const Gap(8),
IconButton.filled(
tooltip: Strings.tooltipApplyChanges,
onPressed: () {
_focusNode.unfocus();
widget.onChanged(_textEditingController.text);
setState(() => _isEditing = false);
},
icon: const Icon(Icons.check_circle_outline),
),
],
if (!_isEditing && widget.value != widget.defaultValue) ...[
const Gap(8),
IconButton.filledTonal(
tooltip: Strings.tooltipResetToDefault,
onPressed: () {
_focusNode.unfocus();
_textEditingController.text = widget.defaultValue;
widget.onChanged(widget.defaultValue);
},
icon: const Icon(Icons.refresh),
)
],
],
);
}

View File

@ -0,0 +1,152 @@
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/strings.dart';
import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_bloc.dart';
import 'package:minecraft_server_installer/properties/adapter/presenter/server_properties_view_model.dart';
import 'package:minecraft_server_installer/properties/domain/enum/difficulty.dart';
import 'package:minecraft_server_installer/properties/domain/enum/game_mode.dart';
import 'package:minecraft_server_installer/properties/framework/ui/inline_text_field.dart';
class ServerPropertiesTab extends StatelessWidget {
const ServerPropertiesTab({super.key});
@override
Widget build(BuildContext context) => SingleChildScrollView(
child: BlocConsumer<ServerPropertiesBloc, ServerPropertiesViewModel>(
listener: (_, __) {},
builder: (context, state) {
final serverPropertiesBloc = context.read<ServerPropertiesBloc>();
return Column(
children: [
InlineTextField(
labelText: 'server-port | ${Strings.fieldServerPort}',
defaultValue: ServerPropertiesViewModel.defaultValue.serverPort.toString(),
value: state.serverPort.toString(),
onChanged: (value) =>
serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(serverPort: int.tryParse(value))),
),
const Gap(16),
InlineTextField(
labelText: 'max-player | ${Strings.fieldMaxPlayers}',
defaultValue: ServerPropertiesViewModel.defaultValue.maxPlayers.toString(),
value: state.maxPlayers.toString(),
onChanged: (value) =>
serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(maxPlayers: int.tryParse(value))),
),
const Gap(16),
InlineTextField(
labelText: 'spawn-protection | ${Strings.fieldSpawnProtection}',
defaultValue: ServerPropertiesViewModel.defaultValue.spawnProtection.toString(),
value: state.spawnProtection.toString(),
onChanged: (value) =>
serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(spawnProtection: int.tryParse(value))),
),
const Gap(16),
InlineTextField(
labelText: 'view-distance | ${Strings.fieldViewDistance}',
defaultValue: ServerPropertiesViewModel.defaultValue.viewDistance.toString(),
value: state.viewDistance.toString(),
onChanged: (value) =>
serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(viewDistance: int.tryParse(value))),
),
const Gap(16),
_dropdown<bool>(
labelText: 'pvp | ${Strings.fieldPvp}',
defaultValue: ServerPropertiesViewModel.defaultValue.pvp,
value: state.pvp,
onChanged: (value) => serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(pvp: value)),
items: {true: Strings.textEnable, false: Strings.textDisable},
),
const Gap(16),
_dropdown<GameMode>(
labelText: 'gamemode | ${Strings.fieldGameMode}',
defaultValue: ServerPropertiesViewModel.defaultValue.gameMode,
value: state.gameMode,
onChanged: (value) => serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(gameMode: value)),
items: {
GameMode.survival: Strings.gamemodeSurvival,
GameMode.creative: Strings.gamemodeCreative,
GameMode.adventure: Strings.gamemodeAdventure,
GameMode.spectator: Strings.gamemodeSpectator,
},
),
const Gap(16),
_dropdown<Difficulty>(
labelText: 'difficulty | ${Strings.fieldDifficulty}',
defaultValue: ServerPropertiesViewModel.defaultValue.difficulty,
value: state.difficulty,
onChanged: (value) => serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(difficulty: value)),
items: {
Difficulty.peaceful: Strings.difficultyPeaceful,
Difficulty.easy: Strings.difficultyEasy,
Difficulty.normal: Strings.difficultyNormal,
Difficulty.hard: Strings.difficultyHard,
},
),
const Gap(16),
_dropdown<bool>(
labelText: 'enable-command-block | ${Strings.fieldEnableCommandBlock}',
defaultValue: ServerPropertiesViewModel.defaultValue.enableCommandBlock,
value: state.enableCommandBlock,
onChanged: (value) =>
serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(enableCommandBlock: value)),
items: {true: Strings.textEnable, false: Strings.textDisable},
),
const Gap(16),
InlineTextField(
labelText: 'motd | ${Strings.fieldMotd}',
defaultValue: ServerPropertiesViewModel.defaultValue.motd,
value: state.motd,
onChanged: (value) => serverPropertiesBloc.add(ServerPropertiesUpdatedEvent(motd: value)),
),
],
);
}),
);
Widget _dropdown<T>({
required String labelText,
required Map<T, String> items,
required T defaultValue,
required T value,
required void Function(T) onChanged,
}) =>
Builder(
builder: (context) => Row(
children: [
Expanded(
child: DropdownMenu<T>(
requestFocusOnTap: false,
expandedInsets: EdgeInsets.zero,
inputDecorationTheme: Theme.of(context)
.inputDecorationTheme
.copyWith(border: OutlineInputBorder(borderRadius: BorderRadius.circular(8))),
label: Text(labelText),
initialSelection: value,
onSelected: (value) {
if (value != null) {
onChanged(value);
}
},
dropdownMenuEntries: items.entries
.map((item) => DropdownMenuEntry<T>(
value: item.key,
label: item.value,
))
.toList(),
),
),
if (value != defaultValue) ...[
const Gap(8),
IconButton.filledTonal(
tooltip: Strings.tooltipResetToDefault,
onPressed: () => onChanged(defaultValue),
icon: const Icon(Icons.refresh),
),
],
],
),
);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/game_version_view_model.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_state.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/vanilla_state.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';
class VanillaBloc extends Bloc<VanillaEvent, VanillaState> { class VanillaBloc extends Bloc<VanillaEvent, VanillaState> {

View File

@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart'; 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/presenter/game_version_view_model.dart';
class VanillaState with EquatableMixin { class VanillaState with EquatableMixin {
final List<GameVersionViewModel> gameVersions; final List<GameVersionViewModel> gameVersions;

View File

@ -1,10 +1,10 @@
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/adapter/presenter/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/presenter/vanilla_bloc.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/game_version_view_model.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/game_version_view_model.dart';
import 'package:minecraft_server_installer/vanilla/adapter/presentation/vanilla_state.dart'; import 'package:minecraft_server_installer/vanilla/adapter/presenter/vanilla_state.dart';
class GameVersionDropdown extends StatelessWidget { class GameVersionDropdown extends StatelessWidget {
const GameVersionDropdown({super.key}); const GameVersionDropdown({super.key});