diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index f90d5aa..77a340d 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -277,100 +277,108 @@ class _AppDetailContent extends HookConsumerWidget { ), ), - // Latest release section — shown as soon as version is known - if (latestMetadata != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: Container( - height: 1, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.2), + // Latest release section — always shown; skeletons until metadata loads + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Container( + height: 1, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.2), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Text( + 'LATEST RELEASE', + style: context.textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.85), + letterSpacing: 1.5, + fontWeight: FontWeight.bold, ), ), - Padding( + ), + Expanded( + child: Container( + height: 1, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.2), + ), + ), + ], + ), + const SizedBox(height: 16), + latestMetadata != null + ? Container( padding: const EdgeInsets.symmetric( - horizontal: 16, + horizontal: 12, + vertical: 8, ), - child: Text( - 'LATEST RELEASE', - style: context.textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.85), - letterSpacing: 1.5, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Container( - height: 1, + decoration: BoxDecoration( color: Theme.of( context, - ).colorScheme.onSurface.withValues(alpha: 0.2), + ).colorScheme.onSurface.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Version:', + style: context.textTheme.bodyMedium, + ), + Gap(4), + Text( + latestMetadata.version, + style: context.textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + Gap(4), + Text( + '(${formatDate(latestMetadata.createdAt)})', + style: context.textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), + ), + ), + ], + ), + ) + : ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + height: 32, + width: 220, + child: buildGradientLoader(context), ), ), - ], - ), - const SizedBox(height: 16), - 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: [ - Text( - 'Version:', - style: context.textTheme.bodyMedium, - ), - Gap(4), - Text( - latestMetadata.version, - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Gap(4), - Text( - '(${formatDate(latestMetadata.createdAt)})', - style: context.textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: latestRelease != null - ? ReleaseNotes(release: latestRelease) - : const ReleaseNotesSkeleton(), - ), - ], - ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: latestRelease != null + ? ReleaseNotes(release: latestRelease) + : const ReleaseNotesSkeleton(), + ), + ], ), + ), - if (latestMetadata != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: AppInfoTable(app: app, fileMetadata: latestMetadata), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AppInfoTable(app: app, fileMetadata: latestMetadata), + ), if (latestMetadata != null) CommentsSection(app: app, fileMetadata: latestMetadata), @@ -388,7 +396,7 @@ class _AppDetailContent extends HookConsumerWidget { ), // Sticky install button - InstallButton(app: app, release: latestRelease), + InstallButton(app: app), // Floating three-dot menu _buildFloatingMenu(context, ref, app, isInstalled, isSignedIn), diff --git a/lib/widgets/app_info_table.dart b/lib/widgets/app_info_table.dart index 9608608..840596f 100644 --- a/lib/widgets/app_info_table.dart +++ b/lib/widgets/app_info_table.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.dart'; import 'package:models/models.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:zapstore/utils/extensions.dart'; +import 'package:zapstore/widgets/app_detail_widgets.dart'; import 'package:zapstore/widgets/download_text_container.dart'; import 'package:zapstore/theme.dart'; @@ -86,51 +87,56 @@ class AppInfoTable extends HookConsumerWidget { ); } - if (fileMetadata?.hash != null) { - final full = fileMetadata!.hash; - rows.add( - _InfoRow(label: 'File hash', value: full.abbreviate(), copyValue: full), - ); - } + if (fileMetadata != null) { + if (fileMetadata!.hash.isNotEmpty) { + final full = fileMetadata!.hash; + rows.add( + _InfoRow(label: 'File hash', value: full.abbreviate(), copyValue: full), + ); + } - if (fileMetadata?.certificateHash != null) { - final full = fileMetadata!.certificateHash!; - rows.add( - _InfoRow( - label: 'Certificate hash', - value: full.abbreviate(), - copyValue: full, - ), - ); - } - - if (fileMetadata?.size != null) { - final sizeInBytes = fileMetadata!.size!; - final sizeInMB = (sizeInBytes / (1024 * 1024)).toStringAsFixed(2); - rows.add(_InfoRow(label: 'Size', value: '$sizeInMB MB')); - } - - if (fileMetadata?.versionCode != null) { - // Get installed package info for comparison - final installedPackage = app.installedPackage; - final installedVersionCode = installedPackage?.versionCode; - final availableVersionCode = fileMetadata!.versionCode!; - - rows.add( - _InfoRow( - label: 'Version code', - value: availableVersionCode.toString(), - valueWidget: _buildVersionCodePills( - context, - ref, - installedVersionCode, - availableVersionCode, + if (fileMetadata!.certificateHash != null) { + final full = fileMetadata!.certificateHash!; + rows.add( + _InfoRow( + label: 'Certificate hash', + value: full.abbreviate(), + copyValue: full, ), - ), - ); + ); + } + + if (fileMetadata!.size != null) { + final sizeInBytes = fileMetadata!.size!; + final sizeInMB = (sizeInBytes / (1024 * 1024)).toStringAsFixed(2); + rows.add(_InfoRow(label: 'Size', value: '$sizeInMB MB')); + } + + if (fileMetadata!.versionCode != null) { + final installedPackage = app.installedPackage; + final installedVersionCode = installedPackage?.versionCode; + final availableVersionCode = fileMetadata!.versionCode!; + + rows.add( + _InfoRow( + label: 'Version code', + value: availableVersionCode.toString(), + valueWidget: _buildVersionCodePills( + context, + ref, + installedVersionCode, + availableVersionCode, + ), + ), + ); + } + } else { + rows.add(const _InfoRowSkeleton(label: 'File hash')); + rows.add(const _InfoRowSkeleton(label: 'Size')); + rows.add(const _InfoRowSkeleton(label: 'Version code')); } - // Add release date as the last row + // Release date — skeleton when release not yet loaded final release = app.latestRelease.value; if (release?.createdAt != null) { rows.add( @@ -139,6 +145,8 @@ class AppInfoTable extends HookConsumerWidget { value: DateFormat('MMM d, y').format(release!.createdAt), ), ); + } else if (fileMetadata == null) { + rows.add(const _InfoRowSkeleton(label: 'Release date')); } return rows; @@ -225,6 +233,47 @@ class AppInfoTable extends HookConsumerWidget { } } +class _InfoRowSkeleton extends StatelessWidget { + const _InfoRowSkeleton({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + label, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Colors.grey[600]), + maxLines: 1, + ), + const SizedBox(width: 16), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: SizedBox( + height: 14, + width: 80, + child: buildGradientLoader(context), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + // SPDX id -> human friendly name (compact) const Map _spdxIdToName = { '0BSD': 'BSD Zero Clause', diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 9f0be34..02d60f6 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -16,12 +16,10 @@ class InstallButton extends ConsumerWidget { const InstallButton({ super.key, required this.app, - this.release, this.compact = false, }); final App app; - final Release? release; final bool compact; @override @@ -39,7 +37,6 @@ class InstallButton extends ConsumerWidget { isInstalled && (operation == null || operation is Completed); final hasUpdate = app.hasUpdate; final hasDowngrade = app.hasDowngrade; - final hasRelease = release != null; final fileMetadata = app.installable; // Listen for errors to show toasts @@ -59,7 +56,6 @@ class InstallButton extends ConsumerWidget { isInstalled: isInstalled, hasUpdate: hasUpdate, hasDowngrade: hasDowngrade, - hasRelease: hasRelease, fileMetadata: fileMetadata, fontSize: fontSize, ); @@ -103,7 +99,6 @@ class InstallButton extends ConsumerWidget { required bool isInstalled, required bool hasUpdate, required bool hasDowngrade, - required bool hasRelease, required Installable? fileMetadata, required double fontSize, }) { @@ -243,13 +238,18 @@ class InstallButton extends ConsumerWidget { } if (hasUpdate) { + if (fileMetadata == null) { + return _buildSimpleButton( + context, 'Update', null, + fontSize: fontSize, + isDisabled: true, + ); + } return _buildAsyncButton( context, ref, text: 'Update', - onPressed: hasRelease && fileMetadata != null - ? () => _startDownload(context, ref, fileMetadata) - : null, + onPressed: () => _startDownload(context, ref, fileMetadata), fontSize: fontSize, ); } @@ -266,13 +266,18 @@ class InstallButton extends ConsumerWidget { } // Not installed + if (fileMetadata == null) { + return _buildSimpleButton( + context, 'Install', null, + fontSize: fontSize, + isDisabled: true, + ); + } return _buildAsyncButton( context, ref, text: 'Install', - onPressed: hasRelease && fileMetadata != null - ? () => _startDownload(context, ref, fileMetadata) - : null, + onPressed: () => _startDownload(context, ref, fileMetadata), fontSize: fontSize, needsTrustCheck: true, );