mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Align skeletons, logo
This commit is contained in:
+3
-6
@@ -20,6 +20,7 @@ import 'package:zapstore/services/package_manager/android_package_manager.dart';
|
||||
import 'package:zapstore/services/package_manager/dummy_package_manager.dart';
|
||||
import 'package:zapstore/services/market_intent_service.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/breathing_logo.dart';
|
||||
|
||||
/// Global provider container for error reporting (accessible outside widget tree)
|
||||
late final ProviderContainer _providerContainer;
|
||||
@@ -188,12 +189,8 @@ class ZapstoreHome extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.rocket_launch,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const BreathingLogo(size: 120),
|
||||
const SizedBox(height: 24),
|
||||
Text('Zapstore', style: context.textTheme.headlineMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
|
||||
@@ -38,10 +38,6 @@ class ProfileScreen extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Debug Messages Section
|
||||
const _DebugMessagesSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Settings Heading
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -53,18 +49,6 @@ class ProfileScreen extends ConsumerWidget {
|
||||
// Lightning Wallet Section
|
||||
const NWCConnectionCard(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Advanced Heading
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text('Advanced', style: context.textTheme.headlineSmall),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Relay Management Section - disabled due to API changes
|
||||
// const _RelayManagementSection(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Data Management Section
|
||||
@@ -74,6 +58,11 @@ class ProfileScreen extends ConsumerWidget {
|
||||
|
||||
// About Section
|
||||
_AboutSection(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Debug Messages Section
|
||||
const _DebugMessagesSection(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
@@ -233,9 +222,6 @@ class _AuthenticationSection extends ConsumerWidget {
|
||||
Future<void> _signOut(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
await ref.read(amberSignerProvider).signOut();
|
||||
if (context.mounted) {
|
||||
context.showInfo('Signed out successfully');
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Sign out failed: $e');
|
||||
|
||||
@@ -75,8 +75,7 @@ class CategorizedAppsNotifier extends StateNotifier<CategorizedApps> {
|
||||
},
|
||||
and: (app) => {
|
||||
app.latestRelease,
|
||||
if (app.latestRelease.value != null)
|
||||
app.latestRelease.value!.latestMetadata,
|
||||
app.latestRelease.value?.latestMetadata,
|
||||
},
|
||||
source: const LocalAndRemoteSource(
|
||||
relays: 'AppCatalog',
|
||||
|
||||
+113
-32
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
@@ -57,6 +58,13 @@ class AppCard extends HookConsumerWidget {
|
||||
|
||||
// Use the working author (provided, relationship, or fallback)
|
||||
final actualAuthor = author ?? relationshipAuthor ?? fallbackAuthor;
|
||||
final descriptionStyle = context.textTheme.bodyMedium?.copyWith(
|
||||
height: 1.5,
|
||||
color: AppColors.darkOnSurfaceSecondary,
|
||||
);
|
||||
final descriptionText = app!.description.isNotEmpty
|
||||
? _stripMarkdown(app!.description)
|
||||
: 'No description available';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -88,40 +96,57 @@ class AppCard extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// App Name and Version Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
app!.name ?? app!.identifier,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
// App Name and Version
|
||||
if (showUpdateButton) ...[
|
||||
// When showing update button: name on top, version below
|
||||
Text(
|
||||
app!.name ?? app!.identifier,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
VersionPillWidget(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: VersionPillWidget(
|
||||
app: app!,
|
||||
showUpdateArrow: showUpdateArrow,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Default: name and version in same row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
app!.name ?? app!.identifier,
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
VersionPillWidget(
|
||||
app: app!,
|
||||
showUpdateArrow: showUpdateArrow,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// App Description with improved readability
|
||||
// App Description rendered as plain text (markdown stripped)
|
||||
Text(
|
||||
app!.description.isNotEmpty
|
||||
? app!.description
|
||||
: 'No description available',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
height: 1.5,
|
||||
color: AppColors.darkOnSurfaceSecondary,
|
||||
),
|
||||
descriptionText,
|
||||
style: descriptionStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
softWrap: true,
|
||||
@@ -268,13 +293,59 @@ class AppCard extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses markdown and returns plain text without formatting artifacts.
|
||||
String _stripMarkdown(String input) {
|
||||
final doc = md.Document(encodeHtml: false);
|
||||
final nodes = doc.parseLines(input.split('\n'));
|
||||
final buffer = StringBuffer();
|
||||
|
||||
void writeNode(md.Node node) {
|
||||
if (node is md.Text) {
|
||||
buffer.write(node.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node is md.Element) {
|
||||
// Preserve spacing for block-level elements
|
||||
final isBlock = const {
|
||||
'p',
|
||||
'li',
|
||||
'ul',
|
||||
'ol',
|
||||
'blockquote',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
}.contains(node.tag);
|
||||
|
||||
if (node.tag == 'br') buffer.write('\n');
|
||||
|
||||
for (final child in node.children ?? []) {
|
||||
writeNode(child);
|
||||
}
|
||||
|
||||
if (isBlock) buffer.write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
for (final node in nodes) {
|
||||
writeNode(node);
|
||||
}
|
||||
|
||||
return buffer.toString().replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
}
|
||||
|
||||
Widget _buildSkeleton(BuildContext context) {
|
||||
return SkeletonizerConfig(
|
||||
data: AppColors.getSkeletonizerConfig(Theme.of(context).brightness),
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
// Match actual content: vertical: 6, not 8
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -287,6 +358,7 @@ class AppCard extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// App icon skeleton
|
||||
Container(
|
||||
@@ -326,7 +398,7 @@ class AppCard extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Description skeleton
|
||||
// Description skeleton - 3 lines to match actual maxLines: 3
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -338,10 +410,19 @@ class AppCard extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
height: 16,
|
||||
width: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
height: 16,
|
||||
width: 140,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -349,9 +430,9 @@ class AppCard extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Author skeleton (only if showSignedBy is true)
|
||||
// Author skeleton (only if showSignedBy is true) - match Gap(14) spacing
|
||||
if (showSignedBy) ...[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
|
||||
@@ -69,7 +69,7 @@ class AppPackContainer extends HookConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Skeleton pills
|
||||
// Skeleton pills - match actual pill structure with "by (author)" section
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
@@ -83,15 +83,7 @@ class AppPackContainer extends HookConsumerWidget {
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Container(
|
||||
width: 120,
|
||||
height: 32,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
(index) => _buildSkeletonPill(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -99,13 +91,72 @@ class AppPackContainer extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Skeleton grid
|
||||
const SizedBox(height: 24),
|
||||
// Skeleton grid - match actual spacing (16, not 24)
|
||||
const SizedBox(height: 16),
|
||||
_buildSelectedAppsGrid(context, []), // Empty list shows skeletons
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Skeleton pill that matches actual pill dimensions including "by (author)" section
|
||||
static Widget _buildSkeletonPill(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Pack name skeleton
|
||||
Container(
|
||||
height: 16,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
// "by" text skeleton
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Author avatar skeleton
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Author name skeleton
|
||||
Container(
|
||||
height: 14,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSkeletonBase,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context, Object exception) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
/// An animated breathing logo widget used for loading states
|
||||
/// Smoothly scales the Zapstore logo in a "breathe in, breathe out" motion
|
||||
class BreathingLogo extends HookWidget {
|
||||
final double size;
|
||||
const BreathingLogo({super.key, this.size = 200});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useAnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
final scaleAnimation = useMemoized(
|
||||
() => Tween<double>(begin: 0.85, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
),
|
||||
[controller],
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: scaleAnimation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: size,
|
||||
width: size,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +183,15 @@ class LatestReleasesContainer extends HookConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Latest Releases', style: context.textTheme.headlineMedium),
|
||||
TextButton(
|
||||
onPressed: null, // Disabled during loading
|
||||
child: const Text('See more'),
|
||||
// Match actual content: headlineSmall, not headlineMedium
|
||||
Text('Latest Releases', style: context.textTheme.headlineSmall),
|
||||
// Match actual content: show button but make it invisible to reserve space
|
||||
Opacity(
|
||||
opacity: 0,
|
||||
child: TextButton(
|
||||
onPressed: null,
|
||||
child: const Text('See more'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
/// An animated spinning logo widget used for loading states
|
||||
/// Continuously rotates the Zapstore logo image
|
||||
class SpinningLogo extends HookWidget {
|
||||
final double size;
|
||||
const SpinningLogo({super.key, this.size = 200});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useAnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat();
|
||||
|
||||
return Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: controller.value * 2.0 * 3.14159,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/logo-fg.png',
|
||||
height: size,
|
||||
width: size,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -610,7 +610,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||
|
||||
@@ -53,6 +53,7 @@ dependencies:
|
||||
connectivity_plus: ^7.0.0
|
||||
app_links: ^6.4.1
|
||||
flutter_secure_storage: ^10.0.0
|
||||
markdown: ^7.3.0
|
||||
|
||||
dependency_overrides:
|
||||
models:
|
||||
|
||||
Reference in New Issue
Block a user