feat(studio): rich theme picker (presets + custom colour) + editor 0.11.0
Some checks failed
Security / Security check (push) Failing after 1s

Settings dialog's Theme Plugin section is now a grid of
swatched tiles:

- Built-in (none) — falls back to FaiTheme.light/.dark
- One tile per installed studio.theme.* plugin, each
  showing the plugin's primary/secondary/tertiary as
  live colour dots. Tile loads its preview lazily so a
  dozen installed themes don't block the picker.
- Custom — opens a colour-picker dialog with 12 curated
  Material presets + a hex input + live preview. Selecting
  applies ColorScheme.fromSeed for both brightnesses.

main.dart's _pluginThemes parses a 'custom:#RRGGBB' sigil
in the same notifier slot as plugin capability ids, so the
existing persistence + restoration paths cover the custom
case with no new state.

Bumps editor to 0.11.0 (type-checked port connections +
dynamic card width fix + card-height border allowance) and
Studio to 0.58.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-02 01:29:34 +02:00
parent 69864da934
commit c1c60d5434
30 changed files with 1280 additions and 846 deletions

View file

@ -9,14 +9,13 @@ import '../data/hub.dart';
import '../data/hub_auth_token.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';
import 'fai_error_box.dart';
import 'fai_pill.dart';
import 'fai_system_ai_editor.dart';
import 'theme_picker_grid.dart';
class FaiSettingsDialog extends StatefulWidget {
const FaiSettingsDialog({super.key});
@ -45,10 +44,12 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
SystemAiStatus? _aiStatus;
List<McpClientInfo>? _mcpClients;
List<N8nEndpointInfo>? _n8nEndpoints;
/// Length of the configured registry token, or null when the
/// `~/.fai/registry-token` file is missing / empty. Reloaded
/// after save / clear.
int? _registryTokenChars;
/// Length of the configured hub gRPC bearer token, or null
/// when `~/.fai/hub-auth-token` is missing / empty. Reloaded
/// after save / clear; UI only ever sees the trimmed length.
@ -74,7 +75,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
final n = await HubAuthToken.charCount();
if (!mounted) return;
setState(() => _hubAuthTokenChars = n);
} catch (_) {/* fail-quiet */}
} catch (_) {
/* fail-quiet */
}
}
Future<void> _saveHubAuthToken(String token) async {
@ -84,9 +87,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
await _loadHubAuthToken();
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.hubAuthTokenSavedToast)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.hubAuthTokenSavedToast)));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
@ -103,9 +106,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
await _loadHubAuthToken();
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.hubAuthTokenClearedToast)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.hubAuthTokenClearedToast)));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
@ -120,7 +123,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
final n = await RegistryToken.charCount();
if (!mounted) return;
setState(() => _registryTokenChars = n);
} catch (_) {/* fail-quiet */}
} catch (_) {
/* fail-quiet */
}
}
Future<void> _saveRegistryToken(String token) async {
@ -129,9 +134,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
await _loadRegistryToken();
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.registryTokenSavedToast)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.registryTokenSavedToast)));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
@ -147,9 +152,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
await _loadRegistryToken();
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.registryTokenClearedToast)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.registryTokenClearedToast)));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
@ -164,7 +169,9 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
final list = await HubService.instance.listN8nEndpoints();
if (!mounted) return;
setState(() => _n8nEndpoints = list);
} catch (_) {/* fail-quiet */}
} catch (_) {
/* fail-quiet */
}
}
Future<void> _addN8nEndpoint(N8nEndpointDraft draft) async {
@ -178,15 +185,15 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
if (!mounted) return;
setState(() => _n8nEndpoints = list);
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.addedN8nToast(draft.name))),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.addedN8nToast(draft.name))));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.addFailedToast(e.toString()))),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.addFailedToast(e.toString()))));
}
}
@ -239,15 +246,15 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
if (!mounted) return;
setState(() => _mcpClients = list);
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.addedMcpToast(draft.name))),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.addedMcpToast(draft.name))));
} catch (e) {
if (!mounted) return;
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.addFailedToast(e.toString()))),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.addFailedToast(e.toString()))));
}
}
@ -402,197 +409,201 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.settingsHubEndpointHint,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: FaiSpace.lg),
TextField(
controller: _host,
decoration: InputDecoration(
labelText: l.settingsHost,
border: const OutlineInputBorder(),
isDense: true,
),
autofocus: true,
),
const SizedBox(height: FaiSpace.md),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _port,
decoration: InputDecoration(
labelText: l.settingsPort,
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: FaiSpace.md),
Expanded(
flex: 3,
child: SwitchListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(l.settingsTls),
subtitle: Text(
l.settingsTlsSubtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
value: _secure,
onChanged: (v) => setState(() => _secure = v),
),
),
],
),
if (_error != null) ...[
const SizedBox(height: FaiSpace.md),
Text(
_error!,
l.settingsHubEndpointHint,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
color: theme.colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: FaiSpace.md),
Container(
padding: const EdgeInsets.all(FaiSpace.sm),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm),
const SizedBox(height: FaiSpace.lg),
TextField(
controller: _host,
decoration: InputDecoration(
labelText: l.settingsHost,
border: const OutlineInputBorder(),
isDense: true,
),
autofocus: true,
),
child: Row(
const SizedBox(height: FaiSpace.md),
Row(
children: [
Icon(
Icons.link,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
Expanded(
flex: 2,
child: TextField(
controller: _port,
decoration: InputDecoration(
labelText: l.settingsPort,
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: FaiSpace.sm),
Text(
_previewUrl(),
style: FaiTheme.mono(
size: 11,
color: theme.colorScheme.onSurface,
const SizedBox(width: FaiSpace.md),
Expanded(
flex: 3,
child: SwitchListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(l.settingsTls),
subtitle: Text(
l.settingsTlsSubtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
value: _secure,
onChanged: (v) => setState(() => _secure = v),
),
),
],
),
),
if (_channels != null) ...[
const SizedBox(height: FaiSpace.lg),
Text(
l.channelsHeader,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
letterSpacing: 0.6,
fontSize: 10,
if (_error != null) ...[
const SizedBox(height: FaiSpace.md),
Text(
_error!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
],
const SizedBox(height: FaiSpace.md),
Container(
padding: const EdgeInsets.all(FaiSpace.sm),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(FaiRadius.sm),
),
child: Row(
children: [
Icon(
Icons.link,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: FaiSpace.sm),
Text(
_previewUrl(),
style: FaiTheme.mono(
size: 11,
color: theme.colorScheme.onSurface,
),
),
],
),
),
if (_channels != null) ...[
const SizedBox(height: FaiSpace.lg),
Text(
l.channelsHeader,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
letterSpacing: 0.6,
fontSize: 10,
),
),
const SizedBox(height: 2),
Text(
l.channelsBlurb,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: FaiSpace.sm),
for (final ch in _channels!.channels)
_ChannelRow(
channel: ch,
active: ch.name == _channels!.active,
onConnect: _saving ? null : () => _connectToChannel(ch),
onSwitch: _saving ? null : () => _switchChannel(ch.name),
onEnableAutostart: _saving
? null
: () => _runDaemon(
'enable autostart',
() => SystemActions.faiDaemonEnable(ch.name),
),
onDisableAutostart: _saving
? null
: () => _runDaemon(
'disable autostart',
() => SystemActions.faiDaemonDisable(ch.name),
),
),
if (_channelToast != null) ...[
const SizedBox(height: FaiSpace.sm),
FaiErrorBox(text: _channelToast!, maxHeight: 200),
],
],
if (_aiStatus != null) ...[
const SizedBox(height: FaiSpace.lg),
_SystemAiPanel(
status: _aiStatus!,
onEdit: () async {
final updated = await FaiSystemAiEditor.show(
context,
_aiStatus!,
);
if (updated != null && mounted) {
setState(() => _aiStatus = updated);
}
},
),
],
if (_mcpClients != null) ...[
const SizedBox(height: FaiSpace.lg),
_McpClientsPanel(
clients: _mcpClients!,
onAdd: _addMcpClient,
onRemove: _removeMcpClient,
onRefresh: _refreshMcpClients,
),
],
if (_n8nEndpoints != null) ...[
const SizedBox(height: FaiSpace.lg),
_N8nEndpointsPanel(
endpoints: _n8nEndpoints!,
onAdd: _addN8nEndpoint,
onRemove: _removeN8nEndpoint,
onRefresh: _refreshN8nEndpoints,
),
),
const SizedBox(height: 2),
Text(
l.channelsBlurb,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: FaiSpace.sm),
for (final ch in _channels!.channels)
_ChannelRow(
channel: ch,
active: ch.name == _channels!.active,
onConnect: _saving ? null : () => _connectToChannel(ch),
onSwitch: _saving ? null : () => _switchChannel(ch.name),
onEnableAutostart: _saving ? null : () => _runDaemon(
'enable autostart',
() => SystemActions.faiDaemonEnable(ch.name),
),
onDisableAutostart: _saving ? null : () => _runDaemon(
'disable autostart',
() => SystemActions.faiDaemonDisable(ch.name),
),
),
if (_channelToast != null) ...[
const SizedBox(height: FaiSpace.sm),
FaiErrorBox(text: _channelToast!, maxHeight: 200),
],
],
if (_aiStatus != null) ...[
const SizedBox(height: FaiSpace.lg),
_SystemAiPanel(
status: _aiStatus!,
onEdit: () async {
final updated = await FaiSystemAiEditor.show(
context,
_aiStatus!,
_RegistryCredentialsPanel(
configuredChars: _registryTokenChars,
onSave: _saveRegistryToken,
onClear: _clearRegistryToken,
),
const SizedBox(height: FaiSpace.lg),
_HubAuthTokenPanel(
configuredChars: _hubAuthTokenChars,
onSave: _saveHubAuthToken,
onClear: _clearHubAuthToken,
),
const SizedBox(height: FaiSpace.lg),
const _DefaultScopePanel(),
const SizedBox(height: FaiSpace.lg),
const _ThemePluginPanel(),
const SizedBox(height: FaiSpace.lg),
_MaintenancePanel(
onResetDone: () async {
// Daemon just restarted under the same channel +
// port. Bouncing the gRPC connection picks up the
// fresh state without the operator having to
// close+reopen Settings.
await HubService.instance.reconnect(
HubService.instance.currentEndpoint,
);
if (updated != null && mounted) {
setState(() => _aiStatus = updated);
}
if (!mounted) return;
await _loadChannels();
await _loadAiStatus();
await _loadMcpClients();
await _loadN8nEndpoints();
await _loadRegistryToken();
},
),
],
if (_mcpClients != null) ...[
const SizedBox(height: FaiSpace.lg),
_McpClientsPanel(
clients: _mcpClients!,
onAdd: _addMcpClient,
onRemove: _removeMcpClient,
onRefresh: _refreshMcpClients,
),
],
if (_n8nEndpoints != null) ...[
const SizedBox(height: FaiSpace.lg),
_N8nEndpointsPanel(
endpoints: _n8nEndpoints!,
onAdd: _addN8nEndpoint,
onRemove: _removeN8nEndpoint,
onRefresh: _refreshN8nEndpoints,
),
],
const SizedBox(height: FaiSpace.lg),
_RegistryCredentialsPanel(
configuredChars: _registryTokenChars,
onSave: _saveRegistryToken,
onClear: _clearRegistryToken,
),
const SizedBox(height: FaiSpace.lg),
_HubAuthTokenPanel(
configuredChars: _hubAuthTokenChars,
onSave: _saveHubAuthToken,
onClear: _clearHubAuthToken,
),
const SizedBox(height: FaiSpace.lg),
const _DefaultScopePanel(),
const SizedBox(height: FaiSpace.lg),
const _ThemePluginPanel(),
const SizedBox(height: FaiSpace.lg),
_MaintenancePanel(
onResetDone: () async {
// Daemon just restarted under the same channel +
// port. Bouncing the gRPC connection picks up the
// fresh state without the operator having to
// close+reopen Settings.
await HubService.instance.reconnect(
HubService.instance.currentEndpoint,
);
if (!mounted) return;
await _loadChannels();
await _loadAiStatus();
await _loadMcpClients();
await _loadN8nEndpoints();
await _loadRegistryToken();
},
),
],
),
),
),
),
actions: [
@ -616,8 +627,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
String _previewUrl() {
final scheme = _secure ? 'https' : 'http';
final host =
_host.text.trim().isEmpty ? '127.0.0.1' : _host.text.trim();
final host = _host.text.trim().isEmpty ? '127.0.0.1' : _host.text.trim();
final port = _port.text.trim().isEmpty ? '50051' : _port.text.trim();
return '$scheme://$host:$port';
}
@ -627,6 +637,7 @@ class _ChannelRow extends StatelessWidget {
final ChannelInfo channel;
final bool active;
final VoidCallback? onConnect;
/// Switches the active channel pointer (`fai channel switch`).
/// Distinct from [onConnect], which only re-points Studio's
/// gRPC wire at a different running daemon.
@ -691,7 +702,10 @@ class _ChannelRow extends StatelessWidget {
if (active)
Padding(
padding: const EdgeInsets.only(right: FaiSpace.sm),
child: FaiPill(label: l.channelsActive, tone: FaiPillTone.success),
child: FaiPill(
label: l.channelsActive,
tone: FaiPillTone.success,
),
),
PopupMenuButton<String>(
tooltip: l.channelsActionsTooltip,
@ -716,18 +730,12 @@ class _ChannelRow extends StatelessWidget {
PopupMenuItem(
value: 'connect',
enabled: channel.running && onConnect != null,
child: _MenuRow(
icon: Icons.link,
text: l.channelsConnect,
),
child: _MenuRow(icon: Icons.link, text: l.channelsConnect),
),
PopupMenuItem(
value: 'switch',
enabled: !active && onSwitch != null,
child: _MenuRow(
icon: Icons.swap_horiz,
text: l.channelsSwitch,
),
child: _MenuRow(icon: Icons.swap_horiz, text: l.channelsSwitch),
),
const PopupMenuDivider(),
PopupMenuItem(
@ -812,7 +820,9 @@ class _SystemAiPanel extends StatelessWidget {
TextButton.icon(
onPressed: onEdit,
icon: const Icon(Icons.edit_outlined, size: 14),
label: Text(status.enabled ? l.systemAiEdit : l.systemAiConfigure),
label: Text(
status.enabled ? l.systemAiEdit : l.systemAiConfigure,
),
),
],
),
@ -822,7 +832,11 @@ class _SystemAiPanel extends StatelessWidget {
_StatRow(label: 'endpoint', value: status.endpoint, mono: true),
_StatRow(label: 'model', value: status.model, mono: true),
if (status.apiKeyEnv.isNotEmpty)
_StatRow(label: 'api_key_env', value: '\$${status.apiKeyEnv}', mono: true),
_StatRow(
label: 'api_key_env',
value: '\$${status.apiKeyEnv}',
mono: true,
),
] else ...[
Padding(
padding: const EdgeInsets.only(top: 2),
@ -844,11 +858,7 @@ class _StatRow extends StatelessWidget {
final String value;
final bool mono;
const _StatRow({
required this.label,
required this.value,
this.mono = false,
});
const _StatRow({required this.label, required this.value, this.mono = false});
@override
Widget build(BuildContext context) {
@ -974,10 +984,7 @@ class _McpClientsPanel extends StatelessWidget {
)
else
for (final c in clients)
_McpClientRow(
client: c,
onRemove: () => onRemove(c.name),
),
_McpClientRow(client: c, onRemove: () => onRemove(c.name)),
],
);
}
@ -1309,10 +1316,7 @@ class _N8nEndpointsPanel extends StatelessWidget {
)
else
for (final e in endpoints)
_N8nEndpointRow(
endpoint: e,
onRemove: () => onRemove(e.name),
),
_N8nEndpointRow(endpoint: e, onRemove: () => onRemove(e.name)),
],
);
}
@ -1497,7 +1501,8 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
),
FilledButton(
onPressed: () {
if (_name.text.trim().isEmpty || _baseUrl.text.trim().isEmpty) return;
if (_name.text.trim().isEmpty || _baseUrl.text.trim().isEmpty)
return;
Navigator.pop(
context,
N8nEndpointDraft(
@ -1529,27 +1534,13 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
/// 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 {
class _ThemePluginPanel extends StatelessWidget {
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: [
@ -1561,76 +1552,12 @@ class _ThemePluginPanelState extends State<_ThemePluginPanel> {
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,
),
),
],
),
);
},
),
const SizedBox(height: 8),
// Rich grid of theme tiles. Each shows live swatches
// from the plugin's own palette so the operator can
// preview without applying. The "Custom" tile opens
// a seed-colour dialog.
const ThemePickerGrid(),
],
);
}
@ -1782,11 +1709,7 @@ class _MaintenancePanelState extends State<_MaintenancePanel> {
),
if (_resultText != null) ...[
const SizedBox(height: FaiSpace.sm),
FaiErrorBox(
text: _resultText!,
isError: !_resultOk,
maxHeight: 240,
),
FaiErrorBox(text: _resultText!, isError: !_resultOk, maxHeight: 240),
],
],
);
@ -1822,8 +1745,7 @@ class _RegistryCredentialsPanel extends StatefulWidget {
_RegistryCredentialsPanelState();
}
class _RegistryCredentialsPanelState
extends State<_RegistryCredentialsPanel> {
class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
final _controller = TextEditingController();
bool _obscured = true;
@ -2017,9 +1939,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
Row(
children: [
Icon(
isConfigured
? Icons.check_circle
: Icons.radio_button_unchecked,
isConfigured ? Icons.check_circle : Icons.radio_button_unchecked,
size: 14,
color: isConfigured
? theme.colorScheme.primary
@ -2066,8 +1986,7 @@ class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
_obscured ? Icons.visibility : Icons.visibility_off,
size: 16,
),
onPressed: () =>
setState(() => _obscured = !_obscured),
onPressed: () => setState(() => _obscured = !_obscured),
),
),
),
@ -2103,6 +2022,7 @@ class _McpSuggestion {
final IconData icon;
final String endpoint;
final String apiKeyEnv;
/// Localizable description. Use [resolveDescription] to fetch
/// the actual text in the active locale the const list
/// can't hold a closure that takes [AppLocalizations], and
@ -2305,9 +2225,9 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
_saving = false;
});
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.defaultScopeSavedToast)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l.defaultScopeSavedToast)));
} catch (e) {
if (!mounted) return;
setState(() {
@ -2333,8 +2253,9 @@ class _DefaultScopePanelState extends State<_DefaultScopePanel> {
if (current.length == 1) {
// Hub rejects empty; surface the constraint inline rather
// than letting the RPC fail.
setState(() => _error = AppLocalizations.of(context)!
.defaultScopeNonEmptyError);
setState(
() => _error = AppLocalizations.of(context)!.defaultScopeNonEmptyError,
);
return;
}
current.removeAt(i);
@ -2513,10 +2434,7 @@ class _ScopeRow extends StatelessWidget {
),
),
Expanded(
child: Text(
entry,
style: const TextStyle(fontFamily: 'monospace'),
),
child: Text(entry, style: const TextStyle(fontFamily: 'monospace')),
),
IconButton(
icon: const Icon(Icons.arrow_upward, size: 16),