mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Update screen UI improvements
This commit is contained in:
@@ -16,6 +16,16 @@ class UpdatesScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categorized = ref.watch(categorizedAppsProvider);
|
||||
final updatesCount =
|
||||
categorized.automaticUpdates.length + categorized.manualUpdates.length;
|
||||
final upToDateCount = categorized.upToDateApps.length;
|
||||
final bookmarksAsync = ref.watch(bookmarksProvider);
|
||||
final bookmarksCount = bookmarksAsync.maybeWhen(
|
||||
data: (ids) => ids.length,
|
||||
orElse: () => 0,
|
||||
);
|
||||
|
||||
final baseTabStyle = context.textTheme.titleMedium;
|
||||
final tabLabelStyle = (baseTabStyle ?? const TextStyle()).copyWith(
|
||||
fontSize: (baseTabStyle?.fontSize ?? 16) * 0.85,
|
||||
@@ -32,9 +42,27 @@ class UpdatesScreen extends HookConsumerWidget {
|
||||
labelStyle: tabLabelStyle,
|
||||
unselectedLabelStyle: tabLabelStyle,
|
||||
tabs: [
|
||||
Tab(text: 'Updates'),
|
||||
Tab(text: 'Up to date'),
|
||||
Tab(text: 'Bookmarks'),
|
||||
Tab(
|
||||
child: _TabLabelWithBadge(
|
||||
label: 'Updates',
|
||||
count: updatesCount,
|
||||
textStyle: tabLabelStyle,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: _TabLabelWithBadge(
|
||||
label: 'Up to date',
|
||||
count: upToDateCount,
|
||||
textStyle: tabLabelStyle,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: _TabLabelWithBadge(
|
||||
label: 'Bookmarks',
|
||||
count: bookmarksCount,
|
||||
textStyle: tabLabelStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -75,8 +103,8 @@ class _UpdatesTab extends HookConsumerWidget {
|
||||
? ref.watch(
|
||||
query<App>(
|
||||
tags: {'#d': installingAppIds},
|
||||
and: (app) => {app.latestRelease, app.author},
|
||||
andSource: const LocalAndRemoteSource(relays: 'social'),
|
||||
and: (app) => {app.latestRelease},
|
||||
source: const LocalAndRemoteSource(relays: 'AppCatalog'),
|
||||
subscriptionPrefix: 'installing-apps',
|
||||
),
|
||||
)
|
||||
@@ -95,6 +123,42 @@ class _UpdatesTab extends HookConsumerWidget {
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (automaticUpdates.isEmpty &&
|
||||
manualUpdates.isEmpty &&
|
||||
installingApps.isEmpty) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'🎉',
|
||||
style: TextStyle(fontSize: 48),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'All apps are up to date',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Great job staying current!',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
if (installingApps.isNotEmpty) ...[
|
||||
@@ -164,7 +228,7 @@ class _UpdatesTab extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Ready to Update',
|
||||
'Updates',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -281,7 +345,7 @@ class _UpdatesTab extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Ready to Manually Update',
|
||||
'Manual Updates',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -312,13 +376,75 @@ class _UpdatesTab extends HookConsumerWidget {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AsyncButtonBuilder(
|
||||
onPressed: () async {
|
||||
final downloadService = ref.read(
|
||||
downloadServiceProvider.notifier,
|
||||
);
|
||||
for (final app in manualUpdates) {
|
||||
final release = app.latestRelease.value;
|
||||
if (release != null) {
|
||||
try {
|
||||
await downloadService.downloadApp(app, release);
|
||||
} catch (_) {
|
||||
// Ignore download start failures per-app to keep others going
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, child, callback, buttonState) {
|
||||
const pillBg = AppColors.darkPillBackground;
|
||||
const pillText = Colors.white;
|
||||
|
||||
return TextButton.icon(
|
||||
onPressed: buttonState.maybeWhen(
|
||||
loading: () => null,
|
||||
orElse: () => callback,
|
||||
),
|
||||
icon: buttonState.maybeWhen(
|
||||
loading: () => const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: pillText,
|
||||
),
|
||||
),
|
||||
orElse: () => const Icon(
|
||||
Icons.download,
|
||||
size: 14,
|
||||
color: pillText,
|
||||
),
|
||||
),
|
||||
label: const Text(
|
||||
'Update All',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: pillText,
|
||||
),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: pillBg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Text(
|
||||
'These updates require confirming the Android system prompt',
|
||||
'Confirming the Android system prompt is required',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
@@ -464,3 +590,67 @@ class _BookmarksTab extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabLabelWithBadge extends StatelessWidget {
|
||||
const _TabLabelWithBadge({
|
||||
required this.label,
|
||||
required this.count,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int count;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle.merge(
|
||||
style: textStyle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Text(label),
|
||||
if (count > 0)
|
||||
Positioned(
|
||||
top: -6,
|
||||
right: -10,
|
||||
child: _CountBadge(count: count),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountBadge extends StatelessWidget {
|
||||
const _CountBadge({required this.count});
|
||||
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final display = count > 99 ? '99+' : count.toString();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkPillBackground.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
||||
child: Text(
|
||||
display,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,7 @@ class AppHeader extends StatelessWidget {
|
||||
minFontSize: 16,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
VersionPillWidget(app: app, showUpdateArrow: true),
|
||||
],
|
||||
),
|
||||
VersionPillWidget(app: app, showUpdateArrow: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -86,13 +86,15 @@ class VersionPillWidget extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Arrow icon (always arrow, forbidden icon is in the pill itself)
|
||||
Icon(
|
||||
Icons.arrow_right,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Icon(
|
||||
Icons.arrow_right,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
// Available version pill (highlighted for update, greyed for downgrade)
|
||||
_buildVersionPill(
|
||||
context,
|
||||
ref,
|
||||
@@ -120,8 +122,8 @@ class VersionPillWidget extends HookConsumerWidget {
|
||||
bool isInstalledVersion = false,
|
||||
bool isDowngrade = false,
|
||||
}) {
|
||||
// Trim version text to max 8 characters to make room for icon
|
||||
String displayVersion = _displayVersion(version);
|
||||
// Use full version text; no trimming
|
||||
final displayVersion = version;
|
||||
|
||||
// Determine app status for icon
|
||||
Widget? statusIcon;
|
||||
@@ -168,8 +170,9 @@ class VersionPillWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
displayVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: null,
|
||||
overflow: TextOverflow.visible,
|
||||
softWrap: true,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: finalTextColor,
|
||||
@@ -188,9 +191,5 @@ class VersionPillWidget extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _displayVersion(String version) {
|
||||
return version.length > 10 ? '${version.substring(0, 9)}...' : version;
|
||||
}
|
||||
|
||||
// No width enforcement; both pills share the same text style
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user