♻️ 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