From 382e3c4a4cebe0355535fc732fcbcd38b9481f61 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 10 Oct 2024 22:52:05 +0800 Subject: [PATCH] :zap: Optimize post attachment loading --- lib/controllers/post_list_controller.dart | 33 +++++++++-- lib/models/post.dart | 12 ++++ lib/screens/account/profile_page.dart | 3 +- lib/screens/explore.dart | 6 +- lib/screens/feed/search.dart | 4 +- lib/widgets/attachments/attachment_list.dart | 58 ++++++++++++-------- lib/widgets/chat/chat_event.dart | 2 +- lib/widgets/posts/post_item.dart | 9 ++- lib/widgets/posts/post_list.dart | 45 ++++++++++++++- lib/widgets/posts/post_warped_list.dart | 48 ---------------- 10 files changed, 130 insertions(+), 90 deletions(-) delete mode 100644 lib/widgets/posts/post_warped_list.dart diff --git a/lib/controllers/post_list_controller.dart b/lib/controllers/post_list_controller.dart index da1c9b7..151b23c 100644 --- a/lib/controllers/post_list_controller.dart +++ b/lib/controllers/post_list_controller.dart @@ -2,8 +2,10 @@ import 'dart:math'; import 'package:get/get.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/models/attachment.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/models/post.dart'; +import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/last_read.dart'; @@ -111,33 +113,33 @@ class PostListController extends GetxController { Future?> _loadPosts(int pageKey) async { isBusy.value = true; - final PostProvider provider = Get.find(); + final PostProvider posts = Get.find(); Response resp; try { if (author != null) { - resp = await provider.listPost( + resp = await posts.listPost( pageKey, author: author, ); } else { switch (mode.value) { case 2: - resp = await provider.listRecommendations( + resp = await posts.listRecommendations( pageKey, channel: 'shuffle', realm: realm, ); break; case 1: - resp = await provider.listRecommendations( + resp = await posts.listRecommendations( pageKey, channel: 'friends', realm: realm, ); break; default: - resp = await provider.listRecommendations( + resp = await posts.listRecommendations( pageKey, realm: realm, ); @@ -153,6 +155,27 @@ class PostListController extends GetxController { final result = PaginationResult.fromJson(resp.body); final out = result.data?.map((e) => Post.fromJson(e)).toList(); + final AttachmentProvider attach = Get.find(); + + if (out != null) { + final attachmentIds = out + .mapMany((x) => x.body['attachments'] ?? []) + .cast() + .toSet() + .toList(); + final attachmentOut = await attach.listMetadata(attachmentIds); + + for (var idx = 0; idx < out.length; idx++) { + final rids = List.from(out[idx].body['attachments'] ?? []); + out[idx].preload = PostPreload( + attachments: attachmentOut + .where((x) => x != null && rids.contains(x.rid)) + .cast() + .toList(), + ); + } + } + postTotal.value = result.count; return out; diff --git a/lib/models/post.dart b/lib/models/post.dart index f879441..54190df 100755 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -1,10 +1,19 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:solian/models/account.dart'; +import 'package:solian/models/attachment.dart'; import 'package:solian/models/post_categories.dart'; import 'package:solian/models/realm.dart'; part 'post.g.dart'; +class PostPreload { + List attachments; + + PostPreload({ + required this.attachments, + }); +} + @JsonSerializable() class Post { int id; @@ -33,6 +42,9 @@ class Post { Account author; PostMetric? metric; + @JsonKey(includeFromJson: false, includeToJson: false) + PostPreload? preload; + Post({ required this.id, required this.createdAt, diff --git a/lib/screens/account/profile_page.dart b/lib/screens/account/profile_page.dart index 78f2c1f..48eecf8 100644 --- a/lib/screens/account/profile_page.dart +++ b/lib/screens/account/profile_page.dart @@ -26,7 +26,6 @@ import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/daily_sign/history_chart.dart'; import 'package:solian/widgets/posts/post_list.dart'; -import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/sized_container.dart'; @@ -609,7 +608,7 @@ class _AccountProfilePageState extends State { child: Center(child: CircularProgressIndicator()), ), if (_userinfo != null) - PostWarpedListWidget( + ControlledPostListWidget( isPinned: false, controller: _postController.pagingController, onUpdate: () => _postController.reloadAllOver(), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 657c455..c5e404b 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -13,8 +13,8 @@ import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/navigation/realm_switcher.dart'; +import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; -import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/root_container.dart'; class ExploreScreen extends StatefulWidget { @@ -156,7 +156,7 @@ class _ExploreScreenState extends State RefreshIndicator( onRefresh: () => _postController.reloadAllOver(), child: CustomScrollView(slivers: [ - PostWarpedListWidget( + ControlledPostListWidget( controller: _postController.pagingController, onUpdate: () => _postController.reloadAllOver(), ), @@ -167,7 +167,7 @@ class _ExploreScreenState extends State return RefreshIndicator( onRefresh: () => _postController.reloadAllOver(), child: CustomScrollView(slivers: [ - PostWarpedListWidget( + ControlledPostListWidget( controller: _postController.pagingController, onUpdate: () => _postController.reloadAllOver(), ), diff --git a/lib/screens/feed/search.dart b/lib/screens/feed/search.dart index c4d74cd..d913cbd 100644 --- a/lib/screens/feed/search.dart +++ b/lib/screens/feed/search.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/providers/content/posts.dart'; -import 'package:solian/widgets/posts/post_warped_list.dart'; +import 'package:solian/widgets/posts/post_list.dart'; import '../../models/post.dart'; @@ -77,7 +77,7 @@ class _FeedSearchScreenState extends State { onRefresh: () => Future.sync(() => _pagingController.refresh()), child: CustomScrollView( slivers: [ - PostWarpedListWidget( + ControlledPostListWidget( controller: _pagingController, onUpdate: () => _pagingController.refresh(), ), diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index ec94f8a..8a98ab0 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -15,7 +15,8 @@ import 'package:solian/widgets/sized_container.dart'; class AttachmentList extends StatefulWidget { final String parentId; - final List attachmentsId; + final List? attachmentIds; + final List? attachments; final bool isGrid; final bool isColumn; final bool isForceGrid; @@ -29,7 +30,8 @@ class AttachmentList extends StatefulWidget { const AttachmentList({ super.key, required this.parentId, - required this.attachmentsId, + this.attachmentIds, + this.attachments, this.isGrid = false, this.isColumn = false, this.isForceGrid = false, @@ -50,21 +52,21 @@ class _AttachmentListState extends State { double _aspectRatio = 1; - List _attachmentsMeta = List.empty(); + List _attachments = List.empty(); void _getMetadataList() { final AttachmentProvider attach = Get.find(); - if (widget.attachmentsId.isEmpty) { + if (widget.attachmentIds?.isEmpty ?? false) { return; } else { - _attachmentsMeta = List.filled(widget.attachmentsId.length, null); + _attachments = List.filled(widget.attachmentIds!.length, null); } - attach.listMetadata(widget.attachmentsId).then((result) { + attach.listMetadata(widget.attachmentIds!).then((result) { if (mounted) { setState(() { - _attachmentsMeta = result; + _attachments = result; _isLoading = false; }); } @@ -76,7 +78,7 @@ class _AttachmentListState extends State { bool isConsistent = true; double? consistentValue; int portrait = 0, square = 0, landscape = 0; - for (var entry in _attachmentsMeta) { + for (var entry in _attachments) { if (entry == null) continue; if (entry.metadata?['ratio'] != null) { if (entry.metadata?['ratio'] is int) { @@ -117,10 +119,9 @@ class _AttachmentListState extends State { item: element, parentId: widget.parentId, width: width ?? widget.width, - badgeContent: '${idx + 1}/${_attachmentsMeta.length}', - showBadge: - _attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn, - showBorder: widget.attachmentsId.length > 1, + badgeContent: '${idx + 1}/${_attachments.length}', + showBadge: _attachments.length > 1 && !widget.isGrid && !widget.isColumn, + showBorder: _attachments.length > 1, showMature: _showMature, autoload: widget.autoload, onReveal: (value) { @@ -132,7 +133,16 @@ class _AttachmentListState extends State { @override void initState() { super.initState(); - _getMetadataList(); + assert(widget.attachmentIds != null || widget.attachments != null); + if (widget.attachments == null) { + _getMetadataList(); + } else { + setState(() { + _attachments = widget.attachments!; + _isLoading = false; + }); + _calculateAspectRatio(); + } } Color get _unFocusColor => @@ -140,7 +150,7 @@ class _AttachmentListState extends State { @override Widget build(BuildContext context) { - if (widget.attachmentsId.isEmpty) { + if (widget.attachmentIds?.isEmpty ?? widget.attachments!.isEmpty) { return const SizedBox.shrink(); } @@ -154,7 +164,7 @@ class _AttachmentListState extends State { ).paddingOnly(right: 5), Text( 'attachmentHint'.trParams( - {'count': widget.attachmentsId.length.toString()}, + {'count': _attachments.toString()}, ), style: TextStyle(color: _unFocusColor, fontSize: 12), ) @@ -171,8 +181,8 @@ class _AttachmentListState extends State { return Wrap( spacing: 8, runSpacing: 8, - children: widget.attachmentsId.map((x) { - final element = _attachmentsMeta[idx]; + children: _attachments.map((x) { + final element = _attachments[idx]; idx++; if (element == null) return const SizedBox.shrink(); double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9; @@ -202,7 +212,7 @@ class _AttachmentListState extends State { ); } - final isNotPureImage = _attachmentsMeta.any( + final isNotPureImage = _attachments.any( (x) => x?.mimetype.split('/').firstOrNull != 'image', ); if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { @@ -213,13 +223,13 @@ class _AttachmentListState extends State { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: math.min(3, widget.attachmentsId.length), + crossAxisCount: math.min(3, _attachments.length), mainAxisSpacing: 8.0, crossAxisSpacing: 8.0, ), - itemCount: widget.attachmentsId.length, + itemCount: _attachments.length, itemBuilder: (context, idx) { - final element = _attachmentsMeta[idx]; + final element = _attachments[idx]; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHigh, @@ -257,12 +267,12 @@ class _AttachmentListState extends State { animateToClosest: true, aspectRatio: _aspectRatio, viewportFraction: - widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1), + widget.viewport ?? (_attachments.length > 1 ? 0.95 : 1), enableInfiniteScroll: false, ), - itemCount: _attachmentsMeta.length, + itemCount: _attachments.length, itemBuilder: (context, idx, _) { - final element = _attachmentsMeta[idx]; + final element = _attachments[idx]; return _buildEntry(element, idx); }, ), diff --git a/lib/widgets/chat/chat_event.dart b/lib/widgets/chat/chat_event.dart index 00f4945..406d92f 100644 --- a/lib/widgets/chat/chat_event.dart +++ b/lib/widgets/chat/chat_event.dart @@ -78,7 +78,7 @@ class ChatEvent extends StatelessWidget { child: AttachmentList( key: Key('m${item.uuid}attachments'), parentId: item.uuid, - attachmentsId: attachments, + attachmentIds: attachments, isColumn: true, ), ); diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 9fe71ef..a5af3dd 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -455,14 +455,16 @@ class _PostAttachmentWidget extends StatelessWidget { if (attachments.length > 3) { return AttachmentList( parentId: item.id.toString(), - attachmentsId: attachments, + attachmentIds: item.preload == null ? attachments : null, + attachments: item.preload?.attachments, autoload: false, isGrid: true, ).paddingOnly(left: 36, top: 4, bottom: 4); } else if (attachments.length > 1 || isLargeScreen) { return AttachmentList( parentId: item.id.toString(), - attachmentsId: attachments, + attachmentIds: item.preload == null ? attachments : null, + attachments: item.preload?.attachments, autoload: false, isColumn: true, ).paddingOnly(left: 60, right: 24, top: 4, bottom: 4); @@ -470,7 +472,8 @@ class _PostAttachmentWidget extends StatelessWidget { return AttachmentList( flatMaxHeight: MediaQuery.of(context).size.width, parentId: item.id.toString(), - attachmentsId: attachments, + attachmentIds: item.preload == null ? attachments : null, + attachments: item.preload?.attachments, autoload: false, ); } diff --git a/lib/widgets/posts/post_list.dart b/lib/widgets/posts/post_list.dart index 121156c..8bd32c9 100644 --- a/lib/widgets/posts/post_list.dart +++ b/lib/widgets/posts/post_list.dart @@ -48,7 +48,6 @@ class PostListWidget extends StatelessWidget { } class PostListEntryWidget extends StatelessWidget { - final int renderOrder; final bool isShowEmbed; final bool isNestedClickable; final bool isClickable; @@ -59,7 +58,6 @@ class PostListEntryWidget extends StatelessWidget { const PostListEntryWidget({ super.key, - this.renderOrder = 0, required this.isShowEmbed, required this.isNestedClickable, required this.isClickable, @@ -101,3 +99,46 @@ class PostListEntryWidget extends StatelessWidget { ); } } + +class ControlledPostListWidget extends StatelessWidget { + final bool isShowEmbed; + final bool isClickable; + final bool isNestedClickable; + final bool isPinned; + final PagingController controller; + final Function? onUpdate; + + const ControlledPostListWidget({ + super.key, + required this.controller, + this.isShowEmbed = true, + this.isClickable = true, + this.isNestedClickable = true, + this.isPinned = true, + this.onUpdate, + }); + + @override + Widget build(BuildContext context) { + return PagedSliverList.separated( + addRepaintBoundaries: true, + pagingController: controller, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + if (item.pinnedAt != null && !isPinned) { + return const SizedBox.shrink(); + } + return PostListEntryWidget( + isShowEmbed: isShowEmbed, + isNestedClickable: isNestedClickable, + isClickable: isClickable, + showFeaturedReply: true, + item: item, + onUpdate: onUpdate ?? () {}, + ); + }, + ), + separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3), + ); + } +} diff --git a/lib/widgets/posts/post_warped_list.dart b/lib/widgets/posts/post_warped_list.dart deleted file mode 100644 index bc447ad..0000000 --- a/lib/widgets/posts/post_warped_list.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:solian/models/post.dart'; -import 'package:solian/widgets/posts/post_list.dart'; - -class PostWarpedListWidget extends StatelessWidget { - final bool isShowEmbed; - final bool isClickable; - final bool isNestedClickable; - final bool isPinned; - final PagingController controller; - final Function? onUpdate; - - const PostWarpedListWidget({ - super.key, - required this.controller, - this.isShowEmbed = true, - this.isClickable = true, - this.isNestedClickable = true, - this.isPinned = true, - this.onUpdate, - }); - - @override - Widget build(BuildContext context) { - return PagedSliverList.separated( - addRepaintBoundaries: true, - pagingController: controller, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - if (item.pinnedAt != null && !isPinned) { - return const SizedBox.shrink(); - } - return PostListEntryWidget( - renderOrder: index, - isShowEmbed: isShowEmbed, - isNestedClickable: isNestedClickable, - isClickable: isClickable, - showFeaturedReply: true, - item: item, - onUpdate: onUpdate ?? () {}, - ); - }, - ), - separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3), - ); - } -}