From 00fa2c173b9712e914f017eabc27ed99aa8d3606 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Tue, 23 Dec 2025 20:38:49 -0300 Subject: [PATCH] Use app stacks, fix headlines --- lib/constants/app_constants.dart | 4 +- lib/screens/app_detail_screen.dart | 101 +++---- lib/screens/app_stack_screen.dart | 16 +- lib/screens/profile_screen.dart | 16 +- lib/screens/updates_screen.dart | 16 +- lib/screens/user_screen.dart | 121 +++++--- lib/services/bookmarks_service.dart | 12 +- lib/theme.dart | 10 +- lib/widgets/app_card.dart | 1 - lib/widgets/app_detail_widgets.dart | 36 +-- lib/widgets/app_header.dart | 2 - lib/widgets/app_pack_container.dart | 421 --------------------------- lib/widgets/app_stack_container.dart | 91 ++++-- lib/widgets/bookmark_widgets.dart | 213 +++++++------- lib/widgets/comments_section.dart | 24 +- lib/widgets/nwc_widgets.dart | 4 +- lib/widgets/profile_header.dart | 4 +- lib/widgets/zap_widgets.dart | 1 - 18 files changed, 370 insertions(+), 723 deletions(-) delete mode 100644 lib/widgets/app_pack_container.dart diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 8c8376d..3cde44e 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -12,8 +12,8 @@ const kFranzapPubkey = /// Identifier for storing user saved apps const kAppBookmarksIdentifier = 'zapstore-bookmarks'; -/// Event filter for app packs - excludes saved apps and packs with zero App references -bool appPackEventFilter(Map event) { +/// Event filter for app stacks - excludes saved apps and stacks with zero App references +bool appStackEventFilter(Map event) { final tags = event['tags'] as List?; if (tags == null) return false; diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 6369d37..e4b7091 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; @@ -22,7 +23,6 @@ import 'package:zapstore/widgets/download_text_container.dart'; import 'package:zapstore/widgets/expandable_markdown.dart'; import 'package:zapstore/widgets/install_button.dart'; 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, this.authorPubkey}); @@ -309,50 +309,41 @@ class _AppDetailContent extends HookConsumerWidget { ], ), const SizedBox(height: 16), - Row( - children: [ - Text( - 'Version ', - style: context.textTheme.bodyMedium, - ), - ColorFiltered( - colorFilter: const ColorFilter.matrix([ - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]), - child: VersionPillWidget( - app: app, - forceVersion: latestMetadata.version, + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Version:', + style: context.textTheme.bodyMedium, ), - ), - const SizedBox(width: 12), - Text( - '(${formatDate(latestMetadata.createdAt)})', - style: context.textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + Gap(4), + Text( + latestMetadata.version, + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - ], + Gap(4), + Text( + '(${formatDate(latestMetadata.createdAt)})', + style: context.textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), + ), + ), + ], + ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 20), @@ -586,9 +577,9 @@ class _AppDetailContent extends HookConsumerWidget { return; } - // Query for existing pack - final existingPackState = await ref.storage.query( - RequestFilter( + // Query for existing stack + final existingStackState = await ref.storage.query( + RequestFilter( authors: {signedInPubkey}, tags: { '#d': {kAppBookmarksIdentifier}, @@ -596,14 +587,14 @@ class _AppDetailContent extends HookConsumerWidget { ).toRequest(), source: const LocalSource(), ); - final existingPack = existingPackState.firstOrNull; + final existingStack = existingStackState.firstOrNull; - // Get existing app IDs by decrypting if pack exists + // Get existing app IDs by decrypting if stack exists List existingAppIds = []; - if (existingPack != null) { + if (existingStack != null) { try { final decryptedContent = await signer.nip44Decrypt( - existingPack.content, + existingStack.content, signedInPubkey, ); existingAppIds = (jsonDecode(decryptedContent) as List) @@ -631,19 +622,19 @@ class _AppDetailContent extends HookConsumerWidget { } } - // Create new partial pack with updated list - final partialPack = PartialAppPack.withEncryptedApps( + // Create new partial stack with updated list + final partialStack = PartialAppStack.withEncryptedApps( name: 'Saved Apps', identifier: kAppBookmarksIdentifier, apps: existingAppIds, ); // Sign (encrypts the content) - final signedPack = await partialPack.signWith(signer); + final signedStack = await partialStack.signWith(signer); // Save to local storage and publish to relays - await ref.storage.save({signedPack}); - ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social')); + await ref.storage.save({signedStack}); + ref.storage.publish({signedStack}, source: RemoteSource(relays: 'social')); if (context.mounted) { context.showInfo( diff --git a/lib/screens/app_stack_screen.dart b/lib/screens/app_stack_screen.dart index 2768c36..400b0f5 100644 --- a/lib/screens/app_stack_screen.dart +++ b/lib/screens/app_stack_screen.dart @@ -22,7 +22,7 @@ class AppStackScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Query stack with apps relationship final stackState = ref.watch( - query( + query( authors: authorPubkey != null ? {authorPubkey!} : null, tags: { '#d': {stackId}, @@ -61,7 +61,7 @@ class AppStackScreen extends HookConsumerWidget { /// Intermediate widget that loads apps with their release relationships class _AppStackContentWithApps extends HookConsumerWidget { - final AppPack? stack; + final AppStack? stack; const _AppStackContentWithApps({required this.stack}); @@ -132,7 +132,7 @@ class _ErrorScaffold extends StatelessWidget { /// Internal widget that displays stack details class _AppStackContent extends HookConsumerWidget { - final AppPack stack; + final AppStack stack; final List apps; const _AppStackContent({required this.stack, required this.apps}); @@ -179,9 +179,7 @@ class _AppStackContent extends HookConsumerWidget { children: [ Text( 'Apps in this stack', - style: context.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleLarge, ), const SizedBox(width: 8), CountBadge( @@ -235,7 +233,7 @@ class _AppStackContent extends HookConsumerWidget { class _StackHeader extends StatelessWidget { const _StackHeader({required this.stack, this.author}); - final AppPack stack; + final AppStack stack; final Profile? author; @override @@ -246,9 +244,7 @@ class _StackHeader extends StatelessWidget { // Stack name Text( stack.name ?? stack.identifier, - style: context.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.headlineMedium, ), const SizedBox(height: 8), // Published by author (same style as app_detail_screen) diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 0573422..7c8988b 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -375,9 +375,7 @@ class _DebugMessagesSection extends HookConsumerWidget { Expanded( child: Text( 'Debug Info', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.titleMedium, ), ), ], @@ -975,9 +973,7 @@ class _DataManagementSection extends ConsumerWidget { children: [ Text( 'Data Management', - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), ListTile( @@ -1267,9 +1263,7 @@ class _AboutSection extends ConsumerWidget { children: [ Text( 'About', - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), if (isLoading) ...[ @@ -1326,9 +1320,7 @@ class _AboutSection extends ConsumerWidget { children: [ Text( 'About', - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), ListTile( diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index 769a44f..2e3c991 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -187,9 +187,7 @@ class _UpdatesListBody extends StatelessWidget { const SizedBox(height: 12), Text( 'No apps installed yet', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: theme.textTheme.titleMedium, textAlign: TextAlign.center, ), const SizedBox(height: 6), @@ -313,9 +311,7 @@ class _InstallingHeader extends StatelessWidget { const SizedBox(width: 8), Text( 'Installing', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), const SizedBox(width: 8), CountBadge(count: count, color: AppColors.darkPillBackground), @@ -356,9 +352,7 @@ class _UpdatesSectionHeader extends ConsumerWidget { Flexible( child: Text( title, - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), ), const SizedBox(width: 8), @@ -485,9 +479,7 @@ class _UpToDateHeader extends StatelessWidget { const SizedBox(width: 8), Text( 'Up to date', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), const SizedBox(width: 8), CountBadge(count: count, color: AppColors.darkPillBackground), diff --git a/lib/screens/user_screen.dart b/lib/screens/user_screen.dart index c869a80..bf38d20 100644 --- a/lib/screens/user_screen.dart +++ b/lib/screens/user_screen.dart @@ -5,11 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; import 'package:zapstore/utils/extensions.dart'; +import '../constants/app_constants.dart'; import '../theme.dart'; import '../widgets/common/note_parser.dart'; import '../widgets/common/profile_avatar.dart'; import '../widgets/app_card.dart'; -import '../widgets/app_pack_container.dart'; import '../widgets/zap_widgets.dart'; /// User profile screen - shows any user/developer profile @@ -55,9 +55,9 @@ class UserScreen extends HookConsumerWidget { ? userAppsState.models.where((a) => a.isZapstoreApp).toList() : userAppsState.models; - // Query user's app packs - final appPacksState = ref.watch( - query( + // Query user's app stacks + final appStacksState = ref.watch( + query( authors: {pubkey}, limit: 20, and: (pack) => {pack.apps}, @@ -66,11 +66,11 @@ class UserScreen extends HookConsumerWidget { relays: 'AppCatalog', stream: false, ), - schemaFilter: appPackEventFilter, - subscriptionPrefix: 'user-packs', + schemaFilter: appStackEventFilter, + subscriptionPrefix: 'user-stacks', ), ); - final packs = appPacksState.models + final stacks = appStacksState.models ..sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt)); return Scaffold( @@ -99,9 +99,7 @@ class UserScreen extends HookConsumerWidget { padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), child: Text( 'Published Apps', - style: context.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleLarge, ), ), ), @@ -113,33 +111,23 @@ class UserScreen extends HookConsumerWidget { ), ], - // App packs section - only show if packs exist - if (packs.isNotEmpty) ...[ + // App stacks section - only show if stacks exist + if (stacks.isNotEmpty) ...[ SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 32, 16, 8), child: Text( - 'App Packs', - style: context.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + 'App Stacks', + style: context.textTheme.headlineMedium, ), ), ), - for (final pack in packs) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - pack.name ?? pack.identifier, - style: context.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - SliverToBoxAdapter(child: AppsGrid(apps: pack.apps.toList())), - ], + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final stack = stacks[index]; + return _StackLinkCard(stack: stack, pubkey: pubkey); + }, childCount: stacks.length), + ), ], // Bottom padding @@ -172,9 +160,7 @@ class _UserHeader extends StatelessWidget { Text( profile?.nameOrNpub ?? '${Utils.encodeShareableFromString(pubkey, type: 'npub').substring(0, 12)}...', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of(context).textTheme.headlineSmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -272,6 +258,73 @@ class _UserZapsList extends HookConsumerWidget { } } +class _StackLinkCard extends StatelessWidget { + const _StackLinkCard({required this.stack, required this.pubkey}); + + final AppStack stack; + final String pubkey; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: InkWell( + onTap: () { + final segments = GoRouterState.of(context).uri.pathSegments; + final first = segments.isNotEmpty ? segments.first : 'search'; + // Build naddr for the stack + final naddr = Utils.encodeShareableIdentifier( + AddressInput( + identifier: stack.identifier, + author: pubkey, + kind: stack.event.kind, + relays: const [], + ), + ); + context.push('/$first/stack/$naddr'); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stack.name ?? stack.identifier, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + } +} + class _UserBio extends HookWidget { const _UserBio({required this.profile}); diff --git a/lib/services/bookmarks_service.dart b/lib/services/bookmarks_service.dart index 5f0aec3..889bee9 100644 --- a/lib/services/bookmarks_service.dart +++ b/lib/services/bookmarks_service.dart @@ -13,10 +13,10 @@ final bookmarksProvider = FutureProvider>((ref) async { return {}; } - // Query saved apps pack - stream enabled to auto-update when saved apps change locally + // Query saved apps stack - stream enabled to auto-update when saved apps change locally // Initial fetch from remote, then watches local storage for changes - final packState = ref.watch( - query( + final stackState = ref.watch( + query( authors: {signedInPubkey}, tags: { '#d': {kAppBookmarksIdentifier}, @@ -30,20 +30,20 @@ final bookmarksProvider = FutureProvider>((ref) async { ); // Handle different storage states - final pack = switch (packState) { + final stack = switch (stackState) { StorageLoading() => null, StorageError() => null, StorageData(:final models) => models.firstOrNull, }; - if (pack == null || pack.content.isEmpty) { + if (stack == null || stack.content.isEmpty) { return {}; } try { // Content is always encrypted after signing - must explicitly decrypt final decryptedContent = await signer.nip44Decrypt( - pack.content, + stack.content, signedInPubkey, ); diff --git a/lib/theme.dart b/lib/theme.dart index d8d795d..789a4b3 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -115,7 +115,7 @@ TextTheme _createProfessionalTextTheme() { headlineMedium: const TextStyle( fontFamily: kHeadlineFontFamily, fontSize: 31, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w800, letterSpacing: 0.15, color: baseColor, height: 1.3, @@ -123,7 +123,7 @@ TextTheme _createProfessionalTextTheme() { headlineSmall: const TextStyle( fontFamily: kHeadlineFontFamily, fontSize: 26, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w800, letterSpacing: 0.15, color: baseColor, height: 1.35, @@ -133,7 +133,7 @@ TextTheme _createProfessionalTextTheme() { titleLarge: const TextStyle( fontFamily: kHeadlineFontFamily, fontSize: 24, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w700, letterSpacing: 0.1, color: baseColor, height: 1.4, @@ -141,7 +141,7 @@ TextTheme _createProfessionalTextTheme() { titleMedium: const TextStyle( fontFamily: kHeadlineFontFamily, fontSize: 20, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w700, letterSpacing: 0.15, color: baseColor, height: 1.45, @@ -149,7 +149,7 @@ TextTheme _createProfessionalTextTheme() { titleSmall: const TextStyle( fontFamily: kHeadlineFontFamily, fontSize: 18, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w700, letterSpacing: 0.15, color: baseColor, height: 1.45, diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index 32c07bc..c60f1ae 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -243,7 +243,6 @@ class AppCard extends HookConsumerWidget { 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 diff --git a/lib/widgets/app_detail_widgets.dart b/lib/widgets/app_detail_widgets.dart index 5b39bfd..23a6e29 100644 --- a/lib/widgets/app_detail_widgets.dart +++ b/lib/widgets/app_detail_widgets.dart @@ -64,7 +64,7 @@ class ReleaseNotes extends StatelessWidget { } } -/// Row of social action buttons (zap, bookmark, app pack, share) +/// Row of social action buttons (zap, bookmark, app stack, share) class SocialActionsRow extends HookConsumerWidget { const SocialActionsRow({super.key, required this.app, required this.author}); @@ -92,7 +92,7 @@ class SocialActionsRow extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Zap + Save App + App Pack button row + // Zap + Save App + App Stack button row SizedBox( height: 36, child: Row( @@ -141,11 +141,11 @@ class SocialActionsRow extends HookConsumerWidget { ), ), const SizedBox(width: 8), - // App Pack button + // App Stack button Expanded( flex: 16, child: FilledButton( - onPressed: () => _showAddToPackDialog(context, app), + onPressed: () => _showAddToStackDialog(context, app), style: FilledButton.styleFrom( padding: EdgeInsets.zero, backgroundColor: @@ -193,9 +193,9 @@ class SocialActionsRow extends HookConsumerWidget { if (signer == null || signedInPubkey == null) return; - // Query for existing pack - final existingPackState = await ref.storage.query( - RequestFilter( + // Query for existing stack + final existingStackState = await ref.storage.query( + RequestFilter( authors: {signedInPubkey}, tags: { '#d': {kAppBookmarksIdentifier}, @@ -203,14 +203,14 @@ class SocialActionsRow extends HookConsumerWidget { ).toRequest(), source: const LocalSource(), ); - final existingPack = existingPackState.firstOrNull; + final existingStack = existingStackState.firstOrNull; - // Get existing app IDs by decrypting if pack exists + // Get existing app IDs by decrypting if stack exists List existingAppIds = []; - if (existingPack != null) { + if (existingStack != null) { try { final decryptedContent = await signer.nip44Decrypt( - existingPack.content, + existingStack.content, signedInPubkey, ); existingAppIds = (jsonDecode(decryptedContent) as List) @@ -232,19 +232,19 @@ class SocialActionsRow extends HookConsumerWidget { } } - // Create new partial pack with updated list - final partialPack = PartialAppPack.withEncryptedApps( + // Create new partial stack with updated list + final partialStack = PartialAppStack.withEncryptedApps( name: 'Saved Apps', identifier: kAppBookmarksIdentifier, apps: existingAppIds, ); // Sign (encrypts the content) - final signedPack = await partialPack.signWith(signer); + final signedStack = await partialStack.signWith(signer); // Save to local storage and publish to relays - await ref.storage.save({signedPack}); - ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social')); + await ref.storage.save({signedStack}); + ref.storage.publish({signedStack}, source: RemoteSource(relays: 'social')); if (context.mounted) { context.showInfo( @@ -258,10 +258,10 @@ class SocialActionsRow extends HookConsumerWidget { } } - Future _showAddToPackDialog(BuildContext context, App app) async { + Future _showAddToStackDialog(BuildContext context, App app) async { await showBaseDialog( context: context, - dialog: AddToPackDialog(app: app), + dialog: AddToStackDialog(app: app), ); } } diff --git a/lib/widgets/app_header.dart b/lib/widgets/app_header.dart index fc94eb8..364aaa2 100644 --- a/lib/widgets/app_header.dart +++ b/lib/widgets/app_header.dart @@ -61,8 +61,6 @@ class AppHeader extends StatelessWidget { AutoSizeText( app.name ?? app.identifier, style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: 0.1, fontSize: (Theme.of( context, diff --git a/lib/widgets/app_pack_container.dart b/lib/widgets/app_pack_container.dart deleted file mode 100644 index ba11d5e..0000000 --- a/lib/widgets/app_pack_container.dart +++ /dev/null @@ -1,421 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:models/models.dart'; -import 'package:go_router/go_router.dart'; -import 'rounded_image.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:skeletonizer/skeletonizer.dart'; -import 'package:auto_size_text/auto_size_text.dart'; -import '../utils/extensions.dart'; -import '../utils/url_utils.dart'; -import '../theme.dart'; - -/// App Pack Container - horizontally scrollable pills with app collections -class AppPackContainer extends HookConsumerWidget { - const AppPackContainer({super.key, this.showSkeleton = false}); - - final bool showSkeleton; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (showSkeleton) { - return _buildSkeleton(context); - } - - final appPacksState = ref.watch( - query( - limit: 20, - and: (pack) => {pack.apps}, - source: LocalAndRemoteSource(stream: true, relays: 'social'), - andSource: const LocalAndRemoteSource( - relays: 'AppCatalog', - stream: false, - ), - schemaFilter: appPackEventFilter, - subscriptionPrefix: 'app-pack', - ), - ); - - final packs = switch (appPacksState) { - StorageData(:final models) => - models..sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt)), - _ => [], - }; - - if (packs.isEmpty) { - return _buildSkeleton(context); - } - - final selectedId = useState(packs.first.id); - final selectedPack = packs.firstWhere( - (p) => p.id == selectedId.value, - orElse: () => packs.first, - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 48, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: packs - .map( - (pack) => _PackPill( - pack: pack, - isSelected: selectedId.value == pack.id, - onTap: () => selectedId.value = pack.id, - ), - ) - .toList(), - ), - ), - ), - const SizedBox(height: 16), - AppsGrid(apps: selectedPack.apps.toList()), - ], - ); - } - - Widget _buildSkeleton(BuildContext context) { - return SkeletonizerConfig( - data: AppColors.getSkeletonizerConfig(Theme.of(context).brightness), - child: Skeletonizer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row(children: List.generate(3, (_) => _SkeletonPill())), - ), - const SizedBox(height: 16), - const AppsGrid(apps: []), - ], - ), - ), - ); - } -} - -/// Skeleton pill matching actual pill dimensions -class _SkeletonPill extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(right: 16), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(18), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container(height: 16, width: 50, color: AppColors.darkSkeletonBase), - const SizedBox(width: 4), - Container(height: 14, width: 16, color: AppColors.darkSkeletonBase), - const SizedBox(width: 6), - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(9), - ), - ), - const SizedBox(width: 6), - Container(height: 14, width: 50, color: AppColors.darkSkeletonBase), - ], - ), - ), - ); - } -} - -/// Individual pack pill - watches its own author reactively -class _PackPill extends ConsumerWidget { - const _PackPill({ - required this.pack, - required this.isSelected, - required this.onTap, - }); - - final AppPack pack; - final bool isSelected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authorState = ref.watch( - query( - authors: {pack.event.pubkey}, - source: const LocalAndRemoteSource( - relays: {'social', 'vertex'}, - cachedFor: Duration(hours: 2), - ), - ), - ); - final author = switch (authorState) { - StorageData(:final models) => models.firstOrNull, - _ => null, - }; - - return Padding( - padding: const EdgeInsets.only(right: 16), - child: GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.15) - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(18), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - pack.name ?? pack.identifier, - style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (author != null) ...[ - Text( - ' by ', - style: context.textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - GestureDetector( - onTap: isSelected - ? () { - final segments = GoRouterState.of( - context, - ).uri.pathSegments; - final first = segments.isNotEmpty - ? segments.first - : 'search'; - context.push('/$first/user/${pack.event.pubkey}'); - } - : null, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withValues(alpha: 0.3), - width: 1, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: RoundedImage(url: author.pictureUrl, size: 18), - ), - ), - const SizedBox(width: 6), - Text( - author.nameOrNpub, - style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ], - ), - ), - ), - ); - } -} - -/// Horizontally scrolling apps grid - reusable widget for displaying apps in a 2-row grid -class AppsGrid extends StatelessWidget { - const AppsGrid({super.key, required this.apps}); - - final List apps; - - @override - Widget build(BuildContext context) { - final items = apps.isEmpty ? List.generate(6, (_) => null) : apps; - - return SizedBox( - height: 220, - child: GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 12), - scrollDirection: Axis.horizontal, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.8, - crossAxisSpacing: 2, - mainAxisSpacing: 2, - ), - itemCount: items.length, - itemBuilder: (context, index) => AppGridCard(app: items[index]), - ), - ); - } -} - -/// Individual app card with original styling - reusable widget for grid display -class AppGridCard extends ConsumerWidget { - const AppGridCard({super.key, this.app}); - - final App? app; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (app == null) { - return Padding( - padding: const EdgeInsets.all(14), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(8), - ), - ), - const SizedBox(height: 8), - Container( - height: 10, - width: 40, - decoration: BoxDecoration( - color: AppColors.darkSkeletonBase, - borderRadius: BorderRadius.circular(5), - ), - ), - ], - ), - ); - } - - final iconUrl = firstValidHttpUrl(app!.icons); - - return GestureDetector( - onTap: () { - if (app!.latestRelease.value == null) { - final req = app!.latestRelease.req; - if (req != null) { - ref.storage.query( - req, - source: const LocalAndRemoteSource(stream: false), - ); - } - } - final segments = GoRouterState.of(context).uri.pathSegments; - final first = segments.isNotEmpty ? segments.first : 'search'; - // 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), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), - width: 1, - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: iconUrl != null - ? CachedNetworkImage( - imageUrl: iconUrl, - fit: BoxFit.cover, - fadeInDuration: const Duration(milliseconds: 500), - fadeOutDuration: const Duration(milliseconds: 200), - placeholder: (_, __) => const SizedBox.shrink(), - errorWidget: (_, __, ___) => Icon( - Icons.broken_image_outlined, - size: 16, - color: Colors.grey[400], - ), - ) - : Icon( - Icons.apps_outlined, - size: 16, - color: Colors.grey[400], - ), - ), - ), - const SizedBox(height: 12), - AutoSizeText( - app!.name ?? app!.identifier, - style: context.textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.w600, - height: 1.2, - ), - maxLines: 1, - minFontSize: 10, - maxFontSize: 12, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/app_stack_container.dart b/lib/widgets/app_stack_container.dart index 611becd..844191b 100644 --- a/lib/widgets/app_stack_container.dart +++ b/lib/widgets/app_stack_container.dart @@ -6,6 +6,7 @@ import 'package:models/models.dart'; import 'package:go_router/go_router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:skeletonizer/skeletonizer.dart'; +import '../constants/app_constants.dart'; import '../utils/extensions.dart'; import '../utils/url_utils.dart'; import '../theme.dart'; @@ -28,7 +29,7 @@ class AppStackContainer extends HookConsumerWidget { final signedInPubkey = ref.watch(Signer.activePubkeyProvider); final appStacksState = ref.watch( - query( + query( limit: 50, and: (pack) => {pack.apps}, source: LocalAndRemoteSource(stream: true, relays: 'social'), @@ -36,7 +37,7 @@ class AppStackContainer extends HookConsumerWidget { relays: 'AppCatalog', stream: false, ), - schemaFilter: appPackEventFilter, + schemaFilter: appStackEventFilter, subscriptionPrefix: 'app-stack', ), ); @@ -61,7 +62,7 @@ class AppStackContainer extends HookConsumerWidget { final rawStacks = switch (appStacksState) { StorageData(:final models) => models, - _ => [], + _ => [], }; if (rawStacks.isEmpty) { @@ -87,7 +88,9 @@ class AppStackContainer extends HookConsumerWidget { for (int i = 0; i < stacks.length; i += 2) Padding( padding: EdgeInsets.only( - right: i == stacks.length - 2 || i == stacks.length - 1 ? 0 : 10, + right: i == stacks.length - 2 || i == stacks.length - 1 + ? 0 + : 10, ), child: SizedBox( width: 160, @@ -108,8 +111,8 @@ class AppStackContainer extends HookConsumerWidget { ); } - List _sortStacks( - List stacks, { + List _sortStacks( + List stacks, { String? signedInPubkey, Set? followingPubkeys, }) { @@ -119,8 +122,8 @@ class AppStackContainer extends HookConsumerWidget { followingPubkeys != null && followingPubkeys.isNotEmpty) { // Signed in: followed profiles first, then by recency - final followed = []; - final others = []; + final followed = []; + final others = []; for (final stack in stacks) { if (followingPubkeys.contains(stack.pubkey)) { @@ -278,7 +281,7 @@ class _SkeletonStackCard extends StatelessWidget { class _StackCard extends HookConsumerWidget { const _StackCard({required this.stack}); - final AppPack stack; + final AppStack stack; @override Widget build(BuildContext context, WidgetRef ref) { @@ -339,14 +342,13 @@ class _StackCard extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Stack name - Text( + // Stack name with conditional fade out + _FadingText( stack.name ?? stack.identifier, style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + fontFamily: 'Inter', + fontSize: (context.textTheme.titleMedium?.fontSize ?? 16) * 0.9, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), // Author row: avatar + Name @@ -397,9 +399,7 @@ class _AppIconsRow extends StatelessWidget { aspectRatio: 1, child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest + color: Theme.of(context).colorScheme.surfaceContainerHighest .withValues(alpha: 0.8), borderRadius: BorderRadius.circular(10), ), @@ -482,7 +482,9 @@ class _AppIconTile extends StatelessWidget { ), ) : Container( - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, child: const Icon( Icons.apps_outlined, size: 16, @@ -495,3 +497,56 @@ class _AppIconTile extends StatelessWidget { ); } } + +/// Text widget that only applies fade-out effect when text overflows +class _FadingText extends HookWidget { + const _FadingText(this.text, {required this.style}); + + final String text; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // Measure if text would overflow + final textPainter = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(maxWidth: double.infinity); + + final isOverflowing = textPainter.width > constraints.maxWidth; + + final textWidget = Text( + text, + style: style, + maxLines: 1, + overflow: TextOverflow.clip, + ); + + if (!isOverflowing) { + return textWidget; + } + + // Apply fade effect only when overflowing + return ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Theme.of(context).colorScheme.onSurface, + Theme.of(context).colorScheme.onSurface, + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0), + ], + stops: const [0.0, 0.8, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: textWidget, + ); + }, + ); + } +} diff --git a/lib/widgets/bookmark_widgets.dart b/lib/widgets/bookmark_widgets.dart index b44d234..986eb40 100644 --- a/lib/widgets/bookmark_widgets.dart +++ b/lib/widgets/bookmark_widgets.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; +import 'package:zapstore/constants/app_constants.dart'; import 'package:zapstore/services/notification_service.dart'; import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/widgets/auth_widgets.dart'; @@ -104,9 +105,9 @@ class SaveAppDialog extends HookConsumerWidget { return; } - // Query for existing pack - final existingPackState = await ref.storage.query( - RequestFilter( + // Query for existing stack + final existingStackState = await ref.storage.query( + RequestFilter( authors: {signedInPubkey}, tags: { '#d': {kAppBookmarksIdentifier}, @@ -114,14 +115,14 @@ class SaveAppDialog extends HookConsumerWidget { ).toRequest(), source: const LocalSource(), ); - final existingPack = existingPackState.firstOrNull; + final existingStack = existingStackState.firstOrNull; - // Get existing app IDs by decrypting if pack exists + // Get existing app IDs by decrypting if stack exists List existingAppIds = []; - if (existingPack != null) { + if (existingStack != null) { try { final decryptedContent = await signer.nip44Decrypt( - existingPack.content, + existingStack.content, signedInPubkey, ); existingAppIds = (jsonDecode(decryptedContent) as List) @@ -149,19 +150,21 @@ class SaveAppDialog extends HookConsumerWidget { } } - // Create new partial pack with updated list - final partialPack = PartialAppPack.withEncryptedApps( + // Create new partial stack with updated list + final partialStack = PartialAppStack.withEncryptedApps( name: 'Saved Apps', identifier: kAppBookmarksIdentifier, apps: existingAppIds, ); // Sign (encrypts the content) - final signedPack = await partialPack.signWith(signer); + final signedStack = await partialStack.signWith(signer); // Save to local storage and publish to relays - await ref.storage.save({signedPack}); - ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social')); + await ref.storage.save({signedStack}); + ref.storage.publish({ + signedStack, + }, source: RemoteSource(relays: 'social')); if (context.mounted) { Navigator.pop(context); @@ -177,9 +180,9 @@ class SaveAppDialog extends HookConsumerWidget { } } -/// Dialog for managing app packs (public collections) -class AddToPackDialog extends HookConsumerWidget { - const AddToPackDialog({super.key, required this.app}); +/// Dialog for managing app stacks (public collections) +class AddToStackDialog extends HookConsumerWidget { + const AddToStackDialog({super.key, required this.app}); final App app; @@ -187,14 +190,14 @@ class AddToPackDialog extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final signedInPubkey = ref.watch(Signer.activePubkeyProvider); if (signedInPubkey == null) { - return _AddToPackDialogSignedOut(app: app); + return _AddToStackDialogSignedOut(app: app); } - return _AddToPackDialogSignedIn(app: app, signedInPubkey: signedInPubkey); + return _AddToStackDialogSignedIn(app: app, signedInPubkey: signedInPubkey); } } -class _AddToPackDialogSignedOut extends StatelessWidget { - const _AddToPackDialogSignedOut({required this.app}); +class _AddToStackDialogSignedOut extends StatelessWidget { + const _AddToStackDialogSignedOut({required this.app}); final App app; @@ -203,7 +206,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget { return BaseDialog( titleIcon: const Icon(Icons.apps), title: Text( - 'Add to App Packs', + 'Add to App Stacks', style: Theme.of(context).textTheme.headlineSmall, ), content: BaseDialogContent( @@ -231,7 +234,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget { const SizedBox(width: 10), Expanded( child: Text( - 'Add or remove ${app.name} from public app packs that you share with others.', + 'Add or remove ${app.name} from public app stacks that you share with others.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of( context, @@ -244,7 +247,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget { ), const SizedBox(height: 16), const SignInPrompt( - message: 'Sign in to create and manage your public app packs.', + message: 'Sign in to create and manage your public app stacks.', ), ], ), @@ -263,8 +266,8 @@ class _AddToPackDialogSignedOut extends StatelessWidget { } } -class _AddToPackDialogSignedIn extends HookConsumerWidget { - const _AddToPackDialogSignedIn({ +class _AddToStackDialogSignedIn extends HookConsumerWidget { + const _AddToStackDialogSignedIn({ required this.app, required this.signedInPubkey, }); @@ -274,37 +277,38 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final newPackNameController = useTextEditingController(); + final newStackNameController = useTextEditingController(); - final publicPacksState = ref.watch( - query( + final publicStacksState = ref.watch( + query( authors: {signedInPubkey}, - and: (pack) => {pack.apps}, + and: (stack) => {stack.apps}, source: const LocalAndRemoteSource(relays: 'social', stream: false), andSource: const LocalSource(), - subscriptionPrefix: 'user-packs-dialog', + schemaFilter: appStackEventFilter, + subscriptionPrefix: 'user-stacks-dialog', ), ); - final publicPacks = publicPacksState.models - .where((pack) => pack.identifier != kAppBookmarksIdentifier) + final publicStacks = publicStacksState.models + .where((stack) => stack.identifier != kAppBookmarksIdentifier) .toList(); - // Check which packs contain this app + // Check which stacks contain this app final selectedCollections = useState>({}); - // Track new packs with their names (identifier -> name) - final newPackNames = useState>({}); + // Track new stacks with their names (identifier -> name) + final newStackNames = useState>({}); useEffect(() { final selected = {}; - for (final pack in publicPacks) { - if (pack.apps.toList().any((appModel) => appModel.id == app.id)) { - selected.add(pack.identifier); + for (final stack in publicStacks) { + if (stack.apps.toList().any((appModel) => appModel.id == app.id)) { + selected.add(stack.identifier); } } selectedCollections.value = selected; return null; - }, [app.id, publicPacks.length]); + }, [app.id, publicStacks.length]); // Helper to slugify name to identifier String slugify(String text) { @@ -319,7 +323,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { return BaseDialog( titleIcon: const Icon(Icons.apps), title: Text( - 'Add to App Packs', + 'Add to App Stacks', style: Theme.of(context).textTheme.headlineSmall, ), content: BaseDialogContent( @@ -347,7 +351,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { const SizedBox(width: 10), Expanded( child: Text( - 'Add or remove ${app.name} from public app packs that you share with others.', + 'Add or remove ${app.name} from public app stacks that you share with others.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of( context, @@ -360,18 +364,18 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { ), const SizedBox(height: 16), - // Show existing packs and newly created packs as selectable chips - if (publicPacks.isNotEmpty || newPackNames.value.isNotEmpty) ...[ + // Show existing stacks and newly created stacks as selectable chips + if (publicStacks.isNotEmpty || newStackNames.value.isNotEmpty) ...[ Wrap( spacing: 8, runSpacing: 8, children: [ - // Existing packs - ...publicPacks.map((pack) { + // Existing stacks + ...publicStacks.map((stack) { final isSelected = selectedCollections.value.contains( - pack.identifier, + stack.identifier, ); - final displayName = pack.name ?? pack.identifier; + final displayName = stack.name ?? stack.identifier; return FilterChip( label: Text(displayName), selected: isSelected, @@ -379,12 +383,12 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { if (selected) { selectedCollections.value = { ...selectedCollections.value, - pack.identifier, + stack.identifier, }; } else { selectedCollections.value = { ...selectedCollections.value, - }..remove(pack.identifier); + }..remove(stack.identifier); } }, backgroundColor: isSelected @@ -408,11 +412,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { side: BorderSide.none, ); }), - // Newly created packs not yet in publicPacks - ...newPackNames.value.entries + // Newly created stacks not yet in publicStacks + ...newStackNames.value.entries .where( - (entry) => !publicPacks.any( - (pack) => pack.identifier == entry.key, + (entry) => !publicStacks.any( + (stack) => stack.identifier == entry.key, ), ) .map((entry) { @@ -434,11 +438,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { selectedCollections.value = { ...selectedCollections.value, }..remove(identifier); - // Also remove from newPackNames if deselected + // Also remove from newStackNames if deselected final updatedNames = Map.from( - newPackNames.value, + newStackNames.value, )..remove(identifier); - newPackNames.value = updatedNames; + newStackNames.value = updatedNames; } }, backgroundColor: isSelected @@ -469,12 +473,12 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { const SizedBox(height: 16), ], - // Input for new pack + // Input for new stack TextField( - controller: newPackNameController, + controller: newStackNameController, maxLength: 20, decoration: const InputDecoration( - labelText: 'Add to new app pack', + labelText: 'Add to new app stack', hintText: 'e.g., Favorite Apps', border: OutlineInputBorder(), ), @@ -488,8 +492,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { ...selectedCollections.value, identifier, }; - newPackNames.value = {...newPackNames.value, identifier: name}; - newPackNameController.clear(); + newStackNames.value = { + ...newStackNames.value, + identifier: name, + }; + newStackNameController.clear(); } }, ), @@ -502,20 +509,20 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { ), AsyncButtonBuilder( onPressed: () async { - // Check if there's a pending new pack name in the text field - final pendingPackName = newPackNameController.text.trim(); - if (pendingPackName.isNotEmpty) { - final identifier = slugify(pendingPackName); + // Check if there's a pending new stack name in the text field + final pendingStackName = newStackNameController.text.trim(); + if (pendingStackName.isNotEmpty) { + final identifier = slugify(pendingStackName); if (identifier.isNotEmpty && !selectedCollections.value.contains(identifier)) { - // Add the pending pack to the collections + // Add the pending stack to the collections selectedCollections.value = { ...selectedCollections.value, identifier, }; - newPackNames.value = { - ...newPackNames.value, - identifier: pendingPackName, + newStackNames.value = { + ...newStackNames.value, + identifier: pendingStackName, }; } } @@ -525,9 +532,9 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { context, ref, app, - publicPacks, + publicStacks, selectedCollections.value, - newPackNames.value, + newStackNames.value, ); }, builder: (context, child, callback, buttonState) { @@ -557,9 +564,9 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { BuildContext context, WidgetRef ref, App app, - List existingPacks, + List existingStacks, Set selectedCollectionIds, - Map newPackNames, + Map newStackNames, ) async { try { final signer = ref.read(Signer.activeSignerProvider); @@ -567,7 +574,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { if (context.mounted) { context.showError( 'Sign in required', - description: 'You need to sign in to manage app packs.', + description: 'You need to sign in to manage app stacks.', ); } return; @@ -575,11 +582,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { // Get current state of which collections contain this app final currentCollectionsWithApp = {}; - for (final pack in existingPacks) { + for (final stack in existingStacks) { // Get raw app IDs from event tags - final appIds = pack.event.getTagSetValues('a'); + final appIds = stack.event.getTagSetValues('a'); if (appIds.contains(app.id)) { - currentCollectionsWithApp.add(pack.identifier); + currentCollectionsWithApp.add(stack.identifier); } } @@ -590,79 +597,79 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { // Update or create collections for (final collectionId in selectedCollectionIds) { - final existingPack = existingPacks - .where((pack) => pack.identifier == collectionId) + final existingStack = existingStacks + .where((stack) => stack.identifier == collectionId) .firstOrNull; - // Use the provided name from newPackNames, or existing pack name, or identifier - final packName = - newPackNames[collectionId] ?? existingPack?.name ?? collectionId; + // Use the provided name from newStackNames, or existing stack name, or identifier + final stackName = + newStackNames[collectionId] ?? existingStack?.name ?? collectionId; // Get existing app IDs as a list to preserve order final existingAppIds = - existingPack?.event.getTagSetValues('a').toList() ?? []; + existingStack?.event.getTagSetValues('a').toList() ?? []; - // Check if app already exists in the pack + // Check if app already exists in the stack final appAlreadyExists = existingAppIds.contains(app.id); - // Only update if the app is not already in the pack + // Only update if the app is not already in the stack if (!appAlreadyExists) { - final partialPack = PartialAppPack( - name: packName, + final partialStack = PartialAppStack( + name: stackName, identifier: collectionId, ); // Add existing apps in order for (final appId in existingAppIds) { - partialPack.addApp(appId); + partialStack.addApp(appId); } // Add this app at the end - partialPack.addApp(app.id); + partialStack.addApp(app.id); - final signedPack = await partialPack.signWith(signer); - await ref.storage.save({signedPack}); + final signedStack = await partialStack.signWith(signer); + await ref.storage.save({signedStack}); await ref.storage.publish({ - signedPack, + signedStack, }, source: RemoteSource(relays: 'social')); } } // Remove from deselected collections for (final collectionId in collectionsToRemoveFrom) { - final existingPack = existingPacks - .where((pack) => pack.identifier == collectionId) + final existingStack = existingStacks + .where((stack) => stack.identifier == collectionId) .firstOrNull; - if (existingPack != null) { - final partialPack = PartialAppPack( - name: existingPack.name ?? collectionId, + if (existingStack != null) { + final partialStack = PartialAppStack( + name: existingStack.name ?? collectionId, identifier: collectionId, ); // Re-add all apps except the one we're removing, preserving order // Get raw app IDs from event tags as a list to preserve order - final existingAppIds = existingPack.event + final existingAppIds = existingStack.event .getTagSetValues('a') .toList(); for (final appId in existingAppIds) { if (appId != app.id) { - partialPack.addApp(appId); + partialStack.addApp(appId); } } - final signedPack = await partialPack.signWith(signer); - await ref.storage.save({signedPack}); + final signedStack = await partialStack.signWith(signer); + await ref.storage.save({signedStack}); await ref.storage.publish({ - signedPack, + signedStack, }, source: RemoteSource(relays: 'social')); } } if (context.mounted) { if (selectedCollectionIds.isEmpty) { - context.showInfo('Removed from all app packs'); + context.showInfo('Removed from all app stacks'); } else { - context.showInfo('App packs updated'); + context.showInfo('App stacks updated'); } Navigator.pop(context); } diff --git a/lib/widgets/comments_section.dart b/lib/widgets/comments_section.dart index d20fac4..01edc93 100644 --- a/lib/widgets/comments_section.dart +++ b/lib/widgets/comments_section.dart @@ -52,11 +52,11 @@ class CommentsSection extends HookConsumerWidget { } } -/// Comments section for AppPack/Stack detail screen +/// Comments section for AppStack/Stack detail screen class StackCommentsSection extends HookConsumerWidget { const StackCommentsSection({super.key, required this.stack}); - final AppPack stack; + final AppStack stack; @override Widget build(BuildContext context, WidgetRef ref) { @@ -111,12 +111,7 @@ class _CommentsSectionLayout extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Comments', - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), + Text('Comments', style: Theme.of(context).textTheme.titleLarge), if (comments.isNotEmpty) _CommentCountBadge(count: comments.length), ], @@ -251,7 +246,6 @@ class _CommentCard extends HookConsumerWidget { Text( author?.nameOrNpub ?? '', style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, fontSize: (context .textTheme @@ -338,7 +332,7 @@ class _AddCommentButton extends ConsumerWidget { class _AddStackCommentButton extends ConsumerWidget { const _AddStackCommentButton({required this.stack}); - final AppPack stack; + final AppStack stack; @override Widget build(BuildContext context, WidgetRef ref) { @@ -401,9 +395,7 @@ class _CommentComposer extends HookConsumerWidget { children: [ Text( 'Comment on $versionToComment', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), IconButton( onPressed: () => Navigator.pop(context), @@ -532,7 +524,7 @@ class _CommentComposer extends HookConsumerWidget { class _StackCommentComposer extends HookConsumerWidget { const _StackCommentComposer({required this.stack}); - final AppPack stack; + final AppStack stack; @override Widget build(BuildContext context, WidgetRef ref) { @@ -557,9 +549,7 @@ class _StackCommentComposer extends HookConsumerWidget { children: [ Text( 'Comment on $stackName', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), IconButton( onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/nwc_widgets.dart b/lib/widgets/nwc_widgets.dart index 61ac18f..bc1ba0d 100644 --- a/lib/widgets/nwc_widgets.dart +++ b/lib/widgets/nwc_widgets.dart @@ -31,9 +31,7 @@ class NWCConnectionCard extends HookConsumerWidget { const SizedBox(width: 8), Text( 'Lightning Wallet', - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ), ], ), diff --git a/lib/widgets/profile_header.dart b/lib/widgets/profile_header.dart index 6bbebcc..6464fcc 100644 --- a/lib/widgets/profile_header.dart +++ b/lib/widgets/profile_header.dart @@ -38,9 +38,7 @@ class ProfileHeader extends StatelessWidget { // Display name Text( profile!.name ?? profile!.nameOrNpub, - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), diff --git a/lib/widgets/zap_widgets.dart b/lib/widgets/zap_widgets.dart index 6d285ea..29d9a26 100644 --- a/lib/widgets/zap_widgets.dart +++ b/lib/widgets/zap_widgets.dart @@ -117,7 +117,6 @@ class ZappersHorizontalList extends StatelessWidget { '⚡️${formatSatsCompact(totalSats)}', style: theme.textTheme.titleLarge?.copyWith( color: Colors.white, - fontWeight: FontWeight.bold, ), ), const SizedBox(width: 6),