💄 Unifined layout of dashboard cards

This commit is contained in:
2025-12-27 21:32:52 +08:00
parent e13928b03f
commit 804dd029b1
4 changed files with 168 additions and 50 deletions

View File

@@ -12,6 +12,7 @@ import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart';
part 'friends_overview.g.dart'; part 'friends_overview.g.dart';
@@ -50,8 +51,9 @@ class FriendsOverviewWidget extends HookConsumerWidget {
return friendsOverviewAsync.when( return friendsOverviewAsync.when(
data: (friends) { data: (friends) {
// Filter for online friends // Filter for online friends
final onlineFriends = final onlineFriends = friends
friends.where((friend) => friend.status.isOnline).toList(); .where((friend) => friend.status.isOnline)
.toList();
if (onlineFriends.isEmpty && hideWhenEmpty) { if (onlineFriends.isEmpty && hideWhenEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -62,12 +64,23 @@ class FriendsOverviewWidget extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
Row( Row(
spacing: 8,
children: [ children: [
const Icon(Symbols.group), Icon(
Text('friendsOnline').tr(), 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) if (onlineFriends.isEmpty)
Container( Container(
height: 80, height: 80,
@@ -105,16 +118,115 @@ class FriendsOverviewWidget extends HookConsumerWidget {
} }
return result; return result;
}, },
loading: loading: () {
() => const SizedBox( final card = Card(
height: 80, margin: EdgeInsets.zero,
child: Center(child: CircularProgressIndicator()), 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 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 { class _FriendTile extends ConsumerWidget {
final SnFriendOverviewItem friend; final SnFriendOverviewItem friend;
@@ -141,19 +253,19 @@ class _FriendTile extends ConsumerWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 24, radius: 24,
backgroundImage: backgroundImage: uri != null
uri != null ? CachedNetworkImageProvider(uri) : null, ? CachedNetworkImageProvider(uri)
child: : null,
uri == null child: uri == null
? Text( ? Text(
friend.account.nick.isNotEmpty friend.account.nick.isNotEmpty
? friend.account.nick[0].toUpperCase() ? friend.account.nick[0].toUpperCase()
: friend.account.name[0].toUpperCase(), : friend.account.name[0].toUpperCase(),
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
), ),
) )
: null, : null,
), ),
// Online indicator - show play arrow if user has activities, otherwise green dot // Online indicator - show play arrow if user has activities, otherwise green dot
Positioned( Positioned(
@@ -163,32 +275,28 @@ class _FriendTile extends ConsumerWidget {
width: 16, width: 16,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: friend.activities.isNotEmpty
friend.activities.isNotEmpty ? Colors.blue.withOpacity(0.8)
? Colors.blue.withOpacity(0.8) : Colors.green,
: Colors.green, shape: friend.activities.isNotEmpty
shape: ? BoxShape.rectangle
friend.activities.isNotEmpty : BoxShape.circle,
? BoxShape.rectangle borderRadius: friend.activities.isNotEmpty
: BoxShape.circle, ? BorderRadius.circular(4)
borderRadius: : null,
friend.activities.isNotEmpty
? BorderRadius.circular(4)
: null,
border: Border.all( border: Border.all(
color: theme.colorScheme.surface, color: theme.colorScheme.surface,
width: 2, width: 2,
), ),
), ),
child: child: friend.activities.isNotEmpty
friend.activities.isNotEmpty ? Icon(
? Icon( Symbols.play_arrow,
Symbols.play_arrow, size: 10,
size: 10, color: Colors.white,
color: Colors.white, fill: 1,
fill: 1, )
) : null,
: null,
), ),
), ),
], ],

View File

@@ -94,9 +94,19 @@ class PostFeaturedList extends HookConsumerWidget {
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [
const Icon(Symbols.highlight), Icon(
const Text('highlightPost').tr(), Symbols.highlight,
Spacer(), size: 20,
color: Theme.of(context).colorScheme.primary,
),
Expanded(
child: Text(
'highlightPost'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,

View File

@@ -1401,10 +1401,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image name: image
sha256: "48c11d0943b93b6fb29103d956ff89aafeae48f6058a3939649be2093dcff0bf" sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.1" version: "4.7.2"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -102,7 +102,7 @@ dependencies:
livekit_client: ^2.5.4 livekit_client: ^2.5.4
pasteboard: ^0.4.0 pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
image: ^4.7.1 image: ^4.7.2
record: ^6.1.2 record: ^6.1.2
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
flutter_otp_text_field: ^1.5.1+1 flutter_otp_text_field: ^1.5.1+1