feat(studio): localised + explainer-rich Add module source dialog
Some checks failed
Security / Security check (push) Failing after 1s

The previous Add-source dialog was English-only and
operator-hostile — answered 'where do I install from?' without
explaining what a private module *is*. New version, DE + EN:

  - Title + intro + button labels routed through AppLocalizations
    ("Modul-Quelle hinzufügen" / "Installation fehlgeschlagen"
    instead of raw English strings).
  - 'How private modules work' explainer block (3 sentences):
    what a module is (`module.yaml` + WASM), how to package
    it (`fai pack <dir>` → .fai bundle), where to host it
    (any URL: own Forgejo / GitHub / S3), what verification the
    hub does (sha256 + signature against trust store).
  - Honest about the Studio gap: the dialog can install URLs +
    packed bundles, but unpacked source directories still
    require `fai install --link <path>` on the CLI. The
    example command lives in its own code-style box, selectable.
  - Layout: SingleChildScrollView'd so the explainer doesn't
    push the buttons off short viewports.

Studio bumped to 0.66.0; editor path-override pulls in 0.20.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-09 01:36:54 +02:00
parent eba7e51c7f
commit 2be7d241bf
9 changed files with 259 additions and 66 deletions

View file

@ -1448,5 +1448,25 @@
"count": {"type": "int"} "count": {"type": "int"}
} }
}, },
"doctorPathStudioErrors": "Studio-Fehler" "doctorPathStudioErrors": "Studio-Fehler",
"addSourceTitle": "Modul-Quelle hinzufügen",
"addSourceIntro": "`{capability}` ist nicht im öffentlichen Store. Zeig dem Hub eine `.fai`-Bundle-URL oder einen lokalen Bundle-Pfad — der Hub lädt es herunter, prüft (sha256 + Signatur) und installiert.",
"@addSourceIntro": {
"placeholders": {
"capability": {"type": "String"}
}
},
"addSourceField": "URL oder Pfad zum .fai-Bundle",
"addSourceHint": "https://git.flemming.ai/deine-org/dein-modul/releases/download/v0.1.0/foo-0.1.0.fai",
"addSourceHowItWorksTitle": "Wie private Module funktionieren",
"addSourceHowItWorksBody": "Ein Modul ist ein Verzeichnis mit module.yaml + WASM-Artefakt. Zum Teilen: `fai pack <verzeichnis>` baut ein `.fai`-Bundle, das du beliebig hosten kannst (eigene Forgejo / GitHub / S3). Der Hub installiert per URL und prüft die Signatur gegen seinen Trust-Store.\n\nLokal entwickeln? Nimm das CLI — Studio installiert (noch) nicht aus einem unverpackten Verzeichnis:",
"addSourceCliExample": "fai install --link /pfad/zum/modul",
"addSourceInstallButton": "Installieren",
"addSourceCancel": "Abbrechen",
"installFailed": "Installation fehlgeschlagen: {error}",
"@installFailed": {
"placeholders": {
"error": {"type": "String"}
}
}
} }

View file

@ -1451,5 +1451,25 @@
"count": {"type": "int"} "count": {"type": "int"}
} }
}, },
"doctorPathStudioErrors": "Studio errors" "doctorPathStudioErrors": "Studio errors",
"addSourceTitle": "Add module source",
"addSourceIntro": "`{capability}` is not in the public store. Point the hub at a `.fai` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.",
"@addSourceIntro": {
"placeholders": {
"capability": {"type": "String"}
}
},
"addSourceField": "URL or path to .fai bundle",
"addSourceHint": "https://git.flemming.ai/your-org/your-module/releases/download/v0.1.0/foo-0.1.0.fai",
"addSourceHowItWorksTitle": "How private modules work",
"addSourceHowItWorksBody": "A module is a directory with a module.yaml + the WASM artifact. To share it: pack it (`fai pack <dir>`) and host the resulting `.fai` bundle anywhere (your own Forgejo / GitHub / S3). The hub installs by URL and verifies the signature against its trust store.\n\nDeveloping locally? Use the CLI — Studio cannot install from an unpacked directory (yet):",
"addSourceCliExample": "fai install --link /path/to/module",
"addSourceInstallButton": "Install",
"addSourceCancel": "Cancel",
"installFailed": "Install failed: {error}",
"@installFailed": {
"placeholders": {
"error": {"type": "String"}
}
}
} }

View file

@ -3967,6 +3967,66 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Studio errors'** /// **'Studio errors'**
String get doctorPathStudioErrors; String get doctorPathStudioErrors;
/// No description provided for @addSourceTitle.
///
/// In en, this message translates to:
/// **'Add module source'**
String get addSourceTitle;
/// No description provided for @addSourceIntro.
///
/// In en, this message translates to:
/// **'`{capability}` is not in the public store. Point the hub at a `.fai` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.'**
String addSourceIntro(String capability);
/// No description provided for @addSourceField.
///
/// In en, this message translates to:
/// **'URL or path to .fai bundle'**
String get addSourceField;
/// No description provided for @addSourceHint.
///
/// In en, this message translates to:
/// **'https://git.flemming.ai/your-org/your-module/releases/download/v0.1.0/foo-0.1.0.fai'**
String get addSourceHint;
/// No description provided for @addSourceHowItWorksTitle.
///
/// In en, this message translates to:
/// **'How private modules work'**
String get addSourceHowItWorksTitle;
/// No description provided for @addSourceHowItWorksBody.
///
/// In en, this message translates to:
/// **'A module is a directory with a module.yaml + the WASM artifact. To share it: pack it (`fai pack <dir>`) and host the resulting `.fai` bundle anywhere (your own Forgejo / GitHub / S3). The hub installs by URL and verifies the signature against its trust store.\n\nDeveloping locally? Use the CLI — Studio cannot install from an unpacked directory (yet):'**
String get addSourceHowItWorksBody;
/// No description provided for @addSourceCliExample.
///
/// In en, this message translates to:
/// **'fai install --link /path/to/module'**
String get addSourceCliExample;
/// No description provided for @addSourceInstallButton.
///
/// In en, this message translates to:
/// **'Install'**
String get addSourceInstallButton;
/// No description provided for @addSourceCancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get addSourceCancel;
/// No description provided for @installFailed.
///
/// In en, this message translates to:
/// **'Install failed: {error}'**
String installFailed(String error);
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -2322,4 +2322,40 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get doctorPathStudioErrors => 'Studio-Fehler'; String get doctorPathStudioErrors => 'Studio-Fehler';
@override
String get addSourceTitle => 'Modul-Quelle hinzufügen';
@override
String addSourceIntro(String capability) {
return '`$capability` ist nicht im öffentlichen Store. Zeig dem Hub eine `.fai`-Bundle-URL oder einen lokalen Bundle-Pfad — der Hub lädt es herunter, prüft (sha256 + Signatur) und installiert.';
}
@override
String get addSourceField => 'URL oder Pfad zum .fai-Bundle';
@override
String get addSourceHint =>
'https://git.flemming.ai/deine-org/dein-modul/releases/download/v0.1.0/foo-0.1.0.fai';
@override
String get addSourceHowItWorksTitle => 'Wie private Module funktionieren';
@override
String get addSourceHowItWorksBody =>
'Ein Modul ist ein Verzeichnis mit module.yaml + WASM-Artefakt. Zum Teilen: `fai pack <verzeichnis>` baut ein `.fai`-Bundle, das du beliebig hosten kannst (eigene Forgejo / GitHub / S3). Der Hub installiert per URL und prüft die Signatur gegen seinen Trust-Store.\n\nLokal entwickeln? Nimm das CLI — Studio installiert (noch) nicht aus einem unverpackten Verzeichnis:';
@override
String get addSourceCliExample => 'fai install --link /pfad/zum/modul';
@override
String get addSourceInstallButton => 'Installieren';
@override
String get addSourceCancel => 'Abbrechen';
@override
String installFailed(String error) {
return 'Installation fehlgeschlagen: $error';
}
} }

View file

@ -2327,4 +2327,40 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get doctorPathStudioErrors => 'Studio errors'; String get doctorPathStudioErrors => 'Studio errors';
@override
String get addSourceTitle => 'Add module source';
@override
String addSourceIntro(String capability) {
return '`$capability` is not in the public store. Point the hub at a `.fai` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.';
}
@override
String get addSourceField => 'URL or path to .fai bundle';
@override
String get addSourceHint =>
'https://git.flemming.ai/your-org/your-module/releases/download/v0.1.0/foo-0.1.0.fai';
@override
String get addSourceHowItWorksTitle => 'How private modules work';
@override
String get addSourceHowItWorksBody =>
'A module is a directory with a module.yaml + the WASM artifact. To share it: pack it (`fai pack <dir>`) and host the resulting `.fai` bundle anywhere (your own Forgejo / GitHub / S3). The hub installs by URL and verifies the signature against its trust store.\n\nDeveloping locally? Use the CLI — Studio cannot install from an unpacked directory (yet):';
@override
String get addSourceCliExample => 'fai install --link /path/to/module';
@override
String get addSourceInstallButton => 'Install';
@override
String get addSourceCancel => 'Cancel';
@override
String installFailed(String error) {
return 'Install failed: $error';
}
} }

View file

