Compare commits

..

No commits in common. "73777fe74e7fea16a21d0a8e38056a3d94d1c804" and "72e6a6a1f62587dc26246e8420804492c7d204d9" have entirely different histories.

20 changed files with 317 additions and 620 deletions

View File

@ -517,13 +517,8 @@
"accountBirthday": "Born on {}", "accountBirthday": "Born on {}",
"accountBadge": "Badge", "accountBadge": "Badge",
"accountCheckInNoRecords": "No check-in records", "accountCheckInNoRecords": "No check-in records",
"badgeCompanyStaff": "Staff", "badgeCompanyStaff": "Solsynth Staff",
"badgeSiteMigration": "Solar Network Native", "badgeSiteMigration": "Solar Network Native",
"badgeCommunitySurvey": "Survey Participant",
"badgeCommunityVerified": "Verified User",
"badgeCommunityContributor": "Great Contributor",
"badgeSiteAnniversary": "Anniversary",
"badgeUserBirthday": "Birthday",
"accountStatus": "Status", "accountStatus": "Status",
"accountStatusOnline": "Online", "accountStatusOnline": "Online",
"accountStatusOffline": "Offline", "accountStatusOffline": "Offline",
@ -748,10 +743,5 @@
"fieldLocation": "Location", "fieldLocation": "Location",
"fieldLinks": "Links", "fieldLinks": "Links",
"fieldLinkName": "Name", "fieldLinkName": "Name",
"fieldLinkUrl": "URL", "fieldLinkUrl": "URL"
"screenAccountBadges": "Badges",
"accountBadges": "Badges",
"accountBadgesDescription": "View and manage your badges.",
"badgeActivated": "Activated badge {}.",
"viewDetailedAttachment": "Details"
} }

View File

@ -515,13 +515,8 @@
"accountBirthday": "出生于 {}", "accountBirthday": "出生于 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暂无运势记录", "accountCheckInNoRecords": "暂无运势记录",
"badgeCompanyStaff": "工作人员", "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "调研参与者",
"badgeCommunityVerified": "认证用户",
"badgeCommunityContributor": "优秀社区贡献者",
"badgeSiteAnniversary": "周年纪念",
"badgeUserBirthday": "生日纪念",
"accountStatus": "状态", "accountStatus": "状态",
"accountStatusOnline": "在线", "accountStatusOnline": "在线",
"accountStatusOffline": "离线", "accountStatusOffline": "离线",
@ -746,10 +741,5 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "链接", "fieldLinks": "链接",
"fieldLinkName": "名称", "fieldLinkName": "名称",
"fieldLinkUrl": "链接", "fieldLinkUrl": "链接"
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看并管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件详情"
} }

View File

@ -515,13 +515,8 @@
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄", "accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "工作人員", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "調研參與者",
"badgeCommunityVerified": "認證用户",
"badgeCommunityContributor": "優秀社區貢獻者",
"badgeSiteAnniversary": "週年紀念",
"badgeUserBirthday": "生日紀念",
"accountStatus": "狀態", "accountStatus": "狀態",
"accountStatusOnline": "在線", "accountStatusOnline": "在線",
"accountStatusOffline": "離線", "accountStatusOffline": "離線",
@ -746,10 +741,5 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "鏈接", "fieldLinks": "鏈接",
"fieldLinkName": "名稱", "fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接", "fieldLinkUrl": "鏈接"
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
} }

View File

@ -515,13 +515,8 @@
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄", "accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "工作人員", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "調研參與者",
"badgeCommunityVerified": "認證用戶",
"badgeCommunityContributor": "優秀社區貢獻者",
"badgeSiteAnniversary": "週年紀念",
"badgeUserBirthday": "生日紀念",
"accountStatus": "狀態", "accountStatus": "狀態",
"accountStatusOnline": "在線", "accountStatusOnline": "在線",
"accountStatusOffline": "離線", "accountStatusOffline": "離線",
@ -746,10 +741,5 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "鏈接", "fieldLinks": "鏈接",
"fieldLinkName": "名稱", "fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接", "fieldLinkUrl": "鏈接"
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情"
} }

View File

