♻️ Refactor the profile and pub profile
This commit is contained in:
		| @@ -39,6 +39,407 @@ import 'package:url_launcher/url_launcher_string.dart'; | |||||||
|  |  | ||||||
| part 'profile.g.dart'; | part 'profile.g.dart'; | ||||||
|  |  | ||||||
|  | class _AccountBasicInfo extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |   final String uname; | ||||||
|  |   final AsyncValue<SnDeveloper?> 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<Widget> _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<SnRelationship?> accountRelationship; | ||||||
|  |   final AsyncValue<SnChatRoom?> 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 | @riverpod | ||||||
| Future<SnAccount> account(Ref ref, String uname) async { | Future<SnAccount> account(Ref ref, String uname) async { | ||||||
|   if (uname == 'me') { |   if (uname == 'me') { | ||||||
| @@ -217,351 +618,12 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     List<Widget> 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 user = ref.watch(userInfoProvider); | ||||||
|     final isCurrentUser = useMemoized( |     final isCurrentUser = useMemoized( | ||||||
|       () => user.value?.id == account.value?.id, |       () => user.value?.id == account.value?.id, | ||||||
|       [user, account], |       [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( |     return account.when( | ||||||
|       data: |       data: | ||||||
|           (data) => AppScaffold( |           (data) => AppScaffold( | ||||||
| @@ -613,7 +675,13 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                         Flexible( |                         Flexible( | ||||||
|                           child: CustomScrollView( |                           child: CustomScrollView( | ||||||
|                             slivers: [ |                             slivers: [ | ||||||
|                               SliverToBoxAdapter(child: accountBasicInfo(data)), |                               SliverToBoxAdapter( | ||||||
|  |                                 child: _AccountBasicInfo( | ||||||
|  |                                   data: data, | ||||||
|  |                                   uname: name, | ||||||
|  |                                   accountDeveloper: accountDeveloper, | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|                               if (data.badges.isNotEmpty) |                               if (data.badges.isNotEmpty) | ||||||
|                                 SliverToBoxAdapter( |                                 SliverToBoxAdapter( | ||||||
|                                   child: Card( |                                   child: Card( | ||||||
| @@ -642,14 +710,16 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                                 ).padding(horizontal: 4, top: 8), |                                 ).padding(horizontal: 4, top: 8), | ||||||
|                               ), |                               ), | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: accountProfileBio(data).padding(top: 4), |                                 child: _AccountProfileBio( | ||||||
|  |                                   data: data, | ||||||
|  |                                 ).padding(top: 4), | ||||||
|                               ), |                               ), | ||||||
|                               if (data.profile.links.isNotEmpty) |                               if (data.profile.links.isNotEmpty) | ||||||
|                                 SliverToBoxAdapter( |                                 SliverToBoxAdapter( | ||||||
|                                   child: accountProfileLinks(data), |                                   child: _AccountProfileLinks(data: data), | ||||||
|                                 ), |                                 ), | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: accountProfileDetail(data), |                                 child: _AccountProfileDetail(data: data), | ||||||
|                               ), |                               ), | ||||||
|                             ], |                             ], | ||||||
|                           ), |                           ), | ||||||
| @@ -659,7 +729,16 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                             slivers: [ |                             slivers: [ | ||||||
|                               SliverGap(24), |                               SliverGap(24), | ||||||
|                               if (user.value != null && !isCurrentUser) |                               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( |                               SliverToBoxAdapter( | ||||||
|                                 child: Card( |                                 child: Card( | ||||||
|                                   child: FortuneGraphWidget( |                                   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) |                         if (data.badges.isNotEmpty) | ||||||
|                           SliverToBoxAdapter( |                           SliverToBoxAdapter( | ||||||
|                             child: Card( |                             child: Card( | ||||||
| @@ -742,22 +827,31 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                           child: accountProfileBio(data).padding(horizontal: 4), |                           child: _AccountProfileBio( | ||||||
|  |                             data: data, | ||||||
|  |                           ).padding(horizontal: 4), | ||||||
|                         ), |                         ), | ||||||
|                         if (data.profile.links.isNotEmpty) |                         if (data.profile.links.isNotEmpty) | ||||||
|                           SliverToBoxAdapter( |                           SliverToBoxAdapter( | ||||||
|                             child: accountProfileLinks( |                             child: _AccountProfileLinks( | ||||||
|                               data, |                               data: data, | ||||||
|                             ).padding(horizontal: 4), |                             ).padding(horizontal: 4), | ||||||
|                           ), |                           ), | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                           child: accountProfileDetail( |                           child: _AccountProfileDetail( | ||||||
|                             data, |                             data: data, | ||||||
|                           ).padding(horizontal: 4), |                           ).padding(horizontal: 4), | ||||||
|                         ), |                         ), | ||||||
|                         if (user.value != null && !isCurrentUser) |                         if (user.value != null && !isCurrentUser) | ||||||
|                           SliverToBoxAdapter( |                           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( |                         SliverToBoxAdapter( | ||||||
|                           child: Card( |                           child: Card( | ||||||
|   | |||||||
| @@ -27,112 +27,24 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
|  |  | ||||||
| part 'pub_profile.g.dart'; | part 'pub_profile.g.dart'; | ||||||
|  |  | ||||||
| @riverpod | class _PublisherBasisWidget extends StatelessWidget { | ||||||
| Future<SnPublisher> publisher(Ref ref, String uname) async { |   final SnPublisher data; | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final AsyncValue<SnSubscriptionStatus> subStatus; | ||||||
|   final resp = await apiClient.get("/sphere/publishers/$uname"); |   final ValueNotifier<bool> subscribing; | ||||||
|   return SnPublisher.fromJson(resp.data); |   final VoidCallback subscribe; | ||||||
| } |   final VoidCallback unsubscribe; | ||||||
|  |  | ||||||
| @riverpod |   const _PublisherBasisWidget({ | ||||||
| Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async { |     required this.data, | ||||||
|   final pub = await ref.watch(publisherProvider(pubName).future); |     required this.subStatus, | ||||||
|   if (pub.type != 0 || pub.account == null) return []; |     required this.subscribing, | ||||||
|   final apiClient = ref.watch(apiClientProvider); |     required this.subscribe, | ||||||
|   final resp = await apiClient.get("/id/accounts/${pub.account!.name}/badges"); |     required this.unsubscribe, | ||||||
|   return List<SnAccountBadge>.from( |   }); | ||||||
|     resp.data.map((x) => SnAccountBadge.fromJson(x)), |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @riverpod |  | ||||||
| Future<SnSubscriptionStatus> 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<Color?> 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 |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context) { | ||||||
|     final publisher = ref.watch(publisherProvider(name)); |     return Row( | ||||||
|     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<void> 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<void> 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( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       spacing: 20, |       spacing: 20, | ||||||
|       children: [ |       children: [ | ||||||
| @@ -247,25 +159,51 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|     ).padding(horizontal: 24, top: 24); |     ).padding(horizontal: 24, top: 24); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|     Widget publisherBadgesWidget(SnPublisher data) => | class _PublisherBadgesWidget extends StatelessWidget { | ||||||
|         (badges.value?.isNotEmpty ?? false) |   final SnPublisher data; | ||||||
|             ? Card( |   final AsyncValue<List<SnAccountBadge>> badges; | ||||||
|               child: BadgeList( |  | ||||||
|                 badges: badges.value!, |  | ||||||
|               ).padding(horizontal: 26, vertical: 20), |  | ||||||
|             ).padding(horizontal: 4) |  | ||||||
|             : const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     Widget publisherVerificationWidget(SnPublisher data) => |   const _PublisherBadgesWidget({required this.data, required this.badges}); | ||||||
|         (data.verification != null) |  | ||||||
|             ? Card( |  | ||||||
|               margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), |  | ||||||
|               child: VerificationStatusCard(mark: data.verification!), |  | ||||||
|             ) |  | ||||||
|             : const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     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), |       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|       child: Column( |       child: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
| @@ -281,8 +219,17 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|         ], |         ], | ||||||
|       ).padding(horizontal: 20, vertical: 16), |       ).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), |       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|       child: TabBar( |       child: TabBar( | ||||||
|         controller: categoryTabController, |         controller: categoryTabController, | ||||||
| @@ -295,6 +242,113 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<SnPublisher> 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<List<SnAccountBadge>> 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<SnAccountBadge>.from( | ||||||
|  |     resp.data.map((x) => SnAccountBadge.fromJson(x)), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<SnSubscriptionStatus> 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<Color?> 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<void> 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<void> 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( |     return publisher.when( | ||||||
|       data: |       data: | ||||||
| @@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                               SliverGap(16), |                               SliverGap(16), | ||||||
|                               SliverPostList(pubName: name, pinned: true), |                               SliverPostList(pubName: name, pinned: true), | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: publisherCategoryTabWidget(), |                                 child: _PublisherCategoryTabWidget( | ||||||
|  |                                   categoryTabController: categoryTabController, | ||||||
|  |                                 ), | ||||||
|                               ), |                               ), | ||||||
|                               SliverPostList( |                               SliverPostList( | ||||||
|                                 key: ValueKey(categoryTab.value), |                                 key: ValueKey(categoryTab.value), | ||||||
| @@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                               child: Column( |                               child: Column( | ||||||
|                                 crossAxisAlignment: CrossAxisAlignment.stretch, |                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                                 children: [ |                                 children: [ | ||||||
|                                   publisherBasisWidget(data).padding(bottom: 8), |                                   _PublisherBasisWidget( | ||||||
|                                   publisherBadgesWidget(data), |                                     data: data, | ||||||
|                                   publisherVerificationWidget(data), |                                     subStatus: subStatus, | ||||||
|                                   publisherBioWidget(data), |                                     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( |                         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( |                         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), |                         SliverPostList(pubName: name, pinned: true), | ||||||
|                         SliverToBoxAdapter(child: publisherCategoryTabWidget()), |                         SliverToBoxAdapter( | ||||||
|  |                           child: _PublisherCategoryTabWidget( | ||||||
|  |                             categoryTabController: categoryTabController, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|                         SliverPostList( |                         SliverPostList( | ||||||
|                           key: ValueKey(categoryTab.value), |                           key: ValueKey(categoryTab.value), | ||||||
|                           pubName: name, |                           pubName: name, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user