♻️ Refactor the profile and pub profile

This commit is contained in:
2025-09-06 23:25:09 +08:00
parent ae7d967461
commit 187c2ea43e
2 changed files with 657 additions and 481 deletions

View File

@@ -39,6 +39,407 @@ import 'package:url_launcher/url_launcher_string.dart';
part 'profile.g.dart';
class _AccountBasicInfo extends StatelessWidget {
final SnAccount data;
final String uname;
final AsyncValue<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
Future<SnAccount> account(Ref ref, String uname) async {
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 isCurrentUser = useMemoized(
() => user.value?.id == account.value?.id,
[user, account],
);
Widget accountBasicInfo(SnAccount data) => Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(file: data.profile.picture, radius: 32),
const Gap(20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
AccountName(account: data, style: TextStyle(fontSize: 20)),
const Gap(6),
Flexible(
child: Text(
'@${data.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14).opacity(0.85),
),
],
),
if (accountDeveloper.value != null)
Row(
spacing: 7,
children: [
const Icon(Symbols.smart_toy, size: 18),
Text(
'botAutomatedBy'.tr(
args: [accountDeveloper.value!.publisher!.nick],
),
).fontSize(13),
],
).opacity(0.75),
const Gap(4),
AccountStatusWidget(uname: name, padding: EdgeInsets.zero),
],
),
),
IconButton(
onPressed: () {
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('https://id.solian.app/@${data.name}'),
),
);
},
icon: const Icon(Symbols.share),
),
],
),
);
Widget accountProfileBio(SnAccount data) => Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bio').tr().bold().fontSize(15).padding(bottom: 8),
if (data.profile.bio.isEmpty)
Text('descriptionNone').tr().italic()
else
MarkdownTextContent(
content: data.profile.bio,
linesMargin: EdgeInsets.zero,
),
],
).padding(horizontal: 24, vertical: 20),
);
Widget accountProfileDetail(SnAccount data) => Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 24,
children: [
if (buildSubcolumn(data).isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: buildSubcolumn(data),
),
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('timeZone').tr().bold(),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
spacing: 6,
children: [
Text(data.profile.timeZone),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
),
Text(
getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(),
).fontSize(11),
Text(
'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}',
).fontSize(11).opacity(0.75),
],
),
],
),
],
).padding(horizontal: 24, vertical: 16),
);
Widget accountProfileLinks(SnAccount data) => Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
for (final link in data.profile.links)
ListTile(
title: Text(link.name.capitalizeEachWord()),
subtitle: Text(link.url),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
onTap: () {
if (!link.url.startsWith('http') && !link.url.contains('://')) {
launchUrlString('https://${link.url}');
} else {
launchUrlString(link.url);
}
},
),
],
),
);
Widget accountAction(SnAccount data) => Card(
child: Column(
children: [
Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
accountRelationship.value!.status > -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
label:
Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
),
if (accountRelationship.value == null ||
accountRelationship.value!.status <= -100)
Expanded(
child: FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.secondary,
),
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: blockAction,
label:
Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
),
],
),
Row(
spacing: 8,
children: [
Expanded(
child: FilledButton.icon(
onPressed: directMessageAction,
icon: const Icon(Symbols.message),
label:
Text(
accountChat.value == null
? 'createDirectMessage'
: 'gotoDirectMessage',
maxLines: 1,
).tr(),
),
),
IconButton.filled(
onPressed: () {
showAbuseReportSheet(
context,
resourceIdentifier: 'account/${data.id}',
);
},
icon: Icon(
Symbols.flag,
color: Theme.of(context).colorScheme.onError,
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.error,
),
),
),
],
),
],
).padding(horizontal: 16, vertical: 12),
);
return account.when(
data:
(data) => AppScaffold(
@@ -613,7 +675,13 @@ class AccountProfileScreen extends HookConsumerWidget {
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: accountBasicInfo(data)),
SliverToBoxAdapter(
child: _AccountBasicInfo(
data: data,
uname: name,
accountDeveloper: accountDeveloper,
),
),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: Card(
@@ -642,14 +710,16 @@ class AccountProfileScreen extends HookConsumerWidget {
).padding(horizontal: 4, top: 8),
),
SliverToBoxAdapter(
child: accountProfileBio(data).padding(top: 4),
child: _AccountProfileBio(
data: data,
).padding(top: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(data),
child: _AccountProfileLinks(data: data),
),
SliverToBoxAdapter(
child: accountProfileDetail(data),
child: _AccountProfileDetail(data: data),
),
],
),
@@ -659,7 +729,16 @@ class AccountProfileScreen extends HookConsumerWidget {
slivers: [
SliverGap(24),
if (user.value != null && !isCurrentUser)
SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter(
child: _AccountAction(
data: data,
accountRelationship: accountRelationship,
accountChat: accountChat,
relationshipAction: relationshipAction,
blockAction: blockAction,
directMessageAction: directMessageAction,
),
),
SliverToBoxAdapter(
child: Card(
child: FortuneGraphWidget(
@@ -715,7 +794,13 @@ class AccountProfileScreen extends HookConsumerWidget {
],
),
),
SliverToBoxAdapter(child: accountBasicInfo(data)),
SliverToBoxAdapter(
child: _AccountBasicInfo(
data: data,
uname: name,
accountDeveloper: accountDeveloper,
),
),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: Card(
@@ -742,22 +827,31 @@ class AccountProfileScreen extends HookConsumerWidget {
),
),
SliverToBoxAdapter(
child: accountProfileBio(data).padding(horizontal: 4),
child: _AccountProfileBio(
data: data,
).padding(horizontal: 4),
),
if (data.profile.links.isNotEmpty)
SliverToBoxAdapter(
child: accountProfileLinks(
data,
child: _AccountProfileLinks(
data: data,
).padding(horizontal: 4),
),
SliverToBoxAdapter(
child: accountProfileDetail(
data,
child: _AccountProfileDetail(
data: data,
).padding(horizontal: 4),
),
if (user.value != null && !isCurrentUser)
SliverToBoxAdapter(
child: accountAction(data).padding(horizontal: 4),
child: _AccountAction(
data: data,
accountRelationship: accountRelationship,
accountChat: accountChat,
relationshipAction: relationshipAction,
blockAction: blockAction,
directMessageAction: directMessageAction,
).padding(horizontal: 4),
),
SliverToBoxAdapter(
child: Card(

View File

@@ -27,112 +27,24 @@ import 'package:styled_widget/styled_widget.dart';
part 'pub_profile.g.dart';
@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);
}
class _PublisherBasisWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<SnSubscriptionStatus> subStatus;
final ValueNotifier<bool> subscribing;
final VoidCallback subscribe;
final VoidCallback unsubscribe;
@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;
const _PublisherBasisWidget({
required this.data,
required this.subStatus,
required this.subscribing,
required this.subscribe,
required this.unsubscribe,
});
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(
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
@@ -247,25 +159,51 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
],
).padding(horizontal: 24, top: 24);
}
}
Widget publisherBadgesWidget(SnPublisher data) =>
(badges.value?.isNotEmpty ?? false)
class _PublisherBadgesWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<List<SnAccountBadge>> badges;
const _PublisherBadgesWidget({required this.data, required this.badges});
@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();
}
}
Widget publisherVerificationWidget(SnPublisher data) =>
(data.verification != null)
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();
}
}
Widget publisherBioWidget(SnPublisher data) => Card(
class _PublisherBioWidget extends StatelessWidget {
final SnPublisher data;
const _PublisherBioWidget({required this.data});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -281,8 +219,17 @@ class PublisherProfileScreen extends HookConsumerWidget {
],
).padding(horizontal: 20, vertical: 16),
);
}
}
Widget publisherCategoryTabWidget() => Card(
class _PublisherCategoryTabWidget extends StatelessWidget {
final TabController categoryTabController;
const _PublisherCategoryTabWidget({required this.categoryTabController});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TabBar(
controller: categoryTabController,
@@ -295,6 +242,113 @@ class PublisherProfileScreen extends HookConsumerWidget {
],
),
);
}
}
@riverpod
Future<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(
data:
@@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
SliverGap(16),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
@@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
publisherBasisWidget(data).padding(bottom: 8),
publisherBadgesWidget(data),
publisherVerificationWidget(data),
publisherBioWidget(data),
_PublisherBasisWidget(
data: data,
subStatus: subStatus,
subscribing: subscribing,
subscribe: subscribe,
unsubscribe: unsubscribe,
).padding(bottom: 8),
_PublisherBadgesWidget(
data: data,
badges: badges,
),
_PublisherVerificationWidget(data: data),
_PublisherBioWidget(data: data),
],
),
),
@@ -432,15 +497,32 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
),
SliverToBoxAdapter(
child: publisherBasisWidget(data).padding(bottom: 8),
child: _PublisherBasisWidget(
data: data,
subStatus: subStatus,
subscribing: subscribing,
subscribe: subscribe,
unsubscribe: unsubscribe,
).padding(bottom: 8),
),
SliverToBoxAdapter(child: publisherBadgesWidget(data)),
SliverToBoxAdapter(
child: publisherVerificationWidget(data),
child: _PublisherBadgesWidget(
data: data,
badges: badges,
),
),
SliverToBoxAdapter(
child: _PublisherVerificationWidget(data: data),
),
SliverToBoxAdapter(
child: _PublisherBioWidget(data: data),
),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverToBoxAdapter(
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,