mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Fix saved apps section stuck in loading or disappearing - #283
- Show "No saved apps yet" empty state instead of infinite loading - Simplify - Centralize empty state logic in _SavedAppsList - Fix SizedBox.shrink() causing section to disappear when apps not in local cache
This commit is contained in:
+95
-124
@@ -1480,80 +1480,111 @@ class _SavedAppsHeading extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final savedAppsAsync = ref.watch(bookmarksProvider);
|
// Always show heading when signed in
|
||||||
|
return Padding(
|
||||||
return savedAppsAsync.when(
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
loading: () => const SizedBox.shrink(),
|
child: Text('Saved Apps', style: context.textTheme.headlineSmall),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
|
||||||
data: (addressableIds) {
|
|
||||||
if (addressableIds.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final identifiers = addressableIds
|
|
||||||
.map((id) {
|
|
||||||
final parts = id.split(':');
|
|
||||||
return parts.length >= 3 ? parts[2] : null;
|
|
||||||
})
|
|
||||||
.whereType<String>()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
if (identifiers.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Text('Saved Apps', style: context.textTheme.headlineSmall),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SavedAppsSection extends HookConsumerWidget {
|
class _SavedAppsSection extends ConsumerWidget {
|
||||||
const _SavedAppsSection();
|
const _SavedAppsSection();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
||||||
final hasLoadedOnce = useState(false);
|
if (signedInPubkey == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
if (signedInPubkey == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final savedAppsAsync = ref.watch(bookmarksProvider);
|
final savedAppsAsync = ref.watch(bookmarksProvider);
|
||||||
|
|
||||||
return savedAppsAsync.when(
|
// Keep previous value during refresh, if available.
|
||||||
loading: () => _savedAppsLoadingCard(context),
|
final addressableIds = savedAppsAsync.valueOrNull;
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
|
||||||
data: (addressableIds) {
|
|
||||||
if (addressableIds.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final identifiers = addressableIds
|
// Show loading only on first load (when no value exists yet).
|
||||||
.map((id) {
|
if (addressableIds == null) {
|
||||||
final parts = id.split(':');
|
return _savedAppsLoadingCard(context);
|
||||||
return parts.length >= 3 ? parts[2] : null;
|
}
|
||||||
})
|
|
||||||
.whereType<String>()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
if (identifiers.isEmpty) {
|
final identifiers = _toIdentifiers(addressableIds);
|
||||||
return const SizedBox.shrink();
|
return _SavedAppsList(identifiers: identifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _SavedAppsList(
|
Set<String> _toIdentifiers(Set<String> addressableIds) {
|
||||||
identifiers: identifiers,
|
return addressableIds
|
||||||
hasLoadedOnce: hasLoadedOnce,
|
.map((id) => id.split(':'))
|
||||||
);
|
.where((parts) => parts.length >= 3)
|
||||||
},
|
.map((parts) => parts[2])
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _savedAppsLoadingCard(BuildContext context) => Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SavedAppsList extends ConsumerWidget {
|
||||||
|
const _SavedAppsList({required this.identifiers});
|
||||||
|
|
||||||
|
final Set<String> identifiers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// No bookmarks saved - show empty state without querying
|
||||||
|
if (identifiers.isEmpty) {
|
||||||
|
return _emptyState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
final savedAppsState = ref.watch(
|
||||||
|
query<App>(
|
||||||
|
tags: {'#d': identifiers},
|
||||||
|
and: (app) => {app.latestRelease.query()},
|
||||||
|
source: const LocalSource(),
|
||||||
|
subscriptionPrefix: 'profile-saved-apps',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isLoading = savedAppsState is StorageLoading;
|
||||||
|
|
||||||
|
final savedApps = savedAppsState.models.toList()
|
||||||
|
..sort(
|
||||||
|
(a, b) => (a.name ?? a.identifier).toLowerCase().compareTo(
|
||||||
|
(b.name ?? b.identifier).toLowerCase(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show spinner only when we truly have nothing to render yet
|
||||||
|
// If we're refreshing but still have models, keep showing the list
|
||||||
|
if (isLoading && savedApps.isEmpty) {
|
||||||
|
return _loadingCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedApps.isEmpty) {
|
||||||
|
return _emptyState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final app in savedApps)
|
||||||
|
AppCard(app: app, showUpdateArrow: false, showDescription: false),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _savedAppsLoadingCard(BuildContext context) {
|
Widget _loadingCard(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -1565,75 +1596,15 @@ class _SavedAppsSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _SavedAppsList extends ConsumerWidget {
|
Widget _emptyState(BuildContext context) {
|
||||||
const _SavedAppsList({
|
final theme = Theme.of(context);
|
||||||
required this.identifiers,
|
return Padding(
|
||||||
required this.hasLoadedOnce,
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
});
|
child: Text(
|
||||||
|
'No saved apps yet',
|
||||||
final Set<String> identifiers;
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
final ValueNotifier<bool> hasLoadedOnce;
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final savedAppsState = ref.watch(
|
|
||||||
query<App>(
|
|
||||||
tags: {'#d': identifiers},
|
|
||||||
and: (app) => {app.latestRelease.query()},
|
|
||||||
source: const LocalSource(),
|
|
||||||
subscriptionPrefix: 'profile-saved-apps',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final savedApps = savedAppsState.models.toList()
|
|
||||||
..sort(
|
|
||||||
(a, b) => (a.name ?? a.identifier).toLowerCase().compareTo(
|
|
||||||
(b.name ?? b.identifier).toLowerCase(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if we have loaded at least once - defer state update to after build
|
|
||||||
if (savedApps.isNotEmpty && !hasLoadedOnce.value) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
hasLoadedOnce.value = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading state only if we haven't loaded any apps yet
|
|
||||||
if (savedApps.isEmpty && !hasLoadedOnce.value) {
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide if no apps after loading
|
|
||||||
if (savedApps.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: savedApps
|
|
||||||
.map(
|
|
||||||
(app) => AppCard(
|
|
||||||
app: app,
|
|
||||||
showUpdateArrow: false,
|
|
||||||
showDescription: false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user