@ -27,7 +27,7 @@ import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the /// Studio's own build version. Bump on every UI commit so the
/// running app self-identifies visible in the sidebar header /// running app self-identifies visible in the sidebar header
/// and quick-glance proof that you're seeing the current build. /// and quick-glance proof that you're seeing the current build.
const String kStudioVersion = '0.65.1'; const String kStudioVersion = '0.66.0';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();

View file

@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import '../data/flow_run_driver.dart'; import '../data/flow_run_driver.dart';
import '../data/hub.dart'; import '../data/hub.dart';
import '../l10n/app_localizations.dart';
class FlowsPage extends StatefulWidget { class FlowsPage extends StatefulWidget {
/// Pre-load this flow when the editor first builds. Studio /// Pre-load this flow when the editor first builds. Studio
@ -121,10 +122,14 @@ class _FlowsPageState extends State<FlowsPage> {
return updated; return updated;
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final l = AppLocalizations.of(context);
final msg = l != null
? l.installFailed(e.toString())
: 'Install failed: $e';
ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Install failed: $e'), content: Text(msg),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
@ -189,83 +194,99 @@ class _AddModuleSourceDialogState extends State<_AddModuleSourceDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final l = AppLocalizations.of(context)!;
return AlertDialog( return AlertDialog(
title: const Text('Add module source'), title: Text(l.addSourceTitle),
content: SizedBox( content: ConstrainedBox(
width: 480, constraints: const BoxConstraints(maxWidth: 520),
child: Column( child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'`${widget.capability}` is not in the public store. ' Text(
'Point the hub at a `.fai` bundle URL or a local bundle path ' l.addSourceIntro(widget.capability),
'and the hub will install + verify it.', style: theme.textTheme.bodySmall?.copyWith(
style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant,
color: theme.colorScheme.onSurfaceVariant, ),
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), TextField(
TextField( controller: _ctrl,
controller: _ctrl, autofocus: true,
autofocus: true, onChanged: (v) => setState(() => _source = v.trim()),
onChanged: (v) => setState(() => _source = v.trim()), decoration: InputDecoration(
decoration: const InputDecoration( labelText: l.addSourceField,
labelText: 'URL or path to .fai bundle', hintText: l.addSourceHint,
hintText: 'https://example.com/foo-0.1.0.fai', border: const OutlineInputBorder(),
border: OutlineInputBorder(), isDense: true,
isDense: true, ),
), ),
), const SizedBox(height: 12),
const SizedBox(height: 12), Container(
Container( padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(10), decoration: BoxDecoration(
decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh,
color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(6),
borderRadius: BorderRadius.circular(6), ),
), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( l.addSourceHowItWorksTitle,
'Developing a local module?', style: theme.textTheme.labelMedium?.copyWith(
style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, ),
), ),
), const SizedBox(height: 6),
const SizedBox(height: 4), Text(
Text( l.addSourceHowItWorksBody,
'Studio installs from URLs + packed bundles only. For an ' style: theme.textTheme.bodySmall?.copyWith(
'unpacked source directory, use the CLI:', color: theme.colorScheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith( ),
color: theme.colorScheme.onSurfaceVariant,
), ),
), const SizedBox(height: 6),
const SizedBox(height: 6), Container(
SelectableText( padding: const EdgeInsets.symmetric(
'fai install --link /path/to/module', horizontal: 8,
style: theme.textTheme.bodySmall?.copyWith( vertical: 4,
fontFamily: 'JetBrains Mono', ),
fontSize: 11, decoration: BoxDecoration(
color: theme.colorScheme.onSurface, color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(4),
),
child: SelectableText(
l.addSourceCliExample,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontFamilyFallback: const [
'Menlo',
'Consolas',
'Courier New',
'monospace',
],
fontSize: 11,
color: theme.colorScheme.onSurface,
),
),
), ),
), ],
], ),
), ),
), ],
], ),
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'), child: Text(l.addSourceCancel),
), ),
FilledButton( FilledButton(
onPressed: _source.isEmpty onPressed: _source.isEmpty
? null ? null
: () => Navigator.of(context).pop(_source), : () => Navigator.of(context).pop(_source),
child: const Text('Install'), child: Text(l.addSourceInstallButton),
), ),
], ],
); );

View file

@ -126,7 +126,7 @@ packages:
path: "../fai_studio_flow_editor" path: "../fai_studio_flow_editor"
relative: true relative: true
source: path source: path
version: "0.18.0" version: "0.19.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,7 +1,7 @@
name: fai_studio name: fai_studio
description: "F∆I Studio — desktop GUI for the F∆I hub" description: "F∆I Studio — desktop GUI for the F∆I hub"
publish_to: 'none' publish_to: 'none'
version: 0.65.1 version: 0.66.0
environment: environment:
sdk: ^3.11.0-200.1.beta sdk: ^3.11.0-200.1.beta