diff --git a/packages/wolf_3d_flutter/lib/managers/wolf3d_app_manager.dart b/packages/wolf_3d_flutter/lib/managers/wolf3d_app_manager.dart new file mode 100644 index 0000000..2d54e5c --- /dev/null +++ b/packages/wolf_3d_flutter/lib/managers/wolf3d_app_manager.dart @@ -0,0 +1,123 @@ +library; + +import 'dart:async'; +import 'dart:collection'; + +import 'package:file_selector/file_selector.dart'; +import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; + +/// Coordinates app-shell setup actions and shutdown behavior. +class Wolf3dAppManager { + Wolf3dAppManager({required this.engine}); + + final Wolf3dFlutterEngine engine; + + bool _isLoadingGameData = false; + String? _pickerError; + Future? _shutdownFuture; + + bool get isLoadingGameData => _isLoadingGameData; + String? get pickerError => _pickerError; + + Future ensureAudioShutdown() { + final existing = _shutdownFuture; + if (existing != null) { + return existing; + } + + final shutdown = engine.shutdownAudio(); + _shutdownFuture = shutdown; + return shutdown; + } + + Future pickGameDataDirectory({ + required void Function() notifyChanged, + }) async { + _isLoadingGameData = true; + _pickerError = null; + notifyChanged(); + + try { + final String? directoryPath = await getDirectoryPath( + confirmButtonText: 'Use this folder', + ); + + if (directoryPath == null || directoryPath.trim().isEmpty) { + return; + } + + await engine.init(directory: directoryPath.trim()); + } catch (error) { + _pickerError = 'Unable to load selected directory: $error'; + } finally { + _isLoadingGameData = false; + notifyChanged(); + } + } + + Future pickGameDataFiles({ + required void Function() notifyChanged, + }) async { + _isLoadingGameData = true; + _pickerError = null; + notifyChanged(); + + try { + final List selectedFiles = await openFiles(); + + if (selectedFiles.isEmpty) { + return; + } + + final LinkedHashSet directories = LinkedHashSet(); + for (final XFile file in selectedFiles) { + final String directory = _directoryFromFilePath(file.path); + if (directory.isNotEmpty) { + directories.add(directory); + } + } + + if (directories.isEmpty) { + _pickerError = + 'Selected files do not expose local filesystem paths.'; + return; + } + + final List orderedDirectories = directories.toList( + growable: false, + ); + await engine.init( + directory: orderedDirectories.first, + additionalDirectories: orderedDirectories.skip(1), + ); + } catch (error) { + _pickerError = 'Unable to load selected files: $error'; + } finally { + _isLoadingGameData = false; + notifyChanged(); + } + } + + String _directoryFromFilePath(String path) { + final String trimmedPath = path.trim(); + if (trimmedPath.isEmpty) { + return ''; + } + + final int slashIndex = trimmedPath.lastIndexOf('/'); + final int backslashIndex = trimmedPath.lastIndexOf(r'\'); + final int separatorIndex = slashIndex > backslashIndex + ? slashIndex + : backslashIndex; + + if (separatorIndex < 0) { + return ''; + } + + if (separatorIndex == 0) { + return trimmedPath[0]; + } + + return trimmedPath.substring(0, separatorIndex); + } +} diff --git a/packages/wolf_3d_flutter/lib/widgets/wolf3d_app.dart b/packages/wolf_3d_flutter/lib/widgets/wolf3d_app.dart index 4acf54d..b05708e 100644 --- a/packages/wolf_3d_flutter/lib/widgets/wolf3d_app.dart +++ b/packages/wolf_3d_flutter/lib/widgets/wolf3d_app.dart @@ -21,37 +21,47 @@ class Wolf3dApp extends StatefulWidget { } class _Wolf3dAppState extends State with WidgetsBindingObserver { - Future? _shutdownFuture; + late final Wolf3dAppManager _manager; @override void initState() { super.initState(); + _manager = Wolf3dAppManager(engine: widget.engine); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); - unawaited(_ensureAudioShutdown()); + unawaited(_manager.ensureAudioShutdown()); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.detached) { - unawaited(_ensureAudioShutdown()); + unawaited(_manager.ensureAudioShutdown()); } } - Future _ensureAudioShutdown() { - final Future? existing = _shutdownFuture; - if (existing != null) { - return existing; - } + Future _pickGameDataDirectory() { + return _manager.pickGameDataDirectory( + notifyChanged: () { + if (mounted) { + setState(() {}); + } + }, + ); + } - final Future shutdown = widget.engine.shutdownAudio(); - _shutdownFuture = shutdown; - return shutdown; + Future _pickGameDataFiles() { + return _manager.pickGameDataFiles( + notifyChanged: () { + if (mounted) { + setState(() {}); + } + }, + ); } @override @@ -59,6 +69,10 @@ class _Wolf3dAppState extends State with WidgetsBindingObserver { return widget.engine.availableGames.isEmpty ? NoGameDataScreen( configuredDataDirectory: widget.engine.configuredDataDirectory, + onPickGameDataDirectory: _pickGameDataDirectory, + onPickGameDataFiles: _pickGameDataFiles, + isLoadingGameData: _manager.isLoadingGameData, + pickerError: _manager.pickerError, ) : GameScreen(wolf3d: widget.engine); } diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart index 500a25f..7c6339e 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart @@ -30,6 +30,7 @@ export 'managers/game_screen_input_manager.dart' HostShortcutRegistry, GameScreenInputManager, isAltEnterShortcut; + export 'managers/wolf3d_app_manager.dart' show Wolf3dAppManager; export 'screens/audio_gallery.dart' show AudioGallery; export 'screens/debug_tools_screen.dart' show DebugToolsScreen; export 'screens/game_screen.dart' show GameScreen;