@ -28,7 +28,6 @@ class SnPostContentProvider {
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async { Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
Set<String> rids = {}; Set<String> rids = {};
Set<int> uids = {};
for (var i = 0; i < out.length; i++) { for (var i = 0; i < out.length; i++) {
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []); rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
if (out[i].body['thumbnail'] != null) { if (out[i].body['thumbnail'] != null) {
@ -42,9 +41,6 @@ class SnPostContentProvider {
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!), repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
); );
} }
if (out[i].publisher.type == 0) {
uids.add(out[i].publisher.accountId);
}
} }
final attachments = await _attach.getMultiple(rids.toList()); final attachments = await _attach.getMultiple(rids.toList());
@ -69,15 +65,15 @@ class SnPostContentProvider {
); );
} }
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId)); await _ud.listAccount(
await _ud.listAccount(uids); attachments.where((ele) => ele != null).map((ele) => ele!.accountId).toSet(),
);
return out; return out;
} }
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async { Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
Set<String> rids = {}; Set<String> rids = {};
Set<int> uids = {};
rids.addAll(out.body['attachments']?.cast<String>() ?? []); rids.addAll(out.body['attachments']?.cast<String>() ?? []);
if (out.body['thumbnail'] != null) { if (out.body['thumbnail'] != null) {
rids.add(out.body['thumbnail']); rids.add(out.body['thumbnail']);
@ -90,9 +86,6 @@ class SnPostContentProvider {
repostTo: await _preloadRelatedDataSingle(out.repostTo!), repostTo: await _preloadRelatedDataSingle(out.repostTo!),
); );
} }
if (out.publisher.type == 0) {
uids.add(out.publisher.accountId);
}
final attachments = await _attach.getMultiple(rids.toList()); final attachments = await _attach.getMultiple(rids.toList());
@ -115,9 +108,6 @@ class SnPostContentProvider {
), ),
); );
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
await _ud.listAccount(uids);
return out; return out;
} }

View File

@ -4,7 +4,6 @@ import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/account_settings.dart'; import 'package:surface/screens/account/account_settings.dart';
import 'package:surface/screens/account/badges.dart';
import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/profile_edit.dart';
@ -43,8 +42,8 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition( Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition( return FadeThroughTransition(
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
@ -86,13 +85,15 @@ final _appRoutes = [
name: 'postSearch', name: 'postSearch',
builder: (context, state) => PostSearchScreen( builder: (context, state) => PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','), initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','), initialCategories:
state.uri.queryParameters['categories']?.split(','),
), ),
), ),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!),
), ),
GoRoute( GoRoute(
path: '/:slug', path: '/:slug',
@ -105,61 +106,55 @@ final _appRoutes = [
], ],
), ),
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
builder: (context, state) => const AccountScreen(), builder: (context, state) => const AccountScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/badges', path: '/wallet',
name: 'accountBadges', name: 'accountWallet',
builder: (context, state) => const AccountBadgesScreen(), builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
), ),
), GoRoute(
GoRoute( path: '/settings',
path: '/:name', name: 'accountSettings',
name: 'accountProfilePage', builder: (context, state) => AccountSettingsScreen(),
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
), ),
), GoRoute(
], path: '/settings/factors',
), name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
@ -222,7 +217,8 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/:alias', path: '/:alias',
name: 'realmDetail', name: 'realmDetail',
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!), builder: (context, state) =>
RealmDetailScreen(alias: state.pathParameters['alias']!),
), ),
], ],
), ),

View File

@ -173,16 +173,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountWallet'); GoRouter.of(context).pushNamed('accountWallet');
}, },
), ),
ListTile(
title: Text('accountBadges').tr(),
subtitle: Text('accountBadgesDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.award_star),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountBadges');
},
),
ListTile( ListTile(
title: Text('accountSettings').tr(), title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(), subtitle: Text('accountSettingsSubtitle').tr(),

View File

@ -1,134 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart' show kBadgesMeta;
import 'package:surface/theme.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountBadgesScreen extends StatefulWidget {
const AccountBadgesScreen({super.key});
@override
State<AccountBadgesScreen> createState() => _AccountBadgesScreenState();
}
class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
bool _isBusy = false;
List<SnAccountBadge>? _badges;
Future<void> _fetchBadges() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/badges/me');
if (!mounted) return;
setState(
() => _badges = List<SnAccountBadge>.from(
resp.data?.map((e) => SnAccountBadge.fromJson(e)) ?? [],
),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
bool _isActivating = false;
Future<void> _activateBadge(SnAccountBadge badge) async {
try {
setState(() => _isActivating = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/badges/${badge.id}/active');
if (!mounted) return;
context.showSnackbar('badgeActivated'.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
await _fetchBadges();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isActivating = false);
}
}
@override
void initState() {
super.initState();
_fetchBadges();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text('screenAccountBadges').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
if (_badges != null)
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchBadges,
child: ListView.builder(
itemCount: _badges!.length,
itemBuilder: (context, idx) {
final badge = _badges![idx];
return ListTile(
title: Text(
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
).tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (badge.metadata['title'] != null)
Text(badge.metadata['title']).fontSize(14).bold()
else
Text(
'#${badge.id.toString().padLeft(8, '0')}',
style: GoogleFonts.robotoMono(),
).fontSize(14).bold(),
Text(
DateFormat('y/M/d').format(badge.createdAt),
)
],
),
trailing: IconButton(
icon: const Icon(Symbols.check),
onPressed: (badge.isActive || _isActivating)
? null
: () {
_activateBadge(badge);
},
),
leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
color: badge.metadata['color'] != null
? HexColor.fromHex(badge.metadata['color']!)
: kBadgesMeta[badge.type]?.$3,
fill: 1,
),
);
},
),
),
),
),
],
),
);
}
}

View File

@ -20,9 +20,8 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:surface/theme.dart';
final Map<String, (String, IconData, Color)> kBadgesMeta = { const Map<String, (String, IconData, Color)> kBadgesMeta = {
'company.staff': ( 'company.staff': (
'badgeCompanyStaff', 'badgeCompanyStaff',
Symbols.tools_wrench, Symbols.tools_wrench,
@ -33,31 +32,6 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
Symbols.flag, Symbols.flag,
Colors.orange, Colors.orange,
), ),
'site.anniversary': (
'badgeSiteAnniversary',
Symbols.celebration,
Colors.orangeAccent,
),
'user.birthday': (
'badgeUserBirthday',
Symbols.cake,
Colors.red[400]!,
),
'community.survey': (
'badgeCommunitySurvey',
Symbols.star,
Colors.yellow[700]!,
),
'community.verified': (
'badgeCommunityVerified',
Symbols.verified,
Colors.blue,
),
'community.contributor': (
'badgeCommunityContributor',
Symbols.thumb_up,
Colors.lightGreen,
),
}; };
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {
@ -69,7 +43,8 @@ class UserScreen extends StatefulWidget {
State<UserScreen> createState() => _UserScreenState(); State<UserScreen> createState() => _UserScreenState();
} }
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin { class _UserScreenState extends State<UserScreen>
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
SnAccount? _account; SnAccount? _account;
@ -95,7 +70,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<void> _getCheckInRecords() async { Future<void> _getCheckInRecords() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14'); final resp =
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
setState(() { setState(() {
_records = List.from( _records = List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
@ -128,7 +104,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}'); final resp =
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
_publishers = List<SnPublisher>.from( _publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
@ -174,7 +151,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
'related': _account!.name, 'related': _account!.name,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -190,9 +168,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {}); await rel.updateRelationship(
_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return; if (!mounted) return;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}'])); context.showSnackbar(
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -218,12 +198,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); _appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@ -291,18 +273,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _account!.nick, text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.titleLarge!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: '@${_account!.name}', text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style:
color: Colors.white, Theme.of(context).textTheme.bodySmall!.copyWith(
shadows: labelShadows, color: Colors.white,
), shadows: labelShadows,
),
), ),
]), ]),
), ),
@ -370,7 +354,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: ButtonStyle( style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
), ),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
@ -430,7 +415,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey, color: (_status?.isOnline ?? false)
? Colors.green
: Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text( Text(
@ -440,7 +427,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
: 'accountStatusOffline'.tr() : 'accountStatusOffline'.tr()
: 'loading'.tr(), : 'loading'.tr(),
), ),
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null) if (_status != null &&
!_status!.isOnline &&
_status!.lastSeenAt != null)
Text( Text(
'accountStatusLastSeen'.tr(args: [ 'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null _status!.lastSeenAt != null
@ -461,12 +450,13 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr(), text: kBadgesMeta[ele.type]?.$1.tr() ??
), 'unknown'.tr()),
if (ele.metadata['title'] != null) if (ele.metadata['title'] != null)
TextSpan( TextSpan(
text: '\n${ele.metadata['title']}', text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold),
), ),
TextSpan(text: '\n'), TextSpan(text: '\n'),
TextSpan( TextSpan(
@ -475,10 +465,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
], ],
), ),
child: Icon( child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[ele.type]?.$2 ??
color: ele.metadata['color'] != null Symbols.question_mark,
? HexColor.fromHex(ele.metadata['color']!) color: kBadgesMeta[ele.type]?.$3,
: kBadgesMeta[ele.type]?.$3,
fill: 1, fill: 1,
), ),
), ),
@ -493,7 +482,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]), Text('publisherJoinedAt').tr(args: [
DateFormat('y/M/d').format(_account!.createdAt)
]),
], ],
), ),
Row( Row(
@ -526,17 +517,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [ children: [
const Icon(Symbols.star), const Icon(Symbols.star),
const Gap(8), const Gap(8),
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'), Text(
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
const Gap(8), const Gap(8),
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5), Text(calcLevelUpProgressLevel(
_account?.profile?.experience ?? 0))
.fontSize(11)
.opacity(0.5),
const Gap(8), const Gap(8),
Container( Container(
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160), constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: calcLevelUpProgress(_account?.profile?.experience ?? 0), value: calcLevelUpProgress(
_account?.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainer,
).alignment(Alignment.centerLeft), ).alignment(Alignment.centerLeft),
), ),
], ],
@ -556,7 +554,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
return Text( return Text(
'accountCheckInNoRecords', 'accountCheckInNoRecords',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8); )
.tr()
.fontWeight(FontWeight.bold)
.center()
.padding(horizontal: 20, vertical: 8);
} }
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
@ -577,7 +579,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('accountBadge')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox( SizedBox(
height: 80, height: 80,
width: double.infinity, width: double.infinity,
@ -591,10 +597,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: Icon( leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark, kBadgesMeta[badge.type]?.$2 ??
color: badge.metadata['color'] != null Symbols.question_mark,
? HexColor.fromHex(badge.metadata['color']!) color: kBadgesMeta[badge.type]?.$3,
: kBadgesMeta[badge.type]?.$3,
fill: 1, fill: 1,
), ),
title: Text( title: Text(
@ -603,7 +608,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
subtitle: badge.metadata['title'] != null subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title']) ? Text(badge.metadata['title'])
: Text( : Text(
DateFormat('y/M/d').format(badge.createdAt), DateFormat('y/M/d')
.format(badge.createdAt),
), ),
), ),
), ),
@ -699,7 +705,8 @@ class CheckInRecordChart extends StatelessWidget {
), ),
) )
.toList(), .toList(),
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh, getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
), ),
titlesData: FlTitlesData( titlesData: FlTitlesData(

View File

@ -89,20 +89,3 @@ Future<ThemeData> createAppTheme(
), ),
); );
} }
extension HexColor on Color {
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
/// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`).
String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
'${alpha.toRadixString(16).padLeft(2, '0')}'
'${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}';
}

View File

@ -105,7 +105,6 @@ abstract class SnAccountBadge with _$SnAccountBadge {
required dynamic deletedAt, required dynamic deletedAt,
required String type, required String type,
required int accountId, required int accountId,
@Default(false) bool isActive,
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
}) = _SnAccountBadge; }) = _SnAccountBadge;

View File

@ -1835,7 +1835,6 @@ mixin _$SnAccountBadge {
dynamic get deletedAt; dynamic get deletedAt;
String get type; String get type;
int get accountId; int get accountId;
bool get isActive;
Map<String, dynamic> get metadata; Map<String, dynamic> get metadata;
/// Create a copy of SnAccountBadge /// Create a copy of SnAccountBadge
@ -1863,8 +1862,6 @@ mixin _$SnAccountBadge {
(identical(other.type, type) || other.type == type) && (identical(other.type, type) || other.type == type) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
const DeepCollectionEquality().equals(other.metadata, metadata)); const DeepCollectionEquality().equals(other.metadata, metadata));
} }
@ -1878,12 +1875,11 @@ mixin _$SnAccountBadge {
const DeepCollectionEquality().hash(deletedAt), const DeepCollectionEquality().hash(deletedAt),
type, type,
accountId, accountId,
isActive,
const DeepCollectionEquality().hash(metadata)); const DeepCollectionEquality().hash(metadata));
@override @override
String toString() { String toString() {
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)'; return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, metadata: $metadata)';
} }
} }
@ -1900,7 +1896,6 @@ abstract mixin class $SnAccountBadgeCopyWith<$Res> {
dynamic deletedAt, dynamic deletedAt,
String type, String type,
int accountId, int accountId,
bool isActive,
Map<String, dynamic> metadata}); Map<String, dynamic> metadata});
} }
@ -1923,7 +1918,6 @@ class _$SnAccountBadgeCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? type = null, Object? type = null,
Object? accountId = null, Object? accountId = null,
Object? isActive = null,
Object? metadata = null, Object? metadata = null,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
@ -1951,10 +1945,6 @@ class _$SnAccountBadgeCopyWithImpl<$Res>
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
isActive: null == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
metadata: null == metadata metadata: null == metadata
? _self.metadata ? _self.metadata
: metadata // ignore: cast_nullable_to_non_nullable : metadata // ignore: cast_nullable_to_non_nullable
@ -1973,7 +1963,6 @@ class _SnAccountBadge implements SnAccountBadge {
required this.deletedAt, required this.deletedAt,
required this.type, required this.type,
required this.accountId, required this.accountId,
this.isActive = false,
final Map<String, dynamic> metadata = const {}}) final Map<String, dynamic> metadata = const {}})
: _metadata = metadata; : _metadata = metadata;
factory _SnAccountBadge.fromJson(Map<String, dynamic> json) => factory _SnAccountBadge.fromJson(Map<String, dynamic> json) =>
@ -1991,9 +1980,6 @@ class _SnAccountBadge implements SnAccountBadge {
final String type; final String type;
@override @override
final int accountId; final int accountId;
@override
@JsonKey()
final bool isActive;
final Map<String, dynamic> _metadata; final Map<String, dynamic> _metadata;
@override @override
@JsonKey() @JsonKey()
@ -2032,8 +2018,6 @@ class _SnAccountBadge implements SnAccountBadge {
(identical(other.type, type) || other.type == type) && (identical(other.type, type) || other.type == type) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
const DeepCollectionEquality().equals(other._metadata, _metadata)); const DeepCollectionEquality().equals(other._metadata, _metadata));
} }
@ -2047,12 +2031,11 @@ class _SnAccountBadge implements SnAccountBadge {
const DeepCollectionEquality().hash(deletedAt), const DeepCollectionEquality().hash(deletedAt),
type, type,
accountId, accountId,
isActive,
const DeepCollectionEquality().hash(_metadata)); const DeepCollectionEquality().hash(_metadata));
@override @override
String toString() { String toString() {
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)'; return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, metadata: $metadata)';
} }
} }
@ -2071,7 +2054,6 @@ abstract mixin class _$SnAccountBadgeCopyWith<$Res>
dynamic deletedAt, dynamic deletedAt,
String type, String type,
int accountId, int accountId,
bool isActive,
Map<String, dynamic> metadata}); Map<String, dynamic> metadata});
} }
@ -2094,7 +2076,6 @@ class __$SnAccountBadgeCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? type = null, Object? type = null,
Object? accountId = null, Object? accountId = null,
Object? isActive = null,
Object? metadata = null, Object? metadata = null,
}) { }) {
return _then(_SnAccountBadge( return _then(_SnAccountBadge(
@ -2122,10 +2103,6 @@ class __$SnAccountBadgeCopyWithImpl<$Res>
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
isActive: null == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
metadata: null == metadata metadata: null == metadata
? _self._metadata ? _self._metadata
: metadata // ignore: cast_nullable_to_non_nullable : metadata // ignore: cast_nullable_to_non_nullable

View File

@ -187,7 +187,6 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'],
type: json['type'] as String, type: json['type'] as String,
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
isActive: json['is_active'] as bool? ?? false,
metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
); );
@ -199,7 +198,6 @@ Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
'deleted_at': instance.deletedAt, 'deleted_at': instance.deletedAt,
'type': instance.type, 'type': instance.type,
'account_id': instance.accountId, 'account_id': instance.accountId,
'is_active': instance.isActive,
'metadata': instance.metadata, 'metadata': instance.metadata,
}; };

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -10,7 +9,6 @@ class AccountImage extends StatelessWidget {
final Color? backgroundColor; final Color? backgroundColor;
final Color? foregroundColor; final Color? foregroundColor;
final double? radius; final double? radius;
final double? borderRadius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
final Widget? badge; final Widget? badge;
@ -20,7 +18,6 @@ class AccountImage extends StatelessWidget {
this.backgroundColor, this.backgroundColor,
this.foregroundColor, this.foregroundColor,
this.radius, this.radius,
this.borderRadius,
this.fallbackWidget, this.fallbackWidget,
this.badge, this.badge,
}); });
@ -30,36 +27,34 @@ class AccountImage extends StatelessWidget {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final url = sn.getAttachmentUrl(content ?? ''); final url = sn.getAttachmentUrl(content ?? '');
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Stack( return Stack(
clipBehavior: Clip.none,
children: [ children: [
SizedBox( CircleAvatar(
width: (radius != null ? radius! : 20) * 2, key: Key('attachment-${content.hashCode}'),
height: (radius != null ? radius! : 20) * 2, radius: radius,
child: ClipRRect( backgroundColor: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20), backgroundImage: (content?.isNotEmpty ?? false)
child: (content?.isEmpty ?? true) ? ResizeImage(
? Container( UniversalImage.provider(url),
color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer, width: ((radius ?? 20) * devicePixelRatio * 2).round(),
child: (fallbackWidget ?? height: ((radius ?? 20) * devicePixelRatio * 2).round(),
Icon( policy: ResizeImagePolicy.fit,
Symbols.account_circle, )
size: radius != null ? radius! * 1.2 : 24, : null,
color: foregroundColor, child: (content?.isEmpty ?? true)
)) ? (fallbackWidget ??
.center(), Icon(
) Symbols.account_circle,
: AutoResizeUniversalImage( size: radius != null ? radius! * 1.2 : 24,
sn.getAttachmentUrl(url), color: foregroundColor,
key: Key('attachment-${content.hashCode}'), ))
fit: BoxFit.cover, : null,
),
),
), ),
if (badge != null) if (badge != null)
Positioned( Positioned(
right: -4, right: -4,
bottom: -2, bottom: -4,
child: badge!, child: badge!,
), ),
], ],

View File

@ -72,36 +72,34 @@ class AccountPopoverCard extends StatelessWidget {
const Gap(8) const Gap(8)
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (data.badges.isNotEmpty) const Gap(12), const Gap(16),
if (data.badges.isNotEmpty) Wrap(
Wrap( children: data.badges
spacing: 4, .map(
children: data.badges (ele) => Tooltip(
.map( richMessage: TextSpan(
(ele) => Tooltip( children: [
richMessage: TextSpan( TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
children: [ if (ele.metadata['title'] != null)
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), TextSpan(
if (ele.metadata['title'] != null) text: '\n${ele.metadata['title']}',
TextSpan( style: const TextStyle(fontWeight: FontWeight.bold),
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
), ),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
), ),
) ],
.toList(), ),
).padding(horizontal: 24), child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(8), const Gap(8),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -129,33 +127,33 @@ class AccountPopoverCard extends StatelessWidget {
final SnAccountStatusInfo? status = final SnAccountStatusInfo? status =
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null; snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
return Row( return Row(
children: [ children: [
Icon( Icon(
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey, color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text(
status != null
? status.isOnline
? 'accountStatusOnline'.tr()
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (status != null && !status.isOnline && status.lastSeenAt != null)
Text( Text(
'accountStatusLastSeen'.tr(args: [ status != null
status.lastSeenAt != null ? status.isOnline
? RelativeTime(context).format( ? 'accountStatusOnline'.tr()
status.lastSeenAt!.toLocal(), : 'accountStatusOffline'.tr()
) : 'loading'.tr(),
: 'unknown', ),
]), if (status != null && !status.isOnline && status.lastSeenAt != null)
).padding(left: 6).opacity(0.75), Text(
], 'accountStatusLastSeen'.tr(args: [
).padding(horizontal: 24); status.lastSeenAt != null
? RelativeTime(context).format(
status.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
],
).padding(horizontal: 24);
}, },
), ),
// Bottom padding // Bottom padding

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' show max;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
@ -44,9 +43,6 @@ class AttachmentZoomView extends StatefulWidget {
class _AttachmentZoomViewState extends State<AttachmentZoomView> { class _AttachmentZoomViewState extends State<AttachmentZoomView> {
late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0); late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0);
bool _showOverlay = true;
bool _dismissable = true;
void _updatePage() { void _updatePage() {
setState(() { setState(() {
if (_isCompletedDownload) { if (_isCompletedDownload) {
@ -150,7 +146,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none, direction: DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: GestureDetector( child: GestureDetector(
@ -167,9 +163,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
child: PhotoView( child: PhotoView(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
backgroundDecoration: BoxDecoration(color: Colors.transparent), backgroundDecoration: BoxDecoration(color: Colors.transparent),
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
imageProvider: UniversalImage.provider( imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.first.rid), sn.getAttachmentUrl(widget.data.first.rid),
), ),
@ -179,10 +172,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
return PhotoViewGallery.builder( return PhotoViewGallery.builder(
pageController: _pageController, pageController: _pageController,
enableRotation: true, scrollPhysics: const BouncingScrollPhysics(),
scaleStateChangedCallback: (scaleState) {
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
},
builder: (context, idx) { builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
@ -207,27 +197,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
backgroundDecoration: BoxDecoration(color: Colors.transparent), backgroundDecoration: BoxDecoration(color: Colors.transparent),
); );
}), }),
Positioned(
top: max(MediaQuery.of(context).padding.top, 8),
left: 14,
child: IgnorePointer(
ignoring: !_showOverlay,
child: IconButton(
constraints: const BoxConstraints(),
icon: const Icon(Icons.close),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.surface.withOpacity(0.5),
),
),
onPressed: () {
Navigator.of(context).pop();
},
)
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: IgnorePointer( child: IgnorePointer(
@ -245,9 +214,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
), ),
), ),
), ),
) ),
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
Positioned( Positioned(
left: 16, left: 16,
right: 16, right: 16,
@ -351,6 +318,16 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
]), ]),
style: metaTextStyle, style: metaTextStyle,
).padding(right: 2), ).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null && if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null) item.metadata['exif']?['Model'] != null)
Text( Text(
@ -367,44 +344,29 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
'${item.metadata['width']}x${item.metadata['height']}', '${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle, style: metaTextStyle,
), ),
], if (item.metadata['ratio'] != null)
), Text(
), (item.metadata['ratio'] as num).toStringAsFixed(2),
const Gap(4), style: metaTextStyle,
InkWell( ),
onTap: () { Text(
_showDetail = true; item.mimetype,
showModalBottomSheet( style: metaTextStyle,
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data
.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
), ),
).then((_) { ],
_showDetail = false;
});
},
child: Text(
'viewDetailedAttachment'.tr(),
style: metaTextStyle.copyWith(decoration: TextDecoration.underline),
), ),
), ),
], ],
); );
}), }),
) ),
.opacity(_showOverlay ? 1 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
), ),
], ],
), ),
), ),
onTap: () {
setState(() => _showOverlay = !_showOverlay);
},
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
if (_showDetail) return; if (_showDetail) return;
if (details.delta.dy <= -20) { if (details.delta.dy <= -40) {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -416,6 +378,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
}); });
} }
}, },
onTap: () {
Navigator.of(context).pop();
},
), ),
); );
} }
@ -515,14 +480,14 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
), ),
tableGap, tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow( ...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [ children: [
TableCell(child: Text(k).padding(right: 16)), TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())), TableCell(child: Text(data.metadata['exif'][k].toString())),
], ],
)) ?? )) ??
[]), []),
], ],
).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom), ).padding(horizontal: 20, vertical: 8),
), ),
), ),
], ],

View File

@ -117,7 +117,7 @@ class ChatMessage extends StatelessWidget {
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
blurRadius: 5.0, blurRadius: 5.0,
color: Color.fromARGB(200, 0, 0, 0), color: Color.fromARGB(150, 0, 0, 0),
), ),
], ],
) )

View File

@ -28,7 +28,8 @@ class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller; final ChatMessageController controller;
final SnChannelMember? otherMember; final SnChannelMember? otherMember;
const ChatMessageInput({super.key, required this.controller, this.otherMember}); const ChatMessageInput(
{super.key, required this.controller, this.otherMember});
@override @override
State<ChatMessageInput> createState() => ChatMessageInputState(); State<ChatMessageInput> createState() => ChatMessageInputState();
@ -45,12 +46,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
final HotKey _newLineHotKey = HotKey( final HotKey _newLineHotKey = HotKey(
key: PhysicalKeyboardKey.enter, key: PhysicalKeyboardKey.enter,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@ -100,7 +109,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear(); _attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []); _attachments.addAll(
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value); setState(() => _editingMessage = value);
} }
@ -139,7 +149,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
media.name, media.name,
'messaging', 'messaging',
null, null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
); );
final item = await attach.chunkedUploadParts( final item = await attach.chunkedUploadParts(
@ -171,7 +183,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
widget.controller.sendMessage( widget.controller.sendMessage(
_editingMessage != null ? 'messages.edit' : 'messages.new', _editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text, _contentController.text,
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), attachments: _attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
relatedId: _editingMessage?.id, relatedId: _editingMessage?.id,
quoteId: _replyingMessage?.id, quoteId: _replyingMessage?.id,
editingMessage: _editingMessage, editingMessage: _editingMessage,
@ -232,12 +247,15 @@ class ChatMessageInputState extends State<ChatMessageInput> {
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress), tween: Tween(begin: 0, end: _progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_isBusy) else if (_isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
Padding( Padding(
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, padding: _attachments.isNotEmpty
? const EdgeInsets.only(top: 8)
: EdgeInsets.zero,
child: PostMediaPendingList( child: PostMediaPendingList(
attachments: _attachments, attachments: _attachments,
isBusy: _isBusy, isBusy: _isBusy,
@ -249,9 +267,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}, },
onUpdateBusy: (state) => setState(() => _isBusy = state), onUpdateBusy: (state) => setState(() => _isBusy = state),
), ),
) ).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true) const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: _replyingMessage != null child: _replyingMessage != null
@ -272,7 +289,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: Text( child: Text(
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}', _replyingMessage?.body['text'] ??
'${_replyingMessage?.sender.nick}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -289,9 +307,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8), ).padding(vertical: 8),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
) ).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
.height(_replyingMessage != null ? 38 : 0, animate: true) const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: _editingMessage != null child: _editingMessage != null
@ -312,7 +329,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: Text( child: Text(
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}', _editingMessage?.body['text'] ??
'${_editingMessage?.sender.nick}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -330,9 +348,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8), ).padding(vertical: 8),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
) ).height(_editingMessage != null ? 38 : 0, animate: true).animate(
.height(_editingMessage != null ? 38 : 0, animate: true) const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
constraints: BoxConstraints(minHeight: 56, maxHeight: 240), constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
@ -349,11 +366,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
? 'fieldChatMessageDirect'.tr(args: [ ? 'fieldChatMessageDirect'.tr(args: [
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}', '@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
]) ])
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]), : 'fieldChatMessage'.tr(args: [
widget.controller.channel?.name ?? 'loading'.tr()
]),
border: InputBorder.none, border: InputBorder.none,
), ),
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) { onSubmitted: (_) {
if (_isBusy) return; if (_isBusy) return;
_sendMessage(); _sendMessage();
@ -368,7 +388,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.mood, Symbols.mood,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
@ -388,7 +409,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.send, Symbols.send,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), ),
@ -405,7 +427,8 @@ class _StickerPicker extends StatelessWidget {
final Function? onDismiss; final Function? onDismiss;
final Function(String)? onInsert; final Function(String)? onInsert;
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert}); const _StickerPicker(
{this.onDismiss, required this.originalText, this.onInsert});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -416,9 +439,8 @@ class _StickerPicker extends StatelessWidget {
}, },
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: min(360, MediaQuery.of(context).size.width - 40), maxWidth: min(360, MediaQuery.of(context).size.width),
maxHeight: 240, maxHeight: 240),
),
child: Material( child: Material(
elevation: 8, elevation: 8,
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
@ -431,8 +453,10 @@ class _StickerPicker extends StatelessWidget {
return <Widget>[ return <Widget>[
Container( Container(
margin: EdgeInsets.only(bottom: 8), margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding:
color: Theme.of(context).colorScheme.surfaceContainerHigh, EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -444,7 +468,8 @@ class _StickerPicker extends StatelessWidget {
), ),
GridView.builder( GridView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding:
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
shrinkWrap: true, shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48, maxCrossAxisExtent: 48,
@ -467,7 +492,8 @@ class _StickerPicker extends StatelessWidget {
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: ':${element.pack.prefix}${element.alias}:\n', text:
':${element.pack.prefix}${element.alias}:\n',
style: GoogleFonts.robotoMono()), style: GoogleFonts.robotoMono()),
TextSpan(text: element.name).bold(), TextSpan(text: element.name).bold(),
], ],
@ -476,11 +502,15 @@ class _StickerPicker extends StatelessWidget {
width: 48, width: 48,
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(
color: Theme.of(context).colorScheme.surfaceContainerHigh, Radius.circular(8)),
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(
Radius.circular(8)),
child: UniversalImage( child: UniversalImage(
sn.getAttachmentUrl(element.attachment.rid), sn.getAttachmentUrl(element.attachment.rid),
width: 48, width: 48,

View File

@ -22,7 +22,6 @@ import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
@ -43,8 +42,6 @@ import 'package:surface/widgets/post/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import '../../screens/account/profile_page.dart' show kBadgesMeta;
class OpenablePostItem extends StatelessWidget { class OpenablePostItem extends StatelessWidget {
final SnPost data; final SnPost data;
final bool showReactions; final bool showReactions;
@ -870,31 +867,12 @@ class _PostContentHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final user = data.publisher.type == 0 ? ud.getAccountFromCache(data.publisher.accountId) : null;
return Row( return Row(
children: [ children: [
GestureDetector( GestureDetector(
child: AccountImage( child: AccountImage(
content: data.publisher.avatar, content: data.publisher.avatar,
radius: isCompact ? 12 : 20, radius: isCompact ? 12 : 20,
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(200, 0, 0, 0),
),
],
)
: null,
), ),
onTap: () { onTap: () {
showPopover( showPopover(

View File

@ -6,24 +6,17 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import '../../screens/account/profile_page.dart' show kBadgesMeta;
class PublisherPopoverCard extends StatelessWidget { class PublisherPopoverCard extends StatelessWidget {
final SnPublisher data; final SnPublisher data;
const PublisherPopoverCard({super.key, required this.data}); const PublisherPopoverCard({super.key, required this.data});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -48,7 +41,6 @@ class PublisherPopoverCard extends StatelessWidget {
AccountImage( AccountImage(
content: data.avatar, content: data.avatar,
radius: 20, radius: 20,
borderRadius: data.type == 1 ? 8 : 20,
), ),
Gap(16), Gap(16),
Expanded( Expanded(
@ -76,36 +68,6 @@ class PublisherPopoverCard extends StatelessWidget {
const Gap(8) const Gap(8)
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (user != null && user.badges.isNotEmpty) const Gap(16),
if (user != null && user.badges.isNotEmpty)
Wrap(
spacing: 4,
children: user.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(16), const Gap(16),
Row( Row(
children: [ children: [
@ -146,7 +108,10 @@ class PublisherPopoverCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75), Text('publisherTotalDownvote')
.tr()
.fontSize(13)
.opacity(0.75),
Text(data.totalDownvote.toString()), Text(data.totalDownvote.toString()),
], ],
), ),