From 288c0399f924eef1299294987d69bdcce6170cf5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 4 Mar 2025 22:30:17 +0800 Subject: [PATCH] :sparkles: User cache --- lib/main.dart | 4 + lib/providers/user_directory.dart | 72 +++++++- lib/screens/chat.dart | 11 +- lib/screens/chat/channel_detail.dart | 10 +- lib/screens/chat/room.dart | 3 +- lib/screens/realm/realm_detail.dart | 61 ++++-- lib/screens/sharing.dart | 87 ++++++--- lib/widgets/attachment/attachment_zoom.dart | 126 +++++++++---- lib/widgets/chat/chat_message.dart | 2 +- lib/widgets/chat/chat_message_input.dart | 2 +- lib/widgets/chat/chat_typing_indicator.dart | 6 +- lib/widgets/post/post_item.dart | 194 ++++++++++++++------ lib/widgets/post/publisher_popover.dart | 11 +- 13 files changed, 422 insertions(+), 167 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 320ec5b..d6d3a5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -314,6 +314,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { if (!mounted) return; final sticker = context.read(); await sticker.listSticker(); + if (!mounted) return; + final ud = context.read(); + final userCacheSize = await ud.loadAccountCache(); + logging.info('[Users] Loaded local user cache, size: $userCacheSize'); logging.info('[Bootstrap] Everything initialized!'); } catch (err) { if (!mounted) return; diff --git a/lib/providers/user_directory.dart b/lib/providers/user_directory.dart index db01a7c..25545b8 100644 --- a/lib/providers/user_directory.dart +++ b/lib/providers/user_directory.dart @@ -1,19 +1,36 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:surface/database/database.dart'; +import 'package:surface/providers/database.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/account.dart'; class UserDirectoryProvider { late final SnNetworkProvider _sn; + late final DatabaseProvider _dt; UserDirectoryProvider(BuildContext context) { _sn = context.read(); + _dt = context.read(); } final Map _idCache = {}; final Map _cache = {}; + Future loadAccountCache({int max = 100}) async { + final out = await (_dt.db.snLocalAccount.select()..limit(max)).get(); + for (final ele in out) { + _cache[ele.id] = ele.content; + _idCache[ele.name] = ele.id; + } + return out.length; + } + Future> listAccount(Iterable id) async { + // In-memory cache final out = List.generate(id.length, (e) => null); final plannedQuery = {}; for (var idx = 0; idx < out.length; idx++) { @@ -27,6 +44,23 @@ class UserDirectoryProvider { plannedQuery.add(item); } } + // On-disk cache + if (plannedQuery.isEmpty) return out; + final dbResp = await (_dt.db.snLocalAccount.select() + ..where((e) => e.id.isIn(plannedQuery)) + ..limit(plannedQuery.length)) + .get(); + for (var idx = 0; idx < out.length; idx++) { + if (out[idx] != null) continue; + if (dbResp.length <= idx) { + break; + } + out[idx] = dbResp[idx].content; + _cache[dbResp[idx].id] = dbResp[idx].content; + _idCache[dbResp[idx].name] = dbResp[idx].id; + plannedQuery.remove(dbResp[idx].id); + } + // Remote server if (plannedQuery.isEmpty) return out; final resp = await _sn.client .get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')}); @@ -43,17 +77,28 @@ class UserDirectoryProvider { _idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id; sideIdx++; } + if (respDecoded.isNotEmpty) _saveToLocal(respDecoded); return out; } Future getAccount(dynamic id) async { + // In-memory cache if (id is String && _idCache.containsKey(id)) { id = _idCache[id]; } if (_cache.containsKey(id)) { return _cache[id]; } - + // On-disk cache + final dbResp = await (_dt.db.snLocalAccount.select() + ..where((e) => e.id.equals(id))) + .getSingleOrNull(); + if (dbResp != null) { + _cache[dbResp.id] = dbResp.content; + _idCache[dbResp.name] = dbResp.id; + return dbResp.content; + } + // Remote server try { final resp = await _sn.client.get('/cgi/id/users/$id'); final account = SnAccount.fromJson( @@ -61,16 +106,39 @@ class UserDirectoryProvider { ); _cache[account.id] = account; if (id is String) _idCache[id] = account.id; + _saveToLocal([account]); return account; } catch (err) { return null; } } - SnAccount? getAccountFromCache(dynamic id) { + SnAccount? getFromCache(dynamic id) { if (id is String && _idCache.containsKey(id)) { id = _idCache[id]; } return _cache[id]; } + + Future _saveToLocal(Iterable out) async { + // For better on conflict resolution + // And consider the method usually called with usually small amount of data + // Use for to insert each record instead of bulk insert + List> queries = out.map((ele) { + return _dt.db.snLocalAccount.insertOne( + SnLocalAccountCompanion.insert( + id: Value(ele.id), + name: ele.name, + content: ele, + ), + onConflict: DoUpdate( + (_) => SnLocalAccountCompanion.custom( + name: Constant(ele.name), + content: Constant(jsonEncode(ele.toJson())), + ), + ), + ); + }).toList(); + await Future.wait(queries); + } } diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 3aa8ca1..64ae001 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -336,7 +336,7 @@ class _ChatChannelEntry extends StatelessWidget { : null; final title = otherMember != null - ? ud.getAccountFromCache(otherMember.accountId)?.nick ?? channel.name + ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name : channel.name; return ListTile( @@ -354,10 +354,9 @@ class _ChatChannelEntry extends StatelessWidget { ? Row( children: [ Badge( - label: Text(ud - .getAccountFromCache(lastMessage!.sender.accountId) - ?.nick ?? - 'unknown'.tr()), + label: Text( + ud.getFromCache(lastMessage!.sender.accountId)?.nick ?? + 'unknown'.tr()), backgroundColor: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ), @@ -400,7 +399,7 @@ class _ChatChannelEntry extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 16), leading: AccountImage( content: otherMember != null - ? ud.getAccountFromCache(otherMember.accountId)?.avatar + ? ud.getFromCache(otherMember.accountId)?.avatar : channel.realm?.avatar, fallbackWidget: const Icon(Symbols.chat, size: 20), ), diff --git a/lib/screens/chat/channel_detail.dart b/lib/screens/chat/channel_detail.dart index f56bb8c..fe2f794 100644 --- a/lib/screens/chat/channel_detail.dart +++ b/lib/screens/chat/channel_detail.dart @@ -289,15 +289,14 @@ class _ChannelDetailScreenState extends State { ), ListTile( leading: AccountImage( - content: - ud.getAccountFromCache(_profile!.accountId)?.avatar, + content: ud.getFromCache(_profile!.accountId)?.avatar, radius: 18, ), trailing: const Icon(Symbols.chevron_right), title: Text('channelEditProfile').tr(), subtitle: Text( (_profile?.nick?.isEmpty ?? true) - ? ud.getAccountFromCache(_profile!.accountId)!.nick + ? ud.getFromCache(_profile!.accountId)!.nick : _profile!.nick!, ), contentPadding: const EdgeInsets.only(left: 20, right: 20), @@ -575,11 +574,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { return ListTile( contentPadding: const EdgeInsets.only(right: 24, left: 16), leading: AccountImage( - content: ud.getAccountFromCache(member.accountId)?.avatar, + content: ud.getFromCache(member.accountId)?.avatar, ), title: Text( - ud.getAccountFromCache(member.accountId)?.name ?? - 'unknown'.tr(), + ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(), ), subtitle: Text(member.nick ?? 'unknown'.tr()), trailing: SizedBox( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 424ddb8..d9295e1 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -277,8 +277,7 @@ class _ChatRoomScreenState extends State { appBar: AppBar( title: Text( _channel?.type == 1 - ? ud.getAccountFromCache(_otherMember?.accountId)?.nick ?? - _channel!.name + ? ud.getFromCache(_otherMember?.accountId)?.nick ?? _channel!.name : _channel?.name ?? 'loading'.tr(), ), actions: [ diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index cdd11f6..52260a7 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -51,7 +51,8 @@ class _RealmDetailScreenState extends State { Future _fetchPublishers() async { try { final sn = context.read(); - final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}'); + final resp = + await sn.client.get('/cgi/co/publishers?realm=${widget.alias}'); _publishers = List.from( resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], ); @@ -68,7 +69,8 @@ class _RealmDetailScreenState extends State { Future _fetchChannels() async { try { final sn = context.read(); - final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public'); + final resp = + await sn.client.get('/cgi/im/channels/${widget.alias}/public'); _channels = List.from( resp.data.map((e) => SnChannel.fromJson(e)).cast(), ); @@ -98,15 +100,32 @@ class _RealmDetailScreenState extends State { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: Text(_realm?.name ?? 'loading'.tr()), bottom: TabBar( tabs: [ - Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)), - Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)), - Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)), - Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)), + Tab( + icon: Icon(Symbols.home, + color: Theme.of(context) + .appBarTheme + .foregroundColor)), + Tab( + icon: Icon(Symbols.explore, + color: Theme.of(context) + .appBarTheme + .foregroundColor)), + Tab( + icon: Icon(Symbols.group, + color: Theme.of(context) + .appBarTheme + .foregroundColor)), + Tab( + icon: Icon(Symbols.settings, + color: Theme.of(context) + .appBarTheme + .foregroundColor)), ], ), ), @@ -115,7 +134,8 @@ class _RealmDetailScreenState extends State { }, body: TabBarView( children: [ - _RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels), + _RealmDetailHomeWidget( + realm: _realm, publishers: _publishers, channels: _channels), _RealmPostListWidget(realm: _realm), _RealmMemberListWidget(realm: _realm), _RealmSettingsWidget( @@ -137,7 +157,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { final List? publishers; final List? channels; - const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels}); + const _RealmDetailHomeWidget( + {required this.realm, this.publishers, this.channels}); @override Widget build(BuildContext context) { @@ -168,7 +189,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { child: Container( width: double.infinity, color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) + child: Text('realmCommunityPublishersHint'.tr(), + style: Theme.of(context).textTheme.bodyMedium) .padding(horizontal: 24, vertical: 8), ), ), @@ -199,7 +221,8 @@ class _RealmDetailHomeWidget extends StatelessWidget { child: Container( width: double.infinity, color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) + child: Text('realmCommunityPublicChannelsHint'.tr(), + style: Theme.of(context).textTheme.bodyMedium) .padding(horizontal: 24, vertical: 8), ), ), @@ -323,10 +346,12 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { try { final ud = context.read(); final sn = context.read(); - final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: { - 'take': 10, - 'offset': _members.length, - }); + final resp = await sn.client.get( + '/cgi/id/realms/${widget.realm!.alias}/members', + queryParameters: { + 'take': 10, + 'offset': _members.length, + }); final out = List.from( resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [], @@ -432,14 +457,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { return ListTile( contentPadding: const EdgeInsets.only(right: 24, left: 16), leading: AccountImage( - content: ud.getAccountFromCache(member.accountId)?.avatar, + content: ud.getFromCache(member.accountId)?.avatar, fallbackWidget: const Icon(Symbols.group, size: 24), ), title: Text( - ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(), + ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(), ), subtitle: Text( - ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), + ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(), ), trailing: IconButton( icon: const Icon(Symbols.person_remove), diff --git a/lib/screens/sharing.dart b/lib/screens/sharing.dart index f78e242..3c86944 100644 --- a/lib/screens/sharing.dart +++ b/lib/screens/sharing.dart @@ -51,8 +51,10 @@ class _AppSharingListenerState extends State { child: Column( children: [ ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), leading: Icon(Icons.post_add), trailing: const Icon(Icons.chevron_right), title: Text('shareIntentPostStory').tr(), @@ -64,13 +66,20 @@ class _AppSharingListenerState extends State { }, extra: PostEditorExtra( text: value - .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) + .where((e) => [ + SharedMediaType.text, + SharedMediaType.url + ].contains(e.type)) .map((e) => e.path) .join('\n'), attachments: value - .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image] - .contains(e.type)) - .map((e) => PostWriteMedia.fromFile(XFile(e.path))) + .where((e) => [ + SharedMediaType.video, + SharedMediaType.file, + SharedMediaType.image + ].contains(e.type)) + .map((e) => + PostWriteMedia.fromFile(XFile(e.path))) .toList(), ), ); @@ -78,15 +87,18 @@ class _AppSharingListenerState extends State { }, ), ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), leading: Icon(Icons.chat_outlined), trailing: const Icon(Icons.chevron_right), title: Text('shareIntentSendChannel').tr(), onTap: () { showModalBottomSheet( context: context, - builder: (context) => _ShareIntentChannelSelect(value: value), + builder: (context) => + _ShareIntentChannelSelect(value: value), ).then((val) { if (!context.mounted) return; if (val == true) Navigator.pop(context); @@ -110,7 +122,8 @@ class _AppSharingListenerState extends State { } void _initialize() async { - _shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) { + _shareIntentSubscription = + ReceiveSharingIntent.instance.getMediaStream().listen((value) { if (value.isEmpty) return; if (mounted) { _gotoPost(value); @@ -157,7 +170,8 @@ class _ShareIntentChannelSelect extends StatefulWidget { const _ShareIntentChannelSelect({required this.value}); @override - State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState(); + State<_ShareIntentChannelSelect> createState() => + _ShareIntentChannelSelectState(); } class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { @@ -178,8 +192,11 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { final lastMessages = await chan.getLastMessages(channels); _lastMessages = {for (final val in lastMessages) val.channelId: val}; channels.sort((a, b) { - if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { - return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); + if (_lastMessages!.containsKey(a.id) && + _lastMessages!.containsKey(b.id)) { + return _lastMessages![b.id]! + .createdAt + .compareTo(_lastMessages![a.id]!.createdAt); } if (_lastMessages!.containsKey(a.id)) return -1; if (_lastMessages!.containsKey(b.id)) return 1; @@ -232,7 +249,9 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { children: [ const Icon(Symbols.chat, size: 24), const Gap(16), - Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(), + Text('shareIntentSendChannel', + style: Theme.of(context).textTheme.titleLarge) + .tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), LoadingIndicator(isActive: _isBusy), @@ -249,29 +268,34 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { final lastMessage = _lastMessages?[channel.id]; if (channel.type == 1) { - final otherMember = channel.members?.cast().firstWhere( - (ele) => ele?.accountId != ua.user?.id, - orElse: () => null, - ); + final otherMember = + channel.members?.cast().firstWhere( + (ele) => ele?.accountId != ua.user?.id, + orElse: () => null, + ); return ListTile( - title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), + title: Text( + ud.getFromCache(otherMember?.accountId)?.nick ?? + channel.name), subtitle: lastMessage != null ? Text( - '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', + '${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', maxLines: 1, overflow: TextOverflow.ellipsis, ) : Text( 'channelDirectMessageDescription'.tr(args: [ - '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', + '@${ud.getFromCache(otherMember?.accountId)?.name}', ]), maxLines: 1, overflow: TextOverflow.ellipsis, ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16), leading: AccountImage( - content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, + content: + ud.getFromCache(otherMember?.accountId)?.avatar, ), onTap: () { GoRouter.of(context).pushNamed( @@ -291,7 +315,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { title: Text(channel.name), subtitle: lastMessage != null ? Text( - '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', + '${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', maxLines: 1, overflow: TextOverflow.ellipsis, ) @@ -316,13 +340,20 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { }, extra: ChatRoomScreenExtra( initialText: widget.value - .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) + .where((e) => [ + SharedMediaType.text, + SharedMediaType.url + ].contains(e.type)) .map((e) => e.path) .join('\n'), initialAttachments: widget.value - .where((e) => - [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) - .map((e) => PostWriteMedia.fromFile(XFile(e.path))) + .where((e) => [ + SharedMediaType.video, + SharedMediaType.file, + SharedMediaType.image + ].contains(e.type)) + .map( + (e) => PostWriteMedia.fromFile(XFile(e.path))) .toList(), ), ) diff --git a/lib/widgets/attachment/attachment_zoom.dart b/lib/widgets/attachment/attachment_zoom.dart index 3eee61d..b18b05b 100644 --- a/lib/widgets/attachment/attachment_zoom.dart +++ b/lib/widgets/attachment/attachment_zoom.dart @@ -42,7 +42,8 @@ class AttachmentZoomView extends StatefulWidget { } class _AttachmentZoomViewState extends State { - late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0); + late final PageController _pageController = + PageController(initialPage: widget.initialIndex ?? 0); bool _showOverlay = true; bool _dismissable = true; @@ -107,7 +108,9 @@ class _AttachmentZoomViewState extends State { if (!mounted) return; context.showSnackbar( - (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? 'attachmentSaved'.tr() : 'attachmentSavedDesktop'.tr(), + (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) + ? 'attachmentSaved'.tr() + : 'attachmentSavedDesktop'.tr(), action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? SnackBarAction( label: 'openInAlbum'.tr(), @@ -131,7 +134,8 @@ class _AttachmentZoomViewState extends State { super.dispose(); } - Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + Color get _unFocusColor => + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); bool _showDetail = false; @@ -150,7 +154,9 @@ class _AttachmentZoomViewState extends State { onDismissed: () { Navigator.of(context).pop(); }, - direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none, + direction: _dismissable + ? DismissiblePageDismissDirection.multi + : DismissiblePageDismissDirection.none, backgroundColor: Colors.transparent, isFullScreen: true, child: GestureDetector( @@ -165,10 +171,13 @@ class _AttachmentZoomViewState extends State { return Hero( tag: 'attachment-${widget.data.first.rid}-$heroTag', child: PhotoView( - key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), - backgroundDecoration: BoxDecoration(color: Colors.transparent), + key: Key( + 'attachment-detail-${widget.data.first.rid}-$heroTag'), + backgroundDecoration: + BoxDecoration(color: Colors.transparent), scaleStateChangedCallback: (scaleState) { - setState(() => _dismissable = scaleState == PhotoViewScaleState.initial); + setState(() => _dismissable = + scaleState == PhotoViewScaleState.initial); }, imageProvider: UniversalImage.provider( sn.getAttachmentUrl(widget.data.first.rid), @@ -181,10 +190,12 @@ class _AttachmentZoomViewState extends State { pageController: _pageController, enableRotation: true, scaleStateChangedCallback: (scaleState) { - setState(() => _dismissable = scaleState == PhotoViewScaleState.initial); + setState(() => _dismissable = + scaleState == PhotoViewScaleState.initial); }, builder: (context, idx) { - final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); + final heroTag = + widget.heroTags?.elementAt(idx) ?? uuid.v4(); return PhotoViewGalleryPageOptions( imageProvider: UniversalImage.provider( sn.getAttachmentUrl(widget.data.elementAt(idx).rid), @@ -200,11 +211,15 @@ class _AttachmentZoomViewState extends State { width: 20.0, height: 20.0, child: CircularProgressIndicator( - value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), + value: event == null + ? 0 + : event.cumulativeBytesLoaded / + (event.expectedTotalBytes ?? 1), ), ), ), - backgroundDecoration: BoxDecoration(color: Colors.transparent), + backgroundDecoration: + BoxDecoration(color: Colors.transparent), ); }), Positioned( @@ -223,9 +238,8 @@ class _AttachmentZoomViewState extends State { onPressed: () { Navigator.of(context).pop(); }, - ) - .opacity(_showOverlay ? 1 : 0, animate: true) - .animate(const Duration(milliseconds: 300), Curves.easeInOut), + ).opacity(_showOverlay ? 1 : 0, animate: true).animate( + const Duration(milliseconds: 300), Curves.easeInOut), ), ), Align( @@ -257,9 +271,11 @@ class _AttachmentZoomViewState extends State { child: Builder(builder: (context) { final ud = context.read(); final item = widget.data.elementAt( - widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, + widget.data.length > 1 + ? _pageController.page?.round() ?? 0 + : 0, ); - final account = ud.getAccountFromCache(item.accountId); + final account = ud.getFromCache(item.accountId); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -277,15 +293,20 @@ class _AttachmentZoomViewState extends State { Expanded( child: IgnorePointer( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( 'attachmentUploadBy'.tr(), - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context) + .textTheme + .bodySmall, ), Text( account?.nick ?? 'unknown'.tr(), - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context) + .textTheme + .bodyMedium, ), ], ), @@ -299,11 +320,13 @@ class _AttachmentZoomViewState extends State { ).padding(right: 8), ), InkWell( - borderRadius: const BorderRadius.all(Radius.circular(16)), + borderRadius: + const BorderRadius.all(Radius.circular(16)), onTap: _isDownloading ? null - : () => - _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), + : () => _saveToAlbum(widget.data.length > 1 + ? _pageController.page?.round() ?? 0 + : 0), child: Container( padding: const EdgeInsets.all(6), child: !_isDownloading @@ -351,7 +374,8 @@ class _AttachmentZoomViewState extends State { ]), style: metaTextStyle, ).padding(right: 2), - if (item.metadata['exif']?['Megapixels'] != null && + if (item.metadata['exif']?['Megapixels'] != + null && item.metadata['exif']?['Model'] != null) Text( '${item.metadata['exif']?['Megapixels']}MP', @@ -362,7 +386,8 @@ class _AttachmentZoomViewState extends State { item.size.formatBytes(), style: metaTextStyle, ), - if (item.metadata['width'] != null && item.metadata['height'] != null) + if (item.metadata['width'] != null && + item.metadata['height'] != null) Text( '${item.metadata['width']}x${item.metadata['height']}', style: metaTextStyle, @@ -377,8 +402,10 @@ class _AttachmentZoomViewState extends State { showModalBottomSheet( context: context, builder: (context) => _AttachmentZoomDetailPopup( - data: widget.data - .elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), + data: widget.data.elementAt( + widget.data.length > 1 + ? _pageController.page?.round() ?? 0 + : 0), ), ).then((_) { _showDetail = false; @@ -386,15 +413,15 @@ class _AttachmentZoomViewState extends State { }, child: Text( 'viewDetailedAttachment'.tr(), - style: metaTextStyle.copyWith(decoration: TextDecoration.underline), + style: metaTextStyle.copyWith( + decoration: TextDecoration.underline), ), ), ], ); }), - ) - .opacity(_showOverlay ? 1 : 0, animate: true) - .animate(const Duration(milliseconds: 300), Curves.easeInOut), + ).opacity(_showOverlay ? 1 : 0, animate: true).animate( + const Duration(milliseconds: 300), Curves.easeInOut), ), ], ), @@ -409,7 +436,9 @@ class _AttachmentZoomViewState extends State { showModalBottomSheet( context: context, builder: (context) => _AttachmentZoomDetailPopup( - data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), + data: widget.data.elementAt(widget.data.length > 1 + ? _pageController.page?.round() ?? 0 + : 0), ), ).then((_) { _showDetail = false; @@ -429,7 +458,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { @override Widget build(BuildContext context) { final ud = context.read(); - final account = ud.getAccountFromCache(data.accountId); + final account = ud.getFromCache(data.accountId); const tableGap = TableRow( children: [ @@ -447,7 +476,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { children: [ const Icon(Symbols.info, size: 24), const Gap(16), - Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!), + Text('attachmentDetailInfo') + .tr() + .textStyle(Theme.of(context).textTheme.titleLarge!), ], ).padding(horizontal: 20, top: 16, bottom: 12), Expanded( @@ -461,7 +492,8 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { TableRow( children: [ TableCell( - child: Text('attachmentUploadBy').tr().padding(right: 16), + child: + Text('attachmentUploadBy').tr().padding(right: 16), ), TableCell( child: Row( @@ -472,9 +504,13 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { radius: 8, ), const Gap(8), - Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()), + Text(data.accountId > 0 + ? account?.nick ?? 'unknown'.tr() + : 'unknown'.tr()), const Gap(8), - Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75), + Text('#${data.accountId}', + style: GoogleFonts.robotoMono()) + .opacity(0.75), ], ), ), @@ -495,7 +531,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { children: [ Text(data.size.formatBytes()), const Gap(12), - Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75), + Text('${data.size} Bytes', + style: GoogleFonts.robotoMono()) + .opacity(0.75), ], )), ], @@ -510,19 +548,27 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { TableRow( children: [ TableCell(child: Text('Hash').padding(right: 16)), - TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)), + TableCell( + child: Text(data.hash, + style: GoogleFonts.robotoMono(fontSize: 11)) + .opacity(0.9)), ], ), tableGap, ...(data.metadata['exif']?.keys.map((k) => TableRow( children: [ 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, + bottom: MediaQuery.of(context).padding.bottom), ), ), ], diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart index b281068..309d50d 100644 --- a/lib/widgets/chat/chat_message.dart +++ b/lib/widgets/chat/chat_message.dart @@ -51,7 +51,7 @@ class ChatMessage extends StatelessWidget { Widget build(BuildContext context) { final ua = context.read(); final ud = context.read(); - final user = ud.getAccountFromCache(data.sender.accountId); + final user = ud.getFromCache(data.sender.accountId); final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id; diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index fbc6deb..ffbee59 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -380,7 +380,7 @@ class ChatMessageInputState extends State { _isEncrypted ? Icon(Symbols.lock, size: 18) : null, hintText: widget.otherMember != null ? 'fieldChatMessageDirect'.tr(args: [ - '@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}', + '@${ud.getFromCache(widget.otherMember?.accountId)?.name}', ]) : 'fieldChatMessage'.tr(args: [ widget.controller.channel?.name ?? 'loading'.tr() diff --git a/lib/widgets/chat/chat_typing_indicator.dart b/lib/widgets/chat/chat_typing_indicator.dart index cc7a62c..f35d057 100644 --- a/lib/widgets/chat/chat_typing_indicator.dart +++ b/lib/widgets/chat/chat_typing_indicator.dart @@ -33,11 +33,13 @@ class ChatTypingIndicator extends StatelessWidget { const Icon(Symbols.more_horiz, weight: 600, size: 20), const Gap(8), Text( - 'messageTyping'.plural(controller.typingMembers.length, args: [ + 'messageTyping' + .plural(controller.typingMembers.length, args: [ controller.typingMembers .map((ele) => (ele.nick?.isNotEmpty ?? false) ? ele.nick! - : ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown') + : ud.getFromCache(ele.accountId)?.name ?? + 'unknown') .join(', '), ]), ), diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index f28a577..d912ba9 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -95,9 +95,10 @@ class OpenablePostItem extends StatelessWidget { openColor: Colors.transparent, openElevation: 0, transitionType: ContainerTransitionType.fade, - closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( - cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, - ), + closedColor: + Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( + cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, + ), closedShape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), @@ -138,9 +139,11 @@ class PostItem extends StatelessWidget { final box = context.findRenderObject() as RenderBox?; final url = 'https://solsynth.dev/posts/${data.id}'; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { - Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + Share.shareUri(Uri.parse(url), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); } else { - Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + Share.share(url, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); } } @@ -158,7 +161,8 @@ class PostItem extends StatelessWidget { child: MultiProvider( providers: [ Provider(create: (_) => context.read()), - ChangeNotifierProvider(create: (_) => context.read()), + ChangeNotifierProvider( + create: (_) => context.read()), ], child: ResponsiveBreakpoints.builder( breakpoints: ResponsiveBreakpoints.of(context).breakpoints, @@ -186,7 +190,8 @@ class PostItem extends StatelessWidget { sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ); } else { - await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile); + await FileSaver.instance.saveFile( + name: 'Solar Network Post #${data.id}.png', file: imageFile); } await imageFile.delete(); @@ -200,7 +205,9 @@ class PostItem extends StatelessWidget { final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id; // Video full view - if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) { + if (showFullPost && + data.type == 'video' && + ResponsiveBreakpoints.of(context).largerThan(TABLET)) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -220,7 +227,8 @@ class PostItem extends StatelessWidget { if (onDeleted != null) {} }, ).padding(bottom: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8), + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(bottom: 8), _PostHeadline(data: data).padding(horizontal: 4, bottom: 8), _PostFeaturedComment(data: data), _PostBottomAction( @@ -268,7 +276,8 @@ class PostItem extends StatelessWidget { if (onDeleted != null) {} }, ).padding(horizontal: 12, top: 8, bottom: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12), @@ -311,8 +320,13 @@ class PostItem extends StatelessWidget { ], ), ), - Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), - _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), + Text('postArticle') + .tr() + .fontSize(13) + .opacity(0.75) + .padding(horizontal: 24, bottom: 8), + _PostFeaturedComment(data: data, maxWidth: maxWidth) + .padding(horizontal: 12), _PostBottomAction( data: data, showComments: showComments, @@ -327,7 +341,8 @@ class PostItem extends StatelessWidget { } final displayableAttachments = data.preload?.attachments - ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') + ?.where((ele) => + ele?.mediaType != SnMediaType.image || data.type != 'article') .toList(); final cfg = context.read(); @@ -352,9 +367,13 @@ class PostItem extends StatelessWidget { if (onDeleted != null) onDeleted!(); }, ).padding(horizontal: 12, vertical: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), - if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), - if (data.body['title'] != null || data.body['description'] != null) + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), + if (data.type == 'question') + _PostQuestionHint(data: data) + .padding(horizontal: 16, bottom: 8), + if (data.body['title'] != null || + data.body['description'] != null) _PostHeadline( data: data, isEnlarge: data.type == 'article' && showFullPost, @@ -368,7 +387,8 @@ class PostItem extends StatelessWidget { if (data.repostTo != null) _PostQuoteContent(child: data.repostTo!).padding( horizontal: 12, - bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0, + bottom: + data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0, ), if (data.visibility > 0) _PostVisibilityHint(data: data).padding( @@ -380,7 +400,9 @@ class PostItem extends StatelessWidget { horizontal: 16, vertical: 4, ), - if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6), + if (data.tags.isNotEmpty) + _PostTagsList(data: data) + .padding(horizontal: 16, top: 4, bottom: 6), ], ), ), @@ -393,12 +415,16 @@ class PostItem extends StatelessWidget { fit: showFullPost ? BoxFit.cover : BoxFit.contain, padding: const EdgeInsets.symmetric(horizontal: 12), ), - if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4), - if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) + if (data.preload?.poll != null) + PostPoll(poll: data.preload!.poll!) + .padding(horizontal: 12, vertical: 4), + if (data.body['content'] != null && + (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) LinkPreviewWidget( text: data.body['content'], ).padding(horizontal: 4), - _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), + _PostFeaturedComment(data: data, maxWidth: maxWidth) + .padding(horizontal: 12), Container( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Column( @@ -460,7 +486,8 @@ class PostShareImageWidget extends StatelessWidget { showMenu: false, isRelativeDate: false, ).padding(horizontal: 16, bottom: 8), - if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), + if (data.type == 'question') + _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), _PostHeadline( data: data, isEnlarge: data.type == 'article', @@ -475,7 +502,8 @@ class PostShareImageWidget extends StatelessWidget { child: data.repostTo!, isRelativeDate: false, ).padding(horizontal: 16, bottom: 8), - if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) + if (data.type != 'article' && + (data.preload?.attachments?.isNotEmpty ?? false)) StyledWidget(AttachmentList( data: data.preload!.attachments!, columned: true, @@ -484,7 +512,8 @@ class PostShareImageWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (data.visibility > 0) _PostVisibilityHint(data: data), - if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data), + if (data.body['content_truncated'] == true) + _PostTruncatedHint(data: data), ], ).padding(horizontal: 16), _PostBottomAction( @@ -544,7 +573,8 @@ class PostShareImageWidget extends StatelessWidget { version: QrVersions.auto, size: 100, gapless: true, - embeddedImage: AssetImage('assets/icon/icon-light-radius.png'), + embeddedImage: + AssetImage('assets/icon/icon-light-radius.png'), embeddedImageStyle: QrEmbeddedImageStyle( size: Size(28, 28), ), @@ -575,9 +605,11 @@ class _PostQuestionHint extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20), + Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, + size: 20), const Gap(4), - if (data.body['answer'] == null && data.body['reward']?.toDouble() != null) + if (data.body['answer'] == null && + data.body['reward']?.toDouble() != null) Text('postQuestionUnansweredWithReward'.tr(args: [ '${data.body['reward']}', ])).opacity(0.75) @@ -613,7 +645,9 @@ class _PostBottomAction extends StatelessWidget { ); final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty - ? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key + ? data.metric.reactionList.entries + .reduce((a, b) => a.value > b.value ? a : b) + .key : null; return Row( @@ -627,7 +661,8 @@ class _PostBottomAction extends StatelessWidget { InkWell( child: Row( children: [ - if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null) + if (mostTypicalReaction == null || + kTemplateReactions[mostTypicalReaction] == null) Icon(Symbols.add_reaction, size: 20, color: iconColor) else Text( @@ -639,7 +674,8 @@ class _PostBottomAction extends StatelessWidget { ), ), const Gap(8), - if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote) + if (data.totalUpvote > 0 && + data.totalUpvote >= data.totalDownvote) Text('postReactionUpvote').plural( data.totalUpvote, ) @@ -658,8 +694,12 @@ class _PostBottomAction extends StatelessWidget { data: data, onChanged: (value, attr, delta) { onChanged(data.copyWith( - totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote, - totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote, + totalUpvote: attr == 1 + ? data.totalUpvote + delta + : data.totalUpvote, + totalDownvote: attr == 2 + ? data.totalDownvote + delta + : data.totalDownvote, metric: data.metric.copyWith(reactionList: value), )); }, @@ -766,7 +806,9 @@ class _PostHeadline extends StatelessWidget { children: [ Text( 'articleWrittenAt'.tr( - args: [DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())], + args: [ + DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal()) + ], ), style: TextStyle(fontSize: 13), ), @@ -774,7 +816,9 @@ class _PostHeadline extends StatelessWidget { if (data.editedAt != null) Text( 'articleEditedAt'.tr( - args: [DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())], + args: [ + DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal()) + ], ), style: TextStyle(fontSize: 13), ), @@ -871,7 +915,9 @@ class _PostContentHeader extends StatelessWidget { @override Widget build(BuildContext context) { final ud = context.read(); - final user = data.publisher.type == 0 ? ud.getAccountFromCache(data.publisher.accountId) : null; + final user = data.publisher.type == 0 + ? ud.getFromCache(data.publisher.accountId) + : null; return Row( children: [ @@ -882,7 +928,8 @@ class _PostContentHeader extends StatelessWidget { borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20, badge: (user?.badges.isNotEmpty ?? false) ? Icon( - kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark, + kBadgesMeta[user!.badges.first.type]?.$2 ?? + Symbols.question_mark, color: kBadgesMeta[user.badges.first.type]?.$3, fill: 1, size: 18, @@ -926,8 +973,10 @@ class _PostContentHeader extends StatelessWidget { const Gap(4), Text( isRelativeDate - ? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal()) - : DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()), + ? RelativeTime(context).format( + (data.publishedAt ?? data.createdAt).toLocal()) + : DateFormat('y/M/d HH:mm').format( + (data.publishedAt ?? data.createdAt).toLocal()), ).fontSize(13), ], ).opacity(0.8), @@ -945,8 +994,10 @@ class _PostContentHeader extends StatelessWidget { const Gap(4), Text( isRelativeDate - ? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal()) - : DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()), + ? RelativeTime(context).format( + (data.publishedAt ?? data.createdAt).toLocal()) + : DateFormat('y/M/d HH:mm').format( + (data.publishedAt ?? data.createdAt).toLocal()), ).fontSize(13), ], ).opacity(0.8), @@ -1129,7 +1180,8 @@ class _PostContentBody extends StatelessWidget { if (data.body['content'] == null) return const SizedBox.shrink(); final content = MarkdownTextContent( isAutoWarp: data.type == 'story', - isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''), + isEnlargeSticker: + RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''), textScaler: isEnlarge ? TextScaler.linear(1.1) : null, content: data.body['content'], attachments: data.preload?.attachments, @@ -1178,10 +1230,12 @@ class _PostQuoteContent extends StatelessWidget { onDeleted: () {}, ).padding(bottom: 4), _PostContentBody(data: child), - if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4), + if (child.visibility > 0) + _PostVisibilityHint(data: child).padding(top: 4), ], ).padding(horizontal: 16), - if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false)) + if (child.type != 'article' && + (child.preload?.attachments?.isNotEmpty ?? false)) ClipRRect( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(8), @@ -1332,7 +1386,9 @@ class _PostTruncatedHint extends StatelessWidget { const Gap(4), Text('postReadEstimate').tr(args: [ '${Duration( - seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed, + seconds: (data.body['content_length'] as num).toDouble() * + 60 ~/ + kHumanReadSpeed, ).inSeconds}s', ]), ], @@ -1371,7 +1427,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { // If this is a answered question, fetch the answer instead if (widget.data.type == 'question' && widget.data.body['answer'] != null) { final sn = context.read(); - final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}'); + final resp = + await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}'); _isAnswer = true; setState(() => _featuredComment = SnPost.fromJson(resp.data)); return; @@ -1379,9 +1436,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { try { final sn = context.read(); - final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: { - 'take': 1, - }); + final resp = await sn.client.get( + '/cgi/co/posts/${widget.data.id}/replies/featured', + queryParameters: { + 'take': 1, + }); setState(() => _featuredComment = SnPost.fromJson(resp.data[0])); } catch (err) { if (!mounted) return; @@ -1410,7 +1469,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { width: double.infinity, child: Material( borderRadius: const BorderRadius.all(Radius.circular(8)), - color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh, + color: _isAnswer + ? Colors.green.withOpacity(0.5) + : Theme.of(context).colorScheme.surfaceContainerHigh, child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(8)), onTap: () { @@ -1430,11 +1491,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { crossAxisAlignment: CrossAxisAlignment.center, children: [ const Gap(2), - Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20), + Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, + size: 20), const Gap(10), Text( - _isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment', - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15), + _isAnswer + ? 'postQuestionAnswerTitle' + : 'postFeaturedComment', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 15), ).tr(), ], ), @@ -1572,7 +1639,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { } RegExp cleanThinkingRegExp = RegExp(r'[\s\S]*?'); - setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); + setState( + () => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -1595,11 +1663,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { children: [ const Icon(Symbols.book_4_spark, size: 24), const Gap(16), - Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(), + Text('postGetInsightTitle', + style: Theme.of(context).textTheme.titleLarge) + .tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), const Gap(4), - Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20), + Text('postGetInsightDescription', + style: Theme.of(context).textTheme.bodySmall) + .tr() + .padding(horizontal: 20), const Gap(4), if (_response == null) Expanded( @@ -1617,12 +1690,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { leading: const Icon(Symbols.info), title: Text('aiThinkingProcess'.tr()), tilePadding: const EdgeInsets.symmetric(horizontal: 20), - collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, + collapsedBackgroundColor: + Theme.of(context).colorScheme.surfaceContainerHigh, minTileHeight: 32, children: [ SelectableText( _thinkingProcess!, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(fontStyle: FontStyle.italic), ).padding(horizontal: 20, vertical: 8), ], ).padding(vertical: 8), @@ -1659,7 +1736,8 @@ class _PostVideoPlayer extends StatelessWidget { aspectRatio: 16 / 9, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'), + child: AttachmentItem( + data: data.preload!.video!, heroTag: 'post-video-${data.id}'), ), ), ); diff --git a/lib/widgets/post/publisher_popover.dart b/lib/widgets/post/publisher_popover.dart index d29decd..28e16c0 100644 --- a/lib/widgets/post/publisher_popover.dart +++ b/lib/widgets/post/publisher_popover.dart @@ -23,7 +23,7 @@ class PublisherPopoverCard extends StatelessWidget { final sn = context.read(); final ud = context.read(); - final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null; + final user = data.type == 0 ? ud.getFromCache(data.accountId) : null; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -85,7 +85,9 @@ class PublisherPopoverCard extends StatelessWidget { (ele) => Tooltip( richMessage: TextSpan( children: [ - TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), + TextSpan( + text: kBadgesMeta[ele.type]?.$1.tr() ?? + 'unknown'.tr()), if (ele.metadata['title'] != null) TextSpan( text: '\n${ele.metadata['title']}', @@ -146,7 +148,10 @@ class PublisherPopoverCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75), + Text('publisherTotalDownvote') + .tr() + .fontSize(13) + .opacity(0.75), Text(data.totalDownvote.toString()), ], ),