Centralize navigation logic

This commit is contained in:
franzap
2026-04-08 15:59:01 -03:00
parent ffdff068c8
commit 2e10edf850
14 changed files with 229 additions and 194 deletions
+2 -6
View File
@@ -1,11 +1,11 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
import 'package:zapstore/widgets/app_card.dart';
import 'package:zapstore/widgets/author_container.dart';
import 'package:zapstore/widgets/comments_section.dart';
@@ -280,11 +280,7 @@ class _StackHeader extends StatelessWidget {
oneLine: true,
size: 14,
isLoading: isAuthorLoading,
onTap: () {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
context.push('/$first/user/${stack.pubkey}');
},
onTap: () => pushUser(context, stack.pubkey),
),
const SizedBox(height: 4),
// Last updated timestamp
+2 -5
View File
@@ -17,6 +17,7 @@ import 'package:zapstore/main.dart';
import 'package:zapstore/services/bookmarks_service.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
import 'package:zapstore/widgets/common/profile_identity_row.dart';
import 'package:zapstore/widgets/app_card.dart';
import 'package:zapstore/theme.dart';
@@ -285,11 +286,7 @@ class _StackMigrationWarning extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: InkWell(
onTap: () {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
context.push('/$first/stacks');
},
onTap: () => pushStacks(context),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+6 -19
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/nostr_route.dart';
import '../widgets/app_stack_container.dart';
import '../widgets/latest_releases_container.dart';
@@ -30,24 +30,11 @@ class SearchScreen extends HookConsumerWidget {
final performSearch = useCallback((String query) {
final trimmed = query.trim();
final cleaned = trimmed
.replaceFirst('nostr:', '')
.replaceFirst(RegExp(r'https?://zapstore\.dev/(?:apps|stacks)/'), '');
try {
final decoded = Utils.decodeShareableIdentifier(cleaned);
final path = switch (decoded) {
AddressData(:final kind) =>
'/search/${kind == 30267 ? 'stack' : 'app'}/$cleaned',
ProfileData(:final pubkey) => '/search/user/$pubkey',
_ => null,
};
if (path != null) {
searchController.clear();
searchQuery.value = '';
context.push(path);
return;
}
} catch (_) {}
if (navigateToContent(context, trimmed, fallbackLaunch: false)) {
searchController.clear();
searchQuery.value = '';
return;
}
if (trimmed.length < 3) {
searchFocusNode.requestFocus();
return;
+7 -21
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
import '../theme.dart';
import '../widgets/common/note_parser.dart';
import '../widgets/common/profile_identity_row.dart';
@@ -273,20 +273,12 @@ class _StackLinkCard extends StatelessWidget {
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');
},
onTap: () => pushStack(
context,
stack.identifier,
author: pubkey,
kind: stack.event.kind,
),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
@@ -370,12 +362,6 @@ class _UserBio extends HookWidget {
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
onProfileTap: (pubkey) {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
context.push('/$first/user/$pubkey');
},
onHashtagTap: (hashtag) => context.push('/search?q=$hashtag'),
),
);
+14 -9
View File
@@ -4,19 +4,24 @@ import 'package:flutter/foundation.dart';
/// the URI is not a recognized deep link.
///
/// Handles:
/// - `https://zapstore.dev/apps/{id}` (full URI from app_links)
/// - `/apps/{id}` (path-only, as seen by GoRouter's onException)
/// - `https://zapstore.dev/apps/{id}` and `.../stacks/{id}`
/// - `/apps/{id}` and `/stacks/{id}` (path-only, as seen by GoRouter's onException)
/// - `market://details?id=com.example.app`
/// - `market://search?q=search+query`
String? resolveDeepLinkPath(Uri uri) {
// https://zapstore.dev/apps/<id> OR bare /apps/<id> from GoRouter
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'apps') {
final isFullUri = uri.scheme == 'https' && uri.host == 'zapstore.dev';
final isBarePath = uri.host.isEmpty;
if (isFullUri || isBarePath) {
final id = uri.pathSegments[1];
if (id.isNotEmpty) {
return '/search/app/$id';
// https://zapstore.dev/stacks/<id> OR bare /stacks/<id>
if (uri.pathSegments.length == 2) {
final section = uri.pathSegments[0];
if (section == 'apps' || section == 'stacks') {
final isFullUri = uri.scheme == 'https' && uri.host == 'zapstore.dev';
final isBarePath = uri.host.isEmpty;
if (isFullUri || isBarePath) {
final id = uri.pathSegments[1];
if (id.isNotEmpty) {
final route = section == 'apps' ? 'app' : 'stack';
return '/search/$route/$id';
}
}
}
}
+148
View File
@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:models/models.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:zapstore/constants/app_constants.dart';
/// Regex for zapstore.dev app/stack deep links.
final _zapstoreUrlPattern = RegExp(
r'https?://zapstore\.dev/(apps|stacks)/(.+)',
caseSensitive: false,
);
/// Resolves the current navigation branch from the GoRouter state so that
/// pushed routes stay within the active tab (search / updates / profile).
String _currentBranch(BuildContext context) {
final segments = GoRouterState.of(context).uri.pathSegments;
return segments.isNotEmpty ? segments.first : 'search';
}
/// Push to a user profile screen within the current branch.
void pushUser(BuildContext context, String pubkey) {
context.push('/${_currentBranch(context)}/user/$pubkey');
}
/// Push to an app detail screen within the current branch.
///
/// When [author] is provided an naddr is built so the detail screen can
/// uniquely identify the app (identifier + author).
void pushApp(BuildContext context, String identifier, {String? author, int kind = 32267}) {
final id = author != null
? Utils.encodeShareableIdentifier(
AddressInput(
identifier: identifier,
author: author,
kind: kind,
relays: const [kDefaultRelay],
),
)
: identifier;
context.push('/${_currentBranch(context)}/app/$id');
}
/// Push to a stack detail screen within the current branch.
void pushStack(BuildContext context, String identifier, {String? author, int kind = 30267}) {
final id = author != null
? Utils.encodeShareableIdentifier(
AddressInput(
identifier: identifier,
author: author,
kind: kind,
relays: const [],
),
)
: identifier;
context.push('/${_currentBranch(context)}/stack/$id');
}
/// Push to the all-stacks screen within the current branch.
void pushStacks(BuildContext context) {
context.push('/${_currentBranch(context)}/stacks');
}
/// Attempt to navigate in-app for a URL or Nostr identifier.
///
/// Handles:
/// - `https://zapstore.dev/apps/<id>` and `.../stacks/<id>`
/// - `nostr:naddr1...`, `nostr:npub1...`, bare NIP-19 tokens
///
/// Returns `true` if navigation was handled in-app, `false` otherwise.
/// When [fallbackLaunch] is true (the default), unrecognised URLs are opened
/// in an external browser.
bool navigateToContent(
BuildContext context,
String input, {
bool fallbackLaunch = true,
}) {
final cleaned = input.trim();
// 1. zapstore.dev deep links
final zapstoreMatch = _zapstoreUrlPattern.firstMatch(cleaned);
if (zapstoreMatch != null) {
final type = zapstoreMatch.group(1)!; // "apps" or "stacks"
final id = zapstoreMatch.group(2)!;
if (type == 'apps') {
_navigateNip19OrIdentifier(context, id, fallbackKind: 'app');
} else {
_navigateNip19OrIdentifier(context, id, fallbackKind: 'stack');
}
return true;
}
// 2. nostr: prefix or bare NIP-19 token
final nip19 = cleaned.replaceFirst('nostr:', '');
if (_tryNavigateNip19(context, nip19)) return true;
// 3. Not a recognised in-app link
if (fallbackLaunch) {
_launchExternal(cleaned);
}
return false;
}
/// Try to decode a NIP-19 token and navigate. Returns true on success.
bool _tryNavigateNip19(BuildContext context, String token) {
try {
final decoded = Utils.decodeShareableIdentifier(token);
switch (decoded) {
case AddressData(:final kind):
if (kind == 30267) {
pushStack(context, token);
} else {
pushApp(context, token);
}
return true;
case ProfileData(:final pubkey):
pushUser(context, pubkey);
return true;
case EventData():
return false;
}
} catch (_) {}
return false;
}
/// The id might be an naddr or a plain identifier (e.g. `com.example.app`).
void _navigateNip19OrIdentifier(BuildContext context, String id, {required String fallbackKind}) {
if (_tryNavigateNip19(context, id)) return;
final branch = _currentBranch(context);
context.push('/$branch/$fallbackKind/$id');
}
void _launchExternal(String url) async {
try {
var cleanUrl = url;
if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://')) {
cleanUrl = 'https://$cleanUrl';
}
final uri = Uri.parse(cleanUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
try {
await launchUrl(uri, mode: LaunchMode.platformDefault);
} catch (_) {}
}
} catch (_) {}
}
+2 -17
View File
@@ -3,10 +3,10 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:skeletonizer/skeletonizer.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
import 'package:zapstore/utils/url_utils.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/widgets/zap_widgets.dart';
@@ -55,22 +55,7 @@ class AppCard extends HookConsumerWidget {
: 'No description available';
Widget buildCard(Profile? publisher, bool isPublisherLoading) => GestureDetector(
onTap: () {
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 [kDefaultRelay],
),
);
context.push('/$first/app/$naddr');
},
onTap: () => pushApp(context, app!.identifier, author: app!.pubkey, kind: app!.event.kind),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: const EdgeInsets.all(16),
-6
View File
@@ -7,7 +7,6 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:zapstore/services/bookmarks_service.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/services/notification_service.dart';
@@ -49,11 +48,6 @@ class ReleaseNotes extends StatelessWidget {
}
return ExpandableMarkdown(
data: release.releaseNotes!,
onTapLink: (text, url, title) {
if (url != null) {
launchUrl(Uri.parse(url));
}
},
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
blockquoteDecoration: BoxDecoration(
color: const Color(0xFF1E3A5F), // Dark blue
+8 -19
View File
@@ -1,9 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/nostr_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:skeletonizer/skeletonizer.dart';
import '../utils/extensions.dart';
@@ -268,11 +268,7 @@ class _SeeMoreCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
context.push('/$first/stacks');
},
onTap: () => pushStacks(context),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
@@ -378,19 +374,12 @@ class StackCard extends StatelessWidget {
);
return GestureDetector(
onTap: () {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
final naddr = Utils.encodeShareableIdentifier(
AddressInput(
identifier: stack.identifier,
author: stack.pubkey,
kind: stack.event.kind,
relays: const [],
),
);
context.push('/$first/stack/$naddr');
},
onTap: () => pushStack(
context,
stack.identifier,
author: stack.pubkey,
kind: stack.event.kind,
),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
-8
View File
@@ -1,7 +1,6 @@
import 'package:async_button_builder/async_button_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:models/models.dart';
@@ -34,7 +33,6 @@ class CommentsSection extends HookConsumerWidget {
},
source: LocalAndRemoteSource(stream: true, relays: 'social'),
subscriptionPrefix: 'app-comments',
and: (comment) => {comment.replies.query()},
),
);
@@ -76,7 +74,6 @@ class StackCommentsSection extends HookConsumerWidget {
},
source: LocalAndRemoteSource(stream: true, relays: 'social'),
subscriptionPrefix: 'app-stack-comments',
and: (comment) => {comment.replies.query()},
),
);
@@ -401,11 +398,6 @@ class _ThreadedCommentCard extends HookConsumerWidget {
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
onProfileTap: (pubkey) {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/$pubkey');
},
),
),
if (isSignedIn) ...[
+10 -57
View File
@@ -1,29 +1,9 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> _launchUrlSafely(String url) async {
try {
String cleanUrl = url.trim();
if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://')) {
cleanUrl = 'https://$cleanUrl';
}
final uri = Uri.parse(cleanUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
try {
await launchUrl(uri, mode: LaunchMode.platformDefault);
} catch (_) {}
}
} catch (_) {}
}
import 'package:zapstore/utils/nostr_route.dart';
class NoteParser {
static final RegExp nip19Regex = RegExp(
@@ -48,7 +28,6 @@ class NoteParser {
Widget? Function(String httpUrl)? onHttpUrl,
Widget? Function(String hashtag)? onHashtag,
void Function(String hashtag)? onHashtagTap,
void Function(String pubkey)? onProfileTap,
TextStyle? textStyle,
TextStyle? linkStyle,
}) {
@@ -152,7 +131,7 @@ class NoteParser {
text: match.text,
style: style,
recognizer: TapGestureRecognizer()
..onTap = () => _launchUrlSafely(match.text),
..onTap = () => navigateToContent(context, match.text),
));
} else {
final style =
@@ -202,15 +181,11 @@ enum _EntityType { nip19, http, hashtag }
class NostrEntityWidget extends StatelessWidget {
final String entity;
final List<Color> colorPair;
final void Function(String pubkey)? onProfileTap;
final void Function(String hashtag)? onHashtagTap;
const NostrEntityWidget({
super.key,
required this.entity,
required this.colorPair,
this.onProfileTap,
this.onHashtagTap,
});
@override
@@ -222,7 +197,6 @@ class NostrEntityWidget extends StatelessWidget {
ProfileData() => ProfileEntityWidget(
profileData: decoded,
colorPair: colorPair,
onProfileTap: onProfileTap,
),
EventData() => EventEntityWidget(
eventData: decoded,
@@ -242,13 +216,11 @@ class NostrEntityWidget extends StatelessWidget {
class ProfileEntityWidget extends ConsumerWidget {
final ProfileData profileData;
final List<Color> colorPair;
final void Function(String pubkey)? onProfileTap;
const ProfileEntityWidget({
super.key,
required this.profileData,
required this.colorPair,
this.onProfileTap,
});
@override
@@ -264,15 +236,7 @@ class ProfileEntityWidget extends ConsumerWidget {
),
);
void handleTap() {
if (onProfileTap != null) {
onProfileTap!(profileData.pubkey);
} else {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/${profileData.pubkey}');
}
}
void handleTap() => pushUser(context, profileData.pubkey);
return switch (profileState) {
StorageLoading() => GestureDetector(
@@ -294,20 +258,8 @@ class ProfileEntityWidget extends ConsumerWidget {
}
Widget _buildProfileWidget(BuildContext context, Profile profile) {
final displayName = profile.nameOrNpub;
void handleTap() {
if (onProfileTap != null) {
onProfileTap!(profileData.pubkey);
} else {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/${profileData.pubkey}');
}
}
return GestureDetector(
onTap: handleTap,
onTap: () => pushUser(context, profileData.pubkey),
child: DecoratedBox(
decoration: BoxDecoration(
color: colorPair[0].withValues(alpha: 0.1),
@@ -316,7 +268,7 @@ class ProfileEntityWidget extends ConsumerWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Text(
displayName,
profile.nameOrNpub,
style: context.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w500,
color: colorPair[0],
@@ -411,10 +363,11 @@ class AddressEntityWidget extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final segment = addressData.kind == 30267 ? 'stack' : 'app';
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/$segment/${addressData.identifier}');
if (addressData.kind == 30267) {
pushStack(context, addressData.identifier);
} else {
pushApp(context, addressData.identifier);
}
},
child: DecoratedBox(
decoration: BoxDecoration(
+7 -5
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
class ExpandableMarkdown extends HookWidget {
const ExpandableMarkdown({
@@ -21,8 +22,6 @@ class ExpandableMarkdown extends HookWidget {
const maxHeight = 170.0;
bool isLikelyLong(String text) {
// Simple heuristic to avoid expensive measurement
// - Many words OR many newlines => likely long content
final trimmed = text.trim();
if (trimmed.isEmpty) return false;
@@ -30,16 +29,19 @@ class ExpandableMarkdown extends HookWidget {
final newlineCount = '\n'.allMatches(trimmed).length;
final charCount = trimmed.length;
// Thresholds tuned for typical article blurbs
// Show collapse if any of these exceed limits
return wordCount > 90 || newlineCount > 6 || charCount > 600;
}
final shouldCollapse = !expanded.value && isLikelyLong(data);
final effectiveTapLink = onTapLink ??
(String text, String? href, String? title) {
if (href != null) navigateToContent(context, href);
};
final markdown = MarkdownBody(
data: data,
onTapLink: onTapLink,
onTapLink: effectiveTapLink,
styleSheet: styleSheet,
);
+2 -4
View File
@@ -1,7 +1,6 @@
import 'dart:convert';
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:share_plus/share_plus.dart';
@@ -10,6 +9,7 @@ import 'package:zapstore/services/bookmarks_service.dart';
import 'package:zapstore/services/notification_service.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
/// Floating three-dot overflow menu reusable across detail screens.
///
@@ -154,9 +154,7 @@ class FloatingOverflowMenu extends HookConsumerWidget {
}
void _viewPublisher(BuildContext context) {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
context.push('/$first/user/$publisherPubkey');
pushUser(context, publisherPubkey);
}
Future<void> _openInBrowser(BuildContext context) async {
+21 -18
View File
@@ -1,11 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:models/models.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/utils/nostr_route.dart';
import 'package:zapstore/utils/url_utils.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import '../theme.dart';
@@ -31,19 +31,12 @@ class SearchAppCard extends ConsumerWidget {
const iconSize = 48.0;
return GestureDetector(
onTap: () {
final segments = GoRouterState.of(context).uri.pathSegments;
final first = segments.isNotEmpty ? segments.first : 'search';
final naddr = Utils.encodeShareableIdentifier(
AddressInput(
identifier: app!.identifier,
author: app!.pubkey,
kind: app!.event.kind,
relays: const [],
),
);
context.push('/$first/app/$naddr');
},
onTap: () => pushApp(
context,
app!.identifier,
author: app!.pubkey,
kind: app!.event.kind,
),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: const EdgeInsets.all(16),
@@ -164,8 +157,17 @@ class SearchAppCard extends ConsumerWidget {
}
if (node is md.Element) {
final isBlock = const {
'p', 'li', 'ul', 'ol', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p',
'li',
'ul',
'ol',
'blockquote',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
}.contains(node.tag);
if (node.tag == 'br') buffer.write('\n');
for (final child in node.children ?? []) {
@@ -193,8 +195,9 @@ class SearchAppCard extends ConsumerWidget {
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.6),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
color: Theme.of(
context,
).colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),