feat: Implement game selection in sprite and VGA galleries with GalleryGameSelector

refactor: Update VgaGallery and SpriteGallery to use selected game data
chore: Remove unused plugins from generated plugin registrant and CMake files
chore: Clean up pubspec.yaml by removing super_clipboard dependency

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 15:53:24 +01:00
parent ed1e480555
commit 03dd871a46
7 changed files with 212 additions and 128 deletions

View File

@@ -53,7 +53,7 @@ class DebugToolsScreen extends StatelessWidget {
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => VgaGallery(images: wolf3d.vgaImages), builder: (_) => VgaGallery(wolf3d: wolf3d),
), ),
); );
}, },

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
String formatGalleryGameTitle(GameVersion version) {
switch (version) {
case GameVersion.shareware:
return 'SHAREWARE';
case GameVersion.retail:
return 'RETAIL';
case GameVersion.spearOfDestiny:
return 'SPEAR OF DESTINY';
case GameVersion.spearOfDestinyDemo:
return 'SOD DEMO';
}
}
class GalleryGameSelector extends StatelessWidget {
final Wolf3d wolf3d;
final WolfensteinData selectedGame;
final ValueChanged<WolfensteinData> onSelected;
const GalleryGameSelector({
super.key,
required this.wolf3d,
required this.selectedGame,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Center(
child: DropdownButtonHideUnderline(
child: DropdownButton<WolfensteinData>(
value: selectedGame,
borderRadius: BorderRadius.circular(8),
dropdownColor: Theme.of(context).colorScheme.surface,
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
onChanged: (WolfensteinData? game) {
if (game != null) {
onSelected(game);
}
},
items: wolf3d.availableGames.map((WolfensteinData game) {
return DropdownMenuItem<WolfensteinData>(
value: game,
child: Text(formatGalleryGameTitle(game.version)),
);
}).toList(),
),
),
),
);
}
}

View File

