diff --git a/lib/main.dart b/lib/main.dart index 49120c2..ebc4f91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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( diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 7759429..64e34b8 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -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 _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'); diff --git a/lib/services/updates_service.dart b/lib/services/updates_service.dart index 446752f..20c7675 100644 --- a/lib/services/updates_service.dart +++ b/lib/services/updates_service.dart @@ -75,8 +75,7 @@ class CategorizedAppsNotifier extends StateNotifier { }, and: (app) => { app.latestRelease, - if (app.latestRelease.value != null) - app.latestRelease.value!.latestMetadata, + app.latestRelease.value?.latestMetadata, }, source: const LocalAndRemoteSource( relays: 'AppCatalog', diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index c4b9553..239576a 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -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( diff --git a/lib/widgets/app_pack_container.dart b/lib/widgets/app_pack_container.dart index eaf6213..a824012 100644 --- a/lib/widgets/app_pack_container.dart +++ b/lib/widgets/app_pack_container.dart @@ -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), diff --git a/lib/widgets/breathing_logo.dart b/lib/widgets/breathing_logo.dart new file mode 100644 index 0000000..3a74fa6 --- /dev/null +++ b/lib/widgets/breathing_logo.dart @@ -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(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, + ), + ), + ); + } +} + diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index 85db97e..08608bd 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -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'), + ), ), ], ), diff --git a/lib/widgets/spinning_logo.dart b/lib/widgets/spinning_logo.dart deleted file mode 100644 index 4e8e019..0000000 --- a/lib/widgets/spinning_logo.dart +++ /dev/null @@ -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, - ), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 6f174f9..e417a23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -610,7 +610,7 @@ packages: source: hosted version: "1.3.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" diff --git a/pubspec.yaml b/pubspec.yaml index 706efd0..b53c3ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: