From 187c2ea43e7b877a62e22833005d176faaf009db Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 6 Sep 2025 23:25:09 +0800 Subject: [PATCH] :recycle: Refactor the profile and pub profile --- lib/screens/account/profile.dart | 796 ++++++++++++++++------------- lib/screens/posts/pub_profile.dart | 342 ++++++++----- 2 files changed, 657 insertions(+), 481 deletions(-) diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 081ae6cc..297ebc7e 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -39,6 +39,407 @@ import 'package:url_launcher/url_launcher_string.dart'; part 'profile.g.dart'; +class _AccountBasicInfo extends StatelessWidget { + final SnAccount data; + final String uname; + final AsyncValue accountDeveloper; + + const _AccountBasicInfo({ + required this.data, + required this.uname, + required this.accountDeveloper, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfilePictureWidget(file: data.profile.picture, radius: 32), + const Gap(20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + AccountName(account: data, style: TextStyle(fontSize: 20)), + const Gap(6), + Flexible( + child: Text( + '@${data.name}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).fontSize(14).opacity(0.85), + ), + ], + ), + if (accountDeveloper.value != null) + Row( + spacing: 7, + children: [ + const Icon(Symbols.smart_toy, size: 18), + Text( + 'botAutomatedBy'.tr( + args: [accountDeveloper.value!.publisher!.nick], + ), + ).fontSize(13), + ], + ).opacity(0.75), + const Gap(4), + AccountStatusWidget(uname: uname, padding: EdgeInsets.zero), + ], + ), + ), + IconButton( + onPressed: () { + SharePlus.instance.share( + ShareParams( + uri: Uri.parse('https://id.solian.app/@${data.name}'), + ), + ); + }, + icon: const Icon(Symbols.share), + ), + ], + ), + ); + } +} + +class _AccountProfileBio extends StatelessWidget { + final SnAccount data; + + const _AccountProfileBio({required this.data}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('bio').tr().bold().fontSize(15).padding(bottom: 8), + if (data.profile.bio.isEmpty) + Text('descriptionNone').tr().italic() + else + MarkdownTextContent( + content: data.profile.bio, + linesMargin: EdgeInsets.zero, + ), + ], + ).padding(horizontal: 24, vertical: 20), + ); + } +} + +class _AccountProfileDetail extends StatelessWidget { + final SnAccount data; + + const _AccountProfileDetail({required this.data}); + + List _buildSubcolumn() { + return [ + Row( + spacing: 6, + children: [ + const Icon(Symbols.join, size: 17, fill: 1), + Text( + 'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]), + ), + ], + ), + if (data.profile.birthday != null) + Row( + spacing: 6, + children: [ + const Icon(Symbols.cake, size: 17, fill: 1), + Text(data.profile.birthday!.formatCustom('yyyy-MM-dd')), + Text('·').bold(), + Text( + '${DateTime.now().difference(data.profile.birthday!).inDays ~/ 365} yrs old', + ), + ], + ), + if (data.profile.location.isNotEmpty) + Row( + spacing: 6, + children: [ + const Icon(Symbols.location_on, size: 17, fill: 1), + Text(data.profile.location), + ], + ), + if (data.profile.pronouns.isNotEmpty || data.profile.gender.isNotEmpty) + Row( + spacing: 6, + children: [ + const Icon(Symbols.person, size: 17, fill: 1), + Text( + data.profile.gender.isEmpty + ? 'unspecified'.tr() + : data.profile.gender, + ), + Text('·').bold(), + Text( + data.profile.pronouns.isEmpty + ? 'unspecified'.tr() + : data.profile.pronouns, + ), + ], + ), + if (data.profile.firstName.isNotEmpty || + data.profile.middleName.isNotEmpty || + data.profile.lastName.isNotEmpty) + Row( + spacing: 6, + children: [ + const Icon(Symbols.id_card, size: 17, fill: 1), + if (data.profile.firstName.isNotEmpty) Text(data.profile.firstName), + if (data.profile.middleName.isNotEmpty) + Text(data.profile.middleName), + if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName), + ], + ), + Tooltip( + message: 'creditsStatus'.tr(), + child: Row( + spacing: 6, + children: [ + Icon(Symbols.star, size: 17, fill: 1).padding(right: 2), + Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'), + Text('·').bold(), + switch (data.profile.socialCreditsLevel) { + -1 => Text('socialCreditsLevelPoor').tr(), + 0 => Text('socialCreditsLevelNormal').tr(), + 1 => Text('socialCreditsLevelGood').tr(), + 2 => Text('socialCreditsLevelExcellent').tr(), + _ => Text('unknown').tr(), + }, + ], + ), + ), + InkWell( + child: Row( + spacing: 6, + children: [ + Icon(Symbols.fingerprint, size: 17, fill: 1).padding(right: 2), + Text(data.id), + ], + ), + onTap: () { + Clipboard.setData(ClipboardData(text: data.id)); + }, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 24, + children: [ + if (_buildSubcolumn().isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2, + children: _buildSubcolumn(), + ), + if (data.profile.timeZone.isNotEmpty && !kIsWeb) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('timeZone').tr().bold(), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + spacing: 6, + children: [ + Text(data.profile.timeZone), + Text( + getTzInfo( + data.profile.timeZone, + ).$2.formatCustomGlobal('HH:mm'), + ), + Text( + getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(), + ).fontSize(11), + Text( + 'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', + ).fontSize(11).opacity(0.75), + ], + ), + ], + ), + ], + ).padding(horizontal: 24, vertical: 16), + ); + } +} + +class _AccountProfileLinks extends StatelessWidget { + final SnAccount data; + + const _AccountProfileLinks({required this.data}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), + for (final link in data.profile.links) + ListTile( + title: Text(link.name.capitalizeEachWord()), + subtitle: Text(link.url), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + trailing: const Icon(Symbols.chevron_right), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + onTap: () { + if (!link.url.startsWith('http') && !link.url.contains('://')) { + launchUrlString('https://${link.url}'); + } else { + launchUrlString(link.url); + } + }, + ), + ], + ), + ); + } +} + +class _AccountAction extends StatelessWidget { + final SnAccount data; + final AsyncValue accountRelationship; + final AsyncValue accountChat; + final VoidCallback relationshipAction; + final VoidCallback blockAction; + final VoidCallback directMessageAction; + + const _AccountAction({ + required this.data, + required this.accountRelationship, + required this.accountChat, + required this.relationshipAction, + required this.blockAction, + required this.directMessageAction, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Row( + spacing: 8, + children: [ + if (accountRelationship.value == null || + accountRelationship.value!.status > -100) + Expanded( + child: FilledButton.icon( + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + accountRelationship.value == null + ? null + : Theme.of(context).colorScheme.secondary, + ), + foregroundColor: WidgetStatePropertyAll( + accountRelationship.value == null + ? null + : Theme.of(context).colorScheme.onSecondary, + ), + ), + onPressed: relationshipAction, + label: + Text( + accountRelationship.value == null + ? 'addFriendShort' + : 'added', + ).tr(), + icon: + accountRelationship.value == null + ? const Icon(Symbols.person_add) + : const Icon(Symbols.person_check), + ), + ), + if (accountRelationship.value == null || + accountRelationship.value!.status <= -100) + Expanded( + child: FilledButton.icon( + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + accountRelationship.value == null + ? null + : Theme.of(context).colorScheme.secondary, + ), + foregroundColor: WidgetStatePropertyAll( + accountRelationship.value == null + ? null + : Theme.of(context).colorScheme.onSecondary, + ), + ), + onPressed: blockAction, + label: + Text( + accountRelationship.value == null + ? 'blockUser' + : 'unblockUser', + ).tr(), + icon: + accountRelationship.value == null + ? const Icon(Symbols.block) + : const Icon(Symbols.person_cancel), + ), + ), + ], + ), + Row( + spacing: 8, + children: [ + Expanded( + child: FilledButton.icon( + onPressed: directMessageAction, + icon: const Icon(Symbols.message), + label: + Text( + accountChat.value == null + ? 'createDirectMessage' + : 'gotoDirectMessage', + maxLines: 1, + ).tr(), + ), + ), + IconButton.filled( + onPressed: () { + showAbuseReportSheet( + context, + resourceIdentifier: 'account/${data.id}', + ); + }, + icon: Icon( + Symbols.flag, + color: Theme.of(context).colorScheme.onError, + ), + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.error, + ), + ), + ), + ], + ), + ], + ).padding(horizontal: 16, vertical: 12), + ); + } +} + @riverpod Future account(Ref ref, String uname) async { if (uname == 'me') { @@ -217,351 +618,12 @@ class AccountProfileScreen extends HookConsumerWidget { } } - List buildSubcolumn(SnAccount data) { - return [ - Row( - spacing: 6, - children: [ - const Icon(Symbols.join, size: 17, fill: 1), - Text( - 'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]), - ), - ], - ), - if (data.profile.birthday != null) - Row( - spacing: 6, - children: [ - const Icon(Symbols.cake, size: 17, fill: 1), - Text(data.profile.birthday!.formatCustom('yyyy-MM-dd')), - Text('·').bold(), - Text( - '${DateTime.now().difference(data.profile.birthday!).inDays ~/ 365} yrs old', - ), - ], - ), - if (data.profile.location.isNotEmpty) - Row( - spacing: 6, - children: [ - const Icon(Symbols.location_on, size: 17, fill: 1), - Text(data.profile.location), - ], - ), - if (data.profile.pronouns.isNotEmpty || data.profile.gender.isNotEmpty) - Row( - spacing: 6, - children: [ - const Icon(Symbols.person, size: 17, fill: 1), - Text( - data.profile.gender.isEmpty - ? 'unspecified'.tr() - : data.profile.gender, - ), - Text('·').bold(), - Text( - data.profile.pronouns.isEmpty - ? 'unspecified'.tr() - : data.profile.pronouns, - ), - ], - ), - if (data.profile.firstName.isNotEmpty || - data.profile.middleName.isNotEmpty || - data.profile.lastName.isNotEmpty) - Row( - spacing: 6, - children: [ - const Icon(Symbols.id_card, size: 17, fill: 1), - if (data.profile.firstName.isNotEmpty) - Text(data.profile.firstName), - if (data.profile.middleName.isNotEmpty) - Text(data.profile.middleName), - if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName), - ], - ), - Tooltip( - message: 'creditsStatus'.tr(), - child: Row( - spacing: 6, - children: [ - Icon(Symbols.star, size: 17, fill: 1).padding(right: 2), - Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'), - Text('·').bold(), - switch (data.profile.socialCreditsLevel) { - -1 => Text('socialCreditsLevelPoor').tr(), - 0 => Text('socialCreditsLevelNormal').tr(), - 1 => Text('socialCreditsLevelGood').tr(), - 2 => Text('socialCreditsLevelExcellent').tr(), - _ => Text('unknown').tr(), - }, - ], - ), - ), - InkWell( - child: Row( - spacing: 6, - children: [ - Icon(Symbols.fingerprint, size: 17, fill: 1).padding(right: 2), - Text(data.id), - ], - ), - onTap: () { - Clipboard.setData(ClipboardData(text: data.id)); - }, - ), - ]; - } - final user = ref.watch(userInfoProvider); final isCurrentUser = useMemoized( () => user.value?.id == account.value?.id, [user, account], ); - Widget accountBasicInfo(SnAccount data) => Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ProfilePictureWidget(file: data.profile.picture, radius: 32), - const Gap(20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - AccountName(account: data, style: TextStyle(fontSize: 20)), - const Gap(6), - Flexible( - child: Text( - '@${data.name}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).fontSize(14).opacity(0.85), - ), - ], - ), - if (accountDeveloper.value != null) - Row( - spacing: 7, - children: [ - const Icon(Symbols.smart_toy, size: 18), - Text( - 'botAutomatedBy'.tr( - args: [accountDeveloper.value!.publisher!.nick], - ), - ).fontSize(13), - ], - ).opacity(0.75), - const Gap(4), - AccountStatusWidget(uname: name, padding: EdgeInsets.zero), - ], - ), - ), - IconButton( - onPressed: () { - SharePlus.instance.share( - ShareParams( - uri: Uri.parse('https://id.solian.app/@${data.name}'), - ), - ); - }, - icon: const Icon(Symbols.share), - ), - ], - ), - ); - - Widget accountProfileBio(SnAccount data) => Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('bio').tr().bold().fontSize(15).padding(bottom: 8), - if (data.profile.bio.isEmpty) - Text('descriptionNone').tr().italic() - else - MarkdownTextContent( - content: data.profile.bio, - linesMargin: EdgeInsets.zero, - ), - ], - ).padding(horizontal: 24, vertical: 20), - ); - - Widget accountProfileDetail(SnAccount data) => Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 24, - children: [ - if (buildSubcolumn(data).isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 2, - children: buildSubcolumn(data), - ), - if (data.profile.timeZone.isNotEmpty && !kIsWeb) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('timeZone').tr().bold(), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - spacing: 6, - children: [ - Text(data.profile.timeZone), - Text( - getTzInfo( - data.profile.timeZone, - ).$2.formatCustomGlobal('HH:mm'), - ), - Text( - getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(), - ).fontSize(11), - Text( - 'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', - ).fontSize(11).opacity(0.75), - ], - ), - ], - ), - ], - ).padding(horizontal: 24, vertical: 16), - ); - - Widget accountProfileLinks(SnAccount data) => Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), - for (final link in data.profile.links) - ListTile( - title: Text(link.name.capitalizeEachWord()), - subtitle: Text(link.url), - contentPadding: EdgeInsets.symmetric(horizontal: 24), - trailing: const Icon(Symbols.chevron_right), - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - onTap: () { - if (!link.url.startsWith('http') && !link.url.contains('://')) { - launchUrlString('https://${link.url}'); - } else { - launchUrlString(link.url); - } - }, - ), - ], - ), - ); - - Widget accountAction(SnAccount data) => Card( - child: Column( - children: [ - Row( - spacing: 8, - children: [ - if (accountRelationship.value == null || - accountRelationship.value!.status > -100) - Expanded( - child: FilledButton.icon( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - accountRelationship.value == null - ? null - : Theme.of(context).colorScheme.secondary, - ), - foregroundColor: WidgetStatePropertyAll( - accountRelationship.value == null - ? null - : Theme.of(context).colorScheme.onSecondary, - ), - ), - onPressed: relationshipAction, - label: - Text( - accountRelationship.value == null - ? 'addFriendShort' - : 'added', - ).tr(), - icon: - accountRelationship.value == null - ? const Icon(Symbols.person_add) - : const Icon(Symbols.person_check), - ), - ), - if (accountRelationship.value == null || - accountRelationship.value!.status <= -100) - Expanded( - child: FilledButton.icon( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - accountRelationship.value == null - ? null - : Theme.of(context).colorScheme.secondary, - ), - foregroundColor: WidgetStatePropertyAll( - accountRelationship.value == null - ? null - : Theme.of(context).colorScheme.onSecondary, - ), - ), - onPressed: blockAction, - label: - Text( - accountRelationship.value == null - ? 'blockUser' - : 'unblockUser', - ).tr(), - icon: - accountRelationship.value == null - ? const Icon(Symbols.block) - : const Icon(Symbols.person_cancel), - ), - ), - ], - ), - Row( - spacing: 8, - children: [ - Expanded( - child: FilledButton.icon( - onPressed: directMessageAction, - icon: const Icon(Symbols.message), - label: - Text( - accountChat.value == null - ? 'createDirectMessage' - : 'gotoDirectMessage', - maxLines: 1, - ).tr(), - ), - ), - IconButton.filled( - onPressed: () { - showAbuseReportSheet( - context, - resourceIdentifier: 'account/${data.id}', - ); - }, - icon: Icon( - Symbols.flag, - color: Theme.of(context).colorScheme.onError, - ), - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - Theme.of(context).colorScheme.error, - ), - ), - ), - ], - ), - ], - ).padding(horizontal: 16, vertical: 12), - ); - return account.when( data: (data) => AppScaffold( @@ -613,7 +675,13 @@ class AccountProfileScreen extends HookConsumerWidget { Flexible( child: CustomScrollView( slivers: [ - SliverToBoxAdapter(child: accountBasicInfo(data)), + SliverToBoxAdapter( + child: _AccountBasicInfo( + data: data, + uname: name, + accountDeveloper: accountDeveloper, + ), + ), if (data.badges.isNotEmpty) SliverToBoxAdapter( child: Card( @@ -642,14 +710,16 @@ class AccountProfileScreen extends HookConsumerWidget { ).padding(horizontal: 4, top: 8), ), SliverToBoxAdapter( - child: accountProfileBio(data).padding(top: 4), + child: _AccountProfileBio( + data: data, + ).padding(top: 4), ), if (data.profile.links.isNotEmpty) SliverToBoxAdapter( - child: accountProfileLinks(data), + child: _AccountProfileLinks(data: data), ), SliverToBoxAdapter( - child: accountProfileDetail(data), + child: _AccountProfileDetail(data: data), ), ], ), @@ -659,7 +729,16 @@ class AccountProfileScreen extends HookConsumerWidget { slivers: [ SliverGap(24), if (user.value != null && !isCurrentUser) - SliverToBoxAdapter(child: accountAction(data)), + SliverToBoxAdapter( + child: _AccountAction( + data: data, + accountRelationship: accountRelationship, + accountChat: accountChat, + relationshipAction: relationshipAction, + blockAction: blockAction, + directMessageAction: directMessageAction, + ), + ), SliverToBoxAdapter( child: Card( child: FortuneGraphWidget( @@ -715,7 +794,13 @@ class AccountProfileScreen extends HookConsumerWidget { ], ), ), - SliverToBoxAdapter(child: accountBasicInfo(data)), + SliverToBoxAdapter( + child: _AccountBasicInfo( + data: data, + uname: name, + accountDeveloper: accountDeveloper, + ), + ), if (data.badges.isNotEmpty) SliverToBoxAdapter( child: Card( @@ -742,22 +827,31 @@ class AccountProfileScreen extends HookConsumerWidget { ), ), SliverToBoxAdapter( - child: accountProfileBio(data).padding(horizontal: 4), + child: _AccountProfileBio( + data: data, + ).padding(horizontal: 4), ), if (data.profile.links.isNotEmpty) SliverToBoxAdapter( - child: accountProfileLinks( - data, + child: _AccountProfileLinks( + data: data, ).padding(horizontal: 4), ), SliverToBoxAdapter( - child: accountProfileDetail( - data, + child: _AccountProfileDetail( + data: data, ).padding(horizontal: 4), ), if (user.value != null && !isCurrentUser) SliverToBoxAdapter( - child: accountAction(data).padding(horizontal: 4), + child: _AccountAction( + data: data, + accountRelationship: accountRelationship, + accountChat: accountChat, + relationshipAction: relationshipAction, + blockAction: blockAction, + directMessageAction: directMessageAction, + ).padding(horizontal: 4), ), SliverToBoxAdapter( child: Card( diff --git a/lib/screens/posts/pub_profile.dart b/lib/screens/posts/pub_profile.dart index 7e42a8a6..0c42bb07 100644 --- a/lib/screens/posts/pub_profile.dart +++ b/lib/screens/posts/pub_profile.dart @@ -27,112 +27,24 @@ import 'package:styled_widget/styled_widget.dart'; part 'pub_profile.g.dart'; -@riverpod -Future publisher(Ref ref, String uname) async { - final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get("/sphere/publishers/$uname"); - return SnPublisher.fromJson(resp.data); -} +class _PublisherBasisWidget extends StatelessWidget { + final SnPublisher data; + final AsyncValue subStatus; + final ValueNotifier subscribing; + final VoidCallback subscribe; + final VoidCallback unsubscribe; -@riverpod -Future> publisherBadges(Ref ref, String pubName) async { - final pub = await ref.watch(publisherProvider(pubName).future); - if (pub.type != 0 || pub.account == null) return []; - final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get("/id/accounts/${pub.account!.name}/badges"); - return List.from( - resp.data.map((x) => SnAccountBadge.fromJson(x)), - ); -} - -@riverpod -Future publisherSubscriptionStatus( - Ref ref, - String pubName, -) async { - final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get("/sphere/publishers/$pubName/subscription"); - return SnSubscriptionStatus.fromJson(resp.data); -} - -@riverpod -Future publisherAppbarForcegroundColor(Ref ref, String pubName) async { - try { - final publisher = await ref.watch(publisherProvider(pubName).future); - if (publisher.background == null) return null; - final palette = await PaletteGenerator.fromImageProvider( - CloudImageWidget.provider( - fileId: publisher.background!.id, - serverUrl: ref.watch(serverUrlProvider), - ), - ); - final dominantColor = palette.dominantColor?.color; - if (dominantColor == null) return null; - return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; - } catch (_) { - return null; - } -} - -class PublisherProfileScreen extends HookConsumerWidget { - final String name; - const PublisherProfileScreen({super.key, required this.name}); + const _PublisherBasisWidget({ + required this.data, + required this.subStatus, + required this.subscribing, + required this.subscribe, + required this.unsubscribe, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final publisher = ref.watch(publisherProvider(name)); - final badges = ref.watch(publisherBadgesProvider(name)); - final subStatus = ref.watch(publisherSubscriptionStatusProvider(name)); - final appbarColor = ref.watch( - publisherAppbarForcegroundColorProvider(name), - ); - - final categoryTabController = useTabController(initialLength: 3); - final categoryTab = useState(0); - categoryTabController.addListener(() { - categoryTab.value = categoryTabController.index; - }); - - final subscribing = useState(false); - - Future subscribe() async { - final apiClient = ref.watch(apiClientProvider); - subscribing.value = true; - try { - await apiClient.post( - "/sphere/publishers/$name/subscribe", - data: {'tier': 0}, - ); - ref.invalidate(publisherSubscriptionStatusProvider(name)); - HapticFeedback.heavyImpact(); - } catch (err) { - showErrorAlert(err); - } finally { - subscribing.value = false; - } - } - - Future unsubscribe() async { - final apiClient = ref.watch(apiClientProvider); - subscribing.value = true; - try { - await apiClient.post("/sphere/publishers/$name/unsubscribe"); - ref.invalidate(publisherSubscriptionStatusProvider(name)); - HapticFeedback.heavyImpact(); - } catch (err) { - showErrorAlert(err); - } finally { - subscribing.value = false; - } - } - - final appbarShadow = Shadow( - color: appbarColor.value?.invert ?? Colors.transparent, - blurRadius: 5.0, - offset: Offset(1.0, 1.0), - ); - - Widget publisherBasisWidget(SnPublisher data) => Row( + Widget build(BuildContext context) { + return Row( crossAxisAlignment: CrossAxisAlignment.start, spacing: 20, children: [ @@ -247,25 +159,51 @@ class PublisherProfileScreen extends HookConsumerWidget { ), ], ).padding(horizontal: 24, top: 24); + } +} - Widget publisherBadgesWidget(SnPublisher data) => - (badges.value?.isNotEmpty ?? false) - ? Card( - child: BadgeList( - badges: badges.value!, - ).padding(horizontal: 26, vertical: 20), - ).padding(horizontal: 4) - : const SizedBox.shrink(); +class _PublisherBadgesWidget extends StatelessWidget { + final SnPublisher data; + final AsyncValue> badges; - Widget publisherVerificationWidget(SnPublisher data) => - (data.verification != null) - ? Card( - margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: VerificationStatusCard(mark: data.verification!), - ) - : const SizedBox.shrink(); + const _PublisherBadgesWidget({required this.data, required this.badges}); - Widget publisherBioWidget(SnPublisher data) => Card( + @override + Widget build(BuildContext context) { + return (badges.value?.isNotEmpty ?? false) + ? Card( + child: BadgeList( + badges: badges.value!, + ).padding(horizontal: 26, vertical: 20), + ).padding(horizontal: 4) + : const SizedBox.shrink(); + } +} + +class _PublisherVerificationWidget extends StatelessWidget { + final SnPublisher data; + + const _PublisherVerificationWidget({required this.data}); + + @override + Widget build(BuildContext context) { + return (data.verification != null) + ? Card( + margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: VerificationStatusCard(mark: data.verification!), + ) + : const SizedBox.shrink(); + } +} + +class _PublisherBioWidget extends StatelessWidget { + final SnPublisher data; + + const _PublisherBioWidget({required this.data}); + + @override + Widget build(BuildContext context) { + return Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -281,8 +219,17 @@ class PublisherProfileScreen extends HookConsumerWidget { ], ).padding(horizontal: 20, vertical: 16), ); + } +} - Widget publisherCategoryTabWidget() => Card( +class _PublisherCategoryTabWidget extends StatelessWidget { + final TabController categoryTabController; + + const _PublisherCategoryTabWidget({required this.categoryTabController}); + + @override + Widget build(BuildContext context) { + return Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: TabBar( controller: categoryTabController, @@ -295,6 +242,113 @@ class PublisherProfileScreen extends HookConsumerWidget { ], ), ); + } +} + +@riverpod +Future publisher(Ref ref, String uname) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get("/sphere/publishers/$uname"); + return SnPublisher.fromJson(resp.data); +} + +@riverpod +Future> publisherBadges(Ref ref, String pubName) async { + final pub = await ref.watch(publisherProvider(pubName).future); + if (pub.type != 0 || pub.account == null) return []; + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get("/id/accounts/${pub.account!.name}/badges"); + return List.from( + resp.data.map((x) => SnAccountBadge.fromJson(x)), + ); +} + +@riverpod +Future publisherSubscriptionStatus( + Ref ref, + String pubName, +) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get("/sphere/publishers/$pubName/subscription"); + return SnSubscriptionStatus.fromJson(resp.data); +} + +@riverpod +Future publisherAppbarForcegroundColor(Ref ref, String pubName) async { + try { + final publisher = await ref.watch(publisherProvider(pubName).future); + if (publisher.background == null) return null; + final palette = await PaletteGenerator.fromImageProvider( + CloudImageWidget.provider( + fileId: publisher.background!.id, + serverUrl: ref.watch(serverUrlProvider), + ), + ); + final dominantColor = palette.dominantColor?.color; + if (dominantColor == null) return null; + return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; + } catch (_) { + return null; + } +} + +class PublisherProfileScreen extends HookConsumerWidget { + final String name; + const PublisherProfileScreen({super.key, required this.name}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final publisher = ref.watch(publisherProvider(name)); + final badges = ref.watch(publisherBadgesProvider(name)); + final subStatus = ref.watch(publisherSubscriptionStatusProvider(name)); + final appbarColor = ref.watch( + publisherAppbarForcegroundColorProvider(name), + ); + + final categoryTabController = useTabController(initialLength: 3); + final categoryTab = useState(0); + categoryTabController.addListener(() { + categoryTab.value = categoryTabController.index; + }); + + final subscribing = useState(false); + + Future subscribe() async { + final apiClient = ref.watch(apiClientProvider); + subscribing.value = true; + try { + await apiClient.post( + "/sphere/publishers/$name/subscribe", + data: {'tier': 0}, + ); + ref.invalidate(publisherSubscriptionStatusProvider(name)); + HapticFeedback.heavyImpact(); + } catch (err) { + showErrorAlert(err); + } finally { + subscribing.value = false; + } + } + + Future unsubscribe() async { + final apiClient = ref.watch(apiClientProvider); + subscribing.value = true; + try { + await apiClient.post("/sphere/publishers/$name/unsubscribe"); + ref.invalidate(publisherSubscriptionStatusProvider(name)); + HapticFeedback.heavyImpact(); + } catch (err) { + showErrorAlert(err); + } finally { + subscribing.value = false; + } + } + + final appbarShadow = Shadow( + color: appbarColor.value?.invert ?? Colors.transparent, + blurRadius: 5.0, + offset: Offset(1.0, 1.0), + ); return publisher.when( data: @@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget { SliverGap(16), SliverPostList(pubName: name, pinned: true), SliverToBoxAdapter( - child: publisherCategoryTabWidget(), + child: _PublisherCategoryTabWidget( + categoryTabController: categoryTabController, + ), ), SliverPostList( key: ValueKey(categoryTab.value), @@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - publisherBasisWidget(data).padding(bottom: 8), - publisherBadgesWidget(data), - publisherVerificationWidget(data), - publisherBioWidget(data), + _PublisherBasisWidget( + data: data, + subStatus: subStatus, + subscribing: subscribing, + subscribe: subscribe, + unsubscribe: unsubscribe, + ).padding(bottom: 8), + _PublisherBadgesWidget( + data: data, + badges: badges, + ), + _PublisherVerificationWidget(data: data), + _PublisherBioWidget(data: data), ], ), ), @@ -432,15 +497,32 @@ class PublisherProfileScreen extends HookConsumerWidget { ), ), SliverToBoxAdapter( - child: publisherBasisWidget(data).padding(bottom: 8), + child: _PublisherBasisWidget( + data: data, + subStatus: subStatus, + subscribing: subscribing, + subscribe: subscribe, + unsubscribe: unsubscribe, + ).padding(bottom: 8), ), - SliverToBoxAdapter(child: publisherBadgesWidget(data)), SliverToBoxAdapter( - child: publisherVerificationWidget(data), + child: _PublisherBadgesWidget( + data: data, + badges: badges, + ), + ), + SliverToBoxAdapter( + child: _PublisherVerificationWidget(data: data), + ), + SliverToBoxAdapter( + child: _PublisherBioWidget(data: data), ), - SliverToBoxAdapter(child: publisherBioWidget(data)), SliverPostList(pubName: name, pinned: true), - SliverToBoxAdapter(child: publisherCategoryTabWidget()), + SliverToBoxAdapter( + child: _PublisherCategoryTabWidget( + categoryTabController: categoryTabController, + ), + ), SliverPostList( key: ValueKey(categoryTab.value), pubName: name,