💄 Optimized publisher profile & personal profile

This commit is contained in:
LittleSheep 2025-06-14 02:16:52 +08:00
parent 6e74cf3a93
commit 30416f7ca0
11 changed files with 252 additions and 119 deletions

View File

@ -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.", "accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized", "unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.", "unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"publisherVisitAccountPage": "Visit the profile of {}", "publisherBelongsTo": "Belongs to {}",
"postVisibility": "Visibility", "postVisibility": "Visibility",
"postVisibilityPublic": "Public", "postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only", "postVisibilityFriends": "Friends Only",
@ -424,5 +424,6 @@
"checkInResultT1": "Poor", "checkInResultT1": "Poor",
"checkInResultT2": "Mid", "checkInResultT2": "Mid",
"checkInResultT3": "Good", "checkInResultT3": "Good",
"checkInResultT4": "Best" "checkInResultT4": "Best",
"accountProfileView": "View Profile"
} }

View File

@ -15,7 +15,7 @@ import 'package:flutter/material.dart' as _i28;
import 'package:island/models/post.dart' as _i30; import 'package:island/models/post.dart' as _i30;
import 'package:island/route.dart' as _i31; import 'package:island/route.dart' as _i31;
import 'package:island/screens/account.dart' as _i2; 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/settings.dart' as _i3;
import 'package:island/screens/account/me/update.dart' as _i25; import 'package:island/screens/account/me/update.dart' as _i25;
import 'package:island/screens/account/profile.dart' as _i1; import 'package:island/screens/account/profile.dart' as _i1;

View File

@ -6,6 +6,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/event_calendar.dart'; import 'package:island/pods/event_calendar.dart';
import 'package:island/screens/account/profile.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/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/account/event_calendar.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 // Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue) if (name != 'me' && user.hasValue)
Container( AccountNameplate(name: name),
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}'),
),
),
),
], ],
), ),
).center() ).center()
@ -132,28 +111,8 @@ class EventCalanderScreen extends HookConsumerWidget {
// Show user profile if viewing someone else's calendar // Show user profile if viewing someone else's calendar
if (name != 'me' && user.hasValue) if (name != 'me' && user.hasValue)
Container( AccountNameplate(name: name),
decoration: BoxDecoration( Gap(MediaQuery.of(context).padding.bottom + 16),
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}'),
),
),
),
], ],
), ),
), ),

View File

@ -5,11 +5,13 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/services/color.dart'; import 'package:island/services/color.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.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/leveling_progress.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -67,7 +69,14 @@ class AccountProfileScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final now = DateTime.now();
final account = ref.watch(accountProvider(name)); 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 appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name));
final appbarShadow = Shadow( final appbarShadow = Shadow(
@ -173,22 +182,39 @@ class AccountProfileScreen extends HookConsumerWidget {
mark: data.profile.verification!, mark: data.profile.verification!,
), ),
], ],
).padding(horizontal: 20, bottom: 24), ).padding(horizontal: 20),
), ),
SliverToBoxAdapter( 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),
),
], ],
), ),
), ),

View File

@ -385,6 +385,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (['messages.read'].contains(pkt.type)) return; if (['messages.read'].contains(pkt.type)) return;
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) { 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; if (pkt.data?['sender_id'] == chatIdentity.value?.id) return;
final sender = SnChatMember.fromJson( final sender = SnChatMember.fromJson(

View File

@ -17,6 +17,7 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/post_list.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:palette_generator/palette_generator.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';
@ -210,65 +211,85 @@ class PublisherProfileScreen extends HookConsumerWidget {
spacing: 6, spacing: 6,
children: [ children: [
Text(data.nick).fontSize(20), Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Text( Text(
'@${data.name}', '@${data.name}',
).fontSize(14).opacity(0.85), ).fontSize(14).opacity(0.85),
], ],
), ),
if (data.type == 0 && data.account != null) if (data.type == 0 && data.account != null)
InkWell( Row(
onTap: () { crossAxisAlignment: CrossAxisAlignment.center,
context.router.pushPath( spacing: 6,
'/account/${data.account!.name}', children: [
); Icon(
}, data.type == 0
child: Row( ? Symbols.person
crossAxisAlignment: CrossAxisAlignment.center, : Symbols.workspaces,
spacing: 4, fill: 1,
children: [ size: 17,
Text( ),
'publisherVisitAccountPage'.tr( Text(
args: ['@${data.account!.name}'], 'publisherBelongsTo'.tr(
), args: ['@${data.account!.name}'],
).fontSize(14), ),
Icon(Icons.launch, size: 14), ).fontSize(14),
], ],
).opacity(0.85), ).opacity(0.85).padding(bottom: 6),
).padding(bottom: 6),
if (data.type == 0 && data.account != null) if (data.type == 0 && data.account != null)
AccountStatusWidget( AccountStatusWidget(
uname: data.account!.name, uname: data.account!.name,
padding: EdgeInsets.zero, 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( SliverToBoxAdapter(
child: Column( child: Column(
spacing: 24,
children: [ children: [
if (badges.value?.isNotEmpty ?? false) if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!), BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null) 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), SliverPostList(pubName: name),
SliverGap(MediaQuery.of(context).padding.bottom + 16), SliverGap(MediaQuery.of(context).padding.bottom + 16),
], ],

View File

@ -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 { extension DurationFormatter on Duration {
String formatDuration() { String formatDuration() {
final isNegative = inMicroseconds < 0; final isNegative = inMicroseconds < 0;
@ -16,3 +20,21 @@ extension DurationFormatter on Duration {
return '${isNegative ? '-' : ''}$hours:$minutes:$seconds'; 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());
}
}

View File

@ -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()),
),
),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_popup_card/flutter_popup_card.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/account/status.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
class AccountProfileCard extends HookConsumerWidget { class AccountProfileCard extends HookConsumerWidget {
@ -34,17 +36,18 @@ class AccountProfileCard extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
ClipRRect( if (data.profile.background != null)
borderRadius: const BorderRadius.vertical( ClipRRect(
top: Radius.circular(12), 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( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Row(
children: [ children: [
@ -76,6 +79,14 @@ class AccountProfileCard extends HookConsumerWidget {
experience: data.profile.experience, experience: data.profile.experience,
progress: data.profile.levelingProgress, progress: data.profile.levelingProgress,
).padding(top: 12), ).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), ).padding(horizontal: 24, vertical: 16),
], ],
@ -86,9 +97,14 @@ class AccountProfileCard extends HookConsumerWidget {
onRetry: () => ref.invalidate(accountProvider(uname)), onRetry: () => ref.invalidate(accountProvider(uname)),
), ),
loading: loading:
() => Padding( () => SizedBox(
padding: const EdgeInsets.all(24), width: width,
child: CircularProgressIndicator(), height: width,
child:
Padding(
padding: const EdgeInsets.all(24),
child: CircularProgressIndicator(),
).center(),
), ),
), ),
), ),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,6 +24,8 @@ class FortuneGraphWidget extends HookConsumerWidget {
/// Callback when a point is selected /// Callback when a point is selected
final void Function(DateTime)? onPointSelected; final void Function(DateTime)? onPointSelected;
final String? eventCalanderUser;
const FortuneGraphWidget({ const FortuneGraphWidget({
super.key, super.key,
required this.events, required this.events,
@ -30,6 +33,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
this.maxWidth = double.infinity, this.maxWidth = double.infinity,
this.height = 180, this.height = 180,
this.onPointSelected, this.onPointSelected,
this.eventCalanderUser,
}); });
@override @override
@ -48,9 +52,27 @@ class FortuneGraphWidget extends HookConsumerWidget {
final content = Column( final content = Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Row(
'fortuneGraph', mainAxisAlignment: MainAxisAlignment.spaceBetween,
).tr().fontSize(18).bold().padding(all: 16, bottom: 24), 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( SizedBox(
height: height, height: height,
child: filteredEvents.when( child: filteredEvents.when(
@ -75,7 +97,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
final maxDate = data.last.date; final maxDate = data.last.date;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: LineChart( child: LineChart(
LineChartData( LineChartData(
gridData: FlGridData( gridData: FlGridData(

View File

@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/network.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:island/widgets/account/status_creation.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -104,14 +106,15 @@ class AccountStatusWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return Padding(
padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4), padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4),
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
if (userStatus.value?.isOnline ?? false) if (status.value?.isOnline ?? false)
Icon( Icon(
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
@ -119,13 +122,24 @@ class AccountStatusWidget extends HookConsumerWidget {
size: 16, size: 16,
).padding(right: 4) ).padding(right: 4)
else else
Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4), Icon(
if (userStatus.value?.isCustomized ?? false) Symbols.circle,
Text(userStatus.value?.label ?? 'unknown'.tr()) color: Colors.grey,
size: 16,
).padding(right: 4),
if (status.value?.isCustomized ?? false)
Text(status.value?.label ?? 'unknown'.tr())
else 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);
} }
} }