mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
UI and updating fixes
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+74
-79
@@ -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);
|
||||
|
||||
@@ -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]!;
|
||||
|
||||
@@ -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<List<App>> {
|
||||
@override
|
||||
Future<List<App>> 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();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user