feat: Add file selector support and enhance game data directory management

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 19:30:50 +01:00
parent 569a3386a8
commit 70b4fc3fe0
9 changed files with 284 additions and 34 deletions
@@ -1,11 +1,14 @@
library;
import 'dart:collection';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
/// Minimal app shell that binds a prepared [Wolf3dFlutterEngine] instance to
/// host screens.
class Wolf3dApp extends StatelessWidget {
class Wolf3dApp extends StatefulWidget {
/// Shared initialized facade that owns game data, input, and audio services.
final Wolf3dFlutterEngine engine;
@@ -14,12 +17,124 @@ class Wolf3dApp extends StatelessWidget {
required this.engine,
});
@override
State<Wolf3dApp> createState() => _Wolf3dAppState();
}
class _Wolf3dAppState extends State<Wolf3dApp> {
bool _isLoadingGameData = false;
String? _pickerError;
Future<void> _pickGameDataDirectory() async {
setState(() {
_isLoadingGameData = true;
_pickerError = null;
});
try {
final String? directoryPath = await getDirectoryPath(
confirmButtonText: 'Use this folder',
);
if (directoryPath == null || directoryPath.trim().isEmpty) {
return;
}
await widget.engine.init(directory: directoryPath.trim());
} catch (error) {
setState(() {
_pickerError = 'Unable to load selected directory: $error';
});
} finally {
if (mounted) {
setState(() {
_isLoadingGameData = false;
});
}
}
}
Future<void> _pickGameDataFiles() async {
setState(() {
_isLoadingGameData = true;
_pickerError = null;
});
try {
final List<XFile> selectedFiles = await openFiles();
if (selectedFiles.isEmpty) {
return;
}
final LinkedHashSet<String> directories = LinkedHashSet<String>();
for (final XFile file in selectedFiles) {
final String directory = _directoryFromFilePath(file.path);
if (directory.isNotEmpty) {
directories.add(directory);
}
}
if (directories.isEmpty) {
setState(() {
_pickerError = 'Selected files do not expose local filesystem paths.';
});
return;
}
final List<String> orderedDirectories = directories.toList(
growable: false,
);
await widget.engine.init(
directory: orderedDirectories.first,
additionalDirectories: orderedDirectories.skip(1),
);
} catch (error) {
setState(() {
_pickerError = 'Unable to load selected files: $error';
});
} finally {
if (mounted) {
setState(() {
_isLoadingGameData = false;
});
}
}
}
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);
}
@override
Widget build(BuildContext context) {
return engine.availableGames.isEmpty
return widget.engine.availableGames.isEmpty
? NoGameDataScreen(
configuredDataDirectory: engine.configuredDataDirectory,
configuredDataDirectory: widget.engine.configuredDataDirectory,
onPickGameDataDirectory: _pickGameDataDirectory,
onPickGameDataFiles: _pickGameDataFiles,
isLoadingGameData: _isLoadingGameData,
pickerError: _pickerError,
)
: GameScreen(wolf3d: engine);
: GameScreen(wolf3d: widget.engine);
}
}