@@ -5,24 +5,113 @@ import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/gallery_game_selector.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
/// Displays every sprite frame in the active game along with enemy metadata. /// Displays every sprite frame in the active game along with enemy metadata.
class SpriteGallery extends StatelessWidget { class SpriteGallery extends StatefulWidget {
/// Shared application facade used to access the active game's sprite set. /// Shared application facade used to access the active game's sprite set.
final Wolf3d wolf3d; final Wolf3d wolf3d;
/// Creates the sprite gallery for [wolf3d]. /// Creates the sprite gallery for [wolf3d].
const SpriteGallery({super.key, required this.wolf3d}); const SpriteGallery({super.key, required this.wolf3d});
bool get isShareware => wolf3d.activeGame.version == GameVersion.shareware; @override
State<SpriteGallery> createState() => _SpriteGalleryState();
}
class _SpriteGalleryState extends State<SpriteGallery> {
late WolfensteinData _selectedGame;
@override
void initState() {
super.initState();
_selectedGame =
widget.wolf3d.maybeActiveGame ?? widget.wolf3d.availableGames.first;
}
bool get isShareware => _selectedGame.version == GameVersion.shareware;
List<Sprite> get _sprites => _selectedGame.sprites;
void _selectGame(WolfensteinData game) {
if (identical(_selectedGame, game)) {
return;
}
widget.wolf3d.setActiveGame(game);
setState(() {
_selectedGame = game;
});
}
String _buildSpriteLabel(int index) {
String label = 'Sprite Index: $index';
for (final enemy in EnemyType.values) {
// The gallery infers likely ownership from sprite index ranges so
// debugging art packs does not require cross-referencing source.
if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) {
final EnemyAnimation? animation = enemy.getAnimationFromSprite(
index,
isShareware: isShareware,
);
label += '\n${enemy.name}';
if (animation != null) {
label += '\n${animation.name}';
}
break;
}
}
return label;
}
void _showImagePreviewDialog(BuildContext context, int index) {
final Sprite sprite = _sprites[index];
showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(
'${_buildSpriteLabel(index)}\n64 x 64',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Close'),
),
],
content: Center(
child: AspectRatio(
aspectRatio: 1,
child: WolfAssetPainter.sprite(sprite),
),
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Sprite Gallery"), title: const Text('Sprite Gallery'),
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
actions: [
if (widget.wolf3d.availableGames.length > 1)
GalleryGameSelector(
wolf3d: widget.wolf3d,
selectedGame: _selectedGame,
onSelected: _selectGame,
),
],
), ),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: GridView.builder( body: GridView.builder(
@@ -31,45 +120,32 @@ class SpriteGallery extends StatelessWidget {
crossAxisSpacing: 8, crossAxisSpacing: 8,
mainAxisSpacing: 8, mainAxisSpacing: 8,
), ),
itemCount: wolf3d.sprites.length, itemCount: _sprites.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
String label = "Sprite Index: $index"; final String label = _buildSpriteLabel(index);
for (final enemy in EnemyType.values) {
// The gallery infers likely ownership from sprite index ranges so
// debugging art packs does not require cross-referencing source.
if (enemy.claimsSpriteIndex(index, isShareware: isShareware)) {
final EnemyAnimation? animation = enemy.getAnimationFromSprite(
index,
isShareware: isShareware,
);
label += "\n${enemy.name}";
if (animation != null) {
label += "\n${animation.name}";
}
break;
}
}
return Card( return Card(
color: Colors.blueGrey, color: Colors.blueGrey,
child: Column( child: InkWell(
spacing: 8, onTap: () => _showImagePreviewDialog(context, index),
children: [ child: Column(
Text( spacing: 8,
label, children: [
style: const TextStyle(color: Colors.white, fontSize: 10), Text(
textAlign: TextAlign.center, label,
), style: const TextStyle(color: Colors.white, fontSize: 10),
Expanded( textAlign: TextAlign.center,
child: Center( ),
child: AspectRatio( Expanded(
aspectRatio: 1, child: Center(
child: WolfAssetPainter.sprite(wolf3d.sprites[index]), child: AspectRatio(
aspectRatio: 1,
child: WolfAssetPainter.sprite(_sprites[index]),
),
), ),
), ),
), ],
], ),
), ),
); );
}, },

View File

