From 30416f7ca03e4604e992affa74fc88d149f188f0 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 14 Jun 2025 02:16:52 +0800 Subject: [PATCH] :lipstick: Optimized publisher profile & personal profile --- assets/i18n/en-US.json | 5 +- lib/route.gr.dart | 2 +- .../account/{me => }/event_calendar.dart | 49 +--------- lib/screens/account/profile.dart | 50 +++++++--- lib/screens/chat/room.dart | 1 + lib/screens/posts/pub_profile.dart | 93 ++++++++++++------- lib/services/time.dart | 22 +++++ lib/widgets/account/account_nameplate.dart | 51 ++++++++++ lib/widgets/account/account_pfc.dart | 40 +++++--- lib/widgets/account/fortune_graph.dart | 30 +++++- lib/widgets/account/status.dart | 28 ++++-- 11 files changed, 252 insertions(+), 119 deletions(-) rename lib/screens/account/{me => }/event_calendar.dart (65%) create mode 100644 lib/widgets/account/account_nameplate.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 6c9859d..de89f5b 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -314,7 +314,7 @@ "accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.", "unauthorized": "Unauthorized", "unauthorizedHint": "You're not signed in or session expired, please sign in again.", - "publisherVisitAccountPage": "Visit the profile of {}", + "publisherBelongsTo": "Belongs to {}", "postVisibility": "Visibility", "postVisibilityPublic": "Public", "postVisibilityFriends": "Friends Only", @@ -424,5 +424,6 @@ "checkInResultT1": "Poor", "checkInResultT2": "Mid", "checkInResultT3": "Good", - "checkInResultT4": "Best" + "checkInResultT4": "Best", + "accountProfileView": "View Profile" } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index b6047bd..6d630be 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -15,7 +15,7 @@ import 'package:flutter/material.dart' as _i28; import 'package:island/models/post.dart' as _i30; import 'package:island/route.dart' as _i31; import 'package:island/screens/account.dart' as _i2; -import 'package:island/screens/account/me/event_calendar.dart' as _i14; +import 'package:island/screens/account/event_calendar.dart' as _i14; import 'package:island/screens/account/me/settings.dart' as _i3; import 'package:island/screens/account/me/update.dart' as _i25; import 'package:island/screens/account/profile.dart' as _i1; diff --git a/lib/screens/account/me/event_calendar.dart b/lib/screens/account/event_calendar.dart similarity index 65% rename from lib/screens/account/me/event_calendar.dart rename to lib/screens/account/event_calendar.dart index fa8181e..65b1234 100644 --- a/lib/screens/account/me/event_calendar.dart +++ b/lib/screens/account/event_calendar.dart @@ -6,6 +6,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/event_calendar.dart'; import 'package:island/screens/account/profile.dart'; +import 'package:island/widgets/account/account_nameplate.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/account/event_calendar.dart'; @@ -86,29 +87,7 @@ class EventCalanderScreen extends HookConsumerWidget { // Show user profile if viewing someone else's calendar if (name != 'me' && user.hasValue) - Container( - decoration: BoxDecoration( - border: Border.all( - width: - 1 / MediaQuery.of(context).devicePixelRatio, - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - margin: EdgeInsets.all(16), - child: Card( - margin: EdgeInsets.zero, - elevation: 0, - color: Colors.transparent, - child: ListTile( - leading: ProfilePictureWidget( - fileId: user.value!.profile.picture?.id, - ), - title: Text(user.value!.nick).bold(), - subtitle: Text('@${user.value!.name}'), - ), - ), - ), + AccountNameplate(name: name), ], ), ).center() @@ -132,28 +111,8 @@ class EventCalanderScreen extends HookConsumerWidget { // Show user profile if viewing someone else's calendar if (name != 'me' && user.hasValue) - Container( - decoration: BoxDecoration( - border: Border.all( - width: 1 / MediaQuery.of(context).devicePixelRatio, - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - margin: EdgeInsets.all(16), - child: Card( - margin: EdgeInsets.zero, - elevation: 0, - color: Colors.transparent, - child: ListTile( - leading: ProfilePictureWidget( - fileId: user.value!.profile.picture?.id, - ), - title: Text(user.value!.nick).bold(), - subtitle: Text('@${user.value!.name}'), - ), - ), - ), + AccountNameplate(name: name), + Gap(MediaQuery.of(context).padding.bottom + 16), ], ), ), diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index a26cf6b..e35e6d2 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -5,11 +5,13 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/user.dart'; import 'package:island/pods/config.dart'; +import 'package:island/pods/event_calendar.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/color.dart'; import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/badge.dart'; +import 'package:island/widgets/account/fortune_graph.dart'; import 'package:island/widgets/account/leveling_progress.dart'; import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/app_scaffold.dart'; @@ -67,7 +69,14 @@ class AccountProfileScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final now = DateTime.now(); + final account = ref.watch(accountProvider(name)); + final accountEvents = ref.watch( + eventCalendarProvider( + EventCalendarQuery(uname: name, year: now.year, month: now.month), + ), + ); final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name)); final appbarShadow = Shadow( @@ -173,22 +182,39 @@ class AccountProfileScreen extends HookConsumerWidget { mark: data.profile.verification!, ), ], - ).padding(horizontal: 20, bottom: 24), + ).padding(horizontal: 20), ), SliverToBoxAdapter( - child: const Divider(height: 1).padding(bottom: 24), + child: const Divider(height: 1).padding(vertical: 24), + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('bio').tr().bold(), + Text( + data.profile.bio.isEmpty + ? 'descriptionNone'.tr() + : data.profile.bio, + ), + ], + ).padding(horizontal: 24), + ), + + SliverToBoxAdapter( + child: const Divider(height: 1).padding(top: 24), + ), + SliverToBoxAdapter( + child: Column( + children: [ + FortuneGraphWidget( + events: accountEvents, + eventCalanderUser: data.name, + ), + ], + ).padding(all: 8), ), - if (data.profile.bio.isNotEmpty) - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('bio').tr().bold(), - Text(data.profile.bio), - ], - ).padding(horizontal: 24), - ), ], ), ), diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 62e8de3..81cd554 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -385,6 +385,7 @@ class ChatRoomScreen extends HookConsumerWidget { if (['messages.read'].contains(pkt.type)) return; if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) { + if (pkt.data?['room_id'] != chatRoom.value?.id) return; if (pkt.data?['sender_id'] == chatIdentity.value?.id) return; final sender = SnChatMember.fromJson( diff --git a/lib/screens/posts/pub_profile.dart b/lib/screens/posts/pub_profile.dart index af3bf4c..fabe2fd 100644 --- a/lib/screens/posts/pub_profile.dart +++ b/lib/screens/posts/pub_profile.dart @@ -17,6 +17,7 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/post_list.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -210,65 +211,85 @@ class PublisherProfileScreen extends HookConsumerWidget { spacing: 6, children: [ Text(data.nick).fontSize(20), + if (data.verification != null) + VerificationMark(mark: data.verification!), Text( '@${data.name}', ).fontSize(14).opacity(0.85), ], ), if (data.type == 0 && data.account != null) - InkWell( - onTap: () { - context.router.pushPath( - '/account/${data.account!.name}', - ); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 4, - children: [ - Text( - 'publisherVisitAccountPage'.tr( - args: ['@${data.account!.name}'], - ), - ).fontSize(14), - Icon(Icons.launch, size: 14), - ], - ).opacity(0.85), - ).padding(bottom: 6), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 6, + children: [ + Icon( + data.type == 0 + ? Symbols.person + : Symbols.workspaces, + fill: 1, + size: 17, + ), + Text( + 'publisherBelongsTo'.tr( + args: ['@${data.account!.name}'], + ), + ).fontSize(14), + ], + ).opacity(0.85).padding(bottom: 6), if (data.type == 0 && data.account != null) AccountStatusWidget( uname: data.account!.name, padding: EdgeInsets.zero, ), + OutlinedButton.icon( + onPressed: () { + Navigator.pop(context); + context.router.pushPath( + '/account/${data.name}', + ); + }, + icon: const Icon(Symbols.launch), + label: Text('accountProfileView').tr(), + style: ButtonStyle( + visualDensity: VisualDensity(vertical: -2), + ), + ).padding(top: 8), ], ), ), ], - ).padding(horizontal: 24, top: 24, bottom: 24), + ).padding(horizontal: 24, top: 24), ), SliverToBoxAdapter( child: Column( - spacing: 24, children: [ if (badges.value?.isNotEmpty ?? false) - BadgeList(badges: badges.value!), + BadgeList(badges: badges.value!).padding(top: 16), if (data.verification != null) - VerificationStatusCard(mark: data.verification!), + VerificationStatusCard( + mark: data.verification!, + ).padding(top: 16), ], - ).padding(horizontal: 24, bottom: 24), + ).padding(horizontal: 24), + ), + SliverToBoxAdapter( + child: const Divider(height: 1).padding(vertical: 24), + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('bio').tr().bold(), + Text( + data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio, + ), + ], + ).padding(horizontal: 24), + ), + SliverToBoxAdapter( + child: const Divider(height: 1).padding(top: 24), ), - SliverToBoxAdapter(child: const Divider(height: 1)), - if (data.bio.isNotEmpty) - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [Text('bio').tr().bold(), Text(data.bio)], - ).padding(horizontal: 24, top: 24), - ), - if (data.bio.isNotEmpty) - SliverToBoxAdapter( - child: const Divider(height: 1).padding(top: 24), - ), SliverPostList(pubName: name), SliverGap(MediaQuery.of(context).padding.bottom + 16), ], diff --git a/lib/services/time.dart b/lib/services/time.dart index 31bd462..6e61381 100644 --- a/lib/services/time.dart +++ b/lib/services/time.dart @@ -1,3 +1,7 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; +import 'package:relative_time/relative_time.dart'; + extension DurationFormatter on Duration { String formatDuration() { final isNegative = inMicroseconds < 0; @@ -16,3 +20,21 @@ extension DurationFormatter on Duration { return '${isNegative ? '-' : ''}$hours:$minutes:$seconds'; } } + +extension DateTimeFormatter on DateTime { + String formatSystem() { + return DateFormat.yMd().add_jm().format(toLocal()); + } + + String formatCustom(String pattern) { + return DateFormat(pattern).format(toLocal()); + } + + String formatWithLocale(String locale) { + return DateFormat.yMd().add_jm().format(toLocal()).toString(); + } + + String formatRelative(BuildContext context) { + return RelativeTime(context).format(toLocal()); + } +} diff --git a/lib/widgets/account/account_nameplate.dart b/lib/widgets/account/account_nameplate.dart new file mode 100644 index 0000000..b125e96 --- /dev/null +++ b/lib/widgets/account/account_nameplate.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/screens/account/profile.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class AccountNameplate extends HookConsumerWidget { + final String name; + const AccountNameplate({super.key, required this.name}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(accountProvider(name)); + + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 1 / MediaQuery.of(context).devicePixelRatio, + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + margin: EdgeInsets.all(16), + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + color: Colors.transparent, + child: user.when( + data: (account) => ListTile( + leading: ProfilePictureWidget( + fileId: account.profile.picture?.id, + ), + title: Text(account.nick).bold(), + subtitle: Text('@${account.name}'), + ), + loading: () => ListTile( + leading: const CircularProgressIndicator(), + title: const Text('loading').bold().tr(), + subtitle: const Text('...'), + ), + error: (error, stackTrace) => ListTile( + leading: Icon(Icons.error_outline, color: Colors.red), + title: Text('somethingWentWrong').bold().tr(), + subtitle: Text(error.toString()), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/account/account_pfc.dart b/lib/widgets/account/account_pfc.dart index 5c5c88a..14b1129 100644 --- a/lib/widgets/account/account_pfc.dart +++ b/lib/widgets/account/account_pfc.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart'; @@ -12,6 +13,7 @@ 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'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; class AccountProfileCard extends HookConsumerWidget { @@ -34,17 +36,18 @@ class AccountProfileCard extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ClipRRect( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), + if (data.profile.background != null) + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: CloudImageWidget(file: data.profile.background), + ), ), - child: AspectRatio( - aspectRatio: 16 / 9, - child: CloudImageWidget(file: data.profile.background), - ), - ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ @@ -76,6 +79,14 @@ class AccountProfileCard extends HookConsumerWidget { experience: data.profile.experience, progress: data.profile.levelingProgress, ).padding(top: 12), + FilledButton.tonalIcon( + onPressed: () { + Navigator.pop(context); + context.router.pushPath('/account/${data.name}'); + }, + icon: const Icon(Symbols.launch), + label: Text('accountProfileView').tr(), + ).padding(top: 12, horizontal: 2), ], ).padding(horizontal: 24, vertical: 16), ], @@ -86,9 +97,14 @@ class AccountProfileCard extends HookConsumerWidget { onRetry: () => ref.invalidate(accountProvider(uname)), ), loading: - () => Padding( - padding: const EdgeInsets.all(24), - child: CircularProgressIndicator(), + () => SizedBox( + width: width, + height: width, + child: + Padding( + padding: const EdgeInsets.all(24), + child: CircularProgressIndicator(), + ).center(), ), ), ), diff --git a/lib/widgets/account/fortune_graph.dart b/lib/widgets/account/fortune_graph.dart index 46da9ea..26f9704 100644 --- a/lib/widgets/account/fortune_graph.dart +++ b/lib/widgets/account/fortune_graph.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -23,6 +24,8 @@ class FortuneGraphWidget extends HookConsumerWidget { /// Callback when a point is selected final void Function(DateTime)? onPointSelected; + final String? eventCalanderUser; + const FortuneGraphWidget({ super.key, required this.events, @@ -30,6 +33,7 @@ class FortuneGraphWidget extends HookConsumerWidget { this.maxWidth = double.infinity, this.height = 180, this.onPointSelected, + this.eventCalanderUser, }); @override @@ -48,9 +52,27 @@ class FortuneGraphWidget extends HookConsumerWidget { final content = Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - 'fortuneGraph', - ).tr().fontSize(18).bold().padding(all: 16, bottom: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('fortuneGraph').tr().fontSize(18).bold(), + if (eventCalanderUser != null) + IconButton( + icon: const Icon(Icons.calendar_month, size: 20), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + context.router.pushNamed( + '/account/$eventCalanderUser/calendar', + ); + }, + ), + ], + ).padding(all: 16, bottom: 24), SizedBox( height: height, child: filteredEvents.when( @@ -75,7 +97,7 @@ class FortuneGraphWidget extends HookConsumerWidget { final maxDate = data.last.date; return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: LineChart( LineChartData( gridData: FlGridData( diff --git a/lib/widgets/account/status.dart b/lib/widgets/account/status.dart index 901de5a..31c15f7 100644 --- a/lib/widgets/account/status.dart +++ b/lib/widgets/account/status.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/user.dart'; import 'package:island/pods/network.dart'; +import 'package:island/screens/account/profile.dart'; +import 'package:island/services/time.dart'; import 'package:island/widgets/account/status_creation.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -104,14 +106,15 @@ class AccountStatusWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userStatus = ref.watch(accountStatusProvider(uname)); + final status = ref.watch(accountStatusProvider(uname)); + final account = ref.watch(accountProvider(uname)); return Padding( padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4), child: Row( spacing: 4, children: [ - if (userStatus.value?.isOnline ?? false) + if (status.value?.isOnline ?? false) Icon( Symbols.circle, fill: 1, @@ -119,13 +122,24 @@ class AccountStatusWidget extends HookConsumerWidget { size: 16, ).padding(right: 4) else - Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4), - if (userStatus.value?.isCustomized ?? false) - Text(userStatus.value?.label ?? 'unknown'.tr()) + Icon( + Symbols.circle, + color: Colors.grey, + size: 16, + ).padding(right: 4), + if (status.value?.isCustomized ?? false) + Text(status.value?.label ?? 'unknown'.tr()) else - Text((userStatus.value?.label ?? 'offline').toLowerCase()).tr(), + Text((status.value?.label ?? 'offline').toLowerCase()).tr(), + if (!(status.value?.isOnline ?? false) && + account.value?.profile.lastSeenAt != null) + Flexible( + child: Text( + account.value!.profile.lastSeenAt!.formatRelative(context), + ).opacity(0.75), + ), ], ), - ).opacity((userStatus.value?.isCustomized ?? false) ? 1 : 0.85); + ).opacity((status.value?.isCustomized ?? false) ? 1 : 0.85); } }