From dfb823c30c41431f28e42cb0ae6a7dadd09a007d Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Wed, 9 Jul 2025 18:29:03 +0800 Subject: [PATCH] multi mention Signed-off-by: bggRGjQaUbCoE --- lib/models_new/dynamic/dyn_mention/item.dart | 5 +- lib/models_new/fav/fav_detail/media.dart | 3 +- lib/models_new/fav/fav_note/list.dart | 3 +- lib/models_new/fav/fav_pgc/list.dart | 3 +- lib/models_new/history/list.dart | 3 +- lib/models_new/later/list.dart | 4 +- .../publish/common_rich_text_pub_page.dart | 31 ++-- lib/pages/dynamics_mention/controller.dart | 26 +++ lib/pages/dynamics_mention/view.dart | 157 +++++++++++------- lib/pages/dynamics_mention/widgets/item.dart | 15 +- 10 files changed, 170 insertions(+), 80 deletions(-) diff --git a/lib/models_new/dynamic/dyn_mention/item.dart b/lib/models_new/dynamic/dyn_mention/item.dart index ef909c6e..d1a2b06d 100644 --- a/lib/models_new/dynamic/dyn_mention/item.dart +++ b/lib/models_new/dynamic/dyn_mention/item.dart @@ -1,4 +1,7 @@ -class MentionItem { +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; + +class MentionItem with MultiSelectData { String? face; int? fans; String? name; diff --git a/lib/models_new/fav/fav_detail/media.dart b/lib/models_new/fav/fav_detail/media.dart index a0150217..b3f8b50d 100644 --- a/lib/models_new/fav/fav_detail/media.dart +++ b/lib/models_new/fav/fav_detail/media.dart @@ -2,7 +2,8 @@ import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/cnt_info.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/ogv.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/ugc.dart'; -import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; class FavDetailItemModel with MultiSelectData { int? id; diff --git a/lib/models_new/fav/fav_note/list.dart b/lib/models_new/fav/fav_note/list.dart index e76f7f09..08805fc6 100644 --- a/lib/models_new/fav/fav_note/list.dart +++ b/lib/models_new/fav/fav_note/list.dart @@ -1,4 +1,5 @@ -import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; class FavNoteItemModel with MultiSelectData { FavNoteItemModel({ diff --git a/lib/models_new/fav/fav_pgc/list.dart b/lib/models_new/fav/fav_pgc/list.dart index 1d665d5f..4b66735a 100644 --- a/lib/models_new/fav/fav_pgc/list.dart +++ b/lib/models_new/fav/fav_pgc/list.dart @@ -11,7 +11,8 @@ import 'package:PiliPlus/models_new/fav/fav_pgc/rights.dart'; import 'package:PiliPlus/models_new/fav/fav_pgc/section.dart'; import 'package:PiliPlus/models_new/fav/fav_pgc/series.dart'; import 'package:PiliPlus/models_new/fav/fav_pgc/stat.dart'; -import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; class FavPgcItemModel with MultiSelectData { int? seasonId; diff --git a/lib/models_new/history/list.dart b/lib/models_new/history/list.dart index 454747d4..b428a7c6 100644 --- a/lib/models_new/history/list.dart +++ b/lib/models_new/history/list.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/models_new/history/history.dart'; -import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; class HistoryItemModel with MultiSelectData { String? title; diff --git a/lib/models_new/later/list.dart b/lib/models_new/later/list.dart index 4b6322c5..a977d5ac 100644 --- a/lib/models_new/later/list.dart +++ b/lib/models_new/later/list.dart @@ -1,11 +1,11 @@ -import 'package:PiliPlus/pages/common/multi_select_controller.dart'; - import 'package:PiliPlus/models_new/later/bangumi.dart'; import 'package:PiliPlus/models_new/later/dimension.dart'; import 'package:PiliPlus/models_new/later/owner.dart'; import 'package:PiliPlus/models_new/later/page.dart'; import 'package:PiliPlus/models_new/later/rights.dart'; import 'package:PiliPlus/models_new/later/stat.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart' + show MultiSelectData; class LaterItemModel with MultiSelectData { int? aid; diff --git a/lib/pages/common/publish/common_rich_text_pub_page.dart b/lib/pages/common/publish/common_rich_text_pub_page.dart index 40965bc5..4b57648a 100644 --- a/lib/pages/common/publish/common_rich_text_pub_page.dart +++ b/lib/pages/common/publish/common_rich_text_pub_page.dart @@ -240,24 +240,33 @@ abstract class CommonRichTextPubPageState double _mentionOffset = 0; Future onMention([bool fromClick = false]) async { controller.keepChatPanel(); - await DynMentionPanel.onDynMention( + final res = await DynMentionPanel.onDynMention( context, offset: _mentionOffset, callback: (offset) => _mentionOffset = offset, - ).then((MentionItem? res) { - if (res != null) { - onInsertText( - '@${res.name} ', - RichTextType.at, - rawText: res.name, - id: res.uid, - fromClick: fromClick, - ); + ); + if (res != null) { + if (res is MentionItem) { + _onInsertUser(res, fromClick); + } else if (res is Iterable) { + for (var e in res) { + _onInsertUser(e, fromClick); + } } - }); + } controller.restoreChatPanel(); } + void _onInsertUser(MentionItem e, bool fromClick) { + onInsertText( + '@${e.name} ', + RichTextType.at, + rawText: e.name, + id: e.uid, + fromClick: fromClick, + ); + } + void onInsertText( String text, RichTextType type, { diff --git a/lib/pages/dynamics_mention/controller.dart b/lib/pages/dynamics_mention/controller.dart index aaaffc1e..ededf313 100644 --- a/lib/pages/dynamics_mention/controller.dart +++ b/lib/pages/dynamics_mention/controller.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; +import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,12 +13,26 @@ class DynMentionController final RxBool enableClear = false.obs; + final RxBool showBtn = false.obs; + Set? mentionList; + + void updateBtn() { + showBtn.value = mentionList?.isNotEmpty == true; + } + @override void onInit() { super.onInit(); queryData(); } + @override + Future onRefresh() { + mentionList?.clear(); + showBtn.value = false; + return super.onRefresh(); + } + @override Future?>> customGetData() => DynamicsHttp.dynMention(keyword: controller.text); @@ -26,6 +41,17 @@ class DynMentionController void onClose() { focusNode.dispose(); controller.dispose(); + mentionList?.clear(); + mentionList = null; super.onClose(); } + + void onCheck(bool? value, MentionItem item) { + if (value == true) { + (mentionList ??= {}).add(item); + } else { + mentionList!.remove(item); + } + updateBtn(); + } } diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart index a5b8528f..e4ca63d1 100644 --- a/lib/pages/dynamics_mention/view.dart +++ b/lib/pages/dynamics_mention/view.dart @@ -8,7 +8,6 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; -import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart'; import 'package:PiliPlus/pages/dynamics_mention/controller.dart'; import 'package:PiliPlus/pages/dynamics_mention/widgets/item.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -26,12 +25,12 @@ class DynMentionPanel extends StatefulWidget { final ScrollController? scrollController; final ValueChanged? callback; - static Future onDynMention( + static Future onDynMention( BuildContext context, { double offset = 0, ValueChanged? callback, }) { - return showModalBottomSheet( + return showModalBottomSheet( context: Get.context!, useSafeArea: true, isScrollControlled: true, @@ -89,6 +88,8 @@ class _DynMentionPanelState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final padding = MediaQuery.paddingOf(context).bottom; + final viewInset = MediaQuery.viewInsetsOf(context).bottom; return Column( children: [ SizedBox( @@ -163,30 +164,64 @@ class _DynMentionPanelState extends State { ), ), Expanded( - child: NotificationListener( - onNotification: (notification) { - if (notification is UserScrollNotification) { - if (_controller.focusNode.hasFocus) { - _controller.focusNode.unfocus(); - } - } else if (notification is ScrollEndNotification) { - widget.callback?.call(notification.metrics.pixels); - } - return false; - }, - child: CustomScrollView( - controller: widget.scrollController, - slivers: [ - Obx(() => _buildBody(theme, _controller.loadingState.value)), - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.paddingOf(context).bottom + - MediaQuery.viewInsetsOf(context).bottom + - 80, - ), + child: Stack( + clipBehavior: Clip.none, + children: [ + NotificationListener( + onNotification: (notification) { + if (notification is UserScrollNotification) { + if (_controller.focusNode.hasFocus) { + _controller.focusNode.unfocus(); + } + } else if (notification is ScrollEndNotification) { + widget.callback?.call(notification.metrics.pixels); + } + return false; + }, + child: CustomScrollView( + controller: widget.scrollController, + slivers: [ + Obx(() => + _buildBody(theme, _controller.loadingState.value)), + SliverToBoxAdapter( + child: SizedBox( + height: padding + viewInset + 80, + ), + ), + ], ), - ], - ), + ), + Obx(() { + return Positioned( + right: 16, + bottom: padding + + 16 + + (_controller.showBtn.value ? viewInset : 0), + child: AnimatedSlide( + offset: _controller.showBtn.value + ? Offset.zero + : const Offset(0, 3), + duration: const Duration(milliseconds: 120), + child: FloatingActionButton( + onPressed: () { + if (_controller.mentionList.isNullOrEmpty) { + _controller.showBtn.value = false; + return; + } + Get.back(result: _controller.mentionList!.toSet()); + for (var e in _controller.mentionList!) { + e.checked = null; + } + _controller + ..mentionList!.clear() + ..showBtn.value = false; + }, + child: const Icon(Icons.check), + ), + ), + ); + }), + ], ), ), ], @@ -200,42 +235,44 @@ class _DynMentionPanelState extends State { padding: const EdgeInsets.only(top: 8), sliver: linearLoading, ), - Success?>(:var response) => - response?.isNotEmpty == true - ? SliverMainAxisGroup( - slivers: response!.map((group) { - if (group.items.isNullOrEmpty) { - return const SliverToBoxAdapter(); - } - return SliverMainAxisGroup( - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: Container( - height: 40, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text(group.groupName!), - ), + Success?>(:var response) => response?.isNotEmpty == + true + ? SliverMainAxisGroup( + slivers: response!.map((group) { + if (group.items.isNullOrEmpty) { + return const SliverToBoxAdapter(); + } + return SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: theme.colorScheme.surface, + child: Container( + height: 40, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(group.groupName!), ), ), - SliverList.builder( - itemCount: group.items!.length, - itemBuilder: (context, index) { - return DynMentionItem( - item: group.items![index], - onTap: (e) => Get.back(result: e), - ); - }, - ), - ], - ); - }).toList(), - ) - : HttpError(onReload: _controller.onReload), + ), + SliverList.builder( + itemCount: group.items!.length, + itemBuilder: (context, index) { + final item = group.items![index]; + return DynMentionItem( + item: item, + onTap: () => Get.back(result: item), + onCheck: (value) => _controller.onCheck(value, item), + ); + }, + ), + ], + ); + }).toList(), + ) + : HttpError(onReload: _controller.onReload), Error(:var errMsg) => HttpError( errMsg: errMsg, onReload: _controller.onReload, diff --git a/lib/pages/dynamics_mention/widgets/item.dart b/lib/pages/dynamics_mention/widgets/item.dart index 42479838..8039edba 100644 --- a/lib/pages/dynamics_mention/widgets/item.dart +++ b/lib/pages/dynamics_mention/widgets/item.dart @@ -9,10 +9,12 @@ class DynMentionItem extends StatelessWidget { super.key, required this.item, required this.onTap, + required this.onCheck, }); final MentionItem item; - final ValueChanged onTap; + final VoidCallback onTap; + final ValueChanged onCheck; @override Widget build(BuildContext context) { @@ -20,7 +22,7 @@ class DynMentionItem extends StatelessWidget { type: MaterialType.transparency, child: ListTile( dense: true, - onTap: () => onTap(item), + onTap: onTap, leading: NetworkImgLayer( src: item.face, width: 42, @@ -35,6 +37,15 @@ class DynMentionItem extends StatelessWidget { '${NumUtil.numFormat(item.fans)}粉丝', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), + trailing: Checkbox( + tristate: false, + value: item.checked ?? false, + onChanged: (value) { + item.checked = value; + (context as Element).markNeedsBuild(); + onCheck(value); + }, + ), ), ); }