@@ -1,93 +1,49 @@
/// Visual browser for decoded VGA pictures and UI art. /// Visual browser for decoded VGA pictures and UI art.
library; library;
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
import 'package:wolf_3d_gui/screens/gallery_game_selector.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
/// Shows each VGA image extracted from the currently selected game data set. /// Shows each VGA image extracted from the currently selected game data set.
class VgaGallery extends StatelessWidget { class VgaGallery extends StatefulWidget {
/// Raw VGA images decoded from the active asset pack. /// Shared app facade used to access available game data sets.
final List<VgaImage> images; final Wolf3d wolf3d;
/// Creates the gallery for [images]. /// Creates the gallery for the currently selected or browsed game.
const VgaGallery({super.key, required this.images}); const VgaGallery({super.key, required this.wolf3d});
Future<ui.Image> _buildVgaUiImage(VgaImage image) { @override
final completer = Completer<ui.Image>(); State<VgaGallery> createState() => _VgaGalleryState();
final Uint8List pixels = Uint8List(image.width * image.height * 4); }
for (int y = 0; y < image.height; y++) { class _VgaGalleryState extends State<VgaGallery> {
for (int x = 0; x < image.width; x++) { late WolfensteinData _selectedGame;
final int colorByte = image.decodePixel(x, y);
final int abgr = ColorPalette.vga32Bit[colorByte];
final int offset = (y * image.width + x) * 4;
pixels[offset] = abgr & 0xFF; // R
pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
}
}
ui.decodeImageFromPixels( List<VgaImage> get _images => _selectedGame.vgaImages;
pixels,
image.width, @override
image.height, void initState() {
ui.PixelFormat.rgba8888, super.initState();
(ui.Image decoded) => completer.complete(decoded), _selectedGame =
); widget.wolf3d.maybeActiveGame ?? widget.wolf3d.availableGames.first;
return completer.future;
} }
Future<void> _copyImageToClipboard(BuildContext context, int index) async { void _selectGame(WolfensteinData game) {
final clipboard = SystemClipboard.instance; if (identical(_selectedGame, game)) {
if (clipboard == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Clipboard API is unavailable here.')),
);
}
return; return;
} }
final VgaImage image = images[index]; widget.wolf3d.setActiveGame(game);
ui.Image? uiImage; setState(() {
try { _selectedGame = game;
uiImage = await _buildVgaUiImage(image); });
final ByteData? pngData = await uiImage.toByteData(
format: ui.ImageByteFormat.png,
);
if (pngData == null) {
throw StateError('Failed to encode image as PNG.');
}
final item = DataWriterItem();
item.add(Formats.png(pngData.buffer.asUint8List()));
await clipboard.write([item]);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Copied VGA image #$index to clipboard.')),
);
}
} catch (error) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Copy failed: $error')),
);
}
} finally {
uiImage?.dispose();
}
} }
void _showImagePreviewDialog(BuildContext context, int index) { void _showImagePreviewDialog(BuildContext context, int index) {
final VgaImage image = images[index]; final VgaImage image = _images[index];
showDialog<void>( showDialog<void>(
context: context, context: context,
@@ -105,11 +61,6 @@ class VgaGallery extends StatelessWidget {
onPressed: () => Navigator.of(dialogContext).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Close'), child: const Text('Close'),
), ),
ElevatedButton.icon(
onPressed: () => _copyImageToClipboard(dialogContext, index),
icon: const Icon(Icons.copy),
label: const Text('Copy'),
),
], ],
content: Center( content: Center(
child: AspectRatio( child: AspectRatio(
@@ -125,7 +76,17 @@ class VgaGallery extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("VGA Image Gallery")), appBar: AppBar(
title: const Text('VGA Image Gallery'),
actions: [
if (widget.wolf3d.availableGames.length > 1)
GalleryGameSelector(
wolf3d: widget.wolf3d,
selectedGame: _selectedGame,
onSelected: _selectGame,
),
],
),
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: GridView.builder( body: GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
@@ -133,7 +94,7 @@ class VgaGallery extends StatelessWidget {
crossAxisSpacing: 8, crossAxisSpacing: 8,
mainAxisSpacing: 8, mainAxisSpacing: 8,
), ),
itemCount: images.length, itemCount: _images.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Card( return Card(
color: Colors.blueGrey, color: Colors.blueGrey,
@@ -144,15 +105,16 @@ class VgaGallery extends StatelessWidget {
spacing: 8, spacing: 8,
children: [ children: [
Text( Text(
"Index: $index\n${images[index].width} x ${images[index].height}", 'Index: $index\n${_images[index].width} x ${_images[index].height}',
style: const TextStyle(color: Colors.white, fontSize: 12), style: const TextStyle(color: Colors.white, fontSize: 12),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
Expanded( Expanded(
child: Center( child: Center(
child: AspectRatio( child: AspectRatio(
aspectRatio: images[index].width / images[index].height, aspectRatio:
child: WolfAssetPainter.vga(images[index]), _images[index].width / _images[index].height,
child: WolfAssetPainter.vga(_images[index]),
), ),
), ),
), ),

View File

@@ -7,24 +7,16 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h> #include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar = g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar); window_manager_plugin_register_with_registrar(window_manager_registrar);

View File

@@ -4,9 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux audioplayers_linux
irondash_engine_context
screen_retriever_linux screen_retriever_linux
super_native_extensions
window_manager window_manager
) )

View File

@@ -16,7 +16,6 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
super_clipboard: ^0.9.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: