feat(studio): de-jargonize, reveal-in-OS, copyable errors, today carousel, AppBar alignment, F∆I window title (v0.34.0)
Sweeping pass against six user reports collected this session. 1. "Capabilities ist nicht deutsch, Chain auch nicht." The DE locale still leaked English vocabulary. Replaced "Capabilities" → "Fähigkeiten" and "Chain" / "Hash-Chain" → "Kette" / "Hash-Kette" everywhere — store search hint, recommended-source body, federated toast, doctor summary chain row, modules panel summary, MCP / n8n hints, the approvals history blurb. Wire-level identifiers (`chain.reset`) stay as code. 2. "Bei Fehlern unten muss man die auch ins clipboard kopieren können." New `FaiErrorBox` widget: selectable monospace block with a small copy-to-clipboard icon button that flips to a checkmark for two seconds after click. Applied to the Doctor update banner output and the Settings channel toast — the two places long stderr / stdout lands. 3. "Öffnen bei Log kann es nicht öffnen. Audit-DB auch nicht. PID auch nicht." Cause: `SystemActions.openInOs` shells out to `open` / `xdg-open` on file paths the OS has no default handler for (SQLite DB, PID file, log without an .ext that binds). New `revealInOs` uses `open -R` on macOS, `explorer /select,` on Windows, and the parent directory via `xdg-open` on Linux. Doctor's path rows carry an `isDirectory` flag that routes through the new `openOrReveal` so files reveal in Finder / Explorer instead of failing silently. 4. "Oben im Store könnte man diesen Redaktionshinweis auch so bauen, dass man mit pfeil nach rechts links auch weitere anzeigen kann." The Today-Hero became a carousel. Curated fallback list grew from one entry to four (public sources, the sandbox-by-default permission story, the hash-chained audit story, the air-gap-ready single-binary pitch). Hero gets prev / next chevrons plus a dot indicator when the current snapshot has more than one slide. Operator-accepted stories stay single — the carousel collapses when there's only one to show. 5. "Ich fände es schöner wenn rechts und links im Store die Abstände konsistent sind, das Reload-Symbol rechts ist zu weit rechts und Store links auch nicht bündig." AppBar now has `titleSpacing: FaiSpace.xl` so the title's left edge sits flush with the body's left padding (24 dp), and the trailing `SizedBox` after the reload icon shrunk so the icon's outer edge meets the right edge of the rightmost grid card. 6. "Oben der Titel zeigt fai_studio an, das sollte F∆I Studio sein." The OS window title was the pubspec-derived "fai_studio". Macos/Linux/Windows runners now hard-code "F∆I Studio" (with the U+2206 triangle escape so the C++ source stays ASCII). macOS bundle name and display name lifted out of the PRODUCT_NAME variable for the same reason. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
22a851b7e2
commit
3b5fb027c3
17 changed files with 454 additions and 119 deletions
|
|
@ -39,6 +39,50 @@ class SystemActions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reveal [path] in the platform file manager. Works for binary
|
||||||
|
/// files (the SQLite audit DB, the PID file) where `openInOs`
|
||||||
|
/// would fall flat — macOS refuses to "open" a file with no
|
||||||
|
/// registered app. macOS uses `open -R`; Windows
|
||||||
|
/// `explorer /select,`; Linux falls back to opening the parent
|
||||||
|
/// directory because there is no portable reveal-and-highlight.
|
||||||
|
static Future<({bool ok, String stderr})> revealInOs(String path) async {
|
||||||
|
if (path.isEmpty) {
|
||||||
|
return (ok: false, stderr: 'empty path');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final ProcessResult r;
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
r = await Process.run('open', ['-R', path]);
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
// /select, requires no space after the comma; explorer
|
||||||
|
// returns 1 even on success, so don't gate on exitCode.
|
||||||
|
r = await Process.run('explorer', ['/select,$path']);
|
||||||
|
return (ok: true, stderr: '');
|
||||||
|
} else {
|
||||||
|
// Linux: open the containing directory in the default
|
||||||
|
// file manager. Best portable approximation.
|
||||||
|
final parent = File(path).parent.path;
|
||||||
|
r = await Process.run('xdg-open', [parent]);
|
||||||
|
}
|
||||||
|
if (r.exitCode == 0) return (ok: true, stderr: '');
|
||||||
|
return (ok: false, stderr: r.stderr.toString().trim());
|
||||||
|
} catch (e) {
|
||||||
|
return (ok: false, stderr: e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: reveal binary files, open directories /
|
||||||
|
/// text-shaped paths normally. Doctor's path rows use this so
|
||||||
|
/// the operator never has to know which strategy fits which
|
||||||
|
/// kind of file.
|
||||||
|
static Future<({bool ok, String stderr})> openOrReveal(
|
||||||
|
String path, {
|
||||||
|
required bool isDirectory,
|
||||||
|
}) async {
|
||||||
|
if (isDirectory) return openInOs(path);
|
||||||
|
return revealInOs(path);
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a `fai daemon ...` subcommand and surface the captured
|
/// Run a `fai daemon ...` subcommand and surface the captured
|
||||||
/// output. Used by Doctor's "Restart daemon" button.
|
/// output. Used by Doctor's "Restart daemon" button.
|
||||||
static Future<({bool ok, String stdout, String stderr})> faiDaemon(
|
static Future<({bool ok, String stdout, String stderr})> faiDaemon(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
"buttonOpen": "Öffnen",
|
"buttonOpen": "Öffnen",
|
||||||
"buttonExplain": "Erklären",
|
"buttonExplain": "Erklären",
|
||||||
"buttonReadDocs": "Doku öffnen",
|
"buttonReadDocs": "Doku öffnen",
|
||||||
|
"buttonCopy": "In Zwischenablage kopieren",
|
||||||
|
"buttonCopied": "Kopiert",
|
||||||
|
|
||||||
"settingsTitle": "Hub-Endpunkt",
|
"settingsTitle": "Hub-Endpunkt",
|
||||||
"settingsHost": "Host",
|
"settingsHost": "Host",
|
||||||
|
|
@ -62,7 +64,7 @@
|
||||||
"n8nAddTooltip": "n8n-Endpunkt hinzufügen",
|
"n8nAddTooltip": "n8n-Endpunkt hinzufügen",
|
||||||
"n8nRemoveTooltip": "Endpunkt entfernen",
|
"n8nRemoveTooltip": "Endpunkt entfernen",
|
||||||
"addMcpTitle": "MCP-Server hinzufügen",
|
"addMcpTitle": "MCP-Server hinzufügen",
|
||||||
"addMcpIntro": "Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Capabilities.",
|
"addMcpIntro": "Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Fähigkeiten.",
|
||||||
"addMcpSuggestionsLabel": "Vorschläge — zum Vorausfüllen klicken",
|
"addMcpSuggestionsLabel": "Vorschläge — zum Vorausfüllen klicken",
|
||||||
"addMcpNameLabel": "Name (keine Punkte)",
|
"addMcpNameLabel": "Name (keine Punkte)",
|
||||||
"addMcpNameHint": "filesystem",
|
"addMcpNameHint": "filesystem",
|
||||||
|
|
@ -73,7 +75,7 @@
|
||||||
"addMcpNotesLabel": "Notizen (optional)",
|
"addMcpNotesLabel": "Notizen (optional)",
|
||||||
"addMcpAddButton": "Hinzufügen + Discovery",
|
"addMcpAddButton": "Hinzufügen + Discovery",
|
||||||
"addN8nTitle": "n8n-Endpunkt hinzufügen",
|
"addN8nTitle": "n8n-Endpunkt hinzufügen",
|
||||||
"addN8nIntro": "Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Capabilities.",
|
"addN8nIntro": "Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Fähigkeiten.",
|
||||||
"addN8nNameLabel": "Name (keine Punkte)",
|
"addN8nNameLabel": "Name (keine Punkte)",
|
||||||
"addN8nNameHint": "ops",
|
"addN8nNameHint": "ops",
|
||||||
"addN8nBaseUrlLabel": "Basis-URL (ohne /api/v1)",
|
"addN8nBaseUrlLabel": "Basis-URL (ohne /api/v1)",
|
||||||
|
|
@ -102,7 +104,7 @@
|
||||||
"channelsEnableAutostart": "Autostart bei Anmeldung aktivieren",
|
"channelsEnableAutostart": "Autostart bei Anmeldung aktivieren",
|
||||||
"channelsDisableAutostart": "Autostart deaktivieren",
|
"channelsDisableAutostart": "Autostart deaktivieren",
|
||||||
|
|
||||||
"storeSearchHint": "Module suchen — Name, Tagline, Tag, Capability…",
|
"storeSearchHint": "Module suchen — Name, Tagline, Tag, Fähigkeit…",
|
||||||
"storeAskHint": "Was brauchst du? Versuch's mit „ein Modul, das tabellarische Daten liest\" oder tippe einfach einen Begriff.",
|
"storeAskHint": "Was brauchst du? Versuch's mit „ein Modul, das tabellarische Daten liest\" oder tippe einfach einen Begriff.",
|
||||||
"storeAskSubmit": "Suchen",
|
"storeAskSubmit": "Suchen",
|
||||||
"storeAskAi": "KI fragen",
|
"storeAskAi": "KI fragen",
|
||||||
|
|
@ -187,7 +189,7 @@
|
||||||
"storeOpenBrowserFailed": "Browser konnte nicht geöffnet werden: {error}",
|
"storeOpenBrowserFailed": "Browser konnte nicht geöffnet werden: {error}",
|
||||||
"@storeOpenBrowserFailed": { "placeholders": { "error": { "type": "String" } } },
|
"@storeOpenBrowserFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"storeSectionTags": "Tags",
|
"storeSectionTags": "Tags",
|
||||||
"storeSectionRequiredCapabilities": "Erforderliche Capabilities",
|
"storeSectionRequiredCapabilities": "Erforderliche Fähigkeiten",
|
||||||
"storeSectionRequiredHostServices": "Erforderliche Host-Dienste",
|
"storeSectionRequiredHostServices": "Erforderliche Host-Dienste",
|
||||||
"storeSectionScreenshots": "Screenshots",
|
"storeSectionScreenshots": "Screenshots",
|
||||||
"storeSectionDocumentation": "Dokumentation",
|
"storeSectionDocumentation": "Dokumentation",
|
||||||
|
|
@ -221,19 +223,19 @@
|
||||||
"storeProvenanceN8n": "n8n · {provider}",
|
"storeProvenanceN8n": "n8n · {provider}",
|
||||||
"@storeProvenanceN8n": { "placeholders": { "provider": { "type": "String" } } },
|
"@storeProvenanceN8n": { "placeholders": { "provider": { "type": "String" } } },
|
||||||
"storeRecommendedTitle": "Öffentliche Quelle hinzufügen — ein Klick",
|
"storeRecommendedTitle": "Öffentliche Quelle hinzufügen — ein Klick",
|
||||||
"storeRecommendedBody": "Kuratierte MCP-Server über HTTPS erreichbar. Kein Subprozess, kein API-Key, kein Node. Föderierte Capabilities erscheinen sofort im Store.",
|
"storeRecommendedBody": "Kuratierte MCP-Server über HTTPS erreichbar. Kein Subprozess, kein API-Key, kein Node. Föderierte Fähigkeiten erscheinen sofort im Store.",
|
||||||
"storeRecommendedAddTooltip": "Hinzufügen und entdecken",
|
"storeRecommendedAddTooltip": "Hinzufügen und entdecken",
|
||||||
"storeRecommendedAdding": "Wird hinzugefügt…",
|
"storeRecommendedAdding": "Wird hinzugefügt…",
|
||||||
"storeRecommendedDeepWikiTagline": "GitHub-Repo-Doku-Suche (KI-gestützt)",
|
"storeRecommendedDeepWikiTagline": "GitHub-Repo-Doku-Suche (KI-gestützt)",
|
||||||
"storeRecommendedSemgrepTagline": "Security-Scanning für Code-Schwachstellen",
|
"storeRecommendedSemgrepTagline": "Security-Scanning für Code-Schwachstellen",
|
||||||
"storeProviderAddedToast": "{name} hinzugefügt — {n} {n, plural, =1{föderierte Capability} other{föderierte Capabilities}}.",
|
"storeProviderAddedToast": "{name} hinzugefügt — {n} {n, plural, =1{föderierte Fähigkeit} other{föderierte Fähigkeiten}}.",
|
||||||
"@storeProviderAddedToast": {
|
"@storeProviderAddedToast": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": { "type": "String" },
|
"name": { "type": "String" },
|
||||||
"n": { "type": "int" }
|
"n": { "type": "int" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storeProviderAddedZeroToast": "{name} ist eingetragen, aber der Server hat noch keine Capabilities geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.",
|
"storeProviderAddedZeroToast": "{name} ist eingetragen, aber der Server hat noch keine Fähigkeiten geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.",
|
||||||
"@storeProviderAddedZeroToast": {
|
"@storeProviderAddedZeroToast": {
|
||||||
"placeholders": { "name": { "type": "String" } }
|
"placeholders": { "name": { "type": "String" } }
|
||||||
},
|
},
|
||||||
|
|
@ -253,7 +255,7 @@
|
||||||
"auditFilterStep": "Step",
|
"auditFilterStep": "Step",
|
||||||
"auditFilterModule": "Modul",
|
"auditFilterModule": "Modul",
|
||||||
"auditClearLogTooltip": "Protokoll löschen (nur local/dev Kanal)",
|
"auditClearLogTooltip": "Protokoll löschen (nur local/dev Kanal)",
|
||||||
"auditClearedToast": "{n} Ereignisse auf Kanal „{channel}\" gelöscht. Chain mit Marker neu geseedet.",
|
"auditClearedToast": "{n} Ereignisse auf Kanal „{channel}\" gelöscht. Kette mit Marker neu geseedet.",
|
||||||
"@auditClearedToast": {
|
"@auditClearedToast": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"n": { "type": "int" },
|
"n": { "type": "int" },
|
||||||
|
|
@ -276,7 +278,7 @@
|
||||||
"@auditLiveStatus": { "placeholders": { "n": { "type": "int" } } },
|
"@auditLiveStatus": { "placeholders": { "n": { "type": "int" } } },
|
||||||
"auditDisconnected": "getrennt · {error}",
|
"auditDisconnected": "getrennt · {error}",
|
||||||
"@auditDisconnected": { "placeholders": { "error": { "type": "String" } } },
|
"@auditDisconnected": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"auditHashChainVerified": "Hash-Chain verifiziert",
|
"auditHashChainVerified": "Hash-Kette verifiziert",
|
||||||
"auditDetailHeader": "DETAIL",
|
"auditDetailHeader": "DETAIL",
|
||||||
"auditAskingFull": "Frage an die konfigurierte System-AI läuft…",
|
"auditAskingFull": "Frage an die konfigurierte System-AI läuft…",
|
||||||
"auditFixLabel": "FIX",
|
"auditFixLabel": "FIX",
|
||||||
|
|
@ -308,7 +310,7 @@
|
||||||
"@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } },
|
"@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } },
|
||||||
"modulesActivityInstalled": "installiert",
|
"modulesActivityInstalled": "installiert",
|
||||||
"modulesActivityUninstalled": "deinstalliert",
|
"modulesActivityUninstalled": "deinstalliert",
|
||||||
"modulesCapabilitiesLabel": "Capabilities",
|
"modulesCapabilitiesLabel": "Fähigkeiten",
|
||||||
"modulesUninstallTitle": "Modul deinstallieren?",
|
"modulesUninstallTitle": "Modul deinstallieren?",
|
||||||
"modulesUninstallBody": "Entfernt {name} v{version} von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.",
|
"modulesUninstallBody": "Entfernt {name} v{version} von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.",
|
||||||
"@modulesUninstallBody": {
|
"@modulesUninstallBody": {
|
||||||
|
|
@ -329,7 +331,7 @@
|
||||||
"modulesUninstalling": "Wird deinstalliert…",
|
"modulesUninstalling": "Wird deinstalliert…",
|
||||||
"moduleSheetFailedToLoad": "Laden fehlgeschlagen: {error}",
|
"moduleSheetFailedToLoad": "Laden fehlgeschlagen: {error}",
|
||||||
"@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
"@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"moduleSheetCapabilities": "Capabilities",
|
"moduleSheetCapabilities": "Fähigkeiten",
|
||||||
"moduleSheetPermissions": "Deklarierte Berechtigungen",
|
"moduleSheetPermissions": "Deklarierte Berechtigungen",
|
||||||
"moduleSheetNoPermissions": "(keine — reines Berechnungsmodul)",
|
"moduleSheetNoPermissions": "(keine — reines Berechnungsmodul)",
|
||||||
|
|
||||||
|
|
@ -381,7 +383,7 @@
|
||||||
"approvalsRejectFailed": "Ablehnung fehlgeschlagen: {error}",
|
"approvalsRejectFailed": "Ablehnung fehlgeschlagen: {error}",
|
||||||
"@approvalsRejectFailed": { "placeholders": { "error": { "type": "String" } } },
|
"@approvalsRejectFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"approvalsHistoryEmpty": "Noch kein Verlauf",
|
"approvalsHistoryEmpty": "Noch kein Verlauf",
|
||||||
"approvalsHistoryEmptyHint": "Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-chained Datensatz.",
|
"approvalsHistoryEmptyHint": "Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-verketteten Datensatz.",
|
||||||
"approvalsDialogDecided": "entschieden",
|
"approvalsDialogDecided": "entschieden",
|
||||||
"approvalsDialogCreated": "erstellt",
|
"approvalsDialogCreated": "erstellt",
|
||||||
"approvalsDialogReason": "Begründung",
|
"approvalsDialogReason": "Begründung",
|
||||||
|
|
@ -396,10 +398,10 @@
|
||||||
"doctorSummaryApprovals": "Freigaben",
|
"doctorSummaryApprovals": "Freigaben",
|
||||||
"doctorSummaryAudit": "Protokoll",
|
"doctorSummaryAudit": "Protokoll",
|
||||||
"doctorSummaryServices": "Dienste",
|
"doctorSummaryServices": "Dienste",
|
||||||
"doctorSummaryCapabilities": "{n} Capabilities",
|
"doctorSummaryCapabilities": "{n} Fähigkeiten",
|
||||||
"@doctorSummaryCapabilities": { "placeholders": { "n": { "type": "int" } } },
|
"@doctorSummaryCapabilities": { "placeholders": { "n": { "type": "int" } } },
|
||||||
"doctorSummaryPending": "offen",
|
"doctorSummaryPending": "offen",
|
||||||
"doctorSummaryChain": "{verified}/{total} Chain",
|
"doctorSummaryChain": "{verified}/{total} Kette",
|
||||||
"@doctorSummaryChain": {
|
"@doctorSummaryChain": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"verified": { "type": "int" },
|
"verified": { "type": "int" },
|
||||||
|
|
@ -407,7 +409,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doctorSummaryDeclared": "deklariert",
|
"doctorSummaryDeclared": "deklariert",
|
||||||
"doctorModulesPanelSummary": "{n} Module · {m} Capabilities",
|
"doctorModulesPanelSummary": "{n} Module · {m} Fähigkeiten",
|
||||||
"@doctorModulesPanelSummary": {
|
"@doctorModulesPanelSummary": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"n": { "type": "int" },
|
"n": { "type": "int" },
|
||||||
|
|
@ -428,7 +430,7 @@
|
||||||
"doctorHostServicesSection": "Host-Dienste",
|
"doctorHostServicesSection": "Host-Dienste",
|
||||||
"doctorDaemonFilesSection": "Daemon-Dateien",
|
"doctorDaemonFilesSection": "Daemon-Dateien",
|
||||||
"doctorDaemonControlSection": "Daemon-Steuerung",
|
"doctorDaemonControlSection": "Daemon-Steuerung",
|
||||||
"doctorChainIntact": "Hash-Chain intakt",
|
"doctorChainIntact": "Hash-Kette intakt",
|
||||||
"doctorChainIntactDetail": "{n} Ereignisse · prev_event_sha256 lückenlos verifiziert",
|
"doctorChainIntactDetail": "{n} Ereignisse · prev_event_sha256 lückenlos verifiziert",
|
||||||
"@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } },
|
"@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } },
|
||||||
"doctorChainTampered": "Manipulation erkannt",
|
"doctorChainTampered": "Manipulation erkannt",
|
||||||
|
|
@ -491,10 +493,10 @@
|
||||||
"doctorApplyDone": "Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.",
|
"doctorApplyDone": "Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.",
|
||||||
|
|
||||||
"mcpHeader": "MCP-CLIENTS",
|
"mcpHeader": "MCP-CLIENTS",
|
||||||
"mcpHint": "Externe MCP-Server bringen ihre Tools als `mcp.<server>.<tool>` Capabilities ein. Einmal konfigurieren, im Store sichtbar.",
|
"mcpHint": "Externe MCP-Server bringen ihre Tools als `mcp.<server>.<tool>` Fähigkeiten ein. Einmal konfigurieren, im Store sichtbar.",
|
||||||
"mcpEmpty": "Keine MCP-Server konfiguriert. + klicken zum Hinzufügen.",
|
"mcpEmpty": "Keine MCP-Server konfiguriert. + klicken zum Hinzufügen.",
|
||||||
"n8nHeader": "N8N-ENDPUNKTE",
|
"n8nHeader": "N8N-ENDPUNKTE",
|
||||||
"n8nHint": "Aktive n8n-Workflows erscheinen als `n8n.<endpoint>.<slug>` Capabilities. Einmal konfigurieren, im Store sichtbar.",
|
"n8nHint": "Aktive n8n-Workflows erscheinen als `n8n.<endpoint>.<slug>` Fähigkeiten. Einmal konfigurieren, im Store sichtbar.",
|
||||||
"n8nEmpty": "Keine n8n-Endpunkte konfiguriert. + klicken zum Hinzufügen.",
|
"n8nEmpty": "Keine n8n-Endpunkte konfiguriert. + klicken zum Hinzufügen.",
|
||||||
|
|
||||||
"hubUnreachable": "Hub nicht erreichbar",
|
"hubUnreachable": "Hub nicht erreichbar",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@
|
||||||
"buttonOpen": "Open",
|
"buttonOpen": "Open",
|
||||||
"buttonExplain": "Explain",
|
"buttonExplain": "Explain",
|
||||||
"buttonReadDocs": "Read docs",
|
"buttonReadDocs": "Read docs",
|
||||||
|
"buttonCopy": "Copy to clipboard",
|
||||||
|
"buttonCopied": "Copied",
|
||||||
|
|
||||||
"settingsTitle": "Hub endpoint",
|
"settingsTitle": "Hub endpoint",
|
||||||
"settingsHost": "Host",
|
"settingsHost": "Host",
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Read docs'**
|
/// **'Read docs'**
|
||||||
String get buttonReadDocs;
|
String get buttonReadDocs;
|
||||||
|
|
||||||
|
/// No description provided for @buttonCopy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copy to clipboard'**
|
||||||
|
String get buttonCopy;
|
||||||
|
|
||||||
|
/// No description provided for @buttonCopied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copied'**
|
||||||
|
String get buttonCopied;
|
||||||
|
|
||||||
/// No description provided for @settingsTitle.
|
/// No description provided for @settingsTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonReadDocs => 'Doku öffnen';
|
String get buttonReadDocs => 'Doku öffnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonCopy => 'In Zwischenablage kopieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonCopied => 'Kopiert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsTitle => 'Hub-Endpunkt';
|
String get settingsTitle => 'Hub-Endpunkt';
|
||||||
|
|
||||||
|
|
@ -193,7 +199,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addMcpIntro =>
|
String get addMcpIntro =>
|
||||||
'Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Capabilities.';
|
'Tools, die der Server via `tools/list` anbietet, erscheinen im Store als `mcp.<name>.<tool>`-Fähigkeiten.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addMcpSuggestionsLabel => 'Vorschläge — zum Vorausfüllen klicken';
|
String get addMcpSuggestionsLabel => 'Vorschläge — zum Vorausfüllen klicken';
|
||||||
|
|
@ -227,7 +233,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addN8nIntro =>
|
String get addN8nIntro =>
|
||||||
'Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Capabilities.';
|
'Aktive Workflows erscheinen im Store als `n8n.<name>.<workflow_slug>`-Fähigkeiten.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addN8nNameLabel => 'Name (keine Punkte)';
|
String get addN8nNameLabel => 'Name (keine Punkte)';
|
||||||
|
|
@ -304,7 +310,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeSearchHint =>
|
String get storeSearchHint =>
|
||||||
'Module suchen — Name, Tagline, Tag, Capability…';
|
'Module suchen — Name, Tagline, Tag, Fähigkeit…';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeAskHint =>
|
String get storeAskHint =>
|
||||||
|
|
@ -502,7 +508,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get storeSectionTags => 'Tags';
|
String get storeSectionTags => 'Tags';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeSectionRequiredCapabilities => 'Erforderliche Capabilities';
|
String get storeSectionRequiredCapabilities => 'Erforderliche Fähigkeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeSectionRequiredHostServices => 'Erforderliche Host-Dienste';
|
String get storeSectionRequiredHostServices => 'Erforderliche Host-Dienste';
|
||||||
|
|
@ -605,7 +611,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeRecommendedBody =>
|
String get storeRecommendedBody =>
|
||||||
'Kuratierte MCP-Server über HTTPS erreichbar. Kein Subprozess, kein API-Key, kein Node. Föderierte Capabilities erscheinen sofort im Store.';
|
'Kuratierte MCP-Server über HTTPS erreichbar. Kein Subprozess, kein API-Key, kein Node. Föderierte Fähigkeiten erscheinen sofort im Store.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeRecommendedAddTooltip => 'Hinzufügen und entdecken';
|
String get storeRecommendedAddTooltip => 'Hinzufügen und entdecken';
|
||||||
|
|
@ -626,15 +632,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
n,
|
n,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'föderierte Capabilities',
|
other: 'föderierte Fähigkeiten',
|
||||||
one: 'föderierte Capability',
|
one: 'föderierte Fähigkeit',
|
||||||
);
|
);
|
||||||
return '$name hinzugefügt — $n $_temp0.';
|
return '$name hinzugefügt — $n $_temp0.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeProviderAddedZeroToast(String name) {
|
String storeProviderAddedZeroToast(String name) {
|
||||||
return '$name ist eingetragen, aber der Server hat noch keine Capabilities geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.';
|
return '$name ist eingetragen, aber der Server hat noch keine Fähigkeiten geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -669,7 +675,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String auditClearedToast(int n, String channel) {
|
String auditClearedToast(int n, String channel) {
|
||||||
return '$n Ereignisse auf Kanal „$channel\" gelöscht. Chain mit Marker neu geseedet.';
|
return '$n Ereignisse auf Kanal „$channel\" gelöscht. Kette mit Marker neu geseedet.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -723,7 +729,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get auditHashChainVerified => 'Hash-Chain verifiziert';
|
String get auditHashChainVerified => 'Hash-Kette verifiziert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get auditDetailHeader => 'DETAIL';
|
String get auditDetailHeader => 'DETAIL';
|
||||||
|
|
@ -802,7 +808,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get modulesActivityUninstalled => 'deinstalliert';
|
String get modulesActivityUninstalled => 'deinstalliert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get modulesCapabilitiesLabel => 'Capabilities';
|
String get modulesCapabilitiesLabel => 'Fähigkeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get modulesUninstallTitle => 'Modul deinstallieren?';
|
String get modulesUninstallTitle => 'Modul deinstallieren?';
|
||||||
|
|
@ -831,7 +837,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get moduleSheetCapabilities => 'Capabilities';
|
String get moduleSheetCapabilities => 'Fähigkeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get moduleSheetPermissions => 'Deklarierte Berechtigungen';
|
String get moduleSheetPermissions => 'Deklarierte Berechtigungen';
|
||||||
|
|
@ -951,7 +957,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get approvalsHistoryEmptyHint =>
|
String get approvalsHistoryEmptyHint =>
|
||||||
'Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-chained Datensatz.';
|
'Entschiedene Freigaben (freigegeben / abgelehnt / abgelaufen) erscheinen hier.\nDas Audit-Log enthält den vollständigen hash-verketteten Datensatz.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get approvalsDialogDecided => 'entschieden';
|
String get approvalsDialogDecided => 'entschieden';
|
||||||
|
|
@ -992,7 +998,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String doctorSummaryCapabilities(int n) {
|
String doctorSummaryCapabilities(int n) {
|
||||||
return '$n Capabilities';
|
return '$n Fähigkeiten';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1000,7 +1006,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String doctorSummaryChain(int verified, int total) {
|
String doctorSummaryChain(int verified, int total) {
|
||||||
return '$verified/$total Chain';
|
return '$verified/$total Kette';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1008,7 +1014,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String doctorModulesPanelSummary(int n, int m) {
|
String doctorModulesPanelSummary(int n, int m) {
|
||||||
return '$n Module · $m Capabilities';
|
return '$n Module · $m Fähigkeiten';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1060,7 +1066,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get doctorDaemonControlSection => 'Daemon-Steuerung';
|
String get doctorDaemonControlSection => 'Daemon-Steuerung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get doctorChainIntact => 'Hash-Chain intakt';
|
String get doctorChainIntact => 'Hash-Kette intakt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String doctorChainIntactDetail(int n) {
|
String doctorChainIntactDetail(int n) {
|
||||||
|
|
@ -1193,7 +1199,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mcpHint =>
|
String get mcpHint =>
|
||||||
'Externe MCP-Server bringen ihre Tools als `mcp.<server>.<tool>` Capabilities ein. Einmal konfigurieren, im Store sichtbar.';
|
'Externe MCP-Server bringen ihre Tools als `mcp.<server>.<tool>` Fähigkeiten ein. Einmal konfigurieren, im Store sichtbar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get mcpEmpty =>
|
String get mcpEmpty =>
|
||||||
|
|
@ -1204,7 +1210,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get n8nHint =>
|
String get n8nHint =>
|
||||||
'Aktive n8n-Workflows erscheinen als `n8n.<endpoint>.<slug>` Capabilities. Einmal konfigurieren, im Store sichtbar.';
|
'Aktive n8n-Workflows erscheinen als `n8n.<endpoint>.<slug>` Fähigkeiten. Einmal konfigurieren, im Store sichtbar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get n8nEmpty =>
|
String get n8nEmpty =>
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonReadDocs => 'Read docs';
|
String get buttonReadDocs => 'Read docs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonCopy => 'Copy to clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonCopied => 'Copied';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsTitle => 'Hub endpoint';
|
String get settingsTitle => 'Hub endpoint';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,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.33.1';
|
const String kStudioVersion = '0.34.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -334,13 +334,17 @@ class _DaemonPathsPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
final entries = <(String, String, IconData)>[
|
// Per-row layout: (label, path, icon, isDirectory). The
|
||||||
(l.doctorPathLog, paths.logPath, Icons.description_outlined),
|
// isDirectory bit drives `openOrReveal` so binary files
|
||||||
(l.doctorPathConfig, paths.configPath, Icons.settings_outlined),
|
// (the SQLite audit DB, the PID file) reveal in Finder /
|
||||||
(l.doctorPathDb, paths.dbPath, Icons.storage_outlined),
|
// Explorer instead of failing the OS "open" handler.
|
||||||
(l.doctorPathModules, paths.modulesDir, Icons.extension_outlined),
|
final entries = <(String, String, IconData, bool)>[
|
||||||
(l.doctorPathFlows, paths.flowsDir, Icons.account_tree_outlined),
|
(l.doctorPathLog, paths.logPath, Icons.description_outlined, false),
|
||||||
(l.doctorPathPid, paths.pidPath, Icons.fingerprint),
|
(l.doctorPathConfig, paths.configPath, Icons.settings_outlined, false),
|
||||||
|
(l.doctorPathDb, paths.dbPath, Icons.storage_outlined, false),
|
||||||
|
(l.doctorPathModules, paths.modulesDir, Icons.extension_outlined, true),
|
||||||
|
(l.doctorPathFlows, paths.flowsDir, Icons.account_tree_outlined, true),
|
||||||
|
(l.doctorPathPid, paths.pidPath, Icons.fingerprint, false),
|
||||||
].where((e) => e.$2.isNotEmpty).toList();
|
].where((e) => e.$2.isNotEmpty).toList();
|
||||||
|
|
||||||
if (entries.isEmpty) {
|
if (entries.isEmpty) {
|
||||||
|
|
@ -356,7 +360,13 @@ class _DaemonPathsPanel extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (final e in entries) _PathRow(label: e.$1, path: e.$2, icon: e.$3),
|
for (final e in entries)
|
||||||
|
_PathRow(
|
||||||
|
label: e.$1,
|
||||||
|
path: e.$2,
|
||||||
|
icon: e.$3,
|
||||||
|
isDirectory: e.$4,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -367,8 +377,14 @@ class _PathRow extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final String path;
|
final String path;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
final bool isDirectory;
|
||||||
|
|
||||||
const _PathRow({required this.label, required this.path, required this.icon});
|
const _PathRow({
|
||||||
|
required this.label,
|
||||||
|
required this.path,
|
||||||
|
required this.icon,
|
||||||
|
required this.isDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -400,7 +416,10 @@ class _PathRow extends StatelessWidget {
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final r = await SystemActions.openInOs(path);
|
final r = await SystemActions.openOrReveal(
|
||||||
|
path,
|
||||||
|
isDirectory: isDirectory,
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
|
|
@ -409,7 +428,10 @@ class _PathRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.open_in_new, size: 14),
|
icon: Icon(
|
||||||
|
isDirectory ? Icons.folder_open : Icons.open_in_new,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
label: Text(AppLocalizations.of(context)!.buttonOpen),
|
label: Text(AppLocalizations.of(context)!.buttonOpen),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
|
@ -895,22 +917,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
||||||
),
|
),
|
||||||
if (_applyOutput != null) ...[
|
if (_applyOutput != null) ...[
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
Container(
|
FaiErrorBox(text: _applyOutput!, maxHeight: 240),
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(FaiSpace.sm),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
|
||||||
child: SelectableText(
|
|
||||||
_applyOutput!,
|
|
||||||
style: FaiTheme.mono(
|
|
||||||
size: 11,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,24 @@ class _StorePageState extends State<StorePage> {
|
||||||
/// session. Not persisted — the next Studio launch shows it
|
/// session. Not persisted — the next Studio launch shows it
|
||||||
/// again so a release-bumped story has a chance to be seen.
|
/// again so a release-bumped story has a chance to be seen.
|
||||||
bool _todayDismissed = false;
|
bool _todayDismissed = false;
|
||||||
/// Loaded once at init from `~/.fai/today/active.yaml`; falls
|
/// The carousel of stories rendered in the hero. When an
|
||||||
/// back to the compiled-in story when the pipeline hasn't
|
/// operator-accepted story exists at `~/.fai/today/active.yaml`,
|
||||||
/// produced an accepted candidate yet.
|
/// it is the only entry (carousel collapses to a single
|
||||||
late final TodayStoryData _todayStory =
|
/// slide). Otherwise the curated fallback list rotates.
|
||||||
TodayStoryLoader.loadOrFallback(_kFallbackTodayStory);
|
late final List<TodayStoryData> _todayStories = (() {
|
||||||
|
final loaded =
|
||||||
|
TodayStoryLoader.loadOrFallback(_kFallbackTodayStories.first);
|
||||||
|
// When the loader fell back to the const, it returned the
|
||||||
|
// first entry of our list — surface the whole list so the
|
||||||
|
// operator can browse. Otherwise the loader produced a real
|
||||||
|
// accepted story and we honour it as the only item.
|
||||||
|
if (identical(loaded, _kFallbackTodayStories.first)) {
|
||||||
|
return _kFallbackTodayStories;
|
||||||
|
}
|
||||||
|
return <TodayStoryData>[loaded];
|
||||||
|
})();
|
||||||
|
/// Carousel index. Wraps around modulo `_todayStories.length`.
|
||||||
|
int _todayIndex = 0;
|
||||||
late Future<List<StoreItem>> _future;
|
late Future<List<StoreItem>> _future;
|
||||||
|
|
||||||
// ── AI search state ──────────────────────────────────────
|
// ── AI search state ──────────────────────────────────────
|
||||||
|
|
@ -151,11 +164,13 @@ class _StorePageState extends State<StorePage> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
// titleSpacing matches the body's horizontal padding
|
||||||
|
// (FaiSpace.xl == 24) so the title's left edge sits
|
||||||
|
// flush with the cards below. Default 16 left a 8-px
|
||||||
|
// gap that read as a misaligned heading.
|
||||||
|
titleSpacing: FaiSpace.xl,
|
||||||
title: Text(l.searchGroupStore),
|
title: Text(l.searchGroupStore),
|
||||||
actions: [
|
actions: [
|
||||||
// Category dropdown and Filter button live in the
|
|
||||||
// toolbar so the body stays uncluttered. Both refresh
|
|
||||||
// off the current store snapshot via FutureBuilder.
|
|
||||||
FutureBuilder<List<StoreItem>>(
|
FutureBuilder<List<StoreItem>>(
|
||||||
future: _future,
|
future: _future,
|
||||||
builder: (ctx, snap) {
|
builder: (ctx, snap) {
|
||||||
|
|
@ -186,7 +201,11 @@ class _StorePageState extends State<StorePage> {
|
||||||
tooltip: l.storeReloadTooltip,
|
tooltip: l.storeReloadTooltip,
|
||||||
onPressed: _runSearch,
|
onPressed: _runSearch,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
// Trailing pad pulls the IconButton's outer edge in
|
||||||
|
// to match the body's right-side padding. The icon
|
||||||
|
// glyph itself ends up flush with the rightmost grid
|
||||||
|
// card, not floating in space.
|
||||||
|
const SizedBox(width: FaiSpace.md),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|
@ -287,11 +306,31 @@ class _StorePageState extends State<StorePage> {
|
||||||
],
|
],
|
||||||
if (showToday) ...[
|
if (showToday) ...[
|
||||||
_StoreTodayHero(
|
_StoreTodayHero(
|
||||||
story: _todayStory,
|
story:
|
||||||
|
_todayStories[_todayIndex % _todayStories.length],
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
|
currentIndex: _todayIndex,
|
||||||
|
totalCount: _todayStories.length,
|
||||||
|
onPrev: _todayStories.length > 1
|
||||||
|
? () => setState(() {
|
||||||
|
_todayIndex = (_todayIndex -
|
||||||
|
1 +
|
||||||
|
_todayStories.length) %
|
||||||
|
_todayStories.length;
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
onNext: _todayStories.length > 1
|
||||||
|
? () => setState(() {
|
||||||
|
_todayIndex = (_todayIndex + 1) %
|
||||||
|
_todayStories.length;
|
||||||
|
})
|
||||||
|
: null,
|
||||||
onDismiss: () =>
|
onDismiss: () =>
|
||||||
setState(() => _todayDismissed = true),
|
setState(() => _todayDismissed = true),
|
||||||
onCta: _todayStory.cta == TodayCta.openSettings
|
onCta: _todayStories[
|
||||||
|
_todayIndex % _todayStories.length]
|
||||||
|
.cta ==
|
||||||
|
TodayCta.openSettings
|
||||||
? () => FaiSettingsDialog.show(context)
|
? () => FaiSettingsDialog.show(context)
|
||||||
: null,
|
: null,
|
||||||
recommendedSources: embedRecommended
|
recommendedSources: embedRecommended
|
||||||
|
|
@ -2659,11 +2698,14 @@ class _ScreenshotPlaceholder extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compiled-in fallback story shown when
|
/// Compiled-in fallback stories shown when
|
||||||
/// `~/.fai/today/active.yaml` is absent or fails schema
|
/// `~/.fai/today/active.yaml` is absent or fails schema
|
||||||
/// validation. The pipeline operator can override at any time
|
/// validation. Operator can override at any time without
|
||||||
/// without recompiling Studio — see docs/today-pipeline.md.
|
/// recompiling Studio — see docs/today-pipeline.md. When the
|
||||||
const TodayStoryData _kFallbackTodayStory = TodayStoryData(
|
/// operator accepts a story, that single story becomes the
|
||||||
|
/// active item and the carousel collapses to a single slide.
|
||||||
|
const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
|
||||||
|
TodayStoryData(
|
||||||
badgeEn: 'PUBLIC SOURCES',
|
badgeEn: 'PUBLIC SOURCES',
|
||||||
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
||||||
titleEn: 'Fill your store in two clicks',
|
titleEn: 'Fill your store in two clicks',
|
||||||
|
|
@ -2676,7 +2718,50 @@ const TodayStoryData _kFallbackTodayStory = TodayStoryData(
|
||||||
ctaLabelDe: 'Quellen verwalten',
|
ctaLabelDe: 'Quellen verwalten',
|
||||||
icon: Icons.hub_outlined,
|
icon: Icons.hub_outlined,
|
||||||
cta: TodayCta.openSettings,
|
cta: TodayCta.openSettings,
|
||||||
);
|
),
|
||||||
|
TodayStoryData(
|
||||||
|
badgeEn: 'SANDBOX BY DEFAULT',
|
||||||
|
badgeDe: 'SANDBOX VON ANFANG AN',
|
||||||
|
titleEn: 'Every module declares what it can touch',
|
||||||
|
titleDe: 'Jedes Modul deklariert, worauf es zugreifen darf',
|
||||||
|
bodyEn:
|
||||||
|
'F∆I modules ship with an explicit permission list — network endpoints, files, environment variables. The hub enforces it; nothing reaches outside the sandbox without the operator approving it. Click any installed module to see exactly which permissions it asked for.',
|
||||||
|
bodyDe:
|
||||||
|
'F∆I-Module bringen eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox. Klicke ein installiertes Modul, um seine genauen Berechtigungen zu sehen.',
|
||||||
|
ctaLabelEn: '',
|
||||||
|
ctaLabelDe: '',
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
cta: TodayCta.none,
|
||||||
|
),
|
||||||
|
TodayStoryData(
|
||||||
|
badgeEn: 'TAMPER-EVIDENT AUDIT',
|
||||||
|
badgeDe: 'MANIPULATIONSSICHERES AUDIT',
|
||||||
|
titleEn: 'Every event leaves a hash-chained trace',
|
||||||
|
titleDe: 'Jedes Ereignis hinterlässt eine hash-verkettete Spur',
|
||||||
|
bodyEn:
|
||||||
|
'Flow runs, install / uninstall actions, approval decisions — all of them write into a hash-chained audit log. The Doctor page verifies the chain end-to-end on every load; if a single byte gets touched, you see it. Audit-grade by construction, not by policy.',
|
||||||
|
bodyDe:
|
||||||
|
'Flow-Läufe, Installationen und Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett; ein verändertes Byte ist sofort sichtbar. Audit-tauglich qua Konstruktion, nicht qua Policy.',
|
||||||
|
ctaLabelEn: '',
|
||||||
|
ctaLabelDe: '',
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
cta: TodayCta.none,
|
||||||
|
),
|
||||||
|
TodayStoryData(
|
||||||
|
badgeEn: 'AIR-GAP READY',
|
||||||
|
badgeDe: 'AIR-GAP-TAUGLICH',
|
||||||
|
titleEn: 'One binary, no Docker, no external services',
|
||||||
|
titleDe: 'Ein Binary, kein Docker, keine externen Dienste',
|
||||||
|
bodyEn:
|
||||||
|
'The whole hub fits in a single binary that runs on Linux, macOS and Windows. SQLite for state, no message broker, no background services to babysit. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments where outbound calls have to be justified.',
|
||||||
|
bodyDe:
|
||||||
|
'Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. SQLite für den State, kein Message-Broker, keine Hintergrunddienste, die gepflegt werden müssen. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen, in denen jeder ausgehende Call begründet werden muss.',
|
||||||
|
ctaLabelEn: '',
|
||||||
|
ctaLabelDe: '',
|
||||||
|
icon: Icons.rocket_launch,
|
||||||
|
cta: TodayCta.none,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
/// Editorial hero strip — the first surface a browsing operator
|
/// Editorial hero strip — the first surface a browsing operator
|
||||||
/// sees. Plays the role of Apple's Today tab: one curated
|
/// sees. Plays the role of Apple's Today tab: one curated
|
||||||
|
|
@ -2686,6 +2771,15 @@ const TodayStoryData _kFallbackTodayStory = TodayStoryData(
|
||||||
class _StoreTodayHero extends StatelessWidget {
|
class _StoreTodayHero extends StatelessWidget {
|
||||||
final TodayStoryData story;
|
final TodayStoryData story;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
/// Carousel position (0-based) and total slide count. Drives
|
||||||
|
/// the dot indicator and the visibility of the prev/next
|
||||||
|
/// arrows. When `totalCount == 1` the chrome collapses to a
|
||||||
|
/// single-card layout — operator-accepted stories never wear
|
||||||
|
/// carousel dressing.
|
||||||
|
final int currentIndex;
|
||||||
|
final int totalCount;
|
||||||
|
final VoidCallback? onPrev;
|
||||||
|
final VoidCallback? onNext;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
/// CTA action — null hides the button. Wired by the parent
|
/// CTA action — null hides the button. Wired by the parent
|
||||||
/// because navigation targets live in the store-page state,
|
/// because navigation targets live in the store-page state,
|
||||||
|
|
@ -2703,6 +2797,10 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
const _StoreTodayHero({
|
const _StoreTodayHero({
|
||||||
required this.story,
|
required this.story,
|
||||||
required this.locale,
|
required this.locale,
|
||||||
|
required this.currentIndex,
|
||||||
|
required this.totalCount,
|
||||||
|
required this.onPrev,
|
||||||
|
required this.onNext,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
required this.onCta,
|
required this.onCta,
|
||||||
this.recommendedSources = const <_RecommendedSource>[],
|
this.recommendedSources = const <_RecommendedSource>[],
|
||||||
|
|
@ -2848,12 +2946,63 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Right-edge column: dismiss + (when more than one
|
||||||
|
// slide exists) a carousel control row. Stacking them
|
||||||
|
// vertically keeps the hero's main row tidy whatever
|
||||||
|
// the slide count.
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 16),
|
icon: const Icon(Icons.close, size: 16),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: l.storeTodayDismissTooltip,
|
tooltip: l.storeTodayDismissTooltip,
|
||||||
onPressed: onDismiss,
|
onPressed: onDismiss,
|
||||||
),
|
),
|
||||||
|
if (totalCount > 1) ...[
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_left, size: 18),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: onPrev,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < totalCount; i++)
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: i == currentIndex
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_right, size: 18),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: onNext,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
113
lib/widgets/fai_error_box.dart
Normal file
113
lib/widgets/fai_error_box.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
// FaiErrorBox — selectable monospace error / output block with
|
||||||
|
// a small copy-to-clipboard button in the top-right corner.
|
||||||
|
//
|
||||||
|
// Used wherever the operator might want to paste daemon stderr
|
||||||
|
// into a chat or bug report: the update banner, install
|
||||||
|
// progress dialog, system-AI editor, audit-clear failures, etc.
|
||||||
|
// SelectableText alone works for keyboard users but the copy
|
||||||
|
// affordance has to be explicit so trackpad operators see it.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../theme/theme.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
class FaiErrorBox extends StatefulWidget {
|
||||||
|
/// The text the operator wants to read (and copy). Rendered
|
||||||
|
/// monospace, selectable, multi-line.
|
||||||
|
final String text;
|
||||||
|
/// When true, the box border + foreground colour come from the
|
||||||
|
/// error palette. False renders neutral chrome — useful for
|
||||||
|
/// success / informational copy that still wants the copy
|
||||||
|
/// button.
|
||||||
|
final bool isError;
|
||||||
|
/// Optional max-height before the content scrolls inside the
|
||||||
|
/// box. Falls back to no constraint when null so short
|
||||||
|
/// messages don't get a useless scrollbar.
|
||||||
|
final double? maxHeight;
|
||||||
|
|
||||||
|
const FaiErrorBox({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.isError = false,
|
||||||
|
this.maxHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FaiErrorBox> createState() => _FaiErrorBoxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaiErrorBoxState extends State<FaiErrorBox> {
|
||||||
|
bool _justCopied = false;
|
||||||
|
|
||||||
|
Future<void> _copy() async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: widget.text));
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _justCopied = true);
|
||||||
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
|
if (mounted) setState(() => _justCopied = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final accent = widget.isError
|
||||||
|
? theme.colorScheme.error
|
||||||
|
: theme.colorScheme.outlineVariant;
|
||||||
|
final fg = widget.isError
|
||||||
|
? theme.colorScheme.error
|
||||||
|
: theme.colorScheme.onSurface;
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
FaiSpace.sm,
|
||||||
|
FaiSpace.xs,
|
||||||
|
FaiSpace.xs,
|
||||||
|
FaiSpace.sm,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: accent.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Copy button sits above the text on its own row so
|
||||||
|
// long messages never collide with it. Compact enough
|
||||||
|
// not to dominate short single-line messages.
|
||||||
|
Tooltip(
|
||||||
|
message: _justCopied ? l.buttonCopied : l.buttonCopy,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_justCopied ? Icons.check : Icons.content_copy,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
onPressed: _copy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: widget.maxHeight ?? double.infinity,
|
||||||
|
),
|
||||||
|
child: Scrollbar(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: SelectableText(
|
||||||
|
widget.text,
|
||||||
|
style: FaiTheme.mono(size: 11, color: fg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import '../data/system_actions.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
|
import 'fai_error_box.dart';
|
||||||
import 'fai_pill.dart';
|
import 'fai_pill.dart';
|
||||||
import 'fai_system_ai_editor.dart';
|
import 'fai_system_ai_editor.dart';
|
||||||
|
|
||||||
|
|
@ -416,22 +417,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
),
|
),
|
||||||
if (_channelToast != null) ...[
|
if (_channelToast != null) ...[
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
Container(
|
FaiErrorBox(text: _channelToast!, maxHeight: 200),
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(FaiSpace.sm),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
|
||||||
child: SelectableText(
|
|
||||||
_channelToast!,
|
|
||||||
style: FaiTheme.mono(
|
|
||||||
size: 11,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
if (_aiStatus != null) ...[
|
if (_aiStatus != null) ...[
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export 'fai_card.dart';
|
||||||
export 'fai_data_row.dart';
|
export 'fai_data_row.dart';
|
||||||
export 'fai_delta_mark.dart';
|
export 'fai_delta_mark.dart';
|
||||||
export 'fai_empty_state.dart';
|
export 'fai_empty_state.dart';
|
||||||
|
export 'fai_error_box.dart';
|
||||||
export 'fai_module_sheet.dart';
|
export 'fai_module_sheet.dart';
|
||||||
export 'fai_pill.dart';
|
export 'fai_pill.dart';
|
||||||
export 'fai_settings_dialog.dart';
|
export 'fai_settings_dialog.dart';
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
|
||||||
if (use_header_bar) {
|
if (use_header_bar) {
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "fai_studio");
|
gtk_header_bar_set_title(header_bar, "F\u2206I Studio");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
} else {
|
||||||
gtk_window_set_title(window, "fai_studio");
|
gtk_window_set_title(window, "F\u2206I Studio");
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>F∆I Studio</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>F∆I Studio</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ class MainFlutterWindow: NSWindow {
|
||||||
self.contentViewController = flutterViewController
|
self.contentViewController = flutterViewController
|
||||||
self.setFrame(windowFrame, display: true)
|
self.setFrame(windowFrame, display: true)
|
||||||
|
|
||||||
|
// Override the window title so the OS task switcher and
|
||||||
|
// window chrome read "F∆I Studio" instead of the
|
||||||
|
// pubspec-derived "fai_studio".
|
||||||
|
self.title = "F∆I Studio"
|
||||||
|
|
||||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
|
||||||
|
|
@ -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.33.1
|
version: 0.34.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"fai_studio", origin, size)) {
|
if (!window.Create(L"F\u2206I Studio", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue