diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 212d84e5..495519dd 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -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) { diff --git a/lib/widgets/account/account_pfc.dart b/lib/widgets/account/account_pfc.dart index f4565376..4d618ee7 100644 --- a/lib/widgets/account/account_pfc.dart +++ b/lib/widgets/account/account_pfc.dart @@ -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), ], diff --git a/lib/widgets/account/activity_presence.dart b/lib/widgets/account/activity_presence.dart index 25047988..23081c33 100644 --- a/lib/widgets/account/activity_presence.dart +++ b/lib/widgets/account/activity_presence.dart @@ -68,8 +68,15 @@ const kPresenseActivityIcons = [ 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 _buildDiscordImages(WidgetRef ref, SnPresenceActivity activity) { final List 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), ), ),