💄 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: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,
),
),
],

View File

@@ -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,

View File

@@ -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:

View File

@@ -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