From c389876a2fa9a8fa1ffdd3c4c6e2ef47d3b97be6 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sat, 9 May 2026 11:58:24 +0200 Subject: [PATCH] feat(studio): clickable install pills + open-in-editor on flow cards (v0.43.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a saved flow is missing modules, each capability pill in the "Needs:" row is now clickable: tap one and a per-item progress dialog walks the bare capability name through installModule, then the flows page refreshes. When more than one capability is missing, an extra "Install all (N)" button runs the same dialog over the whole list sequentially so a fresh operator can take an unrunnable flow and one click later have its dependencies resolved. Also adds a pencil icon next to Run that hands the YAML path to the OS via SystemActions.openInOs — the operator's default editor for .yaml decides what opens. Makes the path next to the flow name actionable instead of decorative. The install dialog renders one row per spec with a status icon (pending circle / spinner / check / error), shows the installed version on success, and surfaces the full error in a copyable FaiErrorBox on failure so the operator can paste it back. Signed-off-by: flemming-it --- lib/l10n/app_de.arb | 25 + lib/l10n/app_en.arb | 25 + lib/l10n/app_localizations.dart | 78 +++ lib/l10n/app_localizations_de.dart | 50 ++ lib/l10n/app_localizations_en.dart | 51 ++ lib/pages/flows.dart | 482 +++++++++++++++--- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.yaml | 2 +- 9 files changed, 662 insertions(+), 59 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8153459..d4f9030 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -408,6 +408,31 @@ }, "flowsMissingModulesLabel": "Braucht:", "flowsRunDisabledTooltip": "Erst die fehlenden Module installieren.", + "flowsInstallAllButton": "Alle installieren ({n})", + "@flowsInstallAllButton": { "placeholders": { "n": { "type": "int" } } }, + "flowsInstallPillTooltip": "Zum Installieren klicken", + "flowsInstallDepsTitle": "Abhängigkeiten installieren", + "flowsInstallDepsBody": "Die fehlenden Module werden der Reihe nach installiert. Vorhandene Installationen bleiben unberührt.", + "flowsInstallStatusPending": "Wartet", + "flowsInstallStatusInstalling": "Wird installiert…", + "flowsInstallStatusDone": "Installiert v{version}", + "@flowsInstallStatusDone": { "placeholders": { "version": { "type": "String" } } }, + "flowsInstallStatusFailed": "Fehlgeschlagen", + "flowsInstallStatusFailedDetail": "{error}", + "@flowsInstallStatusFailedDetail": { "placeholders": { "error": { "type": "String" } } }, + "flowsInstallDepsAllDone": "Alle Abhängigkeiten installiert.", + "flowsInstallDepsAnyFailed": "{ok} installiert, {failed} fehlgeschlagen.", + "@flowsInstallDepsAnyFailed": { + "placeholders": { + "ok": { "type": "int" }, + "failed": { "type": "int" } + } + }, + "flowsOpenInEditorTooltip": "YAML-Datei im Standard-Editor öffnen", + "flowsOpenInEditorFailed": "Konnte Datei nicht öffnen: {error}", + "@flowsOpenInEditorFailed": { + "placeholders": { "error": { "type": "String" } } + }, "welcomeChecklistAllSetTitle": "Du bist eingerichtet.", "welcomeChecklistAllSetBody": "Drei Stränge, an denen du als nächstes ziehen kannst:", "welcomeChecklistNextAuditTitle": "Audit-Log lesen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eb06d30..8ff24a5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -409,6 +409,31 @@ }, "flowsMissingModulesLabel": "Needs:", "flowsRunDisabledTooltip": "Install the missing modules first.", + "flowsInstallAllButton": "Install all ({n})", + "@flowsInstallAllButton": { "placeholders": { "n": { "type": "int" } } }, + "flowsInstallPillTooltip": "Click to install", + "flowsInstallDepsTitle": "Install dependencies", + "flowsInstallDepsBody": "Walking through the missing modules in order. Existing installs are left alone.", + "flowsInstallStatusPending": "Pending", + "flowsInstallStatusInstalling": "Installing…", + "flowsInstallStatusDone": "Installed v{version}", + "@flowsInstallStatusDone": { "placeholders": { "version": { "type": "String" } } }, + "flowsInstallStatusFailed": "Failed", + "flowsInstallStatusFailedDetail": "{error}", + "@flowsInstallStatusFailedDetail": { "placeholders": { "error": { "type": "String" } } }, + "flowsInstallDepsAllDone": "All dependencies installed.", + "flowsInstallDepsAnyFailed": "{ok} installed, {failed} failed.", + "@flowsInstallDepsAnyFailed": { + "placeholders": { + "ok": { "type": "int" }, + "failed": { "type": "int" } + } + }, + "flowsOpenInEditorTooltip": "Open the YAML file in your default editor", + "flowsOpenInEditorFailed": "Could not open file: {error}", + "@flowsOpenInEditorFailed": { + "placeholders": { "error": { "type": "String" } } + }, "welcomeChecklistAllSetTitle": "You're set up.", "welcomeChecklistAllSetBody": "Three threads to pull on next:", "welcomeChecklistNextAuditTitle": "Read the audit log", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e509158..c5984f3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1892,6 +1892,84 @@ abstract class AppLocalizations { /// **'Install the missing modules first.'** String get flowsRunDisabledTooltip; + /// No description provided for @flowsInstallAllButton. + /// + /// In en, this message translates to: + /// **'Install all ({n})'** + String flowsInstallAllButton(int n); + + /// No description provided for @flowsInstallPillTooltip. + /// + /// In en, this message translates to: + /// **'Click to install'** + String get flowsInstallPillTooltip; + + /// No description provided for @flowsInstallDepsTitle. + /// + /// In en, this message translates to: + /// **'Install dependencies'** + String get flowsInstallDepsTitle; + + /// No description provided for @flowsInstallDepsBody. + /// + /// In en, this message translates to: + /// **'Walking through the missing modules in order. Existing installs are left alone.'** + String get flowsInstallDepsBody; + + /// No description provided for @flowsInstallStatusPending. + /// + /// In en, this message translates to: + /// **'Pending'** + String get flowsInstallStatusPending; + + /// No description provided for @flowsInstallStatusInstalling. + /// + /// In en, this message translates to: + /// **'Installing…'** + String get flowsInstallStatusInstalling; + + /// No description provided for @flowsInstallStatusDone. + /// + /// In en, this message translates to: + /// **'Installed v{version}'** + String flowsInstallStatusDone(String version); + + /// No description provided for @flowsInstallStatusFailed. + /// + /// In en, this message translates to: + /// **'Failed'** + String get flowsInstallStatusFailed; + + /// No description provided for @flowsInstallStatusFailedDetail. + /// + /// In en, this message translates to: + /// **'{error}'** + String flowsInstallStatusFailedDetail(String error); + + /// No description provided for @flowsInstallDepsAllDone. + /// + /// In en, this message translates to: + /// **'All dependencies installed.'** + String get flowsInstallDepsAllDone; + + /// No description provided for @flowsInstallDepsAnyFailed. + /// + /// In en, this message translates to: + /// **'{ok} installed, {failed} failed.'** + String flowsInstallDepsAnyFailed(int ok, int failed); + + /// No description provided for @flowsOpenInEditorTooltip. + /// + /// In en, this message translates to: + /// **'Open the YAML file in your default editor'** + String get flowsOpenInEditorTooltip; + + /// No description provided for @flowsOpenInEditorFailed. + /// + /// In en, this message translates to: + /// **'Could not open file: {error}'** + String flowsOpenInEditorFailed(String error); + /// No description provided for @welcomeChecklistAllSetTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a396de7..d126c58 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1067,6 +1067,56 @@ class AppLocalizationsDe extends AppLocalizations { String get flowsRunDisabledTooltip => 'Erst die fehlenden Module installieren.'; + @override + String flowsInstallAllButton(int n) { + return 'Alle installieren ($n)'; + } + + @override + String get flowsInstallPillTooltip => 'Zum Installieren klicken'; + + @override + String get flowsInstallDepsTitle => 'Abhängigkeiten installieren'; + + @override + String get flowsInstallDepsBody => + 'Die fehlenden Module werden der Reihe nach installiert. Vorhandene Installationen bleiben unberührt.'; + + @override + String get flowsInstallStatusPending => 'Wartet'; + + @override + String get flowsInstallStatusInstalling => 'Wird installiert…'; + + @override + String flowsInstallStatusDone(String version) { + return 'Installiert v$version'; + } + + @override + String get flowsInstallStatusFailed => 'Fehlgeschlagen'; + + @override + String flowsInstallStatusFailedDetail(String error) { + return '$error'; + } + + @override + String get flowsInstallDepsAllDone => 'Alle Abhängigkeiten installiert.'; + + @override + String flowsInstallDepsAnyFailed(int ok, int failed) { + return '$ok installiert, $failed fehlgeschlagen.'; + } + + @override + String get flowsOpenInEditorTooltip => 'YAML-Datei im Standard-Editor öffnen'; + + @override + String flowsOpenInEditorFailed(String error) { + return 'Konnte Datei nicht öffnen: $error'; + } + @override String get welcomeChecklistAllSetTitle => 'Du bist eingerichtet.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3f7407c..c2f0d59 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1080,6 +1080,57 @@ class AppLocalizationsEn extends AppLocalizations { @override String get flowsRunDisabledTooltip => 'Install the missing modules first.'; + @override + String flowsInstallAllButton(int n) { + return 'Install all ($n)'; + } + + @override + String get flowsInstallPillTooltip => 'Click to install'; + + @override + String get flowsInstallDepsTitle => 'Install dependencies'; + + @override + String get flowsInstallDepsBody => + 'Walking through the missing modules in order. Existing installs are left alone.'; + + @override + String get flowsInstallStatusPending => 'Pending'; + + @override + String get flowsInstallStatusInstalling => 'Installing…'; + + @override + String flowsInstallStatusDone(String version) { + return 'Installed v$version'; + } + + @override + String get flowsInstallStatusFailed => 'Failed'; + + @override + String flowsInstallStatusFailedDetail(String error) { + return '$error'; + } + + @override + String get flowsInstallDepsAllDone => 'All dependencies installed.'; + + @override + String flowsInstallDepsAnyFailed(int ok, int failed) { + return '$ok installed, $failed failed.'; + } + + @override + String get flowsOpenInEditorTooltip => + 'Open the YAML file in your default editor'; + + @override + String flowsOpenInEditorFailed(String error) { + return 'Could not open file: $error'; + } + @override String get welcomeChecklistAllSetTitle => 'You\'re set up.'; diff --git a/lib/pages/flows.dart b/lib/pages/flows.dart index 271739c..7dbe938 100644 --- a/lib/pages/flows.dart +++ b/lib/pages/flows.dart @@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import '../data/hub.dart'; +import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; @@ -77,6 +78,31 @@ class _FlowsPageState extends State { ); } + /// Walk through one or more capability specs (e.g. + /// `text.extract@^0`) sequentially, showing per-item progress. + /// Refreshes the flows page on dismiss so the row updates if + /// any install actually changed the installed-set. + Future _installDeps(List specs) async { + final changed = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => _InstallDependenciesDialog(specs: specs), + ); + if (changed == true && mounted) _refresh(); + } + + /// Hand the YAML file to the OS — `open` on macOS, `xdg-open` + /// on Linux, `start` on Windows. The operator's default editor + /// for `.yaml` decides what actually opens. + Future _openInEditor(SavedFlow flow) async { + final r = await SystemActions.openInOs(flow.path); + if (!mounted || r.ok) return; + final l = AppLocalizations.of(context)!; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l.flowsOpenInEditorFailed(r.stderr))), + ); + } + @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; @@ -131,6 +157,11 @@ class _FlowsPageState extends State { flow: flow, missingCapabilities: missing, onRun: missing.isEmpty ? () => _runFlow(flow) : null, + onInstallSingle: (spec) => _installDeps([spec]), + onInstallAll: missing.length > 1 + ? () => _installDeps(missing) + : null, + onOpenInEditor: () => _openInEditor(flow), ); }, ); @@ -164,11 +195,24 @@ class _FlowCard extends StatelessWidget { /// Run button greys out + a tooltip points the operator /// at the install path. Non-null otherwise. final VoidCallback? onRun; + /// Install a single capability spec (verbatim `use:` string, + /// version constraint included — the dialog strips the `@` + /// suffix before calling installModule). + final ValueChanged onInstallSingle; + /// Install every missing capability in sequence. Null when + /// only one is missing (the single-install pill is enough). + final VoidCallback? onInstallAll; + /// Hand the flow YAML to the OS so the operator's default + /// editor opens it. + final VoidCallback onOpenInEditor; const _FlowCard({ required this.flow, required this.missingCapabilities, required this.onRun, + required this.onInstallSingle, + required this.onInstallAll, + required this.onOpenInEditor, }); @override @@ -177,73 +221,142 @@ class _FlowCard extends StatelessWidget { final l = AppLocalizations.of(context)!; final isRunnable = missingCapabilities.isEmpty; return FaiCard( - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.account_tree_outlined, - size: 20, - color: theme.colorScheme.primary, + Row( + children: [ + Icon( + Icons.account_tree_outlined, + size: 20, + color: theme.colorScheme.primary, + ), + const SizedBox(width: FaiSpace.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + flow.name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + flow.path, + style: FaiTheme.mono( + size: 11, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + FaiPill( + label: '${flow.sizeBytes} B', + tone: FaiPillTone.neutral, + monospace: true, + ), + const SizedBox(width: FaiSpace.sm), + IconButton( + icon: const Icon(Icons.edit_outlined, size: 18), + tooltip: l.flowsOpenInEditorTooltip, + onPressed: onOpenInEditor, + ), + const SizedBox(width: FaiSpace.xs), + Tooltip( + message: isRunnable ? '' : l.flowsRunDisabledTooltip, + child: FilledButton.icon( + onPressed: onRun, + icon: const Icon(Icons.play_arrow, size: 16), + label: Text(l.flowsRunButton), + ), + ), + ], ), - const SizedBox(width: FaiSpace.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - flow.name, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + if (!isRunnable) ...[ + const SizedBox(height: FaiSpace.sm), + Padding( + padding: const EdgeInsets.only(left: 32), + child: Wrap( + spacing: FaiSpace.xs, + runSpacing: FaiSpace.xs, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + l.flowsMissingModulesLabel, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.error, + letterSpacing: 0.4, + ), ), - ), - const SizedBox(height: 2), - Text( - flow.path, - style: FaiTheme.mono( - size: 11, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (!isRunnable) ...[ - const SizedBox(height: FaiSpace.sm), - Wrap( - spacing: FaiSpace.xs, - runSpacing: FaiSpace.xs, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - l.flowsMissingModulesLabel, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.error, - letterSpacing: 0.4, + for (final cap in missingCapabilities) + _ClickablePill( + label: cap, + tooltip: l.flowsInstallPillTooltip, + onTap: () => onInstallSingle(cap), + ), + if (onInstallAll != null) + Padding( + padding: const EdgeInsets.only(left: FaiSpace.sm), + child: FilledButton.tonalIcon( + onPressed: onInstallAll, + icon: const Icon(Icons.download_outlined, size: 14), + label: Text( + l.flowsInstallAllButton(missingCapabilities.length), + ), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: FaiSpace.md, + vertical: 0, + ), + minimumSize: const Size(0, 28), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), - for (final cap in missingCapabilities) - FaiPill( - label: cap, - tone: FaiPillTone.danger, - monospace: true, - ), - ], - ), + ), ], - ], + ), ), - ), - FaiPill( - label: '${flow.sizeBytes} B', - tone: FaiPillTone.neutral, + ], + ], + ), + ); + } +} + +/// Tappable variant of `FaiPill` used for the missing-capability +/// row. Wraps the pill in `InkWell` + `Tooltip` so the operator +/// gets a click affordance and a "Click to install" hint. +class _ClickablePill extends StatelessWidget { + final String label; + final String tooltip; + final VoidCallback onTap; + + const _ClickablePill({ + required this.label, + required this.tooltip, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(FaiRadius.sm), + child: InkWell( + borderRadius: BorderRadius.circular(FaiRadius.sm), + onTap: onTap, + child: FaiPill( + label: label, + tone: FaiPillTone.danger, + icon: Icons.download_outlined, monospace: true, ), - const SizedBox(width: FaiSpace.md), - Tooltip( - message: isRunnable ? '' : l.flowsRunDisabledTooltip, - child: FilledButton.icon( - onPressed: onRun, - icon: const Icon(Icons.play_arrow, size: 16), - label: Text(l.flowsRunButton), - ), - ), - ], + ), ), ); } @@ -731,3 +844,256 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { ); } } + +/// Per-spec install state for the dependencies dialog. The +/// dialog walks the list sequentially, advancing each row from +/// `pending` → `installing` → `done` | `failed` so the operator +/// sees one row light up at a time. +enum _InstallStatus { pending, installing, done, failed } + +class _InstallItemState { + /// Verbatim spec from the flow's `use:` line, e.g. + /// `text.extract@^0`. Shown as-is so the operator recognises + /// the same string they saw on the flow card. + final String spec; + + /// Bare capability name (everything left of `@`). What the + /// hub's `installModule` actually resolves through the store + /// index — the version constraint is dropped here because + /// the index already picks a matching version. + final String bareName; + + _InstallStatus status; + String? installedVersion; + String? error; + + _InstallItemState(this.spec) + : bareName = _stripVersion(spec), + status = _InstallStatus.pending; + + static String _stripVersion(String spec) { + final at = spec.indexOf('@'); + return at >= 0 ? spec.substring(0, at) : spec; + } +} + +/// Sequential installer for a list of capability specs. Pops +/// with `true` if at least one install completed successfully +/// (so the parent knows to refresh its installed-modules view), +/// `false` otherwise. +class _InstallDependenciesDialog extends StatefulWidget { + final List specs; + + const _InstallDependenciesDialog({required this.specs}); + + @override + State<_InstallDependenciesDialog> createState() => + _InstallDependenciesDialogState(); +} + +class _InstallDependenciesDialogState + extends State<_InstallDependenciesDialog> { + late final List<_InstallItemState> _items; + bool _running = true; + + @override + void initState() { + super.initState(); + _items = widget.specs.map(_InstallItemState.new).toList(); + // Kick off the install sequence after first frame so any + // initState-time setState calls inside the chain hit a + // mounted widget. + WidgetsBinding.instance.addPostFrameCallback((_) => _runAll()); + } + + Future _runAll() async { + for (final item in _items) { + if (!mounted) return; + setState(() => item.status = _InstallStatus.installing); + try { + final r = await HubService.instance.installModule( + source: item.bareName, + ); + if (!mounted) return; + setState(() { + item.status = _InstallStatus.done; + item.installedVersion = r.version; + }); + } catch (e) { + if (!mounted) return; + setState(() { + item.status = _InstallStatus.failed; + item.error = e.toString(); + }); + } + } + if (!mounted) return; + setState(() => _running = false); + } + + bool get _anySucceeded => + _items.any((i) => i.status == _InstallStatus.done); + + int get _doneCount => + _items.where((i) => i.status == _InstallStatus.done).length; + + int get _failedCount => + _items.where((i) => i.status == _InstallStatus.failed).length; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return AlertDialog( + title: Text(l.flowsInstallDepsTitle), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(FaiRadius.md), + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520, maxHeight: 480), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.flowsInstallDepsBody, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: FaiSpace.md), + for (final item in _items) ...[ + _InstallRow(item: item), + const SizedBox(height: FaiSpace.sm), + ], + if (!_running) ...[ + const SizedBox(height: FaiSpace.xs), + Text( + _failedCount == 0 + ? l.flowsInstallDepsAllDone + : l.flowsInstallDepsAnyFailed(_doneCount, _failedCount), + style: theme.textTheme.bodySmall?.copyWith( + color: _failedCount == 0 + ? null + : theme.colorScheme.error, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: _running + ? null + : () => Navigator.pop(context, _anySucceeded), + child: Text(l.buttonClose), + ), + ], + ); + } +} + +/// One row in the install-dependencies dialog. Status icon + +/// capability name + per-state detail line (version on success, +/// truncated error on failure). +class _InstallRow extends StatelessWidget { + final _InstallItemState item; + + const _InstallRow({required this.item}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Center(child: _statusIcon(theme)), + ), + const SizedBox(width: FaiSpace.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.spec, + style: FaiTheme.mono( + size: 12, + weight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + _detailLine(l), + style: theme.textTheme.bodySmall?.copyWith( + color: item.status == _InstallStatus.failed + ? theme.colorScheme.error + : theme.colorScheme.onSurfaceVariant, + ), + ), + if (item.status == _InstallStatus.failed && + item.error != null) ...[ + const SizedBox(height: FaiSpace.xs), + FaiErrorBox( + text: item.error!, + isError: true, + maxHeight: 120, + ), + ], + ], + ), + ), + ], + ); + } + + Widget _statusIcon(ThemeData theme) { + switch (item.status) { + case _InstallStatus.pending: + return Icon( + Icons.radio_button_unchecked, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ); + case _InstallStatus.installing: + return const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ); + case _InstallStatus.done: + return Icon( + Icons.check_circle, + size: 16, + color: theme.colorScheme.primary, + ); + case _InstallStatus.failed: + return Icon( + Icons.error_outline, + size: 16, + color: theme.colorScheme.error, + ); + } + } + + String _detailLine(AppLocalizations l) { + switch (item.status) { + case _InstallStatus.pending: + return l.flowsInstallStatusPending; + case _InstallStatus.installing: + return l.flowsInstallStatusInstalling; + case _InstallStatus.done: + return l.flowsInstallStatusDone(item.installedVersion ?? '?'); + case _InstallStatus.failed: + return l.flowsInstallStatusFailed; + } + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..438e7e5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import file_picker import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 1385d0f..6ebac85 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,20 +1,26 @@ PODS: + - file_picker (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS DEPENDENCIES: + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) EXTERNAL SOURCES: + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos FlutterMacOS: :path: Flutter/ephemeral shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin SPEC CHECKSUMS: + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb diff --git a/pubspec.yaml b/pubspec.yaml index 73ec444..0aa64ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.42.0 +version: 0.43.0 environment: sdk: ^3.11.0-200.1.beta