diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index d468ebd..8c8376d 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -9,15 +9,15 @@ const kZapstorePubkey = const kFranzapPubkey = '726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11'; -/// Identifier for storing user bookmarks +/// Identifier for storing user saved apps const kAppBookmarksIdentifier = 'zapstore-bookmarks'; -/// Event filter for app packs - excludes bookmarks and packs with zero App references +/// Event filter for app packs - excludes saved apps and packs with zero App references bool appPackEventFilter(Map event) { final tags = event['tags'] as List?; if (tags == null) return false; - // Check for bookmarks identifier in 'd' tag + // Check for saved apps identifier in 'd' tag for (final tag in tags) { if (tag is List && tag.isNotEmpty && tag[0] == 'd') { if (tag.length > 1 && tag[1] == kAppBookmarksIdentifier) { diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 2a27227..86edf93 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -1,9 +1,14 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:zapstore/services/bookmarks_service.dart'; +import 'package:zapstore/services/notification_service.dart'; import 'package:zapstore/utils/debug_utils.dart'; import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/services/package_manager/package_manager.dart'; @@ -11,7 +16,9 @@ import 'package:zapstore/widgets/app_detail_widgets.dart'; import 'package:zapstore/widgets/app_header.dart'; import 'package:zapstore/widgets/app_info_table.dart'; import 'package:zapstore/widgets/author_container.dart'; +import 'package:zapstore/widgets/bookmark_widgets.dart'; import 'package:zapstore/widgets/comments_section.dart'; +import 'package:zapstore/widgets/common/base_dialog.dart'; import 'package:zapstore/widgets/download_text_container.dart'; import 'package:zapstore/widgets/expandable_markdown.dart'; import 'package:zapstore/widgets/install_button.dart'; @@ -128,6 +135,13 @@ class _AppDetailContent extends HookConsumerWidget { final latestRelease = app.latestRelease.value; final latestMetadata = app.latestFileMetadata; + // Check if app is installed for menu options + final installedPackage = ref + .watch(packageManagerProvider) + .where((p) => p.appId == app.identifier) + .firstOrNull; + final isInstalled = installedPackage != null; + // Show skeleton while relationships are loading if (latestRelease == null || latestMetadata == null) { return Scaffold( @@ -142,6 +156,7 @@ class _AppDetailContent extends HookConsumerWidget { ), ), InstallButton(app: app, release: latestRelease), + _buildFloatingMenu(context, ref, app, isInstalled), ], ), ), @@ -304,9 +319,252 @@ class _AppDetailContent extends HookConsumerWidget { // Sticky install button InstallButton(app: app, release: latestRelease), + + // Floating three-dot menu + _buildFloatingMenu(context, ref, app, isInstalled), ], ), ), ); } + + Widget _buildFloatingMenu( + BuildContext context, + WidgetRef ref, + App app, + bool isInstalled, + ) { + return Positioned( + top: 8, + right: 8, + child: _buildOverflowMenu(context, ref, app, isInstalled), + ); + } + + Widget _buildOverflowMenu( + BuildContext context, + WidgetRef ref, + App app, + bool isInstalled, + ) { + // Watch saved apps to check if app is saved + final savedAppsAsync = ref.watch(bookmarksProvider); + final savedAppIds = savedAppsAsync.when( + data: (ids) => ids, + loading: () => {}, + error: (_, __) => {}, + ); + final appAddressableId = + '${app.event.kind}:${app.pubkey}:${app.identifier}'; + final isSaved = savedAppIds.contains(appAddressableId); + + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + onSelected: (value) { + switch (value) { + case 'share': + _shareApp(context, app); + break; + case 'copy_link': + _copyLink(context, app); + break; + case 'save_app': + _toggleSaveApp(context, ref, app, isSaved); + break; + case 'view_publisher': + _viewPublisher(context, app); + break; + case 'open_browser': + _openInBrowser(context, app); + break; + case 'open': + _openApp(context, ref, app); + break; + case 'delete': + _uninstallApp(context, ref, app); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'share', + child: Row( + children: [Icon(Icons.share), SizedBox(width: 12), Text('Share')], + ), + ), + const PopupMenuItem( + value: 'copy_link', + child: Row( + children: [ + Icon(Icons.link), + SizedBox(width: 12), + Text('Copy link'), + ], + ), + ), + PopupMenuItem( + value: 'save_app', + child: Row( + children: [ + Icon(isSaved ? Icons.bookmark : Icons.bookmark_border), + const SizedBox(width: 12), + Text(isSaved ? 'Remove from saved' : 'Save app'), + ], + ), + ), + const PopupMenuItem( + value: 'view_publisher', + child: Row( + children: [ + Icon(Icons.person), + SizedBox(width: 12), + Text('View publisher'), + ], + ), + ), + const PopupMenuItem( + value: 'open_browser', + child: Row( + children: [ + Icon(Icons.open_in_browser), + SizedBox(width: 12), + Text('Open in browser'), + ], + ), + ), + if (isInstalled) ...[ + const PopupMenuItem( + value: 'open', + child: Row( + children: [ + Icon(Icons.open_in_new), + SizedBox(width: 12), + Text('Open'), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete_outline), + SizedBox(width: 12), + Text('Delete'), + ], + ), + ), + ], + ], + ); + } + + String _getAppUrl(App app) { + final naddr = Utils.encodeShareableIdentifier( + AddressInput( + identifier: app.identifier, + author: app.pubkey, + kind: app.event.kind, + relays: [], + ), + ); + return 'https://zapstore.dev/apps/$naddr'; + } + + void _shareApp(BuildContext context, App app) { + try { + final shareUrl = _getAppUrl(app); + SharePlus.instance.share(ShareParams(text: shareUrl)); + } catch (e) { + if (context.mounted) { + context.showError('Failed to share app', description: '$e'); + } + } + } + + void _copyLink(BuildContext context, App app) { + try { + final shareUrl = _getAppUrl(app); + Clipboard.setData(ClipboardData(text: shareUrl)); + context.showInfo('Link copied to clipboard'); + } catch (e) { + if (context.mounted) { + context.showError('Failed to copy link', description: '$e'); + } + } + } + + Future _toggleSaveApp( + BuildContext context, + WidgetRef ref, + App app, + bool isCurrentlySaved, + ) async { + await showBaseDialog( + context: context, + dialog: SaveAppDialog(app: app, isPrivatelySaved: isCurrentlySaved), + ); + } + + void _viewPublisher(BuildContext context, App app) { + final segments = GoRouterState.of(context).uri.pathSegments; + final first = segments.isNotEmpty ? segments.first : 'search'; + context.push('/$first/user/${app.pubkey}'); + } + + Future _openInBrowser(BuildContext context, App app) async { + try { + final url = _getAppUrl(app); + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + context.showError('Could not open browser'); + } + } + } catch (e) { + if (context.mounted) { + context.showError('Failed to open browser', description: '$e'); + } + } + } + + Future _openApp(BuildContext context, WidgetRef ref, App app) async { + try { + final packageManager = ref.read(packageManagerProvider.notifier); + context.showInfo('Launching ${app.name ?? app.identifier}...'); + await packageManager.launchApp(app.identifier); + } catch (e) { + if (!context.mounted) return; + context.showError( + 'Failed to launch ${app.name ?? app.identifier}', + description: + 'The app may have been uninstalled or moved. Try reinstalling.\n\n$e', + ); + } + } + + Future _uninstallApp( + BuildContext context, + WidgetRef ref, + App app, + ) async { + try { + final packageManager = ref.read(packageManagerProvider.notifier); + await packageManager.uninstall(app.identifier); + // Only reaches here after successful uninstall + if (context.mounted) { + context.showInfo('${app.name ?? app.identifier} has been uninstalled'); + } + } catch (e) { + if (context.mounted) { + // Don't show error for user cancellation + final message = e.toString(); + if (!message.contains('cancelled')) { + context.showError('Uninstall failed', description: '$e'); + } + } + } + } } diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index e6156b0..57a71ef 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -20,8 +20,8 @@ class UpdatesScreen extends HookConsumerWidget { final updatesCount = categorized.automaticUpdates.length + categorized.manualUpdates.length; final upToDateCount = categorized.upToDateApps.length; - final bookmarksAsync = ref.watch(bookmarksProvider); - final bookmarksCount = bookmarksAsync.maybeWhen( + final savedAppsAsync = ref.watch(bookmarksProvider); + final savedAppsCount = savedAppsAsync.maybeWhen( data: (ids) => ids.length, orElse: () => 0, ); @@ -60,8 +60,8 @@ class UpdatesScreen extends HookConsumerWidget { ), Tab( child: _TabLabelWithBadge( - label: 'Bookmarks', - count: bookmarksCount, + label: 'Saved', + count: savedAppsCount, textStyle: tabLabelStyle, badgeColor: Colors.blue.shade700.withValues(alpha: 0.4), ), @@ -72,7 +72,7 @@ class UpdatesScreen extends HookConsumerWidget { body: const Padding( padding: EdgeInsets.only(top: 16), child: TabBarView( - children: [_UpdatesTab(), _UpToDateTab(), _BookmarksTab()], + children: [_UpdatesTab(), _UpToDateTab(), _SavedAppsTab()], ), ), ), @@ -539,8 +539,8 @@ class _UpToDateTab extends HookConsumerWidget { } } -class _BookmarksTab extends HookConsumerWidget { - const _BookmarksTab(); +class _SavedAppsTab extends HookConsumerWidget { + const _SavedAppsTab(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -551,7 +551,7 @@ class _BookmarksTab extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(24), child: Text( - 'Sign in to view bookmarks', + 'Sign in to view saved apps', style: context.textTheme.bodyMedium?.copyWith( color: Theme.of( context, @@ -562,9 +562,9 @@ class _BookmarksTab extends HookConsumerWidget { ); } - final bookmarksAsync = ref.watch(bookmarksProvider); + final savedAppsAsync = ref.watch(bookmarksProvider); - return bookmarksAsync.when( + return savedAppsAsync.when( loading: () => Center( child: CircularProgressIndicator( color: Theme.of(context).colorScheme.primary, @@ -572,7 +572,7 @@ class _BookmarksTab extends HookConsumerWidget { ), error: (_, __) => Center( child: Text( - 'Error loading bookmarks', + 'Error loading saved apps', style: context.textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.error, ), @@ -586,48 +586,48 @@ class _BookmarksTab extends HookConsumerWidget { }) .whereType() .toSet(); - return _BookmarkedAppsSection(identifiers: identifiers); + return _SavedAppsSection(identifiers: identifiers); }, ); } } -class _BookmarkedAppsSection extends StatelessWidget { - const _BookmarkedAppsSection({required this.identifiers}); +class _SavedAppsSection extends StatelessWidget { + const _SavedAppsSection({required this.identifiers}); final Set identifiers; @override Widget build(BuildContext context) { - if (identifiers.isEmpty) return const _NoBookmarkedAppsEmptyState(); - return _BookmarkedAppsSectionWithIds(identifiers: identifiers); + if (identifiers.isEmpty) return const _NoSavedAppsEmptyState(); + return _SavedAppsSectionWithIds(identifiers: identifiers); } } -class _BookmarkedAppsSectionWithIds extends ConsumerWidget { - const _BookmarkedAppsSectionWithIds({required this.identifiers}); +class _SavedAppsSectionWithIds extends ConsumerWidget { + const _SavedAppsSectionWithIds({required this.identifiers}); final Set identifiers; @override Widget build(BuildContext context, WidgetRef ref) { - final bookmarkedAppsState = ref.watch( + final savedAppsState = ref.watch( query( tags: {'#d': identifiers}, and: (app) => {app.latestRelease}, source: const LocalSource(), - subscriptionPrefix: 'bookmark-apps', + subscriptionPrefix: 'saved-apps', ), ); - final savedApps = bookmarkedAppsState.models.toList() + final savedApps = savedAppsState.models.toList() ..sort( (a, b) => (a.name ?? a.identifier).toLowerCase().compareTo( (b.name ?? b.identifier).toLowerCase(), ), ); - if (savedApps.isEmpty) return const _NoBookmarkedAppsEmptyState(); + if (savedApps.isEmpty) return const _NoSavedAppsEmptyState(); return ListView.builder( padding: EdgeInsets.zero, @@ -643,8 +643,8 @@ class _BookmarkedAppsSectionWithIds extends ConsumerWidget { } } -class _NoBookmarkedAppsEmptyState extends StatelessWidget { - const _NoBookmarkedAppsEmptyState(); +class _NoSavedAppsEmptyState extends StatelessWidget { + const _NoSavedAppsEmptyState(); @override Widget build(BuildContext context) { @@ -652,7 +652,7 @@ class _NoBookmarkedAppsEmptyState extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(24), child: Text( - 'No bookmarked apps yet', + 'No saved apps yet', style: context.textTheme.bodyMedium?.copyWith( color: Theme.of( context, diff --git a/lib/services/bookmarks_service.dart b/lib/services/bookmarks_service.dart index 4e31941..5f0aec3 100644 --- a/lib/services/bookmarks_service.dart +++ b/lib/services/bookmarks_service.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; import 'package:zapstore/utils/extensions.dart'; -/// Provider that watches the user's bookmark pack and provides decrypted bookmark state +/// Provider that watches the user's saved apps pack and provides decrypted saved apps state /// Fetches from remote once on boot, then uses local storage only final bookmarksProvider = FutureProvider>((ref) async { final signer = ref.watch(Signer.activeSignerProvider); @@ -13,7 +13,7 @@ final bookmarksProvider = FutureProvider>((ref) async { return {}; } - // Query bookmark pack - stream enabled to auto-update when bookmarks change locally + // Query saved apps pack - 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( @@ -25,7 +25,7 @@ final bookmarksProvider = FutureProvider>((ref) async { relays: 'social', stream: false, ), - subscriptionPrefix: 'user-bookmarks', + subscriptionPrefix: 'user-saved-apps', ), ); @@ -58,27 +58,27 @@ final bookmarksProvider = FutureProvider>((ref) async { } }); -/// Extension to check if an app is bookmarked -extension BookmarkChecker on WidgetRef { - /// Check if the given app is bookmarked - bool isAppBookmarked(App app) { - final bookmarksState = watch(bookmarksProvider); +/// Extension to check if an app is saved +extension SavedAppsChecker on WidgetRef { + /// Check if the given app is saved + bool isAppSaved(App app) { + final savedAppsState = watch(bookmarksProvider); final appAddressableId = '${app.event.kind}:${app.pubkey}:${app.identifier}'; - return bookmarksState.when( - data: (bookmarks) => bookmarks.contains(appAddressableId), + return savedAppsState.when( + data: (savedApps) => savedApps.contains(appAddressableId), loading: () => false, error: (_, __) => false, ); } - /// Get all bookmarked app addressable IDs - Set getBookmarkedIds() { - final bookmarksState = watch(bookmarksProvider); + /// Get all saved app addressable IDs + Set getSavedAppIds() { + final savedAppsState = watch(bookmarksProvider); - return bookmarksState.when( - data: (bookmarks) => bookmarks, + return savedAppsState.when( + data: (savedApps) => savedApps, loading: () => {}, error: (_, __) => {}, ); diff --git a/lib/widgets/app_detail_widgets.dart b/lib/widgets/app_detail_widgets.dart index 1812c79..9836717 100644 --- a/lib/widgets/app_detail_widgets.dart +++ b/lib/widgets/app_detail_widgets.dart @@ -1,10 +1,11 @@ +import 'dart:convert'; + import 'package:async_button_builder/async_button_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:zapstore/services/bookmarks_service.dart'; @@ -70,46 +71,53 @@ class SocialActionsRow extends HookConsumerWidget { final signedInPubkey = ref.watch(Signer.activePubkeyProvider); final isSignedIn = signedInPubkey != null; - // Watch bookmarks from centralized provider (handles decryption) - final bookmarksAsync = ref.watch(bookmarksProvider); - final bookmarkedIds = bookmarksAsync.when( + // Watch saved apps from centralized provider (handles decryption) + final savedAppsAsync = ref.watch(bookmarksProvider); + final savedAppIds = savedAppsAsync.when( data: (ids) => ids, loading: () => {}, error: (_, __) => {}, ); - // Check if this app is bookmarked + // Check if this app is saved final appAddressableId = '${app.event.kind}:${app.pubkey}:${app.identifier}'; - final isPrivatelySaved = bookmarkedIds.contains(appAddressableId); + final isPrivatelySaved = savedAppIds.contains(appAddressableId); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Zap + Bookmark + App Pack + Share button row (50% / 16% / 16% / 16% split) + // Zap + Save App + App Pack button row SizedBox( height: 36, child: Row( children: [ - // Zap button (50%) + // Zap button Expanded( - flex: 50, + flex: 68, child: ZapButton(app: app, author: author), ), const SizedBox(width: 8), - // Bookmark button (16%) + // Save App button Expanded( flex: 16, child: FilledButton( - onPressed: () => - _showBookmarkDialog(context, ref, app, isPrivatelySaved), + onPressed: () => _handleSaveApp( + context, + ref, + app, + isPrivatelySaved, + isSignedIn, + ), style: FilledButton.styleFrom( padding: EdgeInsets.zero, backgroundColor: isPrivatelySaved && isSignedIn ? (Theme.of(context).brightness == Brightness.dark - ? const Color(0xFF4A6BA0) // Deep blue - : const Color(0xFF1E4D8B)) // Darker blue - : Theme.of(context).colorScheme.surfaceContainerHighest, + ? const Color(0xFF9B4F5E) // Red wine/burgundy + : const Color(0xFF7D3C4D)) // Deep burgundy + : (Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF3A3A3F) // Dark neutral + : const Color(0xFFE8E3E8)), // Light neutral foregroundColor: isPrivatelySaved && isSignedIn ? Colors.white : Theme.of( @@ -128,7 +136,7 @@ class SocialActionsRow extends HookConsumerWidget { ), ), const SizedBox(width: 8), - // App Pack button (16%) + // App Pack button Expanded( flex: 16, child: FilledButton( @@ -137,8 +145,8 @@ class SocialActionsRow extends HookConsumerWidget { padding: EdgeInsets.zero, backgroundColor: Theme.of(context).brightness == Brightness.dark - ? const Color(0xFF3A6FCC) // Theme dark primary - : const Color(0xFF2563A8), // Muted blue + ? const Color(0xFF4A7BA7) // Soft steel blue + : const Color(0xFF5B8FB9), // Light ocean blue foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -147,26 +155,6 @@ class SocialActionsRow extends HookConsumerWidget { child: const Icon(Icons.apps, size: 20), ), ), - const SizedBox(width: 8), - // Share button (16%) - Expanded( - flex: 16, - child: FilledButton( - onPressed: () => _shareApp(context, app), - style: FilledButton.styleFrom( - padding: EdgeInsets.zero, - backgroundColor: - Theme.of(context).brightness == Brightness.dark - ? const Color(0xFF2D5A8F) // Navy blue - : const Color(0xFF1A4673), // Darker navy - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Icon(Icons.share, size: 20), - ), - ), ], ), ), @@ -177,16 +165,92 @@ class SocialActionsRow extends HookConsumerWidget { ); } - Future _showBookmarkDialog( + Future _handleSaveApp( BuildContext context, WidgetRef ref, App app, bool isPrivatelySaved, + bool isSignedIn, ) async { - await showBaseDialog( - context: context, - dialog: BookmarkDialog(app: app, isPrivatelySaved: isPrivatelySaved), - ); + // If not signed in, show dialog to prompt sign in + if (!isSignedIn) { + await showBaseDialog( + context: context, + dialog: SaveAppDialog(app: app, isPrivatelySaved: isPrivatelySaved), + ); + return; + } + + // If signed in, save directly without dialog + try { + final signer = ref.read(Signer.activeSignerProvider); + final signedInPubkey = ref.read(Signer.activePubkeyProvider); + + if (signer == null || signedInPubkey == null) return; + + // Query for existing pack + final existingPackState = await ref.storage.query( + RequestFilter( + authors: {signedInPubkey}, + tags: { + '#d': {kAppBookmarksIdentifier}, + }, + ).toRequest(), + source: const LocalSource(), + ); + final existingPack = existingPackState.firstOrNull; + + // Get existing app IDs by decrypting if pack exists + List existingAppIds = []; + if (existingPack != null) { + try { + final decryptedContent = await signer.nip44Decrypt( + existingPack.content, + signedInPubkey, + ); + existingAppIds = (jsonDecode(decryptedContent) as List) + .cast(); + } catch (_) { + // Silently start fresh if decryption fails + } + } + + // Modify the list + final appAddressableId = + '${app.event.kind}:${app.pubkey}:${app.identifier}'; + + if (isPrivatelySaved) { + existingAppIds.remove(appAddressableId); + } else { + if (!existingAppIds.contains(appAddressableId)) { + existingAppIds.add(appAddressableId); + } + } + + // Create new partial pack with updated list + final partialPack = PartialAppPack.withEncryptedApps( + name: 'Saved Apps', + identifier: kAppBookmarksIdentifier, + apps: existingAppIds, + ); + + // Sign (encrypts the content) + final signedPack = await partialPack.signWith(signer); + + // Save to local storage and publish to relays + await ref.storage.save({signedPack}); + ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social')); + + if (context.mounted) { + context.showInfo( + isPrivatelySaved ? 'App removed from saved' : 'App saved privately', + ); + } + } catch (e) { + if (context.mounted) { + context.showError('Failed to save app', description: '$e'); + } + } } Future _showAddToPackDialog(BuildContext context, App app) async { @@ -195,28 +259,6 @@ class SocialActionsRow extends HookConsumerWidget { dialog: AddToPackDialog(app: app), ); } - - void _shareApp(BuildContext context, App app) { - try { - // Generate naddr for the app - final naddr = Utils.encodeShareableIdentifier( - AddressInput( - identifier: app.identifier, - author: app.pubkey, - kind: app.event.kind, - relays: [], - ), - ); - final shareUrl = 'https://zapstore.dev/apps/$naddr'; - - // Share using Android's share sheet - SharePlus.instance.share(ShareParams(text: shareUrl)); - } catch (e) { - if (context.mounted) { - context.showError('Failed to share app', description: '$e'); - } - } - } } class _ZappersListSection extends StatelessWidget { diff --git a/lib/widgets/bookmark_widgets.dart b/lib/widgets/bookmark_widgets.dart index 8cd75a4..b44d234 100644 --- a/lib/widgets/bookmark_widgets.dart +++ b/lib/widgets/bookmark_widgets.dart @@ -10,9 +10,9 @@ import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/widgets/auth_widgets.dart'; import 'package:zapstore/widgets/common/base_dialog.dart'; -/// Dialog for bookmarking an app privately (encrypted) -class BookmarkDialog extends HookConsumerWidget { - const BookmarkDialog({ +/// Dialog for saving an app privately (encrypted) +class SaveAppDialog extends HookConsumerWidget { + const SaveAppDialog({ super.key, required this.app, required this.isPrivatelySaved, @@ -28,7 +28,7 @@ class BookmarkDialog extends HookConsumerWidget { return BaseDialog( titleIcon: const Icon(Icons.bookmark), title: Text( - 'Private bookmark', + 'Save App Privately', style: Theme.of(context).textTheme.headlineSmall, ), content: BaseDialogContent( @@ -44,7 +44,7 @@ class BookmarkDialog extends HookConsumerWidget { ), if (!isSignedIn) ...[ const SizedBox(height: 16), - const SignInPrompt(message: 'Sign in to bookmark apps privately.'), + const SignInPrompt(message: 'Sign in to save apps privately.'), ], ], ), @@ -75,7 +75,7 @@ class BookmarkDialog extends HookConsumerWidget { size: 18, ), ), - label: Text(isPrivatelySaved ? 'Remove bookmark' : 'Bookmark'), + label: Text(isPrivatelySaved ? 'Remove' : 'Save'), ); }, child: const SizedBox.shrink(), @@ -98,7 +98,7 @@ class BookmarkDialog extends HookConsumerWidget { if (context.mounted) { context.showError( 'Sign in required', - description: 'You need to sign in to save apps to your bookmarks.', + description: 'You need to sign in to save apps privately.', ); } return; @@ -129,9 +129,9 @@ class BookmarkDialog extends HookConsumerWidget { } catch (e) { if (context.mounted) { context.showError( - 'Could not read existing bookmarks', + 'Could not read existing saved apps', description: - 'Your previous bookmarks could not be decrypted. Starting fresh.\n\n$e', + 'Your previous saved apps could not be decrypted. Starting fresh.\n\n$e', ); } } @@ -151,7 +151,7 @@ class BookmarkDialog extends HookConsumerWidget { // Create new partial pack with updated list final partialPack = PartialAppPack.withEncryptedApps( - name: 'Bookmarks', + name: 'Saved Apps', identifier: kAppBookmarksIdentifier, apps: existingAppIds, ); @@ -165,7 +165,9 @@ class BookmarkDialog extends HookConsumerWidget { if (context.mounted) { Navigator.pop(context); - context.showInfo(isCurrentlySaved ? 'Bookmark removed' : 'Bookmarked'); + context.showInfo( + isCurrentlySaved ? 'App removed from saved' : 'App saved privately', + ); } } catch (e) { if (context.mounted) { @@ -201,31 +203,44 @@ class _AddToPackDialogSignedOut extends StatelessWidget { return BaseDialog( titleIcon: const Icon(Icons.apps), title: Text( - 'Manage App Packs', + 'Add to App Packs', style: Theme.of(context).textTheme.headlineSmall, ), content: BaseDialogContent( children: [ const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.public, - size: 16, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Add or remove ${app.name} from public app packs', - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon( + Icons.info_outline, + size: 18, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), ), - ), - ], + const SizedBox(width: 10), + Expanded( + child: Text( + 'Add or remove ${app.name} from public app packs that you share with others.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + ], + ), ), const SizedBox(height: 16), const SignInPrompt( @@ -304,31 +319,44 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget { return BaseDialog( titleIcon: const Icon(Icons.apps), title: Text( - 'Manage App Packs', + 'Add to App Packs', style: Theme.of(context).textTheme.headlineSmall, ), content: BaseDialogContent( children: [ const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.public, - size: 16, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Add or remove ${app.name} from public app packs', - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon( + Icons.info_outline, + size: 18, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), ), - ), - ], + const SizedBox(width: 10), + Expanded( + child: Text( + 'Add or remove ${app.name} from public app packs that you share with others.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + ], + ), ), const SizedBox(height: 16),