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
|
|
@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
|
|||
|
||||
import 'data/hub.dart';
|
||||
import 'data/system_actions.dart';
|
||||
import 'data/theme_plugin.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'pages/approvals.dart';
|
||||
import 'pages/audit.dart';
|
||||
|
|
@ -30,22 +31,33 @@ const String kStudioVersion = '0.42.0';
|
|||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Restore the persisted endpoint, theme mode, and locale
|
||||
// before the first frame so none of them flicker on startup.
|
||||
// Restore the persisted endpoint, theme mode, locale, and
|
||||
// (when installed) the operator's theme-plugin choice
|
||||
// before the first frame so nothing flickers on startup.
|
||||
await HubService.instance.loadPersistedEndpoint();
|
||||
final themeMode = await HubService.instance.loadThemeMode();
|
||||
final locale = await HubService.instance.loadLocale();
|
||||
runApp(StudioApp(initialThemeMode: themeMode, initialLocale: locale));
|
||||
final themePlugin = await loadActiveThemePlugin();
|
||||
runApp(StudioApp(
|
||||
initialThemeMode: themeMode,
|
||||
initialLocale: locale,
|
||||
initialThemePlugin: themePlugin,
|
||||
));
|
||||
}
|
||||
|
||||
class StudioApp extends StatefulWidget {
|
||||
final ThemeModeValue initialThemeMode;
|
||||
final Locale initialLocale;
|
||||
/// Capability name of the active theme plugin (e.g.
|
||||
/// `studio.theme.solarized`) or `null` for the built-in
|
||||
/// FaiTheme.
|
||||
final String? initialThemePlugin;
|
||||
|
||||
const StudioApp({
|
||||
super.key,
|
||||
required this.initialThemeMode,
|
||||
required this.initialLocale,
|
||||
this.initialThemePlugin,
|
||||
});
|
||||
|
||||
/// Lookup helper so descendants can flip the theme without
|
||||
|
|
@ -68,6 +80,12 @@ class StudioAppState extends State<StudioApp> {
|
|||
/// this, MaterialApp re-renders with the new
|
||||
/// AppLocalizations, and every translated string updates.
|
||||
late final ValueNotifier<Locale> localeNotifier;
|
||||
/// Capability name of the active `studio.theme.*` plugin
|
||||
/// (e.g. `studio.theme.solarized`) or `null` for
|
||||
/// FaiTheme's built-in palette. Flipping this triggers a
|
||||
/// re-fetch of the plugin's ColorSchemes; the MaterialApp
|
||||
/// rebuilds with the new themes.
|
||||
late final ValueNotifier<String?> themePluginNotifier;
|
||||
|
||||
Future<void> setMode(ThemeModeValue m) async {
|
||||
modeNotifier.value = m;
|
||||
|
|
@ -79,17 +97,24 @@ class StudioAppState extends State<StudioApp> {
|
|||
await HubService.instance.saveLocale(l);
|
||||
}
|
||||
|
||||
Future<void> setThemePlugin(String? capability) async {
|
||||
themePluginNotifier.value = capability;
|
||||
await saveActiveThemePlugin(capability);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
modeNotifier = ValueNotifier(widget.initialThemeMode);
|
||||
localeNotifier = ValueNotifier(widget.initialLocale);
|
||||
themePluginNotifier = ValueNotifier(widget.initialThemePlugin);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
modeNotifier.dispose();
|
||||
localeNotifier.dispose();
|
||||
themePluginNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -104,22 +129,59 @@ class StudioAppState extends State<StudioApp> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Build (or return null) ThemeData pair for the active
|
||||
/// theme plugin. Returns immediately with `null` when no
|
||||
/// plugin is selected; otherwise awaits the plugin's
|
||||
/// `theme_for` round-trip for both brightnesses.
|
||||
Future<({ThemeData? light, ThemeData? dark})> _pluginThemes(
|
||||
String? capability,
|
||||
) async {
|
||||
if (capability == null || capability.isEmpty) {
|
||||
return (light: null, dark: null);
|
||||
}
|
||||
try {
|
||||
final schemes = await loadThemePluginSchemes(capability);
|
||||
return (
|
||||
light: schemes.light == null ? null : themeDataFromScheme(schemes.light!),
|
||||
dark: schemes.dark == null ? null : themeDataFromScheme(schemes.dark!),
|
||||
);
|
||||
} catch (_) {
|
||||
// Plugin unreachable, hub down, etc. Fall back to
|
||||
// FaiTheme defaults silently — the Settings dialog is
|
||||
// where the operator surfaces / fixes the problem.
|
||||
return (light: null, dark: null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<ThemeModeValue>(
|
||||
valueListenable: modeNotifier,
|
||||
builder: (_, mode, _) => ValueListenableBuilder<Locale>(
|
||||
valueListenable: localeNotifier,
|
||||
builder: (_, locale, _) => MaterialApp(
|
||||
title: 'F∆I Studio',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: FaiTheme.light(),
|
||||
darkTheme: FaiTheme.dark(),
|
||||
themeMode: _flutterMode(mode),
|
||||
locale: locale,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: const StudioShell(),
|
||||
builder: (_, locale, _) => ValueListenableBuilder<String?>(
|
||||
valueListenable: themePluginNotifier,
|
||||
builder: (_, pluginCap, _) => FutureBuilder<({ThemeData? light, ThemeData? dark})>(
|
||||
// Re-key on the capability so a plugin change
|
||||
// discards the in-flight future for the previous
|
||||
// plugin and starts fresh.
|
||||
key: ValueKey('plugin_themes:$pluginCap'),
|
||||
future: _pluginThemes(pluginCap),
|
||||
builder: (context, snap) {
|
||||
final p = snap.data ?? (light: null, dark: null);
|
||||
return MaterialApp(
|
||||
title: 'F∆I Studio',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: p.light ?? FaiTheme.light(),
|
||||
darkTheme: p.dark ?? FaiTheme.dark(),
|
||||
themeMode: _flutterMode(mode),
|
||||
locale: locale,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: const StudioShell(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue