diff --git a/lib/widgets/account/friends_overview.dart b/lib/widgets/account/friends_overview.dart index 578a89dc..1a0d379b 100644 --- a/lib/widgets/account/friends_overview.dart +++ b/lib/widgets/account/friends_overview.dart @@ -12,6 +12,7 @@ import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:gap/gap.dart'; +import 'package:skeletonizer/skeletonizer.dart'; part 'friends_overview.g.dart'; @@ -50,8 +51,9 @@ class FriendsOverviewWidget extends HookConsumerWidget { return friendsOverviewAsync.when( data: (friends) { // Filter for online friends - final onlineFriends = - friends.where((friend) => friend.status.isOnline).toList(); + final onlineFriends = friends + .where((friend) => friend.status.isOnline) + .toList(); if (onlineFriends.isEmpty && hideWhenEmpty) { return const SizedBox.shrink(); @@ -62,12 +64,23 @@ class FriendsOverviewWidget extends HookConsumerWidget { child: Column( children: [ Row( - spacing: 8, children: [ - const Icon(Symbols.group), - Text('friendsOnline').tr(), + Icon( + Symbols.group, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'friendsOnline'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), ], - ).padding(horizontal: 16).height(48), + ).padding(horizontal: 16, vertical: 12), if (onlineFriends.isEmpty) Container( height: 80, @@ -105,16 +118,115 @@ class FriendsOverviewWidget extends HookConsumerWidget { } return result; }, - loading: - () => const SizedBox( - height: 80, - child: Center(child: CircularProgressIndicator()), + loading: () { + final card = Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + Row( + children: [ + Icon( + Symbols.group, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'friendsOnline'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ).padding(horizontal: 16, vertical: 12), + SizedBox( + height: 80, + child: ListView( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 4), + scrollDirection: Axis.horizontal, + children: List.generate( + 4, + (index) => const SkeletonFriendTile(), + ), + ), + ), + ], ), + ); + + Widget result = Skeletonizer(child: card); + if (padding != null) { + result = Padding(padding: padding!, child: result); + } + return result; + }, error: (error, stack) => const SizedBox.shrink(), // Hide on error ); } } +class SkeletonFriendTile extends StatelessWidget { + const SkeletonFriendTile({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 60, + margin: const EdgeInsets.only(right: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Avatar with online indicator + Stack( + children: [ + CircleAvatar( + radius: 24, + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Text( + 'A', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + // Online indicator - green dot for skeleton + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.surface, + width: 2, + ), + ), + ), + ), + ], + ), + const Gap(4), + // Name placeholder + Text( + 'Friend', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ).center(); + } +} + class _FriendTile extends ConsumerWidget { final SnFriendOverviewItem friend; @@ -141,19 +253,19 @@ class _FriendTile extends ConsumerWidget { children: [ CircleAvatar( radius: 24, - backgroundImage: - uri != null ? CachedNetworkImageProvider(uri) : null, - child: - uri == null - ? Text( - friend.account.nick.isNotEmpty - ? friend.account.nick[0].toUpperCase() - : friend.account.name[0].toUpperCase(), - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), - ) - : null, + backgroundImage: uri != null + ? CachedNetworkImageProvider(uri) + : null, + child: uri == null + ? Text( + friend.account.nick.isNotEmpty + ? friend.account.nick[0].toUpperCase() + : friend.account.name[0].toUpperCase(), + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ) + : null, ), // Online indicator - show play arrow if user has activities, otherwise green dot Positioned( @@ -163,32 +275,28 @@ class _FriendTile extends ConsumerWidget { width: 16, height: 16, decoration: BoxDecoration( - color: - friend.activities.isNotEmpty - ? Colors.blue.withOpacity(0.8) - : Colors.green, - shape: - friend.activities.isNotEmpty - ? BoxShape.rectangle - : BoxShape.circle, - borderRadius: - friend.activities.isNotEmpty - ? BorderRadius.circular(4) - : null, + color: friend.activities.isNotEmpty + ? Colors.blue.withOpacity(0.8) + : Colors.green, + shape: friend.activities.isNotEmpty + ? BoxShape.rectangle + : BoxShape.circle, + borderRadius: friend.activities.isNotEmpty + ? BorderRadius.circular(4) + : null, border: Border.all( color: theme.colorScheme.surface, width: 2, ), ), - child: - friend.activities.isNotEmpty - ? Icon( - Symbols.play_arrow, - size: 10, - color: Colors.white, - fill: 1, - ) - : null, + child: friend.activities.isNotEmpty + ? Icon( + Symbols.play_arrow, + size: 10, + color: Colors.white, + fill: 1, + ) + : null, ), ), ], diff --git a/lib/widgets/post/post_featured.dart b/lib/widgets/post/post_featured.dart index bd2a94ee..4e3aaeff 100644 --- a/lib/widgets/post/post_featured.dart +++ b/lib/widgets/post/post_featured.dart @@ -94,9 +94,19 @@ class PostFeaturedList extends HookConsumerWidget { child: Row( spacing: 8, children: [ - const Icon(Symbols.highlight), - const Text('highlightPost').tr(), - Spacer(), + Icon( + Symbols.highlight, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + Expanded( + child: Text( + 'highlightPost'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), IconButton( padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, diff --git a/pubspec.lock b/pubspec.lock index 760501cb..d968e296 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1401,10 +1401,10 @@ packages: dependency: "direct main" description: name: image - sha256: "48c11d0943b93b6fb29103d956ff89aafeae48f6058a3939649be2093dcff0bf" + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.7.2" image_picker: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 86f0af9c..e09e61a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,7 +102,7 @@ dependencies: livekit_client: ^2.5.4 pasteboard: ^0.4.0 flutter_colorpicker: ^1.1.0 - image: ^4.7.1 + image: ^4.7.2 record: ^6.1.2 qr_flutter: ^4.1.0 flutter_otp_text_field: ^1.5.1+1