Align skeletons, logo

This commit is contained in:
franzap
2025-12-13 14:49:42 -03:00
parent ecab49fcda
commit 6a9612ee49
10 changed files with 240 additions and 109 deletions
+3 -6
View File
@@ -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/package_manager/dummy_package_manager.dart';
import 'package:zapstore/services/market_intent_service.dart'; import 'package:zapstore/services/market_intent_service.dart';
import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/widgets/breathing_logo.dart';
/// Global provider container for error reporting (accessible outside widget tree) /// Global provider container for error reporting (accessible outside widget tree)
late final ProviderContainer _providerContainer; late final ProviderContainer _providerContainer;
@@ -188,12 +189,8 @@ class ZapstoreHome extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( const BreathingLogo(size: 120),
Icons.rocket_launch, const SizedBox(height: 24),
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text('Zapstore', style: context.textTheme.headlineMedium), Text('Zapstore', style: context.textTheme.headlineMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
+5 -19
View File
@@ -38,10 +38,6 @@ class ProfileScreen extends ConsumerWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
// Debug Messages Section
const _DebugMessagesSection(),
const SizedBox(height: 24),
// Settings Heading // Settings Heading
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -53,18 +49,6 @@ class ProfileScreen extends ConsumerWidget {
// Lightning Wallet Section // Lightning Wallet Section
const NWCConnectionCard(), 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), const SizedBox(height: 16),
// Data Management Section // Data Management Section
@@ -74,6 +58,11 @@ class ProfileScreen extends ConsumerWidget {
// About Section // About Section
_AboutSection(), _AboutSection(),
const SizedBox(height: 24),
// Debug Messages Section
const _DebugMessagesSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
), ),
@@ -233,9 +222,6 @@ class _AuthenticationSection extends ConsumerWidget {
Future<void> _signOut(BuildContext context, WidgetRef ref) async { Future<void> _signOut(BuildContext context, WidgetRef ref) async {
try { try {
await ref.read(amberSignerProvider).signOut(); await ref.read(amberSignerProvider).signOut();
if (context.mounted) {
context.showInfo('Signed out successfully');
}
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
context.showError('Sign out failed: $e'); context.showError('Sign out failed: $e');
+1 -2
View File
@@ -75,8 +75,7 @@ class CategorizedAppsNotifier extends StateNotifier<CategorizedApps> {
}, },
and: (app) => { and: (app) => {
app.latestRelease, app.latestRelease,
if (app.latestRelease.value != null) app.latestRelease.value?.latestMetadata,
app.latestRelease.value!.latestMetadata,
}, },
source: const LocalAndRemoteSource( source: const LocalAndRemoteSource(
relays: 'AppCatalog', relays: 'AppCatalog',
+113 -32
View File
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart'; import 'package:models/models.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/utils/extensions.dart';
@@ -57,6 +58,13 @@ class AppCard extends HookConsumerWidget {
// Use the working author (provided, relationship, or fallback) // Use the working author (provided, relationship, or fallback)
final actualAuthor = author ?? relationshipAuthor ?? fallbackAuthor; 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( return GestureDetector(
onTap: () { onTap: () {
@@ -88,40 +96,57 @@ class AppCard extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// App Name and Version Row // App Name and Version
Row( if (showUpdateButton) ...[
mainAxisAlignment: MainAxisAlignment.spaceBetween, // When showing update button: name on top, version below
children: [ Text(
Expanded( app!.name ?? app!.identifier,
child: Text( style: context.textTheme.titleMedium?.copyWith(
app!.name ?? app!.identifier, fontWeight: FontWeight.w900,
style: context.textTheme.titleMedium?.copyWith( letterSpacing: 0.1,
fontWeight: FontWeight.w900,
letterSpacing: 0.1,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
), ),
const SizedBox(width: 12), overflow: TextOverflow.ellipsis,
VersionPillWidget( maxLines: 1,
),
const SizedBox(height: 6),
Align(
alignment: Alignment.centerLeft,
child: VersionPillWidget(
app: app!, app: app!,
showUpdateArrow: showUpdateArrow, 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), const SizedBox(height: 12),
// App Description with improved readability // App Description rendered as plain text (markdown stripped)
Text( Text(
app!.description.isNotEmpty descriptionText,
? app!.description style: descriptionStyle,
: 'No description available',
style: context.textTheme.bodyMedium?.copyWith(
height: 1.5,
color: AppColors.darkOnSurfaceSecondary,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 3, maxLines: 3,
softWrap: true, 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) { Widget _buildSkeleton(BuildContext context) {
return SkeletonizerConfig( return SkeletonizerConfig(
data: AppColors.getSkeletonizerConfig(Theme.of(context).brightness), data: AppColors.getSkeletonizerConfig(Theme.of(context).brightness),
child: Skeletonizer( child: Skeletonizer(
enabled: true, enabled: true,
child: Container( 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), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -287,6 +358,7 @@ class AppCard extends HookConsumerWidget {
), ),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// App icon skeleton // App icon skeleton
Container( Container(
@@ -326,7 +398,7 @@ class AppCard extends HookConsumerWidget {
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Description skeleton // Description skeleton - 3 lines to match actual maxLines: 3
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -338,10 +410,19 @@ class AppCard extends HookConsumerWidget {
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 6),
Container( Container(
height: 16, 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( decoration: BoxDecoration(
color: AppColors.darkSkeletonBase, color: AppColors.darkSkeletonBase,
borderRadius: BorderRadius.circular(4), 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) ...[ if (showSignedBy) ...[
const SizedBox(height: 12), const SizedBox(height: 14),
Row( Row(
children: [ children: [
CircleAvatar( CircleAvatar(
+63 -12
View File
@@ -69,7 +69,7 @@ class AppPackContainer extends HookConsumerWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Skeleton pills // Skeleton pills - match actual pill structure with "by (author)" section
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Padding( child: Padding(
@@ -83,15 +83,7 @@ class AppPackContainer extends HookConsumerWidget {
child: Row( child: Row(
children: List.generate( children: List.generate(
3, 3,
(index) => Container( (index) => _buildSkeletonPill(context),
width: 120,
height: 32,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: AppColors.darkSkeletonBase,
borderRadius: BorderRadius.circular(16),
),
),
), ),
), ),
), ),
@@ -99,13 +91,72 @@ class AppPackContainer extends HookConsumerWidget {
), ),
), ),
// Skeleton grid // Skeleton grid - match actual spacing (16, not 24)
const SizedBox(height: 24), const SizedBox(height: 16),
_buildSelectedAppsGrid(context, []), // Empty list shows skeletons _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) { Widget _buildErrorState(BuildContext context, Object exception) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
+44
View File
@@ -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,
),
),
);
}
}
+9 -4
View File
@@ -183,10 +183,15 @@ class LatestReleasesContainer extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Latest Releases', style: context.textTheme.headlineMedium), // Match actual content: headlineSmall, not headlineMedium
TextButton( Text('Latest Releases', style: context.textTheme.headlineSmall),
onPressed: null, // Disabled during loading // Match actual content: show button but make it invisible to reserve space
child: const Text('See more'), Opacity(
opacity: 0,
child: TextButton(
onPressed: null,
child: const Text('See more'),
),
), ),
], ],
), ),
-33
View File
@@ -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
View File
@@ -610,7 +610,7 @@ packages:
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
markdown: markdown:
dependency: transitive dependency: "direct main"
description: description:
name: markdown name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
+1
View File
@@ -53,6 +53,7 @@ dependencies:
connectivity_plus: ^7.0.0 connectivity_plus: ^7.0.0
app_links: ^6.4.1 app_links: ^6.4.1
flutter_secure_storage: ^10.0.0 flutter_secure_storage: ^10.0.0
markdown: ^7.3.0
dependency_overrides: dependency_overrides:
models: models: