diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index aa085e3..5bdaab2 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -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( 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, + ), + ); + } +} diff --git a/lib/widgets/app_header.dart b/lib/widgets/app_header.dart index b4e2207..389b4ba 100644 --- a/lib/widgets/app_header.dart +++ b/lib/widgets/app_header.dart @@ -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), ], ), ), diff --git a/lib/widgets/version_pill_widget.dart b/lib/widgets/version_pill_widget.dart index 2c46b54..5c665db 100644 --- a/lib/widgets/version_pill_widget.dart +++ b/lib/widgets/version_pill_widget.dart @@ -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 }