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,24 +2698,70 @@ 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
|
||||||
badgeEn: 'PUBLIC SOURCES',
|
/// active item and the carousel collapses to a single slide.
|
||||||
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
|
||||||
titleEn: 'Fill your store in two clicks',
|
TodayStoryData(
|
||||||
titleDe: 'Den Store in zwei Klicks auffüllen',
|
badgeEn: 'PUBLIC SOURCES',
|
||||||
bodyEn:
|
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
||||||
'Right now the store shows only the modules that ship with the hub. Pick a public source from the buttons below and the store fills with whatever tools that server offers — straight over HTTPS, no install steps, nothing to configure.',
|
titleEn: 'Fill your store in two clicks',
|
||||||
bodyDe:
|
titleDe: 'Den Store in zwei Klicks auffüllen',
|
||||||
'Im Moment zeigt der Store nur die Module, die mit dem Hub mitkommen. Wähle unten eine öffentliche Quelle aus, und der Store füllt sich mit allem, was dieser Server an Tools anbietet — direkt über HTTPS, ohne Installation, ohne weitere Konfiguration.',
|
bodyEn:
|
||||||
ctaLabelEn: 'Manage sources',
|
'Right now the store shows only the modules that ship with the hub. Pick a public source from the buttons below and the store fills with whatever tools that server offers — straight over HTTPS, no install steps, nothing to configure.',
|
||||||
ctaLabelDe: 'Quellen verwalten',
|
bodyDe:
|
||||||
icon: Icons.hub_outlined,
|
'Im Moment zeigt der Store nur die Module, die mit dem Hub mitkommen. Wähle unten eine öffentliche Quelle aus, und der Store füllt sich mit allem, was dieser Server an Tools anbietet — direkt über HTTPS, ohne Installation, ohne weitere Konfiguration.',
|
||||||
cta: TodayCta.openSettings,
|
ctaLabelEn: 'Manage sources',
|
||||||
);
|
ctaLabelDe: 'Quellen verwalten',
|
||||||
|
icon: Icons.hub_outlined,
|
||||||
|
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,11 +2946,62 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
// Right-edge column: dismiss + (when more than one
|
||||||
icon: const Icon(Icons.close, size: 16),
|
// slide exists) a carousel control row. Stacking them
|
||||||
visualDensity: VisualDensity.compact,
|
// vertically keeps the hero's main row tidy whatever
|
||||||
tooltip: l.storeTodayDismissTooltip,
|
// the slide count.
|
||||||
onPressed: onDismiss,
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: l.storeTodayDismissTooltip,
|
||||||
|
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