diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 8640b65..3dfd5e3 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -114,7 +114,8 @@ class SearchScreen extends HookConsumerWidget { )), ), if (searchResultState.value?.isNotEmpty ?? false) - for (final app in searchResultState.value!) AppCard(app: app), + for (final app in searchResultState.value!) + AppCard(model: app), Gap(20), const AppCurationContainer(), Gap(20), diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index 744b3e1..cccad87 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -69,7 +69,7 @@ class UpdatesScreen extends HookConsumerWidget { // Gap(10), for (final app in updatableApps) - AppCard(app: app, showUpdate: true), + AppCard(model: app, showUpdate: true), ], ), Gap(20), @@ -83,7 +83,7 @@ class UpdatesScreen extends HookConsumerWidget { ), ), Gap(10), - for (final app in updatedApps) AppCard(app: app), + for (final app in updatedApps) AppCard(model: app), ], ), ), diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index 0cec216..3b21da7 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:remove_markdown/remove_markdown.dart'; import 'package:skeletonizer/skeletonizer.dart'; +import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/local_app.dart'; import 'package:zapstore/widgets/author_container.dart'; @@ -14,54 +15,22 @@ import 'package:zapstore/widgets/pill_widget.dart'; import 'package:zapstore/widgets/rounded_image.dart'; class AppCard extends HookConsumerWidget { - final App? app; + final App model; final bool showDate; final bool showUpdate; const AppCard( {super.key, - required this.app, + required this.model, this.showUpdate = false, this.showDate = false}); @override Widget build(BuildContext context, WidgetRef ref) { - if (app == null) { - return Skeletonizer.zone( - child: Card( - margin: EdgeInsets.only(top: 6, bottom: 6), - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - children: [ - Bone.square(uniRadius: 10, size: 70), - ], - ), - Gap(16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Bone.text(fontSize: 20), - Padding( - padding: const EdgeInsets.only(top: 4), - child: Bone.multiText(lines: 4), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - final isUpdate = app!.canUpdate && showUpdate; + final state = ref.apps.watchOne(model, alsoWatch: (_) => {_.localApp}); + final app = state.model!; + + final isUpdate = app.canUpdate && showUpdate; return GestureDetector( onTap: () { context.go('${isUpdate ? '/updates' : ''}/details', extra: app); @@ -76,7 +45,7 @@ class AppCard extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ RoundedImage( - url: app!.icons.firstOrNull, + url: app.icons.firstOrNull, size: 64, radius: 15, ), @@ -91,7 +60,7 @@ class AppCard extends HookConsumerWidget { children: [ Expanded( child: AutoSizeText( - app!.name!, + app.name!, minFontSize: 16, style: TextStyle( fontSize: 19, fontWeight: FontWeight.bold), @@ -99,16 +68,15 @@ class AppCard extends HookConsumerWidget { maxLines: 1, ), ), - if (isUpdate) - SizedBox( - width: 90, - height: 40, - child: InstallButton( - app: app!, - compact: true, + if (app.latestMetadata?.version != null) + if (isUpdate) + PillWidget( + text: TextSpan( + text: app.localApp.value!.installedVersion!), + color: Colors.grey[800]!, + size: 11, ), - ), - if (!isUpdate && app!.latestMetadata?.version != null) + if (!isUpdate) PillWidget( text: WidgetSpan( alignment: PlaceholderAlignment.middle, @@ -116,13 +84,13 @@ class AppCard extends HookConsumerWidget { crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( - app!.latestMetadata!.version!, + app.latestMetadata!.version!, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 12, + fontSize: 11, ), ), - if (app!.localApp.value?.status == + if (app.localApp.value?.status == AppInstallStatus.updatable) Row( children: [ @@ -134,36 +102,16 @@ class AppCard extends HookConsumerWidget { ), ), size: 10, - color: app!.localApp.value?.status == + color: app.localApp.value?.status == AppInstallStatus.updatable ? kUpdateColor : Colors.grey[800]!, ), ], ), - if (isUpdate) Gap(6), - if (isUpdate) - Row( - children: [ - PillWidget( - text: TextSpan( - text: app!.localApp.value!.installedVersion!), - color: Colors.grey[800]!, - size: 9, - ), - Icon(Icons.arrow_right), - if (app!.latestMetadata != null) - PillWidget( - text: - TextSpan(text: app!.latestMetadata!.version!), - color: Colors.grey[800]!, - size: 9, - ), - ], - ), Gap(6), Text( - app!.content.removeMarkdown().parseEmojis(), + app.content.removeMarkdown().parseEmojis(), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w300), overflow: TextOverflow.ellipsis, @@ -171,15 +119,64 @@ class AppCard extends HookConsumerWidget { softWrap: true, ), Gap(6), - if (app!.signer.isPresent) + if (app.signer.isPresent) AuthorContainer( - user: app!.signer.value!, + user: app.signer.value!, text: 'Signed by', oneLine: true, size: 12, ), if (showDate) - Text(app!.latestRelease!.createdAt!.toIso8601String()), + Text(app.latestRelease!.createdAt!.toIso8601String()), + if (isUpdate) + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: InstallButton( + app: app, + compact: true, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class SkeletonAppCard extends StatelessWidget { + const SkeletonAppCard({super.key}); + + @override + Widget build(BuildContext context) { + return Skeletonizer.zone( + child: Card( + margin: EdgeInsets.only(top: 6, bottom: 6), + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + children: [ + Bone.square(uniRadius: 10, size: 70), + ], + ), + Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Bone.text(fontSize: 20), + Padding( + padding: const EdgeInsets.only(top: 4), + child: Bone.multiText(lines: 4), + ), ], ), ), @@ -279,5 +276,3 @@ extension StringWidget on String { }); } } - -const kUpdateColor = Color.fromARGB(255, 98, 115, 15); diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 2c201ea..fc7cff6 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -63,7 +63,7 @@ class InstallButton extends ConsumerWidget { } }, child: LinearPercentIndicator( - lineHeight: 40, + lineHeight: compact ? 30 : 42, percent: switch (progress) { VerifyingHashProgress() => 1, DownloadingInstallProgress(:final progress) => progress, @@ -74,7 +74,7 @@ class InstallButton extends ConsumerWidget { }, backgroundColor: switch (progress) { ErrorInstallProgress() => Colors.red, - _ => Colors.blue[700], + _ => kUpdateColor, }, progressColor: Colors.blue[800], barRadius: Radius.circular(20), @@ -88,27 +88,26 @@ class InstallButton extends ConsumerWidget { AppInstallStatus.updated => Text('Open'), _ => switch (progress) { IdleInstallProgress() => app.canUpdate - ? AutoSizeText( - 'Update${compact ? '' : ' to ${app.latestMetadata!.version!}'}', - maxLines: 1, + ? Padding( + padding: const EdgeInsets.only(left: 8, right: 8), + child: AutoSizeText( + 'Update to ${app.latestMetadata!.version!}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: compact ? 10 : 14, + fontWeight: FontWeight.bold), + ), ) : Text('Install'), DownloadingInstallProgress(:final progress) => Text( '${(progress * 100).floor()}%', style: TextStyle(fontWeight: FontWeight.bold), ), - VerifyingHashProgress() => compact - ? SizedBox( - width: 14, height: 14, child: CircularProgressIndicator()) - : Text('Verifying file integrity'), - HashVerifiedInstallProgress() => compact - ? SizedBox( - width: 14, height: 14, child: CircularProgressIndicator()) - : Text( - 'Hash verified, requesting ${app.canUpdate ? 'update' : 'installation'}'), - ErrorInstallProgress() => compact - ? SizedBox(width: 14, height: 14, child: Icon(Icons.error)) - : Text('Error, tap to see message'), + VerifyingHashProgress() => Text('Verifying file integrity'), + HashVerifiedInstallProgress() => Text( + 'Hash verified, requesting ${app.canUpdate ? 'update' : 'installation'}'), + ErrorInstallProgress() => Text('Error, tap to see message'), } }, ), @@ -202,3 +201,5 @@ class InstallAlertDialog extends ConsumerWidget { ); } } + +final kUpdateColor = Colors.blue[700]!; diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index b3fc298..2eda5ae 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -45,11 +45,11 @@ class LatestReleasesContainer extends HookConsumerWidget { children: [ if (state.hasError) Text('Error fetching: ${state.error}'), if (state.isLoading) - for (final _ in List.generate(3, (_) => _)) AppCard(app: null), + for (final _ in List.generate(3, (_) => _)) SkeletonAppCard(), if (state.hasValue) // NOTE: Since we're showing apps but it's really a list of releases // apps will appear repeated, to convert to set - for (final app in state.value!) AppCard(app: app), + for (final app in state.value!) AppCard(model: app), if (state.hasValue) AsyncButtonBuilder( loadingWidget: SizedBox( @@ -92,6 +92,7 @@ class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier> { @override Future> build() async { // TODO: Should be ref.watching a pool state change (from purplebase) + // TODO: Timer resets when rebuilding? final timer = Timer.periodic(Duration(minutes: 10), (_) => fetch()); ref.onDispose(timer.cancel); return localFetch(); diff --git a/lib/widgets/versioned_app_header.dart b/lib/widgets/versioned_app_header.dart index a738f5e..dfcdf7a 100644 --- a/lib/widgets/versioned_app_header.dart +++ b/lib/widgets/versioned_app_header.dart @@ -40,10 +40,23 @@ class VersionedAppHeader extends StatelessWidget { overflow: TextOverflow.ellipsis, ), Gap(10), - PillWidget( - text: TextSpan(text: app.latestMetadata!.version!), - color: Colors.grey[800]!, - size: 11, + Row( + children: [ + if (app.localApp.value?.installedVersion != null) + PillWidget( + text: + TextSpan(text: app.localApp.value!.installedVersion!), + color: Colors.grey[800]!, + size: 11, + ), + Icon(Icons.arrow_right), + if (app.canUpdate) + PillWidget( + text: TextSpan(text: app.latestMetadata!.version!), + color: Colors.blue[700]!, + size: 11, + ), + ], ), ], ),