💄 Optimize the pfc and show the activities

This commit is contained in:
2025-11-02 00:25:08 +08:00
parent c093123e3a
commit bc60ce5d42
3 changed files with 192 additions and 29 deletions

View File

@@ -206,7 +206,7 @@ class _AccountProfileDetail extends StatelessWidget {
child: Row(
spacing: 6,
children: [
Icon(Symbols.star, size: 17, fill: 1).padding(right: 2),
Icon(Symbols.attribution, size: 17, fill: 1).padding(right: 2),
Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'),
Text('·').bold(),
switch (data.profile.socialCreditsLevel) {

View File

@@ -11,8 +11,9 @@ import 'package:island/screens/account/profile.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/activity_presence.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
@@ -54,7 +55,30 @@ class AccountProfileCard extends HookConsumerWidget {
children: [
Row(
children: [
ProfilePictureWidget(file: data.profile.picture),
GestureDetector(
child: Badge(
isLabelVisible: true,
padding: EdgeInsets.all(2),
label: Icon(
Symbols.launch,
size: 12,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor:
Theme.of(context).colorScheme.primary,
offset: Offset(4, 28),
child: ProfilePictureWidget(
file: data.profile.picture,
),
),
onTap: () {
Navigator.pop(context);
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.name},
);
},
),
const Gap(12),
Expanded(
child: Column(
@@ -81,7 +105,7 @@ class AccountProfileCard extends HookConsumerWidget {
spacing: 6,
children: [
Icon(
Symbols.star,
Symbols.attribution,
size: 17,
fill: 1,
).padding(right: 2),
@@ -144,25 +168,40 @@ class AccountProfileCard extends HookConsumerWidget {
).padding(top: 2);
}
}(),
Row(
spacing: 6,
children: [
Icon(
Symbols.stairs,
size: 17,
fill: 1,
).padding(right: 2),
Text(
'levelingProgressLevel'.tr(
args: [data.profile.level.toString()],
),
).fontSize(12),
Expanded(
child: Tooltip(
message:
'${(data.profile.levelingProgress * 100).toStringAsFixed(2)}%',
child: LinearProgressIndicator(
value: data.profile.levelingProgress,
stopIndicatorRadius: 0,
trackGap: 0,
minHeight: 4,
).padding(top: 1),
),
),
],
).padding(top: 2),
if (data.badges.isNotEmpty)
BadgeList(badges: data.badges).padding(top: 12),
LevelingProgressCard(
ActivityPresenceWidget(
uname: uname,
isCompact: true,
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
).padding(top: 12),
FilledButton.tonalIcon(
onPressed: () {
Navigator.pop(context);
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.name},
);
},
icon: const Icon(Symbols.launch),
label: Text('accountProfileView').tr(),
).padding(top: 12, horizontal: 2),
compactPadding: const EdgeInsets.only(top: 12),
),
],
).padding(horizontal: 24, vertical: 16),
],

View File

@@ -68,8 +68,15 @@ const kPresenseActivityIcons = <IconData>[
class ActivityPresenceWidget extends ConsumerWidget {
final String uname;
final bool isCompact;
final EdgeInsets compactPadding;
const ActivityPresenceWidget({super.key, required this.uname});
const ActivityPresenceWidget({
super.key,
required this.uname,
this.isCompact = false,
this.compactPadding = EdgeInsets.zero,
});
List<Widget> _buildDiscordImages(WidgetRef ref, SnPresenceActivity activity) {
final List<Widget> images = [];
@@ -139,6 +146,106 @@ class ActivityPresenceWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activitiesAsync = ref.watch(presenceActivitiesProvider(uname));
if (isCompact) {
return activitiesAsync.when(
data: (activities) {
if (activities.isEmpty) return const SizedBox.shrink();
final activity = activities.first;
return Padding(
padding: compactPadding,
child: Row(
spacing: 8,
children: [
if (activity.largeImage != null &&
activity.largeImage!.startsWith('discord:'))
ref
.watch(
discordAssetsUrlProvider(
activity,
activity.largeImage!.substring('discord:'.length),
),
)
.when(
data:
(url) =>
url != null
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: url,
width: 32,
height: 32,
),
)
: const SizedBox.shrink(),
loading:
() => const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 1),
),
error: (error, stack) => const SizedBox.shrink(),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(activity.title?.isEmpty ?? true)
? 'unknown'.tr()
: activity.title!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(13),
Row(
children: [
Text(
kPresenseActivityTypes[activity.type],
).tr().fontSize(11),
Icon(
kPresenseActivityIcons[activity.type],
size: 15,
fill: 1,
),
],
),
],
),
),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 1)),
builder: (context, snapshot) {
final now = DateTime.now();
// Check if lease has expired and refresh if needed
if (now.isAfter(activity.leaseExpiresAt)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.invalidate(presenceActivitiesProvider(uname));
});
}
final duration = now.difference(activity.createdAt);
final hours = duration.inHours.toString().padLeft(2, '0');
final minutes = (duration.inMinutes % 60)
.toString()
.padLeft(2, '0');
final seconds = (duration.inSeconds % 60)
.toString()
.padLeft(2, '0');
return Text(
'$hours:$minutes:$seconds',
).textColor(Colors.green).fontSize(12);
},
),
],
),
);
},
loading: () => const SizedBox.shrink(),
error: (error, stack) => const SizedBox.shrink(),
);
}
return activitiesAsync.when(
data:
(activities) => Card(
@@ -206,8 +313,12 @@ class ActivityPresenceWidget extends ConsumerWidget {
// Check if lease has expired and refresh if needed
if (now.isAfter(activity.leaseExpiresAt)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.invalidate(presenceActivitiesProvider(uname));
WidgetsBinding.instance.addPostFrameCallback((
_,
) {
ref.invalidate(
presenceActivitiesProvider(uname),
);
});
}
@@ -238,23 +349,36 @@ class ActivityPresenceWidget extends ConsumerWidget {
Row(
spacing: 8,
children: [
if (activity.titleUrl != null && activity.titleUrl!.isNotEmpty)
if (activity.titleUrl != null &&
activity.titleUrl!.isNotEmpty)
ElevatedButton.icon(
onPressed: () => launchUrlString(activity.titleUrl!),
onPressed:
() =>
launchUrlString(activity.titleUrl!),
icon: const Icon(Symbols.link, size: 16),
label: const Text('Open Title Link'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
textStyle: const TextStyle(fontSize: 12),
),
),
if (activity.subtitleUrl != null && activity.subtitleUrl!.isNotEmpty)
if (activity.subtitleUrl != null &&
activity.subtitleUrl!.isNotEmpty)
ElevatedButton.icon(
onPressed: () => launchUrlString(activity.subtitleUrl!),
onPressed:
() => launchUrlString(
activity.subtitleUrl!,
),
icon: const Icon(Symbols.link, size: 16),
label: const Text('Open Subtitle Link'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
textStyle: const TextStyle(fontSize: 12),
),
),