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:
flemming-it 2026-05-08 14:48:33 +02:00
parent 22a851b7e2
commit 3b5fb027c3
17 changed files with 454 additions and 119 deletions

View file

@ -334,13 +334,17 @@ class _DaemonPathsPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final entries = <(String, String, IconData)>[
(l.doctorPathLog, paths.logPath, Icons.description_outlined),
(l.doctorPathConfig, paths.configPath, Icons.settings_outlined),
(l.doctorPathDb, paths.dbPath, Icons.storage_outlined),
(l.doctorPathModules, paths.modulesDir, Icons.extension_outlined),
(l.doctorPathFlows, paths.flowsDir, Icons.account_tree_outlined),
(l.doctorPathPid, paths.pidPath, Icons.fingerprint),
// Per-row layout: (label, path, icon, isDirectory). The
// isDirectory bit drives `openOrReveal` so binary files
// (the SQLite audit DB, the PID file) reveal in Finder /
// Explorer instead of failing the OS "open" handler.
final entries = <(String, String, IconData, bool)>[
(l.doctorPathLog, paths.logPath, Icons.description_outlined, false),
(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();
if (entries.isEmpty) {
@ -356,7 +360,13 @@ class _DaemonPathsPanel extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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 path;
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
Widget build(BuildContext context) {
@ -400,7 +416,10 @@ class _PathRow extends StatelessWidget {
const SizedBox(width: FaiSpace.sm),
OutlinedButton.icon(
onPressed: () async {
final r = await SystemActions.openInOs(path);
final r = await SystemActions.openOrReveal(
path,
isDirectory: isDirectory,
);
if (!context.mounted) return;
if (!r.ok) {
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),
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
@ -895,22 +917,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
),
if (_applyOutput != null) ...[
const SizedBox(height: FaiSpace.md),
Container(
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,
),
),
),
FaiErrorBox(text: _applyOutput!, maxHeight: 240),
],
],
),