mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Use app stacks, fix headlines
This commit is contained in:
@@ -12,8 +12,8 @@ const kFranzapPubkey =
|
|||||||
/// Identifier for storing user saved apps
|
/// Identifier for storing user saved apps
|
||||||
const kAppBookmarksIdentifier = 'zapstore-bookmarks';
|
const kAppBookmarksIdentifier = 'zapstore-bookmarks';
|
||||||
|
|
||||||
/// Event filter for app packs - excludes saved apps and packs with zero App references
|
/// Event filter for app stacks - excludes saved apps and stacks with zero App references
|
||||||
bool appPackEventFilter(Map<String, dynamic> event) {
|
bool appStackEventFilter(Map<String, dynamic> event) {
|
||||||
final tags = event['tags'] as List<dynamic>?;
|
final tags = event['tags'] as List<dynamic>?;
|
||||||
if (tags == null) return false;
|
if (tags == null) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:models/models.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/expandable_markdown.dart';
|
||||||
import 'package:zapstore/widgets/install_button.dart';
|
import 'package:zapstore/widgets/install_button.dart';
|
||||||
import 'package:zapstore/widgets/screenshots_gallery.dart';
|
import 'package:zapstore/widgets/screenshots_gallery.dart';
|
||||||
import 'package:zapstore/widgets/version_pill_widget.dart';
|
|
||||||
|
|
||||||
class AppDetailScreen extends HookConsumerWidget {
|
class AppDetailScreen extends HookConsumerWidget {
|
||||||
const AppDetailScreen({super.key, required this.appId, this.authorPubkey});
|
const AppDetailScreen({super.key, required this.appId, this.authorPubkey});
|
||||||
@@ -309,51 +309,42 @@ class _AppDetailContent extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
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: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Version ',
|
'Version:',
|
||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
ColorFiltered(
|
Gap(4),
|
||||||
colorFilter: const ColorFilter.matrix(<double>[
|
Text(
|
||||||
0.2126,
|
latestMetadata.version,
|
||||||
0.7152,
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
0.0722,
|
fontWeight: FontWeight.bold,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'(${formatDate(latestMetadata.createdAt)})',
|
'(${formatDate(latestMetadata.createdAt)})',
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(context).colorScheme.onSurface
|
||||||
context,
|
.withValues(alpha: 0.6),
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: ReleaseNotes(release: latestRelease),
|
child: ReleaseNotes(release: latestRelease),
|
||||||
@@ -586,9 +577,9 @@ class _AppDetailContent extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for existing pack
|
// Query for existing stack
|
||||||
final existingPackState = await ref.storage.query(
|
final existingStackState = await ref.storage.query(
|
||||||
RequestFilter<AppPack>(
|
RequestFilter<AppStack>(
|
||||||
authors: {signedInPubkey},
|
authors: {signedInPubkey},
|
||||||
tags: {
|
tags: {
|
||||||
'#d': {kAppBookmarksIdentifier},
|
'#d': {kAppBookmarksIdentifier},
|
||||||
@@ -596,14 +587,14 @@ class _AppDetailContent extends HookConsumerWidget {
|
|||||||
).toRequest(),
|
).toRequest(),
|
||||||
source: const LocalSource(),
|
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<String> existingAppIds = [];
|
List<String> existingAppIds = [];
|
||||||
if (existingPack != null) {
|
if (existingStack != null) {
|
||||||
try {
|
try {
|
||||||
final decryptedContent = await signer.nip44Decrypt(
|
final decryptedContent = await signer.nip44Decrypt(
|
||||||
existingPack.content,
|
existingStack.content,
|
||||||
signedInPubkey,
|
signedInPubkey,
|
||||||
);
|
);
|
||||||
existingAppIds = (jsonDecode(decryptedContent) as List)
|
existingAppIds = (jsonDecode(decryptedContent) as List)
|
||||||
@@ -631,19 +622,19 @@ class _AppDetailContent extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new partial pack with updated list
|
// Create new partial stack with updated list
|
||||||
final partialPack = PartialAppPack.withEncryptedApps(
|
final partialStack = PartialAppStack.withEncryptedApps(
|
||||||
name: 'Saved Apps',
|
name: 'Saved Apps',
|
||||||
identifier: kAppBookmarksIdentifier,
|
identifier: kAppBookmarksIdentifier,
|
||||||
apps: existingAppIds,
|
apps: existingAppIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sign (encrypts the content)
|
// Sign (encrypts the content)
|
||||||
final signedPack = await partialPack.signWith(signer);
|
final signedStack = await partialStack.signWith(signer);
|
||||||
|
|
||||||
// Save to local storage and publish to relays
|
// Save to local storage and publish to relays
|
||||||
await ref.storage.save({signedPack});
|
await ref.storage.save({signedStack});
|
||||||
ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social'));
|
ref.storage.publish({signedStack}, source: RemoteSource(relays: 'social'));
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.showInfo(
|
context.showInfo(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class AppStackScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Query stack with apps relationship
|
// Query stack with apps relationship
|
||||||
final stackState = ref.watch(
|
final stackState = ref.watch(
|
||||||
query<AppPack>(
|
query<AppStack>(
|
||||||
authors: authorPubkey != null ? {authorPubkey!} : null,
|
authors: authorPubkey != null ? {authorPubkey!} : null,
|
||||||
tags: {
|
tags: {
|
||||||
'#d': {stackId},
|
'#d': {stackId},
|
||||||
@@ -61,7 +61,7 @@ class AppStackScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Intermediate widget that loads apps with their release relationships
|
/// Intermediate widget that loads apps with their release relationships
|
||||||
class _AppStackContentWithApps extends HookConsumerWidget {
|
class _AppStackContentWithApps extends HookConsumerWidget {
|
||||||
final AppPack? stack;
|
final AppStack? stack;
|
||||||
|
|
||||||
const _AppStackContentWithApps({required this.stack});
|
const _AppStackContentWithApps({required this.stack});
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ class _ErrorScaffold extends StatelessWidget {
|
|||||||
|
|
||||||
/// Internal widget that displays stack details
|
/// Internal widget that displays stack details
|
||||||
class _AppStackContent extends HookConsumerWidget {
|
class _AppStackContent extends HookConsumerWidget {
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
final List<App> apps;
|
final List<App> apps;
|
||||||
|
|
||||||
const _AppStackContent({required this.stack, required this.apps});
|
const _AppStackContent({required this.stack, required this.apps});
|
||||||
@@ -179,9 +179,7 @@ class _AppStackContent extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Apps in this stack',
|
'Apps in this stack',
|
||||||
style: context.textTheme.titleLarge?.copyWith(
|
style: context.textTheme.titleLarge,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
CountBadge(
|
CountBadge(
|
||||||
@@ -235,7 +233,7 @@ class _AppStackContent extends HookConsumerWidget {
|
|||||||
class _StackHeader extends StatelessWidget {
|
class _StackHeader extends StatelessWidget {
|
||||||
const _StackHeader({required this.stack, this.author});
|
const _StackHeader({required this.stack, this.author});
|
||||||
|
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
final Profile? author;
|
final Profile? author;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -246,9 +244,7 @@ class _StackHeader extends StatelessWidget {
|
|||||||
// Stack name
|
// Stack name
|
||||||
Text(
|
Text(
|
||||||
stack.name ?? stack.identifier,
|
stack.name ?? stack.identifier,
|
||||||
style: context.textTheme.headlineMedium?.copyWith(
|
style: context.textTheme.headlineMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Published by author (same style as app_detail_screen)
|
// Published by author (same style as app_detail_screen)
|
||||||
|
|||||||
@@ -375,9 +375,7 @@ class _DebugMessagesSection extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Debug Info',
|
'Debug Info',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -975,9 +973,7 @@ class _DataManagementSection extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Data Management',
|
'Data Management',
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
context,
|
|
||||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -1267,9 +1263,7 @@ class _AboutSection extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'About',
|
'About',
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
context,
|
|
||||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (isLoading) ...[
|
if (isLoading) ...[
|
||||||
@@ -1326,9 +1320,7 @@ class _AboutSection extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'About',
|
'About',
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
context,
|
|
||||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -187,9 +187,7 @@ class _UpdatesListBody extends StatelessWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'No apps installed yet',
|
'No apps installed yet',
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@@ -313,9 +311,7 @@ class _InstallingHeader extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Installing',
|
'Installing',
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
CountBadge(count: count, color: AppColors.darkPillBackground),
|
CountBadge(count: count, color: AppColors.darkPillBackground),
|
||||||
@@ -356,9 +352,7 @@ class _UpdatesSectionHeader extends ConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -485,9 +479,7 @@ class _UpToDateHeader extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Up to date',
|
'Up to date',
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
CountBadge(count: count, color: AppColors.darkPillBackground),
|
CountBadge(count: count, color: AppColors.darkPillBackground),
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:models/models.dart';
|
import 'package:models/models.dart';
|
||||||
import 'package:zapstore/utils/extensions.dart';
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
|
|
||||||
|
import '../constants/app_constants.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import '../widgets/common/note_parser.dart';
|
import '../widgets/common/note_parser.dart';
|
||||||
import '../widgets/common/profile_avatar.dart';
|
import '../widgets/common/profile_avatar.dart';
|
||||||
import '../widgets/app_card.dart';
|
import '../widgets/app_card.dart';
|
||||||
import '../widgets/app_pack_container.dart';
|
|
||||||
import '../widgets/zap_widgets.dart';
|
import '../widgets/zap_widgets.dart';
|
||||||
|
|
||||||
/// User profile screen - shows any user/developer profile
|
/// 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.where((a) => a.isZapstoreApp).toList()
|
||||||
: userAppsState.models;
|
: userAppsState.models;
|
||||||
|
|
||||||
// Query user's app packs
|
// Query user's app stacks
|
||||||
final appPacksState = ref.watch(
|
final appStacksState = ref.watch(
|
||||||
query<AppPack>(
|
query<AppStack>(
|
||||||
authors: {pubkey},
|
authors: {pubkey},
|
||||||
limit: 20,
|
limit: 20,
|
||||||
and: (pack) => {pack.apps},
|
and: (pack) => {pack.apps},
|
||||||
@@ -66,11 +66,11 @@ class UserScreen extends HookConsumerWidget {
|
|||||||
relays: 'AppCatalog',
|
relays: 'AppCatalog',
|
||||||
stream: false,
|
stream: false,
|
||||||
),
|
),
|
||||||
schemaFilter: appPackEventFilter,
|
schemaFilter: appStackEventFilter,
|
||||||
subscriptionPrefix: 'user-packs',
|
subscriptionPrefix: 'user-stacks',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final packs = appPacksState.models
|
final stacks = appStacksState.models
|
||||||
..sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
|
..sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -99,9 +99,7 @@ class UserScreen extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Published Apps',
|
'Published Apps',
|
||||||
style: context.textTheme.titleLarge?.copyWith(
|
style: context.textTheme.titleLarge,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -113,33 +111,23 @@ class UserScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
// App packs section - only show if packs exist
|
// App stacks section - only show if stacks exist
|
||||||
if (packs.isNotEmpty) ...[
|
if (stacks.isNotEmpty) ...[
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 32, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'App Packs',
|
'App Stacks',
|
||||||
style: context.textTheme.headlineMedium?.copyWith(
|
style: context.textTheme.headlineMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
|
final stack = stacks[index];
|
||||||
|
return _StackLinkCard(stack: stack, pubkey: pubkey);
|
||||||
|
}, childCount: stacks.length),
|
||||||
),
|
),
|
||||||
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())),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Bottom padding
|
// Bottom padding
|
||||||
@@ -172,9 +160,7 @@ class _UserHeader extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
profile?.nameOrNpub ??
|
profile?.nameOrNpub ??
|
||||||
'${Utils.encodeShareableFromString(pubkey, type: 'npub').substring(0, 12)}...',
|
'${Utils.encodeShareableFromString(pubkey, type: 'npub').substring(0, 12)}...',
|
||||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
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 {
|
class _UserBio extends HookWidget {
|
||||||
const _UserBio({required this.profile});
|
const _UserBio({required this.profile});
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ final bookmarksProvider = FutureProvider<Set<String>>((ref) async {
|
|||||||
return {};
|
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
|
// Initial fetch from remote, then watches local storage for changes
|
||||||
final packState = ref.watch(
|
final stackState = ref.watch(
|
||||||
query<AppPack>(
|
query<AppStack>(
|
||||||
authors: {signedInPubkey},
|
authors: {signedInPubkey},
|
||||||
tags: {
|
tags: {
|
||||||
'#d': {kAppBookmarksIdentifier},
|
'#d': {kAppBookmarksIdentifier},
|
||||||
@@ -30,20 +30,20 @@ final bookmarksProvider = FutureProvider<Set<String>>((ref) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Handle different storage states
|
// Handle different storage states
|
||||||
final pack = switch (packState) {
|
final stack = switch (stackState) {
|
||||||
StorageLoading() => null,
|
StorageLoading() => null,
|
||||||
StorageError() => null,
|
StorageError() => null,
|
||||||
StorageData(:final models) => models.firstOrNull,
|
StorageData(:final models) => models.firstOrNull,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (pack == null || pack.content.isEmpty) {
|
if (stack == null || stack.content.isEmpty) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Content is always encrypted after signing - must explicitly decrypt
|
// Content is always encrypted after signing - must explicitly decrypt
|
||||||
final decryptedContent = await signer.nip44Decrypt(
|
final decryptedContent = await signer.nip44Decrypt(
|
||||||
pack.content,
|
stack.content,
|
||||||
signedInPubkey,
|
signedInPubkey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -115,7 +115,7 @@ TextTheme _createProfessionalTextTheme() {
|
|||||||
headlineMedium: const TextStyle(
|
headlineMedium: const TextStyle(
|
||||||
fontFamily: kHeadlineFontFamily,
|
fontFamily: kHeadlineFontFamily,
|
||||||
fontSize: 31,
|
fontSize: 31,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: 0.15,
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
@@ -123,7 +123,7 @@ TextTheme _createProfessionalTextTheme() {
|
|||||||
headlineSmall: const TextStyle(
|
headlineSmall: const TextStyle(
|
||||||
fontFamily: kHeadlineFontFamily,
|
fontFamily: kHeadlineFontFamily,
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: 0.15,
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
height: 1.35,
|
height: 1.35,
|
||||||
@@ -133,7 +133,7 @@ TextTheme _createProfessionalTextTheme() {
|
|||||||
titleLarge: const TextStyle(
|
titleLarge: const TextStyle(
|
||||||
fontFamily: kHeadlineFontFamily,
|
fontFamily: kHeadlineFontFamily,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 0.1,
|
letterSpacing: 0.1,
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
@@ -141,7 +141,7 @@ TextTheme _createProfessionalTextTheme() {
|
|||||||
titleMedium: const TextStyle(
|
titleMedium: const TextStyle(
|
||||||
fontFamily: kHeadlineFontFamily,
|
fontFamily: kHeadlineFontFamily,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: 0.15,
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
height: 1.45,
|
height: 1.45,
|
||||||
@@ -149,7 +149,7 @@ TextTheme _createProfessionalTextTheme() {
|
|||||||
titleSmall: const TextStyle(
|
titleSmall: const TextStyle(
|
||||||
fontFamily: kHeadlineFontFamily,
|
fontFamily: kHeadlineFontFamily,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 0.15,
|
letterSpacing: 0.15,
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
height: 1.45,
|
height: 1.45,
|
||||||
|
|||||||
@@ -243,7 +243,6 @@ class AppCard extends HookConsumerWidget {
|
|||||||
final appName = app!.name ?? app!.identifier;
|
final appName = app!.name ?? app!.identifier;
|
||||||
final titleStyle = context.textTheme.titleMedium?.copyWith(
|
final titleStyle = context.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
letterSpacing: 0.1,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no publisher or relay-signed, just show name
|
// If no publisher or relay-signed, just show name
|
||||||
|
|||||||
@@ -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 {
|
class SocialActionsRow extends HookConsumerWidget {
|
||||||
const SocialActionsRow({super.key, required this.app, required this.author});
|
const SocialActionsRow({super.key, required this.app, required this.author});
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Zap + Save App + App Pack button row
|
// Zap + Save App + App Stack button row
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 36,
|
height: 36,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -141,11 +141,11 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// App Pack button
|
// App Stack button
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 16,
|
flex: 16,
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: () => _showAddToPackDialog(context, app),
|
onPressed: () => _showAddToStackDialog(context, app),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -193,9 +193,9 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (signer == null || signedInPubkey == null) return;
|
if (signer == null || signedInPubkey == null) return;
|
||||||
|
|
||||||
// Query for existing pack
|
// Query for existing stack
|
||||||
final existingPackState = await ref.storage.query(
|
final existingStackState = await ref.storage.query(
|
||||||
RequestFilter<AppPack>(
|
RequestFilter<AppStack>(
|
||||||
authors: {signedInPubkey},
|
authors: {signedInPubkey},
|
||||||
tags: {
|
tags: {
|
||||||
'#d': {kAppBookmarksIdentifier},
|
'#d': {kAppBookmarksIdentifier},
|
||||||
@@ -203,14 +203,14 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
).toRequest(),
|
).toRequest(),
|
||||||
source: const LocalSource(),
|
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<String> existingAppIds = [];
|
List<String> existingAppIds = [];
|
||||||
if (existingPack != null) {
|
if (existingStack != null) {
|
||||||
try {
|
try {
|
||||||
final decryptedContent = await signer.nip44Decrypt(
|
final decryptedContent = await signer.nip44Decrypt(
|
||||||
existingPack.content,
|
existingStack.content,
|
||||||
signedInPubkey,
|
signedInPubkey,
|
||||||
);
|
);
|
||||||
existingAppIds = (jsonDecode(decryptedContent) as List)
|
existingAppIds = (jsonDecode(decryptedContent) as List)
|
||||||
@@ -232,19 +232,19 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new partial pack with updated list
|
// Create new partial stack with updated list
|
||||||
final partialPack = PartialAppPack.withEncryptedApps(
|
final partialStack = PartialAppStack.withEncryptedApps(
|
||||||
name: 'Saved Apps',
|
name: 'Saved Apps',
|
||||||
identifier: kAppBookmarksIdentifier,
|
identifier: kAppBookmarksIdentifier,
|
||||||
apps: existingAppIds,
|
apps: existingAppIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sign (encrypts the content)
|
// Sign (encrypts the content)
|
||||||
final signedPack = await partialPack.signWith(signer);
|
final signedStack = await partialStack.signWith(signer);
|
||||||
|
|
||||||
// Save to local storage and publish to relays
|
// Save to local storage and publish to relays
|
||||||
await ref.storage.save({signedPack});
|
await ref.storage.save({signedStack});
|
||||||
ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social'));
|
ref.storage.publish({signedStack}, source: RemoteSource(relays: 'social'));
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.showInfo(
|
context.showInfo(
|
||||||
@@ -258,10 +258,10 @@ class SocialActionsRow extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showAddToPackDialog(BuildContext context, App app) async {
|
Future<void> _showAddToStackDialog(BuildContext context, App app) async {
|
||||||
await showBaseDialog(
|
await showBaseDialog(
|
||||||
context: context,
|
context: context,
|
||||||
dialog: AddToPackDialog(app: app),
|
dialog: AddToStackDialog(app: app),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ class AppHeader extends StatelessWidget {
|
|||||||
AutoSizeText(
|
AutoSizeText(
|
||||||
app.name ?? app.identifier,
|
app.name ?? app.identifier,
|
||||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
letterSpacing: 0.1,
|
|
||||||
fontSize:
|
fontSize:
|
||||||
(Theme.of(
|
(Theme.of(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -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<AppPack>(
|
|
||||||
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)),
|
|
||||||
_ => <AppPack>[],
|
|
||||||
};
|
|
||||||
|
|
||||||
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<Profile>(
|
|
||||||
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<App> 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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import 'package:models/models.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import '../constants/app_constants.dart';
|
||||||
import '../utils/extensions.dart';
|
import '../utils/extensions.dart';
|
||||||
import '../utils/url_utils.dart';
|
import '../utils/url_utils.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
@@ -28,7 +29,7 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
||||||
|
|
||||||
final appStacksState = ref.watch(
|
final appStacksState = ref.watch(
|
||||||
query<AppPack>(
|
query<AppStack>(
|
||||||
limit: 50,
|
limit: 50,
|
||||||
and: (pack) => {pack.apps},
|
and: (pack) => {pack.apps},
|
||||||
source: LocalAndRemoteSource(stream: true, relays: 'social'),
|
source: LocalAndRemoteSource(stream: true, relays: 'social'),
|
||||||
@@ -36,7 +37,7 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
relays: 'AppCatalog',
|
relays: 'AppCatalog',
|
||||||
stream: false,
|
stream: false,
|
||||||
),
|
),
|
||||||
schemaFilter: appPackEventFilter,
|
schemaFilter: appStackEventFilter,
|
||||||
subscriptionPrefix: 'app-stack',
|
subscriptionPrefix: 'app-stack',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -61,7 +62,7 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final rawStacks = switch (appStacksState) {
|
final rawStacks = switch (appStacksState) {
|
||||||
StorageData(:final models) => models,
|
StorageData(:final models) => models,
|
||||||
_ => <AppPack>[],
|
_ => <AppStack>[],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rawStacks.isEmpty) {
|
if (rawStacks.isEmpty) {
|
||||||
@@ -87,7 +88,9 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
for (int i = 0; i < stacks.length; i += 2)
|
for (int i = 0; i < stacks.length; i += 2)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
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(
|
child: SizedBox(
|
||||||
width: 160,
|
width: 160,
|
||||||
@@ -108,8 +111,8 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AppPack> _sortStacks(
|
List<AppStack> _sortStacks(
|
||||||
List<AppPack> stacks, {
|
List<AppStack> stacks, {
|
||||||
String? signedInPubkey,
|
String? signedInPubkey,
|
||||||
Set<String>? followingPubkeys,
|
Set<String>? followingPubkeys,
|
||||||
}) {
|
}) {
|
||||||
@@ -119,8 +122,8 @@ class AppStackContainer extends HookConsumerWidget {
|
|||||||
followingPubkeys != null &&
|
followingPubkeys != null &&
|
||||||
followingPubkeys.isNotEmpty) {
|
followingPubkeys.isNotEmpty) {
|
||||||
// Signed in: followed profiles first, then by recency
|
// Signed in: followed profiles first, then by recency
|
||||||
final followed = <AppPack>[];
|
final followed = <AppStack>[];
|
||||||
final others = <AppPack>[];
|
final others = <AppStack>[];
|
||||||
|
|
||||||
for (final stack in stacks) {
|
for (final stack in stacks) {
|
||||||
if (followingPubkeys.contains(stack.pubkey)) {
|
if (followingPubkeys.contains(stack.pubkey)) {
|
||||||
@@ -278,7 +281,7 @@ class _SkeletonStackCard extends StatelessWidget {
|
|||||||
class _StackCard extends HookConsumerWidget {
|
class _StackCard extends HookConsumerWidget {
|
||||||
const _StackCard({required this.stack});
|
const _StackCard({required this.stack});
|
||||||
|
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -339,14 +342,13 @@ class _StackCard extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Stack name
|
// Stack name with conditional fade out
|
||||||
Text(
|
_FadingText(
|
||||||
stack.name ?? stack.identifier,
|
stack.name ?? stack.identifier,
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
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),
|
const SizedBox(height: 6),
|
||||||
// Author row: avatar + Name
|
// Author row: avatar + Name
|
||||||
@@ -397,9 +399,7 @@ class _AppIconsRow extends StatelessWidget {
|
|||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.surfaceContainerHighest
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHighest
|
|
||||||
.withValues(alpha: 0.8),
|
.withValues(alpha: 0.8),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
@@ -482,7 +482,9 @@ class _AppIconTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.apps_outlined,
|
Icons.apps_outlined,
|
||||||
size: 16,
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+110
-103
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:models/models.dart';
|
import 'package:models/models.dart';
|
||||||
|
import 'package:zapstore/constants/app_constants.dart';
|
||||||
import 'package:zapstore/services/notification_service.dart';
|
import 'package:zapstore/services/notification_service.dart';
|
||||||
import 'package:zapstore/utils/extensions.dart';
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
import 'package:zapstore/widgets/auth_widgets.dart';
|
import 'package:zapstore/widgets/auth_widgets.dart';
|
||||||
@@ -104,9 +105,9 @@ class SaveAppDialog extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for existing pack
|
// Query for existing stack
|
||||||
final existingPackState = await ref.storage.query(
|
final existingStackState = await ref.storage.query(
|
||||||
RequestFilter<AppPack>(
|
RequestFilter<AppStack>(
|
||||||
authors: {signedInPubkey},
|
authors: {signedInPubkey},
|
||||||
tags: {
|
tags: {
|
||||||
'#d': {kAppBookmarksIdentifier},
|
'#d': {kAppBookmarksIdentifier},
|
||||||
@@ -114,14 +115,14 @@ class SaveAppDialog extends HookConsumerWidget {
|
|||||||
).toRequest(),
|
).toRequest(),
|
||||||
source: const LocalSource(),
|
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<String> existingAppIds = [];
|
List<String> existingAppIds = [];
|
||||||
if (existingPack != null) {
|
if (existingStack != null) {
|
||||||
try {
|
try {
|
||||||
final decryptedContent = await signer.nip44Decrypt(
|
final decryptedContent = await signer.nip44Decrypt(
|
||||||
existingPack.content,
|
existingStack.content,
|
||||||
signedInPubkey,
|
signedInPubkey,
|
||||||
);
|
);
|
||||||
existingAppIds = (jsonDecode(decryptedContent) as List)
|
existingAppIds = (jsonDecode(decryptedContent) as List)
|
||||||
@@ -149,19 +150,21 @@ class SaveAppDialog extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new partial pack with updated list
|
// Create new partial stack with updated list
|
||||||
final partialPack = PartialAppPack.withEncryptedApps(
|
final partialStack = PartialAppStack.withEncryptedApps(
|
||||||
name: 'Saved Apps',
|
name: 'Saved Apps',
|
||||||
identifier: kAppBookmarksIdentifier,
|
identifier: kAppBookmarksIdentifier,
|
||||||
apps: existingAppIds,
|
apps: existingAppIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sign (encrypts the content)
|
// Sign (encrypts the content)
|
||||||
final signedPack = await partialPack.signWith(signer);
|
final signedStack = await partialStack.signWith(signer);
|
||||||
|
|
||||||
// Save to local storage and publish to relays
|
// Save to local storage and publish to relays
|
||||||
await ref.storage.save({signedPack});
|
await ref.storage.save({signedStack});
|
||||||
ref.storage.publish({signedPack}, source: RemoteSource(relays: 'social'));
|
ref.storage.publish({
|
||||||
|
signedStack,
|
||||||
|
}, source: RemoteSource(relays: 'social'));
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -177,9 +180,9 @@ class SaveAppDialog extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dialog for managing app packs (public collections)
|
/// Dialog for managing app stacks (public collections)
|
||||||
class AddToPackDialog extends HookConsumerWidget {
|
class AddToStackDialog extends HookConsumerWidget {
|
||||||
const AddToPackDialog({super.key, required this.app});
|
const AddToStackDialog({super.key, required this.app});
|
||||||
|
|
||||||
final App app;
|
final App app;
|
||||||
|
|
||||||
@@ -187,14 +190,14 @@ class AddToPackDialog extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
||||||
if (signedInPubkey == null) {
|
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 {
|
class _AddToStackDialogSignedOut extends StatelessWidget {
|
||||||
const _AddToPackDialogSignedOut({required this.app});
|
const _AddToStackDialogSignedOut({required this.app});
|
||||||
|
|
||||||
final App app;
|
final App app;
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget {
|
|||||||
return BaseDialog(
|
return BaseDialog(
|
||||||
titleIcon: const Icon(Icons.apps),
|
titleIcon: const Icon(Icons.apps),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Add to App Packs',
|
'Add to App Stacks',
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
content: BaseDialogContent(
|
content: BaseDialogContent(
|
||||||
@@ -231,7 +234,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
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(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -244,7 +247,7 @@ class _AddToPackDialogSignedOut extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const SignInPrompt(
|
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 {
|
class _AddToStackDialogSignedIn extends HookConsumerWidget {
|
||||||
const _AddToPackDialogSignedIn({
|
const _AddToStackDialogSignedIn({
|
||||||
required this.app,
|
required this.app,
|
||||||
required this.signedInPubkey,
|
required this.signedInPubkey,
|
||||||
});
|
});
|
||||||
@@ -274,37 +277,38 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final newPackNameController = useTextEditingController();
|
final newStackNameController = useTextEditingController();
|
||||||
|
|
||||||
final publicPacksState = ref.watch(
|
final publicStacksState = ref.watch(
|
||||||
query<AppPack>(
|
query<AppStack>(
|
||||||
authors: {signedInPubkey},
|
authors: {signedInPubkey},
|
||||||
and: (pack) => {pack.apps},
|
and: (stack) => {stack.apps},
|
||||||
source: const LocalAndRemoteSource(relays: 'social', stream: false),
|
source: const LocalAndRemoteSource(relays: 'social', stream: false),
|
||||||
andSource: const LocalSource(),
|
andSource: const LocalSource(),
|
||||||
subscriptionPrefix: 'user-packs-dialog',
|
schemaFilter: appStackEventFilter,
|
||||||
|
subscriptionPrefix: 'user-stacks-dialog',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final publicPacks = publicPacksState.models
|
final publicStacks = publicStacksState.models
|
||||||
.where((pack) => pack.identifier != kAppBookmarksIdentifier)
|
.where((stack) => stack.identifier != kAppBookmarksIdentifier)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Check which packs contain this app
|
// Check which stacks contain this app
|
||||||
final selectedCollections = useState<Set<String>>({});
|
final selectedCollections = useState<Set<String>>({});
|
||||||
// Track new packs with their names (identifier -> name)
|
// Track new stacks with their names (identifier -> name)
|
||||||
final newPackNames = useState<Map<String, String>>({});
|
final newStackNames = useState<Map<String, String>>({});
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final selected = <String>{};
|
final selected = <String>{};
|
||||||
for (final pack in publicPacks) {
|
for (final stack in publicStacks) {
|
||||||
if (pack.apps.toList().any((appModel) => appModel.id == app.id)) {
|
if (stack.apps.toList().any((appModel) => appModel.id == app.id)) {
|
||||||
selected.add(pack.identifier);
|
selected.add(stack.identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedCollections.value = selected;
|
selectedCollections.value = selected;
|
||||||
return null;
|
return null;
|
||||||
}, [app.id, publicPacks.length]);
|
}, [app.id, publicStacks.length]);
|
||||||
|
|
||||||
// Helper to slugify name to identifier
|
// Helper to slugify name to identifier
|
||||||
String slugify(String text) {
|
String slugify(String text) {
|
||||||
@@ -319,7 +323,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
return BaseDialog(
|
return BaseDialog(
|
||||||
titleIcon: const Icon(Icons.apps),
|
titleIcon: const Icon(Icons.apps),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Add to App Packs',
|
'Add to App Stacks',
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
content: BaseDialogContent(
|
content: BaseDialogContent(
|
||||||
@@ -347,7 +351,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
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(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -360,18 +364,18 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Show existing packs and newly created packs as selectable chips
|
// Show existing stacks and newly created stacks as selectable chips
|
||||||
if (publicPacks.isNotEmpty || newPackNames.value.isNotEmpty) ...[
|
if (publicStacks.isNotEmpty || newStackNames.value.isNotEmpty) ...[
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
// Existing packs
|
// Existing stacks
|
||||||
...publicPacks.map((pack) {
|
...publicStacks.map((stack) {
|
||||||
final isSelected = selectedCollections.value.contains(
|
final isSelected = selectedCollections.value.contains(
|
||||||
pack.identifier,
|
stack.identifier,
|
||||||
);
|
);
|
||||||
final displayName = pack.name ?? pack.identifier;
|
final displayName = stack.name ?? stack.identifier;
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
label: Text(displayName),
|
label: Text(displayName),
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
@@ -379,12 +383,12 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
selectedCollections.value = {
|
selectedCollections.value = {
|
||||||
...selectedCollections.value,
|
...selectedCollections.value,
|
||||||
pack.identifier,
|
stack.identifier,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
selectedCollections.value = {
|
selectedCollections.value = {
|
||||||
...selectedCollections.value,
|
...selectedCollections.value,
|
||||||
}..remove(pack.identifier);
|
}..remove(stack.identifier);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor: isSelected
|
backgroundColor: isSelected
|
||||||
@@ -408,11 +412,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
side: BorderSide.none,
|
side: BorderSide.none,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
// Newly created packs not yet in publicPacks
|
// Newly created stacks not yet in publicStacks
|
||||||
...newPackNames.value.entries
|
...newStackNames.value.entries
|
||||||
.where(
|
.where(
|
||||||
(entry) => !publicPacks.any(
|
(entry) => !publicStacks.any(
|
||||||
(pack) => pack.identifier == entry.key,
|
(stack) => stack.identifier == entry.key,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map((entry) {
|
.map((entry) {
|
||||||
@@ -434,11 +438,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
selectedCollections.value = {
|
selectedCollections.value = {
|
||||||
...selectedCollections.value,
|
...selectedCollections.value,
|
||||||
}..remove(identifier);
|
}..remove(identifier);
|
||||||
// Also remove from newPackNames if deselected
|
// Also remove from newStackNames if deselected
|
||||||
final updatedNames = Map<String, String>.from(
|
final updatedNames = Map<String, String>.from(
|
||||||
newPackNames.value,
|
newStackNames.value,
|
||||||
)..remove(identifier);
|
)..remove(identifier);
|
||||||
newPackNames.value = updatedNames;
|
newStackNames.value = updatedNames;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor: isSelected
|
backgroundColor: isSelected
|
||||||
@@ -469,12 +473,12 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Input for new pack
|
// Input for new stack
|
||||||
TextField(
|
TextField(
|
||||||
controller: newPackNameController,
|
controller: newStackNameController,
|
||||||
maxLength: 20,
|
maxLength: 20,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Add to new app pack',
|
labelText: 'Add to new app stack',
|
||||||
hintText: 'e.g., Favorite Apps',
|
hintText: 'e.g., Favorite Apps',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -488,8 +492,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
...selectedCollections.value,
|
...selectedCollections.value,
|
||||||
identifier,
|
identifier,
|
||||||
};
|
};
|
||||||
newPackNames.value = {...newPackNames.value, identifier: name};
|
newStackNames.value = {
|
||||||
newPackNameController.clear();
|
...newStackNames.value,
|
||||||
|
identifier: name,
|
||||||
|
};
|
||||||
|
newStackNameController.clear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -502,20 +509,20 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
AsyncButtonBuilder(
|
AsyncButtonBuilder(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Check if there's a pending new pack name in the text field
|
// Check if there's a pending new stack name in the text field
|
||||||
final pendingPackName = newPackNameController.text.trim();
|
final pendingStackName = newStackNameController.text.trim();
|
||||||
if (pendingPackName.isNotEmpty) {
|
if (pendingStackName.isNotEmpty) {
|
||||||
final identifier = slugify(pendingPackName);
|
final identifier = slugify(pendingStackName);
|
||||||
if (identifier.isNotEmpty &&
|
if (identifier.isNotEmpty &&
|
||||||
!selectedCollections.value.contains(identifier)) {
|
!selectedCollections.value.contains(identifier)) {
|
||||||
// Add the pending pack to the collections
|
// Add the pending stack to the collections
|
||||||
selectedCollections.value = {
|
selectedCollections.value = {
|
||||||
...selectedCollections.value,
|
...selectedCollections.value,
|
||||||
identifier,
|
identifier,
|
||||||
};
|
};
|
||||||
newPackNames.value = {
|
newStackNames.value = {
|
||||||
...newPackNames.value,
|
...newStackNames.value,
|
||||||
identifier: pendingPackName,
|
identifier: pendingStackName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,9 +532,9 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
ref,
|
ref,
|
||||||
app,
|
app,
|
||||||
publicPacks,
|
publicStacks,
|
||||||
selectedCollections.value,
|
selectedCollections.value,
|
||||||
newPackNames.value,
|
newStackNames.value,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
builder: (context, child, callback, buttonState) {
|
builder: (context, child, callback, buttonState) {
|
||||||
@@ -557,9 +564,9 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
App app,
|
App app,
|
||||||
List<AppPack> existingPacks,
|
List<AppStack> existingStacks,
|
||||||
Set<String> selectedCollectionIds,
|
Set<String> selectedCollectionIds,
|
||||||
Map<String, String> newPackNames,
|
Map<String, String> newStackNames,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final signer = ref.read(Signer.activeSignerProvider);
|
final signer = ref.read(Signer.activeSignerProvider);
|
||||||
@@ -567,7 +574,7 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.showError(
|
context.showError(
|
||||||
'Sign in required',
|
'Sign in required',
|
||||||
description: 'You need to sign in to manage app packs.',
|
description: 'You need to sign in to manage app stacks.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -575,11 +582,11 @@ class _AddToPackDialogSignedIn extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Get current state of which collections contain this app
|
// Get current state of which collections contain this app
|
||||||
final currentCollectionsWithApp = <String>{};
|
final currentCollectionsWithApp = <String>{};
|
||||||
for (final pack in existingPacks) {
|
for (final stack in existingStacks) {
|
||||||
// Get raw app IDs from event tags
|
// Get raw app IDs from event tags
|
||||||
final appIds = pack.event.getTagSetValues('a');
|
final appIds = stack.event.getTagSetValues('a');
|
||||||
if (appIds.contains(app.id)) {
|
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
|
// Update or create collections
|
||||||
for (final collectionId in selectedCollectionIds) {
|
for (final collectionId in selectedCollectionIds) {
|
||||||
final existingPack = existingPacks
|
final existingStack = existingStacks
|
||||||
.where((pack) => pack.identifier == collectionId)
|
.where((stack) => stack.identifier == collectionId)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
||||||
// Use the provided name from newPackNames, or existing pack name, or identifier
|
// Use the provided name from newStackNames, or existing stack name, or identifier
|
||||||
final packName =
|
final stackName =
|
||||||
newPackNames[collectionId] ?? existingPack?.name ?? collectionId;
|
newStackNames[collectionId] ?? existingStack?.name ?? collectionId;
|
||||||
|
|
||||||
// Get existing app IDs as a list to preserve order
|
// Get existing app IDs as a list to preserve order
|
||||||
final existingAppIds =
|
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);
|
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) {
|
if (!appAlreadyExists) {
|
||||||
final partialPack = PartialAppPack(
|
final partialStack = PartialAppStack(
|
||||||
name: packName,
|
name: stackName,
|
||||||
identifier: collectionId,
|
identifier: collectionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add existing apps in order
|
// Add existing apps in order
|
||||||
for (final appId in existingAppIds) {
|
for (final appId in existingAppIds) {
|
||||||
partialPack.addApp(appId);
|
partialStack.addApp(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this app at the end
|
// Add this app at the end
|
||||||
partialPack.addApp(app.id);
|
partialStack.addApp(app.id);
|
||||||
|
|
||||||
final signedPack = await partialPack.signWith(signer);
|
final signedStack = await partialStack.signWith(signer);
|
||||||
await ref.storage.save({signedPack});
|
await ref.storage.save({signedStack});
|
||||||
await ref.storage.publish({
|
await ref.storage.publish({
|
||||||
signedPack,
|
signedStack,
|
||||||
}, source: RemoteSource(relays: 'social'));
|
}, source: RemoteSource(relays: 'social'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from deselected collections
|
// Remove from deselected collections
|
||||||
for (final collectionId in collectionsToRemoveFrom) {
|
for (final collectionId in collectionsToRemoveFrom) {
|
||||||
final existingPack = existingPacks
|
final existingStack = existingStacks
|
||||||
.where((pack) => pack.identifier == collectionId)
|
.where((stack) => stack.identifier == collectionId)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
if (existingPack != null) {
|
if (existingStack != null) {
|
||||||
final partialPack = PartialAppPack(
|
final partialStack = PartialAppStack(
|
||||||
name: existingPack.name ?? collectionId,
|
name: existingStack.name ?? collectionId,
|
||||||
identifier: collectionId,
|
identifier: collectionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-add all apps except the one we're removing, preserving order
|
// 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
|
// Get raw app IDs from event tags as a list to preserve order
|
||||||
final existingAppIds = existingPack.event
|
final existingAppIds = existingStack.event
|
||||||
.getTagSetValues('a')
|
.getTagSetValues('a')
|
||||||
.toList();
|
.toList();
|
||||||
for (final appId in existingAppIds) {
|
for (final appId in existingAppIds) {
|
||||||
if (appId != app.id) {
|
if (appId != app.id) {
|
||||||
partialPack.addApp(appId);
|
partialStack.addApp(appId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final signedPack = await partialPack.signWith(signer);
|
final signedStack = await partialStack.signWith(signer);
|
||||||
await ref.storage.save({signedPack});
|
await ref.storage.save({signedStack});
|
||||||
await ref.storage.publish({
|
await ref.storage.publish({
|
||||||
signedPack,
|
signedStack,
|
||||||
}, source: RemoteSource(relays: 'social'));
|
}, source: RemoteSource(relays: 'social'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
if (selectedCollectionIds.isEmpty) {
|
if (selectedCollectionIds.isEmpty) {
|
||||||
context.showInfo('Removed from all app packs');
|
context.showInfo('Removed from all app stacks');
|
||||||
} else {
|
} else {
|
||||||
context.showInfo('App packs updated');
|
context.showInfo('App stacks updated');
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
class StackCommentsSection extends HookConsumerWidget {
|
||||||
const StackCommentsSection({super.key, required this.stack});
|
const StackCommentsSection({super.key, required this.stack});
|
||||||
|
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -111,12 +111,7 @@ class _CommentsSectionLayout extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('Comments', style: Theme.of(context).textTheme.titleLarge),
|
||||||
'Comments',
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
if (comments.isNotEmpty)
|
if (comments.isNotEmpty)
|
||||||
_CommentCountBadge(count: comments.length),
|
_CommentCountBadge(count: comments.length),
|
||||||
],
|
],
|
||||||
@@ -251,7 +246,6 @@ class _CommentCard extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
author?.nameOrNpub ?? '',
|
author?.nameOrNpub ?? '',
|
||||||
style: context.textTheme.titleSmall?.copyWith(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize:
|
fontSize:
|
||||||
(context
|
(context
|
||||||
.textTheme
|
.textTheme
|
||||||
@@ -338,7 +332,7 @@ class _AddCommentButton extends ConsumerWidget {
|
|||||||
class _AddStackCommentButton extends ConsumerWidget {
|
class _AddStackCommentButton extends ConsumerWidget {
|
||||||
const _AddStackCommentButton({required this.stack});
|
const _AddStackCommentButton({required this.stack});
|
||||||
|
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -401,9 +395,7 @@ class _CommentComposer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Comment on $versionToComment',
|
'Comment on $versionToComment',
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
@@ -532,7 +524,7 @@ class _CommentComposer extends HookConsumerWidget {
|
|||||||
class _StackCommentComposer extends HookConsumerWidget {
|
class _StackCommentComposer extends HookConsumerWidget {
|
||||||
const _StackCommentComposer({required this.stack});
|
const _StackCommentComposer({required this.stack});
|
||||||
|
|
||||||
final AppPack stack;
|
final AppStack stack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -557,9 +549,7 @@ class _StackCommentComposer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Comment on $stackName',
|
'Comment on $stackName',
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ class NWCConnectionCard extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Lightning Wallet',
|
'Lightning Wallet',
|
||||||
style: context.textTheme.titleMedium?.copyWith(
|
style: context.textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class ProfileHeader extends StatelessWidget {
|
|||||||
// Display name
|
// Display name
|
||||||
Text(
|
Text(
|
||||||
profile!.name ?? profile!.nameOrNpub,
|
profile!.name ?? profile!.nameOrNpub,
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
context,
|
|
||||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ class ZappersHorizontalList extends StatelessWidget {
|
|||||||
'⚡️${formatSatsCompact(totalSats)}',
|
'⚡️${formatSatsCompact(totalSats)}',
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
|
|||||||
Reference in New Issue
Block a user