Update screen UI improvements

This commit is contained in:
franzap
2025-12-13 20:00:24 -03:00
parent 6d52db9128
commit 8734f9c929
3 changed files with 211 additions and 27 deletions
+198 -8
View File
@@ -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,
),
);
}
}
+1 -6
View File
@@ -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),
],
),
),
+12 -13
View File
@@ -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
}