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
|
|
@ -42,11 +42,24 @@ class _StorePageState extends State<StorePage> {
|
|||
/// session. Not persisted — the next Studio launch shows it
|
||||
/// again so a release-bumped story has a chance to be seen.
|
||||
bool _todayDismissed = false;
|
||||
/// Loaded once at init from `~/.fai/today/active.yaml`; falls
|
||||
/// back to the compiled-in story when the pipeline hasn't
|
||||
/// produced an accepted candidate yet.
|
||||
late final TodayStoryData _todayStory =
|
||||
TodayStoryLoader.loadOrFallback(_kFallbackTodayStory);
|
||||
/// The carousel of stories rendered in the hero. When an
|
||||
/// operator-accepted story exists at `~/.fai/today/active.yaml`,
|
||||
/// it is the only entry (carousel collapses to a single
|
||||
/// slide). Otherwise the curated fallback list rotates.
|
||||
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;
|
||||
|
||||
// ── AI search state ──────────────────────────────────────
|
||||
|
|
@ -151,11 +164,13 @@ class _StorePageState extends State<StorePage> {
|
|||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
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),
|
||||
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>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
|
|
@ -186,7 +201,11 @@ class _StorePageState extends State<StorePage> {
|
|||
tooltip: l.storeReloadTooltip,
|
||||
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(
|
||||
|
|
@ -287,11 +306,31 @@ class _StorePageState extends State<StorePage> {
|
|||
],
|
||||
if (showToday) ...[
|
||||
_StoreTodayHero(
|
||||
story: _todayStory,
|
||||
story:
|
||||
_todayStories[_todayIndex % _todayStories.length],
|
||||
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: () =>
|
||||
setState(() => _todayDismissed = true),
|
||||
onCta: _todayStory.cta == TodayCta.openSettings
|
||||
onCta: _todayStories[
|
||||
_todayIndex % _todayStories.length]
|
||||
.cta ==
|
||||
TodayCta.openSettings
|
||||
? () => FaiSettingsDialog.show(context)
|
||||
: null,
|
||||
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
|
||||
/// validation. The pipeline operator can override at any time
|
||||
/// without recompiling Studio — see docs/today-pipeline.md.
|
||||
const TodayStoryData _kFallbackTodayStory = TodayStoryData(
|
||||
badgeEn: 'PUBLIC SOURCES',
|
||||
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
||||
titleEn: 'Fill your store in two clicks',
|
||||
titleDe: 'Den Store in zwei Klicks auffüllen',
|
||||
bodyEn:
|
||||
'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.',
|
||||
bodyDe:
|
||||
'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.',
|
||||
ctaLabelEn: 'Manage sources',
|
||||
ctaLabelDe: 'Quellen verwalten',
|
||||
icon: Icons.hub_outlined,
|
||||
cta: TodayCta.openSettings,
|
||||
);
|
||||
/// validation. Operator can override at any time without
|
||||
/// recompiling Studio — see docs/today-pipeline.md. When the
|
||||
/// 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',
|
||||
badgeDe: 'ÖFFENTLICHE QUELLEN',
|
||||
titleEn: 'Fill your store in two clicks',
|
||||
titleDe: 'Den Store in zwei Klicks auffüllen',
|
||||
bodyEn:
|
||||
'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.',
|
||||
bodyDe:
|
||||
'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.',
|
||||
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
|
||||
/// sees. Plays the role of Apple's Today tab: one curated
|
||||
|
|
@ -2686,6 +2771,15 @@ const TodayStoryData _kFallbackTodayStory = TodayStoryData(
|
|||
class _StoreTodayHero extends StatelessWidget {
|
||||
final TodayStoryData story;
|
||||
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;
|
||||
/// CTA action — null hides the button. Wired by the parent
|
||||
/// because navigation targets live in the store-page state,
|
||||
|
|
@ -2703,6 +2797,10 @@ class _StoreTodayHero extends StatelessWidget {
|
|||
const _StoreTodayHero({
|
||||
required this.story,
|
||||
required this.locale,
|
||||
required this.currentIndex,
|
||||
required this.totalCount,
|
||||
required this.onPrev,
|
||||
required this.onNext,
|
||||
required this.onDismiss,
|
||||
required this.onCta,
|
||||
this.recommendedSources = const <_RecommendedSource>[],
|
||||
|
|
@ -2848,11 +2946,62 @@ class _StoreTodayHero extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: l.storeTodayDismissTooltip,
|
||||
onPressed: onDismiss,
|
||||
// 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(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue