Add EveFlutter provider picker proof

This commit is contained in:
Meta Vens 2026-07-09 01:24:21 +01:00
commit e2e9b21e2f
6 changed files with 235 additions and 4 deletions

View file

@ -122,6 +122,126 @@ class EveSurfaceState {
}
}
class EveProviderCatalog {
const EveProviderCatalog({required this.providers});
final List<EveProviderEntry> providers;
factory EveProviderCatalog.fromConformanceExport(Map<String, dynamic> json) {
final providers = ((json['providers'] as List<dynamic>?) ?? const [])
.map((provider) {
return EveProviderEntry.fromJson(provider as Map<String, dynamic>);
})
.toList(growable: false);
return EveProviderCatalog(providers: providers);
}
EveProviderEntry? findProvider(String providerId) {
for (final provider in providers) {
if (provider.providerId == providerId) return provider;
}
return null;
}
}
class EveProviderEntry {
const EveProviderEntry({
required this.providerId,
required this.status,
required this.ownerRepo,
required this.advertisementPath,
required this.surfaces,
required this.commands,
required this.pluginRequirements,
});
final String providerId;
final String status;
final String ownerRepo;
final String advertisementPath;
final List<String> surfaces;
final List<String> commands;
final List<EvePluginRequirement> pluginRequirements;
factory EveProviderEntry.fromJson(Map<String, dynamic> json) {
return EveProviderEntry(
providerId: (json['providerId'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
ownerRepo: (json['ownerRepo'] ?? '').toString(),
advertisementPath: (json['advertisementPath'] ?? '').toString(),
surfaces: _stringList(json['surfaces']),
commands: _stringList(json['commands']),
pluginRequirements:
((json['pluginRequirements'] as List<dynamic>?) ?? const [])
.map(
(requirement) => EvePluginRequirement.fromJson(
requirement as Map<String, dynamic>,
),
)
.toList(growable: false),
);
}
}
class EvePluginRequirement {
const EvePluginRequirement({
required this.surfaceId,
required this.pluginId,
required this.requiredCapabilities,
});
final String surfaceId;
final String pluginId;
final List<String> requiredCapabilities;
factory EvePluginRequirement.fromJson(Map<String, dynamic> json) {
return EvePluginRequirement(
surfaceId: (json['surfaceId'] ?? '').toString(),
pluginId: (json['pluginId'] ?? '').toString(),
requiredCapabilities: _stringList(json['requiredCapabilities']),
);
}
}
List<String> _stringList(Object? value) {
return ((value as List<dynamic>?) ?? const [])
.map((item) => item.toString())
.toList(growable: false);
}
class EveProviderPicker extends StatelessWidget {
const EveProviderPicker({
required this.catalog,
required this.selectedProviderId,
required this.onSelected,
super.key,
});
final EveProviderCatalog catalog;
final String selectedProviderId;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: catalog.findProvider(selectedProviderId) == null
? null
: selectedProviderId,
hint: const Text('Select provider'),
items: [
for (final provider in catalog.providers)
DropdownMenuItem<String>(
value: provider.providerId,
child: Text(provider.providerId),
),
],
onChanged: (value) {
if (value != null) onSelected(value);
},
);
}
}
class EveNode {
EveNode({
required this.id,

View file

@ -122,6 +122,49 @@ void main() {
expect(intents.single.clientId, 'flutter-parity');
expect(intents.single.payload['action']['operation'], 'focus_entity');
});
testWidgets('provider picker consumes conformance export providers', (
tester,
) async {
final catalog = EveProviderCatalog.fromConformanceExport(
_providerCatalogExport,
);
var selected = 'aetheria';
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (context, setState) {
return EveProviderPicker(
catalog: catalog,
selectedProviderId: selected,
onSelected: (providerId) {
setState(() => selected = providerId);
},
);
},
),
),
);
expect(catalog.providers, hasLength(2));
expect(catalog.findProvider('gamecult.home.vn')?.ownerRepo, 'Sai');
expect(
catalog
.findProvider('gamecult.home.vn')
?.pluginRequirements
.map((requirement) => requirement.pluginId),
containsAll(['sai.vn', 'norn.graph', 'tex.math']),
);
expect(find.text('aetheria'), findsOneWidget);
await tester.tap(find.byType(DropdownButton<String>));
await tester.pumpAndSettle();
await tester.tap(find.text('gamecult.home.vn').last);
await tester.pumpAndSettle();
expect(selected, 'gamecult.home.vn');
});
}
const fixtureId = String.fromEnvironment(
@ -142,3 +185,48 @@ class _Viewport {
final double width;
final double height;
}
const _providerCatalogExport = {
'schema': 'gamecult.eve.conformance_export.v1',
'providers': [
{
'providerId': 'aetheria',
'status': 'advertised',
'ownerRepo': 'Aetheria',
'advertisementPath': 'web/fixtures/aetheria.provider-advertisement.json',
'surfaces': ['aetheria.daemon.game'],
'commands': ['aetheria.daemon.commands'],
'pluginRequirements': [],
},
{
'providerId': 'gamecult.home.vn',
'status': 'advertised',
'ownerRepo': 'Sai',
'advertisementPath': 'web/fixtures/sai-vn.provider-advertisement.json',
'surfaces': ['sai.visual_novel.surface'],
'commands': ['story.choose', 'story.continue', 'story.jump'],
'pluginRequirements': [
{
'surfaceId': 'sai.visual_novel.surface',
'pluginId': 'sai.vn',
'requiredCapabilities': [
'vn.stage',
'story.choose',
'story.continue',
'story.jump',
],
},
{
'surfaceId': 'sai.visual_novel.surface',
'pluginId': 'norn.graph',
'requiredCapabilities': ['embed.norn'],
},
{
'surfaceId': 'sai.visual_novel.surface',
'pluginId': 'tex.math',
'requiredCapabilities': ['embed.tex'],
},
],
},
],
};