diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e8ccb58..d21e106 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -127,10 +127,22 @@ "storeNoMatchesHint": "Filter anpassen oder Suche leeren, um alle Einträge zu sehen.", "storeClearFilters": "Filter zurücksetzen", "storeStatusAll": "Alle", - "storeStatusPublished": "Veröffentlicht", - "storeStatusAlpha": "Alpha", - "storeStatusPlanned": "Geplant", + "storeStatusPublished": "stabil", + "storeStatusAlpha": "experimentell", + "storeStatusPlanned": "geplant", "storeInstalledOnly": "Installiert", + "storeCategoryLabel": "Kategorie", + "storeCategoryAll": "Alle Kategorien", + "storeCategoryBridge": "Verbindungen", + "storeCategoryDebug": "Beispiele", + "storeCategoryText": "Text", + "storeCategoryLlm": "KI-Modelle", + "storeCategorySystem": "System", + "storeCategoryAuth": "Authentifizierung", + "storeCategoryStorage": "Speicher", + "storeCategoryChannel": "Kanäle", + "storeCategoryWeb": "Web", + "storeCategoryOrchestrator": "Orchestrierung", "storeNResults": "{n} Treffer", "@storeNResults": { "placeholders": { "n": { "type": "int" } } @@ -221,6 +233,10 @@ "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": { + "placeholders": { "name": { "type": "String" } } + }, "storeProviderAddFailed": "{name} konnte nicht hinzugefügt werden: {error}", "@storeProviderAddFailed": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c11d4af..9958d33 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -128,10 +128,22 @@ "storeNoMatchesHint": "Adjust the filters or clear the search to see all entries.", "storeClearFilters": "Clear filters", "storeStatusAll": "All", - "storeStatusPublished": "Published", - "storeStatusAlpha": "Alpha", - "storeStatusPlanned": "Planned", + "storeStatusPublished": "stable", + "storeStatusAlpha": "experimental", + "storeStatusPlanned": "coming soon", "storeInstalledOnly": "Installed", + "storeCategoryLabel": "Category", + "storeCategoryAll": "All categories", + "storeCategoryBridge": "Connectors", + "storeCategoryDebug": "Sample modules", + "storeCategoryText": "Text", + "storeCategoryLlm": "AI models", + "storeCategorySystem": "System", + "storeCategoryAuth": "Authentication", + "storeCategoryStorage": "Storage", + "storeCategoryChannel": "Channels", + "storeCategoryWeb": "Web", + "storeCategoryOrchestrator": "Orchestration", "storeNResults": "{n} result{n, plural, =1{} other{s}}", "@storeNResults": { "placeholders": { "n": { "type": "int" } } @@ -222,6 +234,10 @@ "n": { "type": "int" } } }, + "storeProviderAddedZeroToast": "{name} registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.", + "@storeProviderAddedZeroToast": { + "placeholders": { "name": { "type": "String" } } + }, "storeProviderAddFailed": "Could not add {name}: {error}", "@storeProviderAddFailed": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f150f8a..4e2a321 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -761,19 +761,19 @@ abstract class AppLocalizations { /// No description provided for @storeStatusPublished. /// /// In en, this message translates to: - /// **'Published'** + /// **'stable'** String get storeStatusPublished; /// No description provided for @storeStatusAlpha. /// /// In en, this message translates to: - /// **'Alpha'** + /// **'experimental'** String get storeStatusAlpha; /// No description provided for @storeStatusPlanned. /// /// In en, this message translates to: - /// **'Planned'** + /// **'coming soon'** String get storeStatusPlanned; /// No description provided for @storeInstalledOnly. @@ -782,6 +782,78 @@ abstract class AppLocalizations { /// **'Installed'** String get storeInstalledOnly; + /// No description provided for @storeCategoryLabel. + /// + /// In en, this message translates to: + /// **'Category'** + String get storeCategoryLabel; + + /// No description provided for @storeCategoryAll. + /// + /// In en, this message translates to: + /// **'All categories'** + String get storeCategoryAll; + + /// No description provided for @storeCategoryBridge. + /// + /// In en, this message translates to: + /// **'Connectors'** + String get storeCategoryBridge; + + /// No description provided for @storeCategoryDebug. + /// + /// In en, this message translates to: + /// **'Sample modules'** + String get storeCategoryDebug; + + /// No description provided for @storeCategoryText. + /// + /// In en, this message translates to: + /// **'Text'** + String get storeCategoryText; + + /// No description provided for @storeCategoryLlm. + /// + /// In en, this message translates to: + /// **'AI models'** + String get storeCategoryLlm; + + /// No description provided for @storeCategorySystem. + /// + /// In en, this message translates to: + /// **'System'** + String get storeCategorySystem; + + /// No description provided for @storeCategoryAuth. + /// + /// In en, this message translates to: + /// **'Authentication'** + String get storeCategoryAuth; + + /// No description provided for @storeCategoryStorage. + /// + /// In en, this message translates to: + /// **'Storage'** + String get storeCategoryStorage; + + /// No description provided for @storeCategoryChannel. + /// + /// In en, this message translates to: + /// **'Channels'** + String get storeCategoryChannel; + + /// No description provided for @storeCategoryWeb. + /// + /// In en, this message translates to: + /// **'Web'** + String get storeCategoryWeb; + + /// No description provided for @storeCategoryOrchestrator. + /// + /// In en, this message translates to: + /// **'Orchestration'** + String get storeCategoryOrchestrator; + /// No description provided for @storeNResults. /// /// In en, this message translates to: @@ -1100,6 +1172,12 @@ abstract class AppLocalizations { /// **'Added {name} — {n} federated {n, plural, =1{capability} other{capabilities}}.'** String storeProviderAddedToast(String name, int n); + /// No description provided for @storeProviderAddedZeroToast. + /// + /// In en, this message translates to: + /// **'{name} registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.'** + String storeProviderAddedZeroToast(String name); + /// No description provided for @storeProviderAddFailed. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a919041..b59bf66 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -382,17 +382,53 @@ class AppLocalizationsDe extends AppLocalizations { String get storeStatusAll => 'Alle'; @override - String get storeStatusPublished => 'Veröffentlicht'; + String get storeStatusPublished => 'stabil'; @override - String get storeStatusAlpha => 'Alpha'; + String get storeStatusAlpha => 'experimentell'; @override - String get storeStatusPlanned => 'Geplant'; + String get storeStatusPlanned => 'geplant'; @override String get storeInstalledOnly => 'Installiert'; + @override + String get storeCategoryLabel => 'Kategorie'; + + @override + String get storeCategoryAll => 'Alle Kategorien'; + + @override + String get storeCategoryBridge => 'Verbindungen'; + + @override + String get storeCategoryDebug => 'Beispiele'; + + @override + String get storeCategoryText => 'Text'; + + @override + String get storeCategoryLlm => 'KI-Modelle'; + + @override + String get storeCategorySystem => 'System'; + + @override + String get storeCategoryAuth => 'Authentifizierung'; + + @override + String get storeCategoryStorage => 'Speicher'; + + @override + String get storeCategoryChannel => 'Kanäle'; + + @override + String get storeCategoryWeb => 'Web'; + + @override + String get storeCategoryOrchestrator => 'Orchestrierung'; + @override String storeNResults(int n) { return '$n Treffer'; @@ -596,6 +632,11 @@ class AppLocalizationsDe extends AppLocalizations { return '$name hinzugefügt — $n $_temp0.'; } + @override + 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.'; + } + @override String storeProviderAddFailed(String name, String error) { return '$name konnte nicht hinzugefügt werden: $error'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1a2bccf..0d94baf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -394,17 +394,53 @@ class AppLocalizationsEn extends AppLocalizations { String get storeStatusAll => 'All'; @override - String get storeStatusPublished => 'Published'; + String get storeStatusPublished => 'stable'; @override - String get storeStatusAlpha => 'Alpha'; + String get storeStatusAlpha => 'experimental'; @override - String get storeStatusPlanned => 'Planned'; + String get storeStatusPlanned => 'coming soon'; @override String get storeInstalledOnly => 'Installed'; + @override + String get storeCategoryLabel => 'Category'; + + @override + String get storeCategoryAll => 'All categories'; + + @override + String get storeCategoryBridge => 'Connectors'; + + @override + String get storeCategoryDebug => 'Sample modules'; + + @override + String get storeCategoryText => 'Text'; + + @override + String get storeCategoryLlm => 'AI models'; + + @override + String get storeCategorySystem => 'System'; + + @override + String get storeCategoryAuth => 'Authentication'; + + @override + String get storeCategoryStorage => 'Storage'; + + @override + String get storeCategoryChannel => 'Channels'; + + @override + String get storeCategoryWeb => 'Web'; + + @override + String get storeCategoryOrchestrator => 'Orchestration'; + @override String storeNResults(int n) { String _temp0 = intl.Intl.pluralLogic( @@ -613,6 +649,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Added $name — $n federated $_temp0.'; } + @override + String storeProviderAddedZeroToast(String name) { + return '$name registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.'; + } + @override String storeProviderAddFailed(String name, String error) { return 'Could not add $name: $error'; diff --git a/lib/main.dart b/lib/main.dart index dab42df..85d4747 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ import 'widgets/widgets.dart'; /// Studio's own build version. Bump on every UI commit so the /// running app self-identifies — visible in the sidebar header /// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.32.0'; +const String kStudioVersion = '0.33.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/store.dart b/lib/pages/store.dart index d3bc5ce..913dc57 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -153,10 +153,34 @@ class _StorePageState extends State { appBar: AppBar( title: Text(l.searchGroupStore), actions: [ - // The DE/EN toggle now lives in the sidebar footer - // and flips the entire app's locale. The Store - // detail-sheet content follows automatically because - // _locale reads from `Localizations.localeOf`. + // Category dropdown and Filter button live in the + // toolbar so the body stays uncluttered. Both refresh + // off the current store snapshot via FutureBuilder. + FutureBuilder>( + future: _future, + builder: (ctx, snap) { + final raw = snap.data ?? const []; + final categories = _categoriesIn(raw); + return Padding( + padding: const EdgeInsets.only(right: FaiSpace.sm), + child: _CategoryDropdown( + categories: categories, + selected: _category, + onSelected: (v) { + setState(() => _category = v); + _runSearch(); + }, + ), + ); + }, + ), + Padding( + padding: const EdgeInsets.only(right: FaiSpace.sm), + child: _FilterButton( + activeCount: _activeFilterCount(), + onPressed: _openFilterDialog, + ), + ), IconButton( icon: const Icon(Icons.refresh, size: 18), tooltip: l.storeReloadTooltip, @@ -165,70 +189,20 @@ class _StorePageState extends State { const SizedBox(width: FaiSpace.sm), ], ), - body: Padding( - padding: const EdgeInsets.fromLTRB( - FaiSpace.xl, - FaiSpace.lg, - FaiSpace.xl, - 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _AskBar( - controller: _queryCtrl, - thinking: _aiThinking, - aiAvailable: _systemAiEnabled, - onSubmit: _onAskSubmit, - onClear: _clearQuery, - onTextChange: _onAskTyping, - ), - const SizedBox(height: FaiSpace.sm), - FutureBuilder>( - future: _future, - builder: (context, snap) { - final raw = snap.data ?? const []; - final items = _applyAllFilters(raw); - final categories = _categoriesIn(raw); - final activeFilters = _activeFilterCount(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _FilterButton( - activeCount: activeFilters, - onPressed: _openFilterDialog, - ), - const SizedBox(width: FaiSpace.md), - if (categories.isNotEmpty) - Expanded( - child: _CategoryStrip( - categories: categories, - selected: _category, - onSelected: (v) { - setState(() => _category = v); - _runSearch(); - }, - ), - ) - else - const Spacer(), - Text( - l.storeNResults(items.length), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ], - ); - }, - ), - const SizedBox(height: FaiSpace.md), - Expanded( + body: Column( + children: [ + // Scrollable content: editorial chrome + grid. Sits in + // an Expanded so the bottom AskBar pins to the foot of + // the viewport, mirroring the chat-input pattern of + // Claude / ChatGPT / Slack. + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB( + FaiSpace.xl, + FaiSpace.lg, + FaiSpace.xl, + 0, + ), child: FutureBuilder>( future: _future, builder: (context, snap) { @@ -299,6 +273,18 @@ class _StorePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (_aiThinking || + _aiAnswer != null || + _aiError != null) ...[ + _AiAnswerCard( + thinking: _aiThinking, + answer: _aiAnswer, + matches: _aiMatches ?? const <_AiMatch>[], + error: _aiError, + onClear: _clearQuery, + ), + const SizedBox(height: FaiSpace.lg), + ], if (showToday) ...[ _StoreTodayHero( story: _todayStory, @@ -333,18 +319,6 @@ class _StorePageState extends State { ), const SizedBox(height: FaiSpace.lg), ], - if (_aiThinking || - _aiAnswer != null || - _aiError != null) ...[ - _AiAnswerCard( - thinking: _aiThinking, - answer: _aiAnswer, - matches: _aiMatches ?? const <_AiMatch>[], - error: _aiError, - onClear: _clearQuery, - ), - const SizedBox(height: FaiSpace.lg), - ], _StoreGrid( items: items, locale: _locale, @@ -352,14 +326,39 @@ class _StorePageState extends State { onTap: _openDetail, onInstall: _install, ), + const SizedBox(height: FaiSpace.lg), ], ), ); }, ), ), - ], - ), + ), + // Pinned chat-style ask bar at the foot of the + // viewport. Always visible so the operator can switch + // from browsing to asking with one click — same shape + // as Claude / ChatGPT / Slack composer rows. + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide(color: theme.colorScheme.outlineVariant), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: FaiSpace.xl, + vertical: FaiSpace.md, + ), + child: _AskBar( + controller: _queryCtrl, + thinking: _aiThinking, + aiAvailable: _systemAiEnabled, + onSubmit: _onAskSubmit, + onClear: _clearQuery, + onTextChange: _onAskTyping, + ), + ), + ], ), ); } @@ -532,11 +531,20 @@ class _StorePageState extends State { orElse: () => list.last, ); if (!mounted) return; + // toolCount == 0 happens when the server registered fine + // but discovery returned nothing — typically a transport + // mismatch or a server that wants `notifications/initialized` + // before listing. We surface the difference so the + // operator knows whether to celebrate or to debug. + final msg = added.toolCount > 0 + ? l.storeProviderAddedToast(s.label, added.toolCount) + : l.storeProviderAddedZeroToast(s.label); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - l.storeProviderAddedToast(s.label, added.toolCount), - ), + content: Text(msg), + duration: added.toolCount > 0 + ? const Duration(seconds: 4) + : const Duration(seconds: 8), ), ); _runSearch(); @@ -565,6 +573,59 @@ String _sourceForItem(StoreItem i) { return 'federated'; } +/// Translates a wire-level category id (`bridge`, `debug`, …) +/// into a label the operator can read. Wire ids stay in the +/// data model — only the display side gets localised. Unknown +/// ids fall through unchanged so newly-added categories don't +/// vanish until we update this map. +String _categoryDisplayName(BuildContext ctx, String wire) { + final l = AppLocalizations.of(ctx)!; + switch (wire) { + case '': + return ''; + case 'bridge': + return l.storeCategoryBridge; + case 'debug': + return l.storeCategoryDebug; + case 'text': + case 'text-processing': + return l.storeCategoryText; + case 'llm': + return l.storeCategoryLlm; + case 'system': + return l.storeCategorySystem; + case 'auth': + return l.storeCategoryAuth; + case 'storage': + return l.storeCategoryStorage; + case 'channel': + return l.storeCategoryChannel; + case 'web': + return l.storeCategoryWeb; + case 'orchestrator': + return l.storeCategoryOrchestrator; + default: + return wire; + } +} + +/// Same idea for the `published` / `alpha` / `planned` status +/// values. Operators read "stable / experimental / coming +/// soon"; the wire keeps the strict enum it has always had. +String _statusDisplayName(BuildContext ctx, String wire) { + final l = AppLocalizations.of(ctx)!; + switch (wire) { + case 'published': + return l.storeStatusPublished; + case 'alpha': + return l.storeStatusAlpha; + case 'planned': + return l.storeStatusPlanned; + default: + return wire; + } +} + /// Multi-line ask-and-search input. Drives the toolbar's /// primary action: substring keyword search on every keystroke /// (cheap, identical to the legacy `_SearchField`) plus an @@ -1056,15 +1117,17 @@ class _AiAnswerCard extends StatelessWidget { } } -/// Horizontally-scrollable category chip strip. Always-on -/// "All" chip first, then categories the current result set -/// contains. Disappears when the index is empty. -class _CategoryStrip extends StatelessWidget { +/// Compact category picker that lives in the AppBar's actions +/// row. Replaces the older horizontally-scrolling chip strip +/// because chips ate a full row of viewport for what is in +/// practice a one-click operation; a dropdown is faster to +/// reach and never grows past the toolbar. +class _CategoryDropdown extends StatelessWidget { final List categories; final String selected; - final void Function(String) onSelected; + final ValueChanged onSelected; - const _CategoryStrip({ + const _CategoryDropdown({ required this.categories, required this.selected, required this.onSelected, @@ -1072,25 +1135,53 @@ class _CategoryStrip extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final l = AppLocalizations.of(context)!; - if (categories.isEmpty) return const SizedBox.shrink(); - return SizedBox( - height: 36, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - _ChoiceChip( - label: l.storeStatusAll, - selected: selected.isEmpty, - onSelected: () => onSelected(''), + final activeLabel = selected.isEmpty + ? l.storeCategoryAll + : _categoryDisplayName(context, selected); + return PopupMenuButton( + tooltip: l.storeCategoryLabel, + initialValue: selected, + onSelected: onSelected, + itemBuilder: (_) => [ + PopupMenuItem( + value: '', + child: Text(l.storeCategoryAll), + ), + const PopupMenuDivider(), + for (final c in categories) + PopupMenuItem( + value: c, + child: Text(_categoryDisplayName(context, c)), ), - for (final c in categories) - _ChoiceChip( - label: c, - selected: selected == c, - onSelected: () => onSelected(c), + ], + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: FaiSpace.sm, + vertical: 6, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(FaiRadius.sm), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.folder_outlined, + size: 14, + color: theme.colorScheme.onSurfaceVariant, ), - ], + const SizedBox(width: 6), + Text(activeLabel, style: theme.textTheme.bodySmall), + Icon( + Icons.arrow_drop_down, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), ), ); } @@ -1392,7 +1483,9 @@ class _FeaturedTile extends StatelessWidget { ), if (item.category.isNotEmpty) Text( - item.category, + _categoryDisplayName(context, item.category), + maxLines: 1, + overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1424,7 +1517,7 @@ class _FeaturedTile extends StatelessWidget { const SizedBox(width: FaiSpace.xs), if (item.status.isNotEmpty) FaiPill( - label: item.status, + label: _statusDisplayName(context, item.status), tone: _toneForStatus(item.status), ), const Spacer(), @@ -1523,7 +1616,11 @@ class _StoreCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), Text( - item.category.isEmpty ? '—' : item.category, + item.category.isEmpty + ? '—' + : _categoryDisplayName(context, item.category), + maxLines: 1, + overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -1551,7 +1648,7 @@ class _StoreCard extends StatelessWidget { ) else if (item.status.isNotEmpty) FaiPill( - label: item.status, + label: _statusDisplayName(context, item.status), tone: _toneForStatus(item.status), ), ], @@ -1827,12 +1924,14 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { ), if (item.status.isNotEmpty) FaiPill( - label: item.status, + label: _statusDisplayName( + context, item.status), tone: _toneForStatus(item.status), ), if (item.category.isNotEmpty) FaiPill( - label: item.category, + label: _categoryDisplayName( + context, item.category), tone: FaiPillTone.neutral, ), if (item.installed) @@ -2664,13 +2763,15 @@ class _StoreTodayHero extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + Wrap( + spacing: FaiSpace.sm, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, children: [ FaiPill( label: l.storeTodayBadge, tone: FaiPillTone.accent, ), - const SizedBox(width: FaiSpace.sm), Text( badge, style: theme.textTheme.labelSmall?.copyWith( @@ -2679,16 +2780,8 @@ class _StoreTodayHero extends StatelessWidget { fontSize: 10, ), ), - const SizedBox(width: FaiSpace.sm), Text( - '·', - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: FaiSpace.sm), - Text( - l.storeTodayDeck, + '· ${l.storeTodayDeck}', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, @@ -2700,6 +2793,8 @@ class _StoreTodayHero extends StatelessWidget { const SizedBox(height: FaiSpace.sm), Text( title, + maxLines: 3, + overflow: TextOverflow.ellipsis, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, height: 1.15, @@ -2708,6 +2803,8 @@ class _StoreTodayHero extends StatelessWidget { const SizedBox(height: FaiSpace.sm), Text( body, + maxLines: 6, + overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, height: 1.45, @@ -2926,17 +3023,19 @@ class _RecommendedSourceCard extends StatelessWidget { children: [ Text( source.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), Text( desc, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index d09cc2a..1973096 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.32.0 +version: 0.33.0 environment: sdk: ^3.11.0-200.1.beta