Add multi-select support to pmshare panel (#1779)

* Add multi-select support to share panel

- Replace single selection index with per-user selected flag
- Allow sending to multiple selected users
- Add sending state to prevent multiple clicks
- Update default selection logic to mark first user as selected

* 简化代码逻辑

* update

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>

---------

Signed-off-by: lesetong <oscarlbw@qq.com>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
lesetong
2025-12-17 12:46:36 +08:00
committed by GitHub
parent 348a9e014e
commit ab1e5cb62a
14 changed files with 113 additions and 110 deletions

View File

@@ -47,7 +47,7 @@ class _ContactPageState extends State<ContactPage>
actions: [
IconButton(
onPressed: () async {
UserModel? userModel = await Navigator.of(context).push(
final UserModel? userModel = await Navigator.of(context).push(
GetPageRoute(
page: () => FollowSearchPage(
mid: mid,

View File

@@ -617,7 +617,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
RichTextItem? voteItem = editController.items.firstWhereOrNull(
(e) => e.type == RichTextType.vote,
);
VoteInfo? voteInfo = await Navigator.of(context).push(
final VoteInfo? voteInfo = await Navigator.of(context).push(
GetPageRoute(
page: () => CreateVotePage(
voteId: voteItem?.id == null ? null : int.parse(voteItem!.id!),
@@ -818,7 +818,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
Future<void> _onReserve() async {
controller.keepChatPanel();
ReserveInfoData? reserveInfo = await Navigator.of(context).push(
final ReserveInfoData? reserveInfo = await Navigator.of(context).push(
GetPageRoute(
page: () => CreateReservePage(sid: _reserveCard.value?.id),
),

View File

@@ -74,6 +74,7 @@ class _FansPageState extends FollowTypePageState<FansPage> {
mid: item.mid,
name: item.uname!,
avatar: item.face!,
selected: true,
),
);
return;

View File

@@ -63,7 +63,7 @@ class _FavVideoPageState extends State<FavVideoPage>
heroTag: heroTag,
item: item,
onTap: () async {
var res = await Get.toNamed(
final res = await Get.toNamed(
'/favDetail',
arguments: item,
parameters: {

View File

@@ -33,6 +33,7 @@ class FollowItem extends StatelessWidget {
mid: item.mid,
name: item.uname!,
avatar: item.face!,
selected: true,
),
);
} else {

View File

@@ -172,8 +172,8 @@ class _MemberFavoriteState extends State<MemberFavorite>
height: 98,
child: MemberFavItem(
item: item,
callback: (res) {
if (res == true) {
onDelete: (isDeleted) {
if (isDeleted ?? false) {
_controller.favState
..value.mediaListResponse?.list?.remove(item)
..refresh();

View File

@@ -12,10 +12,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberFavItem extends StatelessWidget {
const MemberFavItem({super.key, required this.item, this.callback});
const MemberFavItem({super.key, required this.item, this.onDelete});
final SpaceFavItemModel item;
final ValueChanged<bool?>? callback;
final ValueChanged<bool?>? onDelete;
@override
Widget build(BuildContext context) {
@@ -34,14 +34,14 @@ class MemberFavItem extends StatelessWidget {
}
if (item.type == 0 || item.type == 11) {
var res = await Get.toNamed(
final bool? isDeleted = await Get.toNamed(
'/favDetail',
parameters: {
'mediaId': item.id.toString(),
'heroTag': Utils.makeHeroTag(item.id),
},
);
callback?.call(res);
onDelete?.call(isDeleted);
} else {
SubDetailPage.toSubDetailPage(
item.id!,

View File

@@ -669,7 +669,7 @@ List<SettingsModel> get styleSettings => [
),
NormalModel(
onTap: (context, setState) async {
var result = await Get.toNamed('/fontSizeSetting');
final double? result = await Get.toNamed('/fontSizeSetting');
if (result != null) {
Get.put(ColorSelectController()).currentTextScale.value = result;
}

View File

@@ -11,15 +11,17 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class UserModel {
const UserModel({
UserModel({
required this.mid,
required this.name,
required this.avatar,
this.selected = false,
});
final int mid;
final String name;
final String avatar;
bool selected;
@override
bool operator ==(Object other) {
@@ -41,11 +43,9 @@ class SharePanel extends StatefulWidget {
super.key,
required this.content,
this.userList,
this.selectedIndex,
});
final Map content;
final int? selectedIndex;
final List<UserModel>? userList;
@override
@@ -53,7 +53,6 @@ class SharePanel extends StatefulWidget {
}
class _SharePanelState extends State<SharePanel> {
int _selectedIndex = -1;
final List<UserModel> _userList = <UserModel>[];
final ScrollController _scrollController = ScrollController();
final FocusNode _focusNode = FocusNode();
@@ -72,9 +71,6 @@ class _SharePanelState extends State<SharePanel> {
super.initState();
if (widget.userList?.isNotEmpty == true) {
_userList.addAll(widget.userList!);
if (widget.selectedIndex != null) {
_selectedIndex = widget.selectedIndex!;
}
}
}
@@ -114,61 +110,66 @@ class _SharePanelState extends State<SharePanel> {
padding: EdgeInsets.zero,
childBuilder: (index) {
final item = _userList[index];
return GestureDetector(
onTap: () {
_selectedIndex = index;
setState(() {});
},
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 65,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
Column(
return Builder(
builder: (context) {
return GestureDetector(
onTap: () {
item.selected = !item.selected;
(context as Element).markNeedsBuild();
},
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 65,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
Padding(
padding: const EdgeInsets.all(5),
child: NetworkImgLayer(
width: 40,
height: 40,
src: item.avatar,
type: ImageType.avatar,
Column(
children: [
Padding(
padding: const EdgeInsets.all(5),
child: NetworkImgLayer(
width: 40,
height: 40,
src: item.avatar,
type: ImageType.avatar,
),
),
const SizedBox(height: 2),
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
],
),
if (item.selected)
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withValues(
alpha: 0.3,
),
shape: BoxShape.circle,
border: Border.all(
width: 1.5,
color: theme.colorScheme.primary,
),
),
child: const Icon(
Icons.check,
size: 20,
color: Colors.white,
),
),
),
const SizedBox(height: 2),
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
],
),
if (index == _selectedIndex)
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(
alpha: 0.3,
),
shape: BoxShape.circle,
border: Border.all(
width: 1.5,
color: theme.colorScheme.primary,
),
),
child: const Icon(
Icons.check,
size: 20,
color: Colors.white,
),
),
],
),
),
),
);
},
);
},
),
@@ -176,14 +177,13 @@ class _SharePanelState extends State<SharePanel> {
GestureDetector(
onTap: () async {
_focusNode.unfocus();
UserModel? userModel = await Navigator.of(context).push(
final UserModel? userModel = await Navigator.of(context).push(
GetPageRoute(page: () => const ContactPage()),
);
if (userModel != null) {
_userList
..remove(userModel)
..insert(0, userModel);
_selectedIndex = 0;
_scrollController.jumpToTop();
setState(() {});
}
@@ -248,17 +248,7 @@ class _SharePanelState extends State<SharePanel> {
),
const SizedBox(width: 12),
FilledButton.tonal(
onPressed: () {
if (_selectedIndex == -1) {
SmartDialog.showToast('请选择分享的用户');
return;
}
RequestUtils.pmShare(
receiverId: _userList[_selectedIndex].mid,
content: widget.content,
message: _controller.text,
);
},
onPressed: _onSend,
style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: const VisualDensity(
@@ -274,4 +264,31 @@ class _SharePanelState extends State<SharePanel> {
),
);
}
Future<void> _onSend() async {
final list = _userList.where((user) => user.selected);
if (list.isEmpty) {
SmartDialog.showToast('请选择分享的用户');
return;
}
SmartDialog.showLoading();
final res = await Future.wait(
list.map(
(user) => RequestUtils.pmShare(
receiverId: user.mid,
content: widget.content,
message: _controller.text,
),
),
);
SmartDialog.dismiss();
if (res.every((e) => e)) {
Get.back();
SmartDialog.showToast('分享成功');
} else if (res.every((e) => !e)) {
SmartDialog.showToast('分享失败');
} else {
SmartDialog.showToast('部分分享失败');
}
}
}

View File

@@ -320,7 +320,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
item(
onTap: () async {
controller.keepChatPanel();
({String title, String url})? res = await Get.to(
final ({String title, String url})? res = await Get.to(
ReplySearchPage(type: widget.replyType, oid: widget.oid),
);
if (res != null) {

View File

@@ -82,7 +82,7 @@ class WhisperDetailController extends CommonListController<RspSessionMsg, Msg> {
SmartDialog.showToast('请先登录');
return;
}
var result = await ImGrpc.sendMsg(
final result = await ImGrpc.sendMsg(
senderUid: account.mid,
receiverId: mid!,
content: msgType == 5