feat(studio): theme-plugin picker + translate-hook wrapper
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Two pieces of the Studio plugin host: - `data/theme_plugin.dart`: discovery, persistence, ColorScheme translation. Plugins listed via list_capabilities filtered to studio.theme.*; selection persisted in SharedPreferences; loadThemePluginSchemes builds a ColorScheme pair from the plugin's 14 ARGB tokens per brightness. - `main.dart`: StudioApp gains themePluginNotifier + setThemePlugin(). MaterialApp wraps a FutureBuilder that re-fetches the plugin's ColorSchemes whenever the capability flips; falls back to FaiTheme.light/dark on null or load failure. - `fai_settings_dialog.dart`: new `_ThemePluginPanel` shows a dropdown of installed studio.theme.* capabilities + a "Built-in" entry. Switching applies immediately. - HubService.invokePluginTranslate added as the typed wrapper for the new gRPC RPC — ready for the FaiEnBadge swap-out in the next iteration. - 4 new l10n keys (themePluginHeader / None / Hint / Empty), EN+DE. flutter analyze + flutter test (widget + friendly_error): both green. The integration test for the theme picker is the existing fai_runtime plugin_theme + Studio-side invokePluginTheme path that round-trips through the new mirror-installable studio-theme-solarized bundle. Signed-off-by: flemming-it <sf@flemming.it> Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
a5bcde6446
commit
91706cea2b
9 changed files with 430 additions and 13 deletions
|
|
@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
|
|||
import '../data/hub.dart';
|
||||
import '../data/registry_token.dart';
|
||||
import '../data/system_actions.dart';
|
||||
import '../data/theme_plugin.dart';
|
||||
import '../main.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
|
@ -510,6 +512,8 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
onClear: _clearRegistryToken,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
const _ThemePluginPanel(),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_MaintenancePanel(
|
||||
onResetDone: () async {
|
||||
// Daemon just restarted under the same channel +
|
||||
|
|
@ -1456,6 +1460,122 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
|||
/// metadata; nothing is auto-spawned. Trust model:
|
||||
/// referencing public projects with their canonical install
|
||||
/// command is the same legal / security shape as a package
|
||||
/// Theme-plugin picker. Lists installed `studio.theme.*`
|
||||
/// capabilities (via list_capabilities) and lets the operator
|
||||
/// pick one or fall back to Studio's built-in palette. The
|
||||
/// choice is persisted in SharedPreferences and applied at
|
||||
/// the MaterialApp level via `StudioApp.of(context).setThemePlugin`.
|
||||
///
|
||||
/// When no theme plugin is installed the panel renders a
|
||||
/// short hint pointing at the Store rather than a useless
|
||||
/// empty dropdown.
|
||||
class _ThemePluginPanel extends StatefulWidget {
|
||||
const _ThemePluginPanel();
|
||||
|
||||
@override
|
||||
State<_ThemePluginPanel> createState() => _ThemePluginPanelState();
|
||||
}
|
||||
|
||||
class _ThemePluginPanelState extends State<_ThemePluginPanel> {
|
||||
Future<List<String>>? _capsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_capsFuture = listThemePluginCapabilities();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final app = StudioApp.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.themePluginHeader,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
FutureBuilder<List<String>>(
|
||||
future: _capsFuture,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
||||
child: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
final caps = snap.data ?? const <String>[];
|
||||
if (caps.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
l.themePluginEmpty,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Dropdown shows "Built-in" + every installed
|
||||
// studio.theme.* cap. Selection writes through
|
||||
// StudioApp's notifier so the MaterialApp rebuilds
|
||||
// with the new ColorScheme without a restart.
|
||||
final active = app?.themePluginNotifier.value;
|
||||
final items = <DropdownMenuItem<String?>>[
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text(l.themePluginNone),
|
||||
),
|
||||
for (final cap in caps)
|
||||
DropdownMenuItem<String?>(
|
||||
value: cap,
|
||||
child: Text(cap, style: FaiTheme.mono(size: 12)),
|
||||
),
|
||||
];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButton<String?>(
|
||||
value: active,
|
||||
items: items,
|
||||
isDense: true,
|
||||
onChanged: app == null
|
||||
? null
|
||||
: (v) {
|
||||
app.setThemePlugin(v);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l.themePluginHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hub maintenance panel — currently one action: reset operator
|
||||
/// state via `fai reset --yes`. Lives at the bottom of Settings
|
||||
/// so the everyday config controls aren't crowded by a destructive
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue