feat(studio): rich theme picker (presets + custom colour) + editor 0.11.0
Some checks failed
Security / Security check (push) Failing after 1s
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:
parent
69864da934
commit
c1c60d5434
30 changed files with 1280 additions and 846 deletions
|
|
@ -28,10 +28,7 @@ class _AuditPageState extends State<AuditPage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_refresh();
|
||||
_poller = Timer.periodic(
|
||||
const Duration(seconds: 2),
|
||||
(_) => _refresh(),
|
||||
);
|
||||
_poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -56,9 +53,9 @@ class _AuditPageState extends State<AuditPage> {
|
|||
_refresh();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.auditClearFailed(_friendly(e)))),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.auditClearFailed(_friendly(e)))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,26 +121,25 @@ class _AuditPageState extends State<AuditPage> {
|
|||
child: !_initialLoaded
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null && _events.isEmpty
|
||||
? FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: theme.colorScheme.error,
|
||||
title: AppLocalizations.of(context)!.hubUnreachable,
|
||||
hint: AppLocalizations.of(context)!.hubUnreachableHint,
|
||||
)
|
||||
: filtered.isEmpty
|
||||
? FaiEmptyState(
|
||||
icon: Icons.timeline_outlined,
|
||||
title: AppLocalizations.of(context)!.auditNoEvents,
|
||||
hint:
|
||||
AppLocalizations.of(context)!.auditNoEventsHint,
|
||||
)
|
||||
: _GroupedEventList(
|
||||
events: filtered,
|
||||
allEvents: _events,
|
||||
toneFor: (t) => _toneFor(t, theme),
|
||||
formatTime: _formatTime,
|
||||
contextLine: _contextLine,
|
||||
),
|
||||
? FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: theme.colorScheme.error,
|
||||
title: AppLocalizations.of(context)!.hubUnreachable,
|
||||
hint: AppLocalizations.of(context)!.hubUnreachableHint,
|
||||
)
|
||||
: filtered.isEmpty
|
||||
? FaiEmptyState(
|
||||
icon: Icons.timeline_outlined,
|
||||
title: AppLocalizations.of(context)!.auditNoEvents,
|
||||
hint: AppLocalizations.of(context)!.auditNoEventsHint,
|
||||
)
|
||||
: _GroupedEventList(
|
||||
events: filtered,
|
||||
allEvents: _events,
|
||||
toneFor: (t) => _toneFor(t, theme),
|
||||
formatTime: _formatTime,
|
||||
contextLine: _contextLine,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -164,7 +160,8 @@ class _AuditPageState extends State<AuditPage> {
|
|||
final mm = local.minute.toString().padLeft(2, '0');
|
||||
final ss = local.second.toString().padLeft(2, '0');
|
||||
final time = '$hh:$mm:$ss';
|
||||
final sameDay = local.year == now.year &&
|
||||
final sameDay =
|
||||
local.year == now.year &&
|
||||
local.month == now.month &&
|
||||
local.day == now.day;
|
||||
if (sameDay) return time;
|
||||
|
|
@ -252,10 +249,8 @@ class _GroupedEventList extends StatelessWidget {
|
|||
trailing: e.durationMs != null ? '${e.durationMs}ms' : null,
|
||||
onTap: () => showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => _EventDetailDialog(
|
||||
event: e,
|
||||
allEvents: allEvents,
|
||||
),
|
||||
builder: (_) =>
|
||||
_EventDetailDialog(event: e, allEvents: allEvents),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -285,11 +280,7 @@ class _GroupedEventList extends StatelessWidget {
|
|||
/// timezone so an event at 23:55 yesterday in Berlin doesn't
|
||||
/// land in "today" because UTC happened to spill into a new
|
||||
/// day.
|
||||
static String _bucketLabel(
|
||||
DateTime ts,
|
||||
DateTime now,
|
||||
AppLocalizations l,
|
||||
) {
|
||||
static String _bucketLabel(DateTime ts, DateTime now, AppLocalizations l) {
|
||||
final local = ts.toLocal();
|
||||
final localNow = now.toLocal();
|
||||
final today = DateTime(localNow.year, localNow.month, localNow.day);
|
||||
|
|
@ -371,13 +362,13 @@ class _ChipButtonState extends State<_ChipButton> {
|
|||
final fg = widget.selected
|
||||
? theme.colorScheme.primary
|
||||
: _hovered
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
final bg = widget.selected
|
||||
? theme.colorScheme.primary.withValues(alpha: 0.12)
|
||||
: _hovered
|
||||
? theme.colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent;
|
||||
? theme.colorScheme.surfaceContainerHigh
|
||||
: Colors.transparent;
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
|
|
@ -444,9 +435,7 @@ class _LiveStatusBar extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
live
|
||||
? l.auditLiveStatus(eventCount)
|
||||
: l.auditDisconnected(error!),
|
||||
live ? l.auditLiveStatus(eventCount) : l.auditDisconnected(error!),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -478,6 +467,7 @@ class _LiveStatusBar extends StatelessWidget {
|
|||
|
||||
class _EventDetailDialog extends StatefulWidget {
|
||||
final AuditEvent event;
|
||||
|
||||
/// Full event window the audit page already fetched. Lets
|
||||
/// the dialog surface every event sharing this event's
|
||||
/// `flow_execution` without a fresh round-trip — flow runs
|
||||
|
|
@ -485,10 +475,7 @@ class _EventDetailDialog extends StatefulWidget {
|
|||
/// page polls on.
|
||||
final List<AuditEvent> allEvents;
|
||||
|
||||
const _EventDetailDialog({
|
||||
required this.event,
|
||||
required this.allEvents,
|
||||
});
|
||||
const _EventDetailDialog({required this.event, required this.allEvents});
|
||||
|
||||
@override
|
||||
State<_EventDetailDialog> createState() => _EventDetailDialogState();
|
||||
|
|
@ -544,8 +531,10 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
..writeln('event_type: ${e.type}')
|
||||
..writeln('flow: ${e.flowName ?? "(none)"}')
|
||||
..writeln('step: ${e.stepId ?? "(none)"}')
|
||||
..writeln('module: ${e.moduleName ?? "(none)"}'
|
||||
'${e.moduleVersion != null ? "@${e.moduleVersion}" : ""}')
|
||||
..writeln(
|
||||
'module: ${e.moduleName ?? "(none)"}'
|
||||
'${e.moduleVersion != null ? "@${e.moduleVersion}" : ""}',
|
||||
)
|
||||
..writeln('error: ${e.error ?? "(none)"}')
|
||||
..writeln('duration: ${e.durationMs ?? 0}ms');
|
||||
if (mode == 'full' && e.detail != null) {
|
||||
|
|
@ -640,14 +629,9 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: SelectableText(
|
||||
detail,
|
||||
style: FaiTheme.mono(size: 11),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: SelectableText(detail, style: FaiTheme.mono(size: 11)),
|
||||
),
|
||||
],
|
||||
if (_explanation != null || _explaining) ...[
|
||||
|
|
@ -690,11 +674,13 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
_explaining
|
||||
? l.auditAsking
|
||||
: _explanation == null
|
||||
? l.auditExplain
|
||||
: l.auditReask,
|
||||
? l.auditExplain
|
||||
: l.auditReask,
|
||||
),
|
||||
)
|
||||
else if (_aiStatus != null && !_aiStatus!.enabled && (event.error != null))
|
||||
else if (_aiStatus != null &&
|
||||
!_aiStatus!.enabled &&
|
||||
(event.error != null))
|
||||
Tooltip(
|
||||
message: l.auditConfigureSystemAi,
|
||||
child: OutlinedButton.icon(
|
||||
|
|
@ -734,10 +720,9 @@ class _FlowRunDialog extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final related = allEvents
|
||||
.where((e) => e.flowExecution == flowExecution)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final related =
|
||||
allEvents.where((e) => e.flowExecution == flowExecution).toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.75;
|
||||
return AlertDialog(
|
||||
title: Text(l.auditFlowRunDialogTitle(flowName)),
|
||||
|
|
@ -761,8 +746,7 @@ class _FlowRunDialog extends StatelessWidget {
|
|||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: related.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const SizedBox(height: FaiSpace.xs),
|
||||
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.xs),
|
||||
itemBuilder: (context, i) {
|
||||
final e = related[i];
|
||||
return FaiDataRow(
|
||||
|
|
@ -770,8 +754,7 @@ class _FlowRunDialog extends StatelessWidget {
|
|||
leading: _formatRelativeTime(e.timestamp),
|
||||
title: e.type,
|
||||
subtitle: _stepLine(e),
|
||||
trailing:
|
||||
e.durationMs != null ? '${e.durationMs}ms' : null,
|
||||
trailing: e.durationMs != null ? '${e.durationMs}ms' : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -865,6 +848,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
final bool explaining;
|
||||
final AskAiResult? result;
|
||||
final String privacyMode;
|
||||
|
||||
/// Triggered by the "Regenerate" affordance — passes
|
||||
/// `forceFresh: true` so the next askAi skips the cache and
|
||||
/// hits the live provider. Null while [explaining] is true.
|
||||
|
|
@ -885,8 +869,8 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
final color = explaining
|
||||
? theme.colorScheme.primary
|
||||
: ok
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.error;
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.error;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(FaiSpace.md),
|
||||
|
|
@ -945,7 +929,9 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (result != null && result!.isSuccess && onRegenerate != null) ...[
|
||||
if (result != null &&
|
||||
result!.isSuccess &&
|
||||
onRegenerate != null) ...[
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Tooltip(
|
||||
message: l.auditRegenerateTooltip,
|
||||
|
|
@ -980,7 +966,9 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
SelectableText(
|
||||
result!.text,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: ok ? theme.colorScheme.onSurface : theme.colorScheme.error,
|
||||
color: ok
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (result!.fixHint.isNotEmpty) ...[
|
||||
|
|
@ -1040,7 +1028,8 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final user = Platform.environment['USER'] ??
|
||||
final user =
|
||||
Platform.environment['USER'] ??
|
||||
Platform.environment['USERNAME'] ??
|
||||
'operator';
|
||||
_reviewer = TextEditingController(text: '$user@studio');
|
||||
|
|
@ -1111,12 +1100,12 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
onPressed: _reason.text.trim().isEmpty
|
||||
? null
|
||||
: () => Navigator.pop(
|
||||
context,
|
||||
_ClearOutcome(
|
||||
reviewer: _reviewer.text.trim(),
|
||||
reason: _reason.text.trim(),
|
||||
),
|
||||
context,
|
||||
_ClearOutcome(
|
||||
reviewer: _reviewer.text.trim(),
|
||||
reason: _reason.text.trim(),
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue