Compare commits
5 Commits
72e6a6a1f6
...
73777fe74e
Author | SHA1 | Date | |
---|---|---|---|
73777fe74e | |||
33a4bd7e71 | |||
17e6b81f76 | |||
22fde6b400 | |||
6e03a00280 |
@ -517,8 +517,13 @@
|
|||||||
"accountBirthday": "Born on {}",
|
"accountBirthday": "Born on {}",
|
||||||
"accountBadge": "Badge",
|
"accountBadge": "Badge",
|
||||||
"accountCheckInNoRecords": "No check-in records",
|
"accountCheckInNoRecords": "No check-in records",
|
||||||
"badgeCompanyStaff": "Solsynth Staff",
|
"badgeCompanyStaff": "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",
|
||||||
@ -743,5 +748,10 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
|
@ -515,8 +515,13 @@
|
|||||||
"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": "离线",
|
||||||
@ -741,5 +746,10 @@
|
|||||||
"fieldLocation": "位置",
|
"fieldLocation": "位置",
|
||||||
"fieldLinks": "链接",
|
"fieldLinks": "链接",
|
||||||
"fieldLinkName": "名称",
|
"fieldLinkName": "名称",
|
||||||
"fieldLinkUrl": "链接"
|
"fieldLinkUrl": "链接",
|
||||||
|
"screenAccountBadges": "徽章",
|
||||||
|
"accountBadges": "徽章",
|
||||||
|
"accountBadgesDescription": "查看并管理你的徽章。",
|
||||||
|
"badgeActivated": "已佩戴徽章 {}。",
|
||||||
|
"viewDetailedAttachment": "查看附件详情"
|
||||||
}
|
}
|
||||||
|
@ -515,8 +515,13 @@
|
|||||||
"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": "離線",
|
||||||
@ -741,5 +746,10 @@
|
|||||||
"fieldLocation": "位置",
|
"fieldLocation": "位置",
|
||||||
"fieldLinks": "鏈接",
|
"fieldLinks": "鏈接",
|
||||||
"fieldLinkName": "名稱",
|
"fieldLinkName": "名稱",
|
||||||
"fieldLinkUrl": "鏈接"
|
"fieldLinkUrl": "鏈接",
|
||||||
|
"screenAccountBadges": "徽章",
|
||||||
|
"accountBadges": "徽章",
|
||||||
|
"accountBadgesDescription": "查看並管理你的徽章。",
|
||||||
|
"badgeActivated": "已佩戴徽章 {}。",
|
||||||
|
"viewDetailedAttachment": "查看附件詳情"
|
||||||
}
|
}
|
||||||
|
@ -515,8 +515,13 @@
|
|||||||
"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": "離線",
|
||||||
@ -741,5 +746,10 @@
|
|||||||
"fieldLocation": "位置",
|
"fieldLocation": "位置",
|
||||||
"fieldLinks": "鏈接",
|
"fieldLinks": "鏈接",
|
||||||
"fieldLinkName": "名稱",
|
"fieldLinkName": "名稱",
|
||||||
"fieldLinkUrl": "鏈接"
|
"fieldLinkUrl": "鏈接",
|
||||||
|
"screenAccountBadges": "徽章",
|
||||||
|
"accountBadges": "徽章",
|
||||||
|
"accountBadgesDescription": "查看並管理你的徽章。",
|
||||||
|
"badgeActivated": "已佩戴徽章 {}。",
|
||||||
|
"viewDetailedAttachment": "查看附件詳情"
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ 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) {
|
||||||
@ -41,6 +42,9 @@ 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());
|
||||||
@ -65,15 +69,15 @@ class SnPostContentProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _ud.listAccount(
|
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId).toSet(),
|
await _ud.listAccount(uids);
|
||||||
);
|
|
||||||
|
|
||||||
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']);
|
||||||
@ -86,6 +90,9 @@ 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());
|
||||||
|
|
||||||
@ -108,6 +115,9 @@ class SnPostContentProvider {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||||
|
await _ud.listAccount(uids);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
114
lib/router.dart
114
lib/router.dart
@ -4,6 +4,7 @@ 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';
|
||||||
@ -42,8 +43,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(BuildContext context, Animation<double> animation,
|
Widget _fadeThroughTransition(
|
||||||
Animation<double> secondaryAnimation, Widget child) {
|
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
return FadeThroughTransition(
|
return FadeThroughTransition(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
secondaryAnimation: secondaryAnimation,
|
secondaryAnimation: secondaryAnimation,
|
||||||
@ -85,15 +86,13 @@ 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:
|
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||||
state.uri.queryParameters['categories']?.split(','),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/publishers/:name',
|
path: '/publishers/:name',
|
||||||
name: 'postPublisher',
|
name: 'postPublisher',
|
||||||
builder: (context, state) =>
|
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
PostPublisherScreen(name: state.pathParameters['name']!),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:slug',
|
path: '/:slug',
|
||||||
@ -106,55 +105,61 @@ 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: '/wallet',
|
path: '/badges',
|
||||||
name: 'accountWallet',
|
name: 'accountBadges',
|
||||||
builder: (context, state) => const WalletScreen(),
|
builder: (context, state) => const AccountBadgesScreen(),
|
||||||
|
),
|
||||||
|
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(
|
),
|
||||||
path: '/settings',
|
GoRoute(
|
||||||
name: 'accountSettings',
|
path: '/:name',
|
||||||
builder: (context, state) => AccountSettingsScreen(),
|
name: 'accountProfilePage',
|
||||||
|
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',
|
||||||
@ -217,8 +222,7 @@ final _appRoutes = [
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:alias',
|
path: '/:alias',
|
||||||
name: 'realmDetail',
|
name: 'realmDetail',
|
||||||
builder: (context, state) =>
|
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||||
RealmDetailScreen(alias: state.pathParameters['alias']!),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -173,6 +173,16 @@ 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(),
|
||||||
|
134
lib/screens/account/badges.dart
Normal file
134
lib/screens/account/badges.dart
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -20,8 +20,9 @@ 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';
|
||||||
|
|
||||||
const Map<String, (String, IconData, Color)> kBadgesMeta = {
|
final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||||
'company.staff': (
|
'company.staff': (
|
||||||
'badgeCompanyStaff',
|
'badgeCompanyStaff',
|
||||||
Symbols.tools_wrench,
|
Symbols.tools_wrench,
|
||||||
@ -32,6 +33,31 @@ const 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 {
|
||||||
@ -43,8 +69,7 @@ class UserScreen extends StatefulWidget {
|
|||||||
State<UserScreen> createState() => _UserScreenState();
|
State<UserScreen> createState() => _UserScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserScreenState extends State<UserScreen>
|
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final ScrollController _scrollController = ScrollController();
|
late final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@ -70,8 +95,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
Future<void> _getCheckInRecords() async {
|
Future<void> _getCheckInRecords() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp =
|
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||||
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)) ?? [],
|
||||||
@ -104,8 +128,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp =
|
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
||||||
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)) ?? [],
|
||||||
);
|
);
|
||||||
@ -151,8 +174,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
'related': _account!.name,
|
'related': _account!.name,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -168,11 +190,9 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(
|
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||||
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -198,14 +218,12 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
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 =
|
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_appBarBlur =
|
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||||
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,20 +291,18 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _account!.nick,
|
text: _account!.nick,
|
||||||
style:
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
Theme.of(context).textTheme.titleLarge!.copyWith(
|
color: Colors.white,
|
||||||
color: Colors.white,
|
shadows: labelShadows,
|
||||||
shadows: labelShadows,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_account!.name}',
|
text: '@${_account!.name}',
|
||||||
style:
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
color: Colors.white,
|
||||||
color: Colors.white,
|
shadows: labelShadows,
|
||||||
shadows: labelShadows,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@ -354,8 +370,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity:
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@ -415,9 +430,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
Symbols.circle,
|
Symbols.circle,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: (_status?.isOnline ?? false)
|
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||||
? Colors.green
|
|
||||||
: Colors.grey,
|
|
||||||
).padding(all: 4),
|
).padding(all: 4),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
@ -427,9 +440,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
: 'accountStatusOffline'.tr()
|
: 'accountStatusOffline'.tr()
|
||||||
: 'loading'.tr(),
|
: 'loading'.tr(),
|
||||||
),
|
),
|
||||||
if (_status != null &&
|
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
||||||
!_status!.isOnline &&
|
|
||||||
_status!.lastSeenAt != null)
|
|
||||||
Text(
|
Text(
|
||||||
'accountStatusLastSeen'.tr(args: [
|
'accountStatusLastSeen'.tr(args: [
|
||||||
_status!.lastSeenAt != null
|
_status!.lastSeenAt != null
|
||||||
@ -450,13 +461,12 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.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(
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
TextSpan(text: '\n'),
|
TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
@ -465,9 +475,10 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
kBadgesMeta[ele.type]?.$2 ??
|
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||||
Symbols.question_mark,
|
color: ele.metadata['color'] != null
|
||||||
color: kBadgesMeta[ele.type]?.$3,
|
? HexColor.fromHex(ele.metadata['color']!)
|
||||||
|
: kBadgesMeta[ele.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -482,9 +493,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherJoinedAt').tr(args: [
|
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
|
||||||
DateFormat('y/M/d').format(_account!.createdAt)
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -517,24 +526,17 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.star),
|
const Icon(Symbols.star),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||||
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(calcLevelUpProgressLevel(
|
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||||
_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(
|
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
|
||||||
_account?.profile?.experience ?? 0),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
backgroundColor: Theme.of(context)
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
.colorScheme
|
|
||||||
.surfaceContainer,
|
|
||||||
).alignment(Alignment.centerLeft),
|
).alignment(Alignment.centerLeft),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -554,11 +556,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
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,
|
||||||
@ -579,11 +577,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('accountBadge')
|
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 80,
|
height: 80,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -597,9 +591,10 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
child: Card(
|
child: Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
kBadgesMeta[badge.type]?.$2 ??
|
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
||||||
Symbols.question_mark,
|
color: badge.metadata['color'] != null
|
||||||
color: kBadgesMeta[badge.type]?.$3,
|
? HexColor.fromHex(badge.metadata['color']!)
|
||||||
|
: kBadgesMeta[badge.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -608,8 +603,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
subtitle: badge.metadata['title'] != null
|
subtitle: badge.metadata['title'] != null
|
||||||
? Text(badge.metadata['title'])
|
? Text(badge.metadata['title'])
|
||||||
: Text(
|
: Text(
|
||||||
DateFormat('y/M/d')
|
DateFormat('y/M/d').format(badge.createdAt),
|
||||||
.format(badge.createdAt),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -705,8 +699,7 @@ class CheckInRecordChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
getTooltipColor: (_) =>
|
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
|
@ -89,3 +89,20 @@ 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')}';
|
||||||
|
}
|
||||||
|
@ -105,6 +105,7 @@ 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;
|
||||||
|
|
||||||
|
@ -1835,6 +1835,7 @@ 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
|
||||||
@ -1862,6 +1863,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1875,11 +1878,12 @@ 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, metadata: $metadata)';
|
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1896,6 +1900,7 @@ 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});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1918,6 +1923,7 @@ 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(
|
||||||
@ -1945,6 +1951,10 @@ 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
|
||||||
@ -1963,6 +1973,7 @@ 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) =>
|
||||||
@ -1980,6 +1991,9 @@ 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()
|
||||||
@ -2018,6 +2032,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2031,11 +2047,12 @@ 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, metadata: $metadata)';
|
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2054,6 +2071,7 @@ 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});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2076,6 +2094,7 @@ 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(
|
||||||
@ -2103,6 +2122,10 @@ 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
|
||||||
|
@ -187,6 +187,7 @@ _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 {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -198,6 +199,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ 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;
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ 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,
|
||||||
});
|
});
|
||||||
@ -27,34 +30,36 @@ 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: [
|
||||||
CircleAvatar(
|
SizedBox(
|
||||||
key: Key('attachment-${content.hashCode}'),
|
width: (radius != null ? radius! : 20) * 2,
|
||||||
radius: radius,
|
height: (radius != null ? radius! : 20) * 2,
|
||||||
backgroundColor: backgroundColor,
|
child: ClipRRect(
|
||||||
backgroundImage: (content?.isNotEmpty ?? false)
|
borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20),
|
||||||
? ResizeImage(
|
child: (content?.isEmpty ?? true)
|
||||||
UniversalImage.provider(url),
|
? Container(
|
||||||
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
|
||||||
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
child: (fallbackWidget ??
|
||||||
policy: ResizeImagePolicy.fit,
|
Icon(
|
||||||
)
|
Symbols.account_circle,
|
||||||
: null,
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
child: (content?.isEmpty ?? true)
|
color: foregroundColor,
|
||||||
? (fallbackWidget ??
|
))
|
||||||
Icon(
|
.center(),
|
||||||
Symbols.account_circle,
|
)
|
||||||
size: radius != null ? radius! * 1.2 : 24,
|
: AutoResizeUniversalImage(
|
||||||
color: foregroundColor,
|
sn.getAttachmentUrl(url),
|
||||||
))
|
key: Key('attachment-${content.hashCode}'),
|
||||||
: null,
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (badge != null)
|
if (badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: -4,
|
right: -4,
|
||||||
bottom: -4,
|
bottom: -2,
|
||||||
child: badge!,
|
child: badge!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -72,34 +72,36 @@ class AccountPopoverCard extends StatelessWidget {
|
|||||||
const Gap(8)
|
const Gap(8)
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(16),
|
if (data.badges.isNotEmpty) const Gap(12),
|
||||||
Wrap(
|
if (data.badges.isNotEmpty)
|
||||||
children: data.badges
|
Wrap(
|
||||||
.map(
|
spacing: 4,
|
||||||
(ele) => Tooltip(
|
children: data.badges
|
||||||
richMessage: TextSpan(
|
.map(
|
||||||
children: [
|
(ele) => Tooltip(
|
||||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
richMessage: TextSpan(
|
||||||
if (ele.metadata['title'] != null)
|
children: [
|
||||||
TextSpan(
|
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||||
text: '\n${ele.metadata['title']}',
|
if (ele.metadata['title'] != null)
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
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,
|
||||||
),
|
),
|
||||||
TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: DateFormat.yMEd().format(ele.createdAt),
|
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
.toList(),
|
||||||
child: Icon(
|
).padding(horizontal: 24),
|
||||||
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,
|
||||||
@ -127,33 +129,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(
|
||||||
status != null
|
'accountStatusLastSeen'.tr(args: [
|
||||||
? status.isOnline
|
status.lastSeenAt != null
|
||||||
? 'accountStatusOnline'.tr()
|
? RelativeTime(context).format(
|
||||||
: 'accountStatusOffline'.tr()
|
status.lastSeenAt!.toLocal(),
|
||||||
: 'loading'.tr(),
|
)
|
||||||
),
|
: 'unknown',
|
||||||
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
]),
|
||||||
Text(
|
).padding(left: 6).opacity(0.75),
|
||||||
'accountStatusLastSeen'.tr(args: [
|
],
|
||||||
status.lastSeenAt != null
|
).padding(horizontal: 24);
|
||||||
? RelativeTime(context).format(
|
|
||||||
status.lastSeenAt!.toLocal(),
|
|
||||||
)
|
|
||||||
: 'unknown',
|
|
||||||
]),
|
|
||||||
).padding(left: 6).opacity(0.75),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Bottom padding
|
// Bottom padding
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
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';
|
||||||
@ -43,6 +44,9 @@ 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) {
|
||||||
@ -146,7 +150,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
direction: DismissiblePageDismissDirection.none,
|
direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -163,6 +167,9 @@ 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),
|
||||||
),
|
),
|
||||||
@ -172,7 +179,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
|
|
||||||
return PhotoViewGallery.builder(
|
return PhotoViewGallery.builder(
|
||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
scrollPhysics: const BouncingScrollPhysics(),
|
enableRotation: true,
|
||||||
|
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(
|
||||||
@ -197,6 +207,27 @@ 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(
|
||||||
@ -214,7 +245,9 @@ 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,
|
||||||
@ -318,16 +351,6 @@ 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(
|
||||||
@ -344,29 +367,44 @@ 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),
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.mimetype,
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(4),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_showDetail = true;
|
||||||
|
showModalBottomSheet(
|
||||||
|
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 <= -40) {
|
if (details.delta.dy <= -20) {
|
||||||
_showDetail = true;
|
_showDetail = true;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@ -378,9 +416,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -480,14 +515,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),
|
).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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(150, 0, 0, 0),
|
color: Color.fromARGB(200, 0, 0, 0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -28,8 +28,7 @@ class ChatMessageInput extends StatefulWidget {
|
|||||||
final ChatMessageController controller;
|
final ChatMessageController controller;
|
||||||
final SnChannelMember? otherMember;
|
final SnChannelMember? otherMember;
|
||||||
|
|
||||||
const ChatMessageInput(
|
const ChatMessageInput({super.key, required this.controller, this.otherMember});
|
||||||
{super.key, required this.controller, this.otherMember});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMessageInput> createState() => ChatMessageInputState();
|
State<ChatMessageInput> createState() => ChatMessageInputState();
|
||||||
@ -46,20 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
|
|
||||||
final HotKey _pasteHotKey = HotKey(
|
final HotKey _pasteHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.keyV,
|
key: PhysicalKeyboardKey.keyV,
|
||||||
modifiers: [
|
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||||
(!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: [
|
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||||
(!kIsWeb && Platform.isMacOS)
|
|
||||||
? HotKeyModifier.meta
|
|
||||||
: HotKeyModifier.control
|
|
||||||
],
|
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -109,8 +100,7 @@ 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(
|
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||||
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
|
||||||
setState(() => _editingMessage = value);
|
setState(() => _editingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,9 +139,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.name,
|
media.name,
|
||||||
'messaging',
|
'messaging',
|
||||||
null,
|
null,
|
||||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
? 'image/png'
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
final item = await attach.chunkedUploadParts(
|
||||||
@ -183,10 +171,7 @@ 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
|
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||||
.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,
|
||||||
@ -247,15 +232,12 @@ 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, _) =>
|
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
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
|
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||||
? const EdgeInsets.only(top: 8)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: PostMediaPendingList(
|
child: PostMediaPendingList(
|
||||||
attachments: _attachments,
|
attachments: _attachments,
|
||||||
isBusy: _isBusy,
|
isBusy: _isBusy,
|
||||||
@ -267,8 +249,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||||
),
|
),
|
||||||
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
)
|
||||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _replyingMessage != null
|
child: _replyingMessage != null
|
||||||
@ -289,8 +272,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_replyingMessage?.body['text'] ??
|
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||||
'${_replyingMessage?.sender.nick}',
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -307,8 +289,9 @@ 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(
|
)
|
||||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _editingMessage != null
|
child: _editingMessage != null
|
||||||
@ -329,8 +312,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_editingMessage?.body['text'] ??
|
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||||
'${_editingMessage?.sender.nick}',
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -348,8 +330,9 @@ 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(
|
)
|
||||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||||
|
.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),
|
||||||
@ -366,14 +349,11 @@ 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: [
|
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
|
||||||
widget.controller.channel?.name ?? 'loading'.tr()
|
|
||||||
]),
|
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (_isBusy) return;
|
if (_isBusy) return;
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
@ -388,8 +368,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
Symbols.mood,
|
Symbols.mood,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity:
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -409,8 +388,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
Symbols.send,
|
Symbols.send,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity:
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
@ -427,8 +405,7 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
final Function? onDismiss;
|
final Function? onDismiss;
|
||||||
final Function(String)? onInsert;
|
final Function(String)? onInsert;
|
||||||
|
|
||||||
const _StickerPicker(
|
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
||||||
{this.onDismiss, required this.originalText, this.onInsert});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -439,8 +416,9 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: min(360, MediaQuery.of(context).size.width),
|
maxWidth: min(360, MediaQuery.of(context).size.width - 40),
|
||||||
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)),
|
||||||
@ -453,10 +431,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
return <Widget>[
|
return <Widget>[
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
padding:
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -468,8 +444,7 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
GridView.builder(
|
GridView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
padding:
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 48,
|
maxCrossAxisExtent: 48,
|
||||||
@ -492,8 +467,7 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text:
|
text: ':${element.pack.prefix}${element.alias}:\n',
|
||||||
':${element.pack.prefix}${element.alias}:\n',
|
|
||||||
style: GoogleFonts.robotoMono()),
|
style: GoogleFonts.robotoMono()),
|
||||||
TextSpan(text: element.name).bold(),
|
TextSpan(text: element.name).bold(),
|
||||||
],
|
],
|
||||||
@ -502,15 +476,11 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
Radius.circular(8)),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHigh,
|
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
Radius.circular(8)),
|
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
sn.getAttachmentUrl(element.attachment.rid),
|
sn.getAttachmentUrl(element.attachment.rid),
|
||||||
width: 48,
|
width: 48,
|
||||||
|
@ -22,6 +22,7 @@ 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';
|
||||||
@ -42,6 +43,8 @@ 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;
|
||||||
@ -867,12 +870,31 @@ 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(
|
||||||
|
@ -6,17 +6,24 @@ 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,
|
||||||
@ -41,6 +48,7 @@ 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(
|
||||||
@ -68,6 +76,36 @@ 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: [
|
||||||
@ -108,10 +146,7 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('publisherTotalDownvote')
|
Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75),
|
||||||
.tr()
|
|
||||||
.fontSize(13)
|
|
||||||
.opacity(0.75),
|
|
||||||
Text(data.totalDownvote.toString()),
|
Text(data.totalDownvote.toString()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user