Use floating menu and update saved/app packs UI

This commit is contained in:
franzap
2025-12-22 20:43:17 -03:00
parent bf30c7f167
commit 22ee79baa8
6 changed files with 485 additions and 157 deletions
+3 -3
View File
@@ -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<String, dynamic> event) {
final tags = event['tags'] as List<dynamic>?;
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) {
+258
View File
@@ -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: () => <String>{},
error: (_, __) => <String>{},
);
final appAddressableId =
'${app.event.kind}:${app.pubkey}:${app.identifier}';
final isSaved = savedAppIds.contains(appAddressableId);
return PopupMenuButton<String>(
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<String>(
value: 'share',
child: Row(
children: [Icon(Icons.share), SizedBox(width: 12), Text('Share')],
),
),
const PopupMenuItem<String>(
value: 'copy_link',
child: Row(
children: [
Icon(Icons.link),
SizedBox(width: 12),
Text('Copy link'),
],
),
),
PopupMenuItem<String>(
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<String>(
value: 'view_publisher',
child: Row(
children: [
Icon(Icons.person),
SizedBox(width: 12),
Text('View publisher'),
],
),
),
const PopupMenuItem<String>(
value: 'open_browser',
child: Row(
children: [
Icon(Icons.open_in_browser),
SizedBox(width: 12),
Text('Open in browser'),
],
),
),
if (isInstalled) ...[
const PopupMenuItem<String>(
value: 'open',
child: Row(
children: [
Icon(Icons.open_in_new),
SizedBox(width: 12),
Text('Open'),
],
),
),
const PopupMenuItem<String>(
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<void> _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<void> _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<void> _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<void> _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');
}
}
}
}
}
+25 -25
View File
@@ -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<String>()
.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<String> 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<String> identifiers;
@override
Widget build(BuildContext context, WidgetRef ref) {
final bookmarkedAppsState = ref.watch(
final savedAppsState = ref.watch(
query<App>(
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,
+15 -15
View File
@@ -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<Set<String>>((ref) async {
final signer = ref.watch(Signer.activeSignerProvider);
@@ -13,7 +13,7 @@ final bookmarksProvider = FutureProvider<Set<String>>((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<AppPack>(
@@ -25,7 +25,7 @@ final bookmarksProvider = FutureProvider<Set<String>>((ref) async {
relays: 'social',
stream: false,
),
subscriptionPrefix: 'user-bookmarks',
subscriptionPrefix: 'user-saved-apps',
),
);
@@ -58,27 +58,27 @@ final bookmarksProvider = FutureProvider<Set<String>>((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<String> getBookmarkedIds() {
final bookmarksState = watch(bookmarksProvider);
/// Get all saved app addressable IDs
Set<String> getSavedAppIds() {
final savedAppsState = watch(bookmarksProvider);
return bookmarksState.when(
data: (bookmarks) => bookmarks,
return savedAppsState.when(
data: (savedApps) => savedApps,
loading: () => {},
error: (_, __) => {},
);
+107 -65
View File
@@ -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: () => <String>{},
error: (_, __) => <String>{},
);
// 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<void> _showBookmarkDialog(
Future<void> _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<AppPack>(
authors: {signedInPubkey},
tags: {
'#d': {kAppBookmarksIdentifier},
},
).toRequest(),
source: const LocalSource(),
);
final existingPack = existingPackState.firstOrNull;
// Get existing app IDs by decrypting if pack exists
List<String> existingAppIds = [];
if (existingPack != null) {
try {
final decryptedContent = await signer.nip44Decrypt(
existingPack.content,
signedInPubkey,
);
existingAppIds = (jsonDecode(decryptedContent) as List)
.cast<String>();
} 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<void> _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 {
+77 -49
View File
@@ -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),