From 5c0f0ba29477e2d1c57708b673d5e72e2d722898 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Fri, 19 Dec 2025 12:41:50 -0300 Subject: [PATCH] Partially fix text size, routing to app detail screen, and app card UI --- lib/main.dart | 2 +- lib/router.dart | 20 +- lib/screens/app_detail_screen.dart | 31 +- lib/screens/updates_screen.dart | 15 +- lib/widgets/app_card.dart | 419 +++++++++--------- lib/widgets/app_pack_container.dart | 13 +- lib/widgets/common/note_parser.dart | 27 +- lib/widgets/install_button.dart | 8 +- lib/widgets/pill_widget.dart | 4 +- .../relevant_who_follow_container.dart | 8 +- lib/widgets/version_pill_widget.dart | 64 +-- 11 files changed, 336 insertions(+), 275 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 54a8866..2937c41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -113,7 +113,7 @@ class ZapstoreApp extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final constrainedTextScale = mediaQuery.textScaler .scale(1.0) - .clamp(1.0, 1.3); + .clamp(1.0, 1.2); final constrainedTextScaler = TextScaler.linear(constrainedTextScale); // Show error overlay if initialization failed (do not block UI during loading) diff --git a/lib/router.dart b/lib/router.dart index 20657cb..60bfd3f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -8,17 +8,20 @@ import 'package:zapstore/screens/search_screen.dart'; import 'package:zapstore/screens/updates_screen.dart'; import 'package:zapstore/screens/profile_screen.dart'; -/// Extract app identifier from naddr or return as-is -String _extractAppIdentifier(String rawId) { +typedef _ResolvedAppRoute = ({String identifier, String? author}); + +_ResolvedAppRoute _resolveAppRouteId(String rawId) { if (rawId.startsWith('naddr1')) { try { final decoded = Utils.decodeShareableIdentifier(rawId); if (decoded is AddressData) { - return decoded.identifier; + return (identifier: decoded.identifier, author: decoded.author); } - } catch (_) {} + } catch (_) { + // Fall back to treating it as a plain identifier. + } } - return rawId; + return (identifier: rawId, author: null); } /// Helper to build app detail route @@ -27,8 +30,11 @@ GoRoute _appDetailRoute() { path: 'app/:id', builder: (context, state) { final rawId = state.pathParameters['id']!; - final appId = _extractAppIdentifier(rawId); - return AppDetailScreen(appId: appId); + final resolved = _resolveAppRouteId(rawId); + return AppDetailScreen( + appId: resolved.identifier, + authorPubkey: resolved.author, + ); }, ); } diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 9a434b5..2a27227 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -19,9 +19,10 @@ import 'package:zapstore/widgets/screenshots_gallery.dart'; import 'package:zapstore/widgets/version_pill_widget.dart'; class AppDetailScreen extends HookConsumerWidget { - const AppDetailScreen({super.key, required this.appId}); + const AppDetailScreen({super.key, required this.appId, this.authorPubkey}); final String appId; + final String? authorPubkey; @override Widget build(BuildContext context, WidgetRef ref) { @@ -30,33 +31,32 @@ class AppDetailScreen extends HookConsumerWidget { // Query app with relationships final appState = ref.watch( query( + authors: authorPubkey != null ? {authorPubkey!} : null, tags: { '#d': {appId}, '#f': {platform}, }, limit: 1, and: (a) => {a.latestRelease, a.latestRelease.value?.latestMetadata}, + // stream=true ensures cached/local results render immediately; remote + // results will merge in as they arrive. source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: false), - subscriptionPrefix: 'app-detail', + subscriptionPrefix: authorPubkey != null + ? 'app-detail-${authorPubkey!}-$appId' + : 'app-detail-$appId', ), ); - final app = appState.models.firstOrNull; - - // Show app detail once we have the app - if (app != null) { - return _AppDetailContent(app: app, appState: appState); - } - // Handle loading/error states when app not yet available return switch (appState) { StorageError(:final exception) => _ErrorScaffold( message: exception.toString(), ), - StorageData() => _ErrorScaffold( - message: 'App "$appId" not found in Zapstore.', + StorageData() => _AppDetailContent( + app: appState.models.firstOrNull, + appState: appState, ), - _ => const Scaffold( + StorageLoading() => const Scaffold( body: SafeArea( child: SingleChildScrollView( padding: EdgeInsets.all(16), @@ -95,13 +95,18 @@ class _ErrorScaffold extends StatelessWidget { /// Internal widget that displays app details class _AppDetailContent extends HookConsumerWidget { - final App app; + final App? app; final StorageState appState; const _AppDetailContent({required this.app, required this.appState}); @override Widget build(BuildContext context, WidgetRef ref) { + final app = this.app; + if (app == null) { + return const _ErrorScaffold(message: 'App not found'); + } + final signedInPubkey = ref.watch(Signer.activePubkeyProvider); final showDebugSections = isDebugMode(signedInPubkey); diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index c9979fc..e6156b0 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -295,6 +295,7 @@ class _UpdatesListBody extends StatelessWidget { showUpdateArrow: false, showUpdateButton: true, showZapEncouragement: true, + showDescription: false, ); case _UpdatesItemType.automaticHeader: return _UpdatesSectionHeader( @@ -309,6 +310,7 @@ class _UpdatesListBody extends StatelessWidget { showUpdateArrow: true, showUpdateButton: true, showZapEncouragement: true, + showDescription: false, ); case _UpdatesItemType.manualHeader: return _UpdatesSectionHeader( @@ -325,6 +327,7 @@ class _UpdatesListBody extends StatelessWidget { showUpdateArrow: true, showUpdateButton: true, showZapEncouragement: true, + showDescription: false, ); } }, @@ -526,7 +529,11 @@ class _UpToDateTab extends HookConsumerWidget { return ListView.builder( itemCount: upToDateApps.length, itemBuilder: (context, index) { - return AppCard(app: upToDateApps[index], showUpdateArrow: false); + return AppCard( + app: upToDateApps[index], + showUpdateArrow: false, + showDescription: false, + ); }, ); } @@ -626,7 +633,11 @@ class _BookmarkedAppsSectionWithIds extends ConsumerWidget { padding: EdgeInsets.zero, itemCount: savedApps.length, itemBuilder: (context, index) { - return AppCard(app: savedApps[index], showUpdateArrow: false); + return AppCard( + app: savedApps[index], + showUpdateArrow: false, + showDescription: false, + ); }, ); } diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index feacfe3..1307fba 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -11,7 +11,7 @@ import 'package:zapstore/utils/url_utils.dart'; import 'package:zapstore/services/download/download_service.dart'; import 'package:zapstore/widgets/zap_widgets.dart'; -import 'author_container.dart'; +import 'common/profile_avatar.dart'; import 'version_pill_widget.dart'; import 'install_button.dart'; import '../theme.dart'; @@ -23,6 +23,7 @@ class AppCard extends HookConsumerWidget { final bool showSignedBy; final bool showUpdateButton; final bool showZapEncouragement; + final bool showDescription; const AppCard({ super.key, @@ -32,6 +33,7 @@ class AppCard extends HookConsumerWidget { this.showSignedBy = true, this.showUpdateButton = false, this.showZapEncouragement = false, + this.showDescription = true, }); @override @@ -55,7 +57,18 @@ class AppCard extends HookConsumerWidget { onTap: () { final segments = GoRouterState.of(context).uri.pathSegments; final first = segments.isNotEmpty ? segments.first : 'search'; - context.push('/$first/app/${app!.identifier}'); + // Prefer naddr so the detail screen can uniquely identify the app + // (identifier + author) and not accidentally resolve to a different + // publisher's app with the same identifier. + final naddr = Utils.encodeShareableIdentifier( + AddressInput( + identifier: app!.identifier, + author: app!.pubkey, + kind: app!.event.kind, + relays: const [], + ), + ); + context.push('/$first/app/$naddr'); }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), @@ -68,108 +81,69 @@ class AppCard extends HookConsumerWidget { width: 1, ), ), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // App Icon (64px, radius 15 to match old design) - _buildAppIcon(context), + // Header row: Icon + Name/Version (icon matches header height) + LayoutBuilder( + builder: (context, constraints) { + // Icon takes max 20% of available width + final iconSize = (constraints.maxWidth * 0.20).clamp( + 48.0, + 64.0, + ); + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // App Icon (stretches to match name + version height, max 20% width) + _buildAppIcon(context, iconSize), - const SizedBox(width: 18), + const SizedBox(width: 14), - // App Details with professional typography - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // App Name and Version - if (showUpdateButton) ...[ - // When showing update button: name on top, version below - Text( - app!.name ?? app!.identifier, - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: 0.1, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(height: 6), - Align( - alignment: Alignment.centerLeft, - child: VersionPillWidget( - app: app!, - showUpdateArrow: showUpdateArrow, - ), - ), - ] else ...[ - // Default: name and version in same row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - app!.name ?? app!.identifier, - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: 0.1, + // App Name and Version (always stacked) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App name with optional "by publisher" inline + _buildAppNameWithPublisher(context, publisher), + const SizedBox(height: 6), + Align( + alignment: Alignment.centerLeft, + child: VersionPillWidget( + app: app!, + showUpdateArrow: showUpdateArrow, + ), ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + ], ), - const SizedBox(width: 12), - VersionPillWidget( - app: app!, - showUpdateArrow: showUpdateArrow, - ), - ], - ), - ], - - const SizedBox(height: 12), - - // App Description rendered as plain text (markdown stripped) - Text( - descriptionText, - style: descriptionStyle, - overflow: TextOverflow.ellipsis, - maxLines: 3, - softWrap: true, + ), + ], ), - - // Author signature (only if app has publisher) - if (showSignedBy && publisher != null) ...[ - Gap(14), - Theme( - data: Theme.of(context).copyWith( - textTheme: context.textTheme.copyWith( - bodySmall: context.textTheme.bodySmall?.copyWith( - color: AppColors.darkOnSurfaceSecondary, - ), - ), - ), - child: AuthorContainer( - profile: publisher, - beforeText: 'Published by', - size: context.textTheme.bodyMedium!.fontSize!, - oneLine: true, - app: app, - ), - ), - ], - - // Update button (for apps with updates or currently downloading/installing) - if (showUpdateButton) _AppCardUpdateButtonSection(app: app!), - - // Zap encouragement (only for downloading/installing developer-signed apps) - if (showZapEncouragement) - _AppCardZapEncouragementSection( - app: app!, - publisher: publisher, - ), - ], - ), + ); + }, ), + + // App Description rendered as plain text (markdown stripped) + if (showDescription) ...[ + const SizedBox(height: 12), + Text( + descriptionText, + style: descriptionStyle, + overflow: TextOverflow.ellipsis, + maxLines: 3, + softWrap: true, + ), + ], + + // Update button (for apps with updates or currently downloading/installing) + if (showUpdateButton) _AppCardUpdateButtonSection(app: app!), + + // Zap encouragement (only for downloading/installing developer-signed apps) + if (showZapEncouragement) + _AppCardZapEncouragementSection(app: app!, publisher: publisher), ], ), ), @@ -199,47 +173,102 @@ class AppCard extends HookConsumerWidget { ); } - Widget _buildAppIcon(BuildContext context) { + Widget _buildAppIcon(BuildContext context, double size) { final iconUrl = firstValidHttpUrl(app!.icons); - return Container( - width: 58, - height: 58, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 2), + return AspectRatio( + aspectRatio: 1, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: size, maxHeight: size), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(14), - child: iconUrl != null - ? CachedNetworkImage( - imageUrl: iconUrl, - fit: BoxFit.cover, - fadeInDuration: const Duration(milliseconds: 500), - fadeOutDuration: const Duration(milliseconds: 200), - placeholder: (_, url) => const SizedBox.shrink(), - errorWidget: (context, url, error) => Center( - child: Icon( - Icons.broken_image_outlined, - size: 32, - color: Colors.grey[400], + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: iconUrl != null + ? CachedNetworkImage( + imageUrl: iconUrl, + fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 500), + fadeOutDuration: const Duration(milliseconds: 200), + placeholder: (_, url) => const SizedBox.shrink(), + errorWidget: (context, url, error) => Center( + child: Icon( + Icons.broken_image_outlined, + size: 32, + color: Colors.grey[400], + ), + ), + ) + : Center( + child: Icon( + Icons.apps_outlined, + size: 32, + color: Colors.grey[400], + ), ), - ), - ) - : Center( - child: Icon( - Icons.apps_outlined, - size: 32, - color: Colors.grey[400], + ), + ), + ), + ); + } + + Widget _buildAppNameWithPublisher(BuildContext context, Profile? publisher) { + final appName = app!.name ?? app!.identifier; + final titleStyle = context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: 0.1, + ); + + // If no publisher or relay-signed, just show name + if (!showSignedBy || publisher == null || app!.isRelaySigned) { + return Text( + appName, + style: titleStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ); + } + + // Show "(app name) by (profile)" with wrapping + final byStyle = context.textTheme.bodyMedium?.copyWith( + color: AppColors.darkOnSurfaceSecondary, + ); + final publisherStyle = byStyle?.copyWith(fontWeight: FontWeight.w600); + final avatarSize = context.textTheme.bodyMedium!.fontSize! * 1.4; + + return Text.rich( + TextSpan( + children: [ + TextSpan(text: appName, style: titleStyle), + TextSpan(text: ' by ', style: byStyle), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: SizedBox( + width: avatarSize, + height: avatarSize, + child: ProfileAvatar( + profile: publisher, + radius: avatarSize / 2, ), ), + ), + ), + TextSpan(text: publisher.nameOrNpub, style: publisherStyle), + ], ), + softWrap: true, + overflow: TextOverflow.visible, ); } @@ -307,48 +336,58 @@ class AppCard extends HookConsumerWidget { width: 1, ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // App icon skeleton - Container( - width: 58, - height: 58, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(14), - ), - ), - const SizedBox(width: 18), - // App details skeleton - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // App name skeleton - Row( - children: [ - Container( - height: 24, - width: 150, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(4), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final iconSize = (constraints.maxWidth * 0.20).clamp(48.0, 64.0); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row: Icon + Name/Version + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // App icon skeleton + Container( + width: iconSize, + height: iconSize, + decoration: BoxDecoration( + color: AppColors.darkSkeletonBase, + borderRadius: BorderRadius.circular(14), ), - const Spacer(), - Container( - height: 20, - width: 60, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(10), - ), + ), + const SizedBox(width: 14), + // App name and version skeleton (stacked) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // App name skeleton (with "by publisher" space) + Container( + height: 20, + width: 180, + decoration: BoxDecoration( + color: AppColors.darkSkeletonBase, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 6), + // Version pill skeleton + Container( + height: 20, + width: 60, + decoration: BoxDecoration( + color: AppColors.darkSkeletonBase, + borderRadius: BorderRadius.circular(10), + ), + ), + ], ), - ], - ), + ), + ], + ), + // Description skeleton - 3 lines to match actual maxLines: 3 + if (showDescription) ...[ const SizedBox(height: 12), - // Description skeleton - 3 lines to match actual maxLines: 3 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -380,31 +419,10 @@ class AppCard extends HookConsumerWidget { ), ], ), - // Author skeleton (only if showSignedBy is true) - match Gap(14) spacing - if (showSignedBy) ...[ - const SizedBox(height: 14), - Row( - children: [ - CircleAvatar( - radius: 12, - backgroundColor: AppColors.darkSkeletonBase, - ), - const SizedBox(width: 8), - Container( - height: 16, - width: 120, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(4), - ), - ), - ], - ), - ], ], - ), - ), - ], + ], + ); + }, ), ), ), @@ -428,8 +446,8 @@ class _AppCardUpdateButtonSection extends ConsumerWidget { return Column( children: [ const Gap(12), - SizedBox( - height: 38, + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 38), child: _CompactInstallButton( app: app, release: app.latestRelease.value, @@ -515,8 +533,8 @@ class _ZapEncouragementInCard extends HookConsumerWidget { ), ), const SizedBox(width: 8), - SizedBox( - height: 28, + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 28), child: TextButton( onPressed: () => showZapDialog(context, ref, app, author), style: TextButton.styleFrom( @@ -532,9 +550,12 @@ class _ZapEncouragementInCard extends HookConsumerWidget { borderRadius: BorderRadius.circular(6), ), ), - child: const Text( + child: Text( 'Zap', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), ), ), diff --git a/lib/widgets/app_pack_container.dart b/lib/widgets/app_pack_container.dart index 9cd7aa5..175a614 100644 --- a/lib/widgets/app_pack_container.dart +++ b/lib/widgets/app_pack_container.dart @@ -474,7 +474,18 @@ class AppGridCard extends ConsumerWidget { } final segments = GoRouterState.of(context).uri.pathSegments; final first = segments.isNotEmpty ? segments.first : 'search'; - context.push('/$first/app/${app!.identifier}'); + // Prefer naddr so the detail screen can uniquely identify the app + // (identifier + author) and not accidentally resolve to a different + // publisher's app with the same identifier. + final naddr = Utils.encodeShareableIdentifier( + AddressInput( + identifier: app!.identifier, + author: app!.pubkey, + kind: app!.event.kind, + relays: const [], + ), + ); + context.push('/$first/app/$naddr'); }, child: Container( margin: const EdgeInsets.all(2), diff --git a/lib/widgets/common/note_parser.dart b/lib/widgets/common/note_parser.dart index cb30499..62cc802 100644 --- a/lib/widgets/common/note_parser.dart +++ b/lib/widgets/common/note_parser.dart @@ -232,7 +232,7 @@ class NoteParser { return Text(content, style: textStyle); } - return RichText(text: TextSpan(children: spans)); + return Text.rich(TextSpan(children: spans)); } /// Checks if a URL is likely a media URL and returns the media type @@ -386,10 +386,15 @@ class ProfileEntityWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final profileState = ref.watch(query( - authors: {profileData.pubkey}, - source: const LocalAndRemoteSource(relays: {'social', 'vertex'}, cachedFor: Duration(hours: 2)), - )); + final profileState = ref.watch( + query( + authors: {profileData.pubkey}, + source: const LocalAndRemoteSource( + relays: {'social', 'vertex'}, + cachedFor: Duration(hours: 2), + ), + ), + ); // Show animated npub while profile is being loaded return switch (profileState) { @@ -411,10 +416,7 @@ class ProfileEntityWidget extends ConsumerWidget { colorPair: colorPair, ), ), - StorageData(:final models) => _buildProfileWidget( - context, - models.first, - ), + StorageData(:final models) => _buildProfileWidget(context, models.first), }; } @@ -498,10 +500,9 @@ class EventEntityWidget extends StatelessWidget { Text( shortId, style: context.textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), fontFeatures: const [FontFeature.tabularFigures()], ), ), diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 3660b0d..dd334db 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -96,8 +96,8 @@ class InstallButton extends ConsumerWidget { child: Row( children: [ Expanded( - child: SizedBox( - height: 48, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48), child: _buildButtonForState(context, ref, state), ), ), @@ -836,8 +836,8 @@ class InstallButton extends ConsumerWidget { return Row( children: [ Expanded( - child: RichText( - text: TextSpan( + child: Text.rich( + TextSpan( style: Theme.of(context).textTheme.bodyMedium, children: [ TextSpan( diff --git a/lib/widgets/pill_widget.dart b/lib/widgets/pill_widget.dart index ef5eba5..48ecaca 100644 --- a/lib/widgets/pill_widget.dart +++ b/lib/widgets/pill_widget.dart @@ -22,8 +22,8 @@ class PillWidget extends StatelessWidget { color: color, borderRadius: BorderRadius.circular(size * 1.1), ), - child: RichText( - text: TextSpan( + child: Text.rich( + TextSpan( style: TextStyle( color: Colors.white, fontSize: size, diff --git a/lib/widgets/relevant_who_follow_container.dart b/lib/widgets/relevant_who_follow_container.dart index dad4969..9ffb6d5 100644 --- a/lib/widgets/relevant_who_follow_container.dart +++ b/lib/widgets/relevant_who_follow_container.dart @@ -61,8 +61,8 @@ class RelevantWhoFollowContainer extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RichText( - text: TextSpan( + Text.rich( + TextSpan( text: 'You follow this publisher. ', style: baseStyle, children: [TextSpan(text: trailingText)], @@ -149,8 +149,8 @@ class RelevantWhoFollowContainer extends HookConsumerWidget { String npub, TextStyle? baseStyle, ) { - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( text: 'View profile on nostr', style: baseStyle?.copyWith( color: Colors.white, diff --git a/lib/widgets/version_pill_widget.dart b/lib/widgets/version_pill_widget.dart index 2c46b54..6a0b8d2 100644 --- a/lib/widgets/version_pill_widget.dart +++ b/lib/widgets/version_pill_widget.dart @@ -75,14 +75,16 @@ class VersionPillWidget extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ // Current version pill (muted colors for installed version) - _buildVersionPill( - context, - ref, - installedPackages, - installedVersion, - Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), - Theme.of(context).colorScheme.onSurface, - isInstalledVersion: true, + Flexible( + child: _buildVersionPill( + context, + ref, + installedPackages, + installedVersion, + Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + Theme.of(context).colorScheme.onSurface, + isInstalledVersion: true, + ), ), // Arrow icon (always arrow, forbidden icon is in the pill itself) @@ -93,18 +95,20 @@ class VersionPillWidget extends HookConsumerWidget { ), // Available version pill (highlighted for update, greyed for downgrade) - _buildVersionPill( - context, - ref, - installedPackages, - availableVersion, - isDowngrade - ? Theme.of(context).colorScheme.outline.withValues(alpha: 0.3) - : AppColors.darkPillBackground, - isDowngrade - ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5) - : Colors.white, - isDowngrade: isDowngrade, + Flexible( + child: _buildVersionPill( + context, + ref, + installedPackages, + availableVersion, + isDowngrade + ? Theme.of(context).colorScheme.outline.withValues(alpha: 0.3) + : AppColors.darkPillBackground, + isDowngrade + ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5) + : Colors.white, + isDowngrade: isDowngrade, + ), ), ], ); @@ -166,14 +170,15 @@ class VersionPillWidget extends HookConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - displayVersion, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.bold, - color: finalTextColor, - fontSize: 11.5, + Flexible( + child: Text( + displayVersion, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + color: finalTextColor, + ), ), ), if (statusIcon != null) ...[ @@ -189,7 +194,8 @@ class VersionPillWidget extends HookConsumerWidget { } String _displayVersion(String version) { - return version.length > 10 ? '${version.substring(0, 9)}...' : version; + // Show full version - let Flexible + TextOverflow.ellipsis handle truncation only when needed + return version; } // No width enforcement; both pills share the same text style