feat: Refactor Wolf3dApp to manage audio shutdown on dispose and add audio shutdown test
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:collection';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:file_selector/file_selector.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
|
|
||||||
@@ -21,108 +20,38 @@ class Wolf3dApp extends StatefulWidget {
|
|||||||
State<Wolf3dApp> createState() => _Wolf3dAppState();
|
State<Wolf3dApp> createState() => _Wolf3dAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Wolf3dAppState extends State<Wolf3dApp> {
|
class _Wolf3dAppState extends State<Wolf3dApp> with WidgetsBindingObserver {
|
||||||
bool _isLoadingGameData = false;
|
Future<void>? _shutdownFuture;
|
||||||
String? _pickerError;
|
|
||||||
|
|
||||||
Future<void> _pickGameDataDirectory() async {
|
@override
|
||||||
setState(() {
|
void initState() {
|
||||||
_isLoadingGameData = true;
|
super.initState();
|
||||||
_pickerError = null;
|
WidgetsBinding.instance.addObserver(this);
|
||||||
});
|
}
|
||||||
|
|
||||||
try {
|
@override
|
||||||
final String? directoryPath = await getDirectoryPath(
|
void dispose() {
|
||||||
confirmButtonText: 'Use this folder',
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
);
|
unawaited(_ensureAudioShutdown());
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
if (directoryPath == null || directoryPath.trim().isEmpty) {
|
@override
|
||||||
return;
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
}
|
if (state == AppLifecycleState.detached) {
|
||||||
|
unawaited(_ensureAudioShutdown());
|
||||||
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 {
|
Future<void> _ensureAudioShutdown() {
|
||||||
setState(() {
|
final Future<void>? existing = _shutdownFuture;
|
||||||
_isLoadingGameData = true;
|
if (existing != null) {
|
||||||
_pickerError = null;
|
return existing;
|
||||||
});
|
|
||||||
|
|
||||||
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 Future<void> shutdown = widget.engine.shutdownAudio();
|
||||||
final int backslashIndex = trimmedPath.lastIndexOf(r'\');
|
_shutdownFuture = shutdown;
|
||||||
final int separatorIndex = slashIndex > backslashIndex
|
return shutdown;
|
||||||
? slashIndex
|
|
||||||
: backslashIndex;
|
|
||||||
|
|
||||||
if (separatorIndex < 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (separatorIndex == 0) {
|
|
||||||
return trimmedPath[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmedPath.substring(0, separatorIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -130,10 +59,6 @@ class _Wolf3dAppState extends State<Wolf3dApp> {
|
|||||||
return widget.engine.availableGames.isEmpty
|
return widget.engine.availableGames.isEmpty
|
||||||
? NoGameDataScreen(
|
? NoGameDataScreen(
|
||||||
configuredDataDirectory: widget.engine.configuredDataDirectory,
|
configuredDataDirectory: widget.engine.configuredDataDirectory,
|
||||||
onPickGameDataDirectory: _pickGameDataDirectory,
|
|
||||||
onPickGameDataFiles: _pickGameDataFiles,
|
|
||||||
isLoadingGameData: _isLoadingGameData,
|
|
||||||
pickerError: _pickerError,
|
|
||||||
)
|
)
|
||||||
: GameScreen(wolf3d: widget.engine);
|
: GameScreen(wolf3d: widget.engine);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,46 @@ class _NoopAudio implements EngineAudio {
|
|||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CountingAudio implements EngineAudio {
|
||||||
|
@override
|
||||||
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
|
int stopAllAudioCallCount = 0;
|
||||||
|
int disposeCallCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> debugSoundTest() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playMenuMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopAllAudio() async {
|
||||||
|
stopAllAudioCallCount++;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
disposeCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('DefaultGameDataDirectoryPersistence', () {
|
group('DefaultGameDataDirectoryPersistence', () {
|
||||||
test('saves and loads configured directory', () async {
|
test('saves and loads configured directory', () async {
|
||||||
@@ -129,4 +169,21 @@ void main() {
|
|||||||
expect(find.textContaining('Configured data directory:'), findsOneWidget);
|
expect(find.textContaining('Configured data directory:'), findsOneWidget);
|
||||||
expect(find.textContaining('/tmp/wolf-data'), findsOneWidget);
|
expect(find.textContaining('/tmp/wolf-data'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Wolf3dApp dispose path shuts down audio', (tester) async {
|
||||||
|
final audio = _CountingAudio();
|
||||||
|
final wolf3d = Wolf3dFlutterEngine(audioBackend: audio);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Wolf3dApp(engine: wolf3d),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(audio.stopAllAudioCallCount, 1);
|
||||||
|
expect(audio.disposeCallCount, 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user