feat: edit dyn

feat: set pub setting

feat: set reply interaction

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-01-13 10:16:45 +08:00
parent 4a2679a589
commit cb58822009
16 changed files with 826 additions and 131 deletions

View File

@@ -43,6 +43,7 @@
## feat ## feat
- [x] 编辑动态
- [x] DLNA 投屏 - [x] DLNA 投屏
- [x] 离线缓存/播放 - [x] 离线缓存/播放
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)

View File

@@ -986,4 +986,14 @@ abstract final class Api {
'${HttpString.liveBaseUrl}/av/v1/SuperChat/report'; '${HttpString.liveBaseUrl}/av/v1/SuperChat/report';
static const String imMsgReport = '${HttpString.tUrl}/x/bplus/im/report/add'; static const String imMsgReport = '${HttpString.tUrl}/x/bplus/im/report/add';
static const String dynPrivatePubSetting =
'/x/dynamic/feed/dyn/private_pub_setting';
static const String editDyn = '/x/dynamic/feed/edit/dyn';
static const String replyInteraction =
'/x/v2/reply/subject/interaction-status';
static const String replySubjectModify = '/x/v2/reply/subject/modify';
} }

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
@@ -151,7 +153,7 @@ abstract final class DynamicsHttp {
} }
} }
static Future createDynamic({ static Future<LoadingState<Map?>> createDynamic({
dynamic mid, dynamic mid,
dynamic dynIdStr, // repost dyn dynamic dynIdStr, // repost dyn
dynamic rid, // repost video dynamic rid, // repost video
@@ -231,15 +233,9 @@ abstract final class DynamicsHttp {
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return Success(res.data['data']);
'status': true,
'data': res.data['data'],
};
} else { } else {
return { return Error(res.data['message']);
'status': false,
'msg': res.data['message'],
};
} }
} }
@@ -675,4 +671,110 @@ abstract final class DynamicsHttp {
return Error(res.data['message']); return Error(res.data['message']);
} }
} }
static Future<LoadingState<Null>> dynPrivatePubSetting({
required Object dynId,
int? dynType,
required String action,
}) async {
final res = await Request().post(
Api.dynPrivatePubSetting,
queryParameters: {
'platform': 'web',
'csrf': Accounts.main.csrf,
},
data: {
"object_id": jsonEncode({
"dyn_id": dynId.toString(),
"dyn_type": ?dynType,
}),
"action": action,
},
options: Options(contentType: Headers.jsonContentType),
);
if (res.data['code'] == 0) {
return const Success(null);
} else {
return Error(res.data['message']);
}
}
static Future<LoadingState<Null>> editDyn({
required Object dynId,
Object? repostDynId,
dynamic rawText,
List? pics,
ReplyOptionType? replyOption,
int? privatePub,
List<Map<String, dynamic>>? extraContent,
Pair<int, String>? topic,
String? title,
Map? attachCard,
}) async {
final uploadId =
"${Accounts.main.mid}_${DateTime.now().millisecondsSinceEpoch ~/ 1000}_${Utils.random.nextInt(9000) + 1000}";
final res = await Request().post(
Api.editDyn,
queryParameters: await WbiSign.makSign({
'platform': 'web',
'csrf': Accounts.main.csrf,
'x-bili-device-req-json':
'{"platform":"web","device":"pc","spmid":"333.1368"}',
'w_dyn_req.upload_id': uploadId,
'w_dyn_req.meta':
'{"app_meta":{"from":"create.dynamic.web","mobi_app":"web"}}',
}),
data: {
"dyn_req": {
"content": {
"contents": [
if (rawText != null)
{
"raw_text": rawText,
"type": 1,
"biz_id": "",
},
...?extraContent,
],
if (title != null && title.isNotEmpty) 'title': title,
},
if (privatePub != null || replyOption != null)
"option": {
'private_pub': ?privatePub,
if (replyOption == ReplyOptionType.close)
"close_comment": 1
else if (replyOption == ReplyOptionType.choose)
"up_choose_comment": 1,
},
"scene": repostDynId != null
? 4
: pics != null
? 2
: 1,
'pics': ?pics,
"attach_card": attachCard,
"upload_id": uploadId,
"meta": {
"app_meta": {"from": "create.dynamic.web", "mobi_app": "web"},
},
if (topic != null)
"topic": {
"id": topic.first,
"name": topic.second,
"from_source": "dyn.web.list",
"from_topic_id": 0,
},
},
"dyn_id_str": dynId.toString(),
if (repostDynId != null)
"web_repost_src": {"dyn_id_str": repostDynId.toString()},
},
options: Options(contentType: Headers.jsonContentType),
);
if (res.data['code'] == 0) {
return const Success(null);
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -6,9 +6,11 @@ import 'package:PiliPlus/models_new/emote/data.dart';
import 'package:PiliPlus/models_new/emote/package.dart'; import 'package:PiliPlus/models_new/emote/package.dart';
import 'package:PiliPlus/models_new/reply/data.dart'; import 'package:PiliPlus/models_new/reply/data.dart';
import 'package:PiliPlus/models_new/reply2reply/data.dart'; import 'package:PiliPlus/models_new/reply2reply/data.dart';
import 'package:PiliPlus/models_new/reply_interaction/data.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
abstract final class ReplyHttp { abstract final class ReplyHttp {
static final Options options = Options( static final Options options = Options(
@@ -206,4 +208,53 @@ abstract final class ReplyHttp {
return Error(res.data['message']); return Error(res.data['message']);
} }
} }
static Future<LoadingState<ReplyInteractData>> replyInteraction({
required Object oid,
required Object type,
}) async {
final res = await Request().get(
Api.replyInteraction,
queryParameters: {
'oid': oid,
'type': type,
'web_location': 333.1369,
},
);
if (res.data['code'] == 0) {
try {
return Success(ReplyInteractData.fromJson(res.data['data']));
} catch (e) {
return Error(e.toString());
}
} else {
return Error(res.data['message']);
}
}
static Future<LoadingState<Null>> replySubjectModify({
required int oid,
required int type,
required int action,
}) async {
final res = await Request().post(
Api.replySubjectModify,
data: {
'oid': oid,
'type': type,
'action': action,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
if (res.data['data']?['action_toast'] case final String toast) {
SmartDialog.showToast(toast);
}
return const Success(null);
} else {
SmartDialog.showToast(res.data['message'].toString());
return const Error(null);
}
}
} }

View File

@@ -410,6 +410,8 @@ class ModuleAuthorModel extends Avatar {
int? pubTs; int? pubTs;
String? type; String? type;
Decorate? decorate; Decorate? decorate;
bool? isTop;
String? badgeText;
ModuleAuthorModel.fromJson(Map<String, dynamic> json) : super.fromJson(json) { ModuleAuthorModel.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
if (json['official'] != null) { if (json['official'] != null) {
@@ -426,6 +428,8 @@ class ModuleAuthorModel extends Avatar {
} else { } else {
pendant = null; pendant = null;
} }
isTop = json['is_top'];
badgeText = _parseString(json['icon_badge']?['text']);
} }
} }
@@ -1186,12 +1190,23 @@ class DynamicNoneModel {
} }
} }
class OpusPicModel { sealed class PicModel {}
class FilePicModel extends PicModel {
String path;
FilePicModel({
required this.path,
});
}
class OpusPicModel extends PicModel {
OpusPicModel({ OpusPicModel({
this.width, this.width,
this.height, this.height,
this.src, this.src,
this.url, this.url,
this.size,
}); });
int? width; int? width;
@@ -1199,6 +1214,7 @@ class OpusPicModel {
String? src; String? src;
String? url; String? url;
String? liveUrl; String? liveUrl;
num? size;
OpusPicModel.fromJson(Map<String, dynamic> json) { OpusPicModel.fromJson(Map<String, dynamic> json) {
width = Utils.safeToInt(json['width']); width = Utils.safeToInt(json['width']);
@@ -1206,7 +1222,15 @@ class OpusPicModel {
src = json['src']; src = json['src'];
url = json['url']; url = json['url'];
liveUrl = json['live_url']; liveUrl = json['live_url'];
size = json['size'];
} }
Map<String, dynamic> toJson() => {
'img_width': width,
'img_height': height,
'img_size': size,
'img_src': url,
};
} }
class DynamicLiveModel { class DynamicLiveModel {
@@ -1269,7 +1293,7 @@ class ModuleTag {
String? text; String? text;
ModuleTag.fromJson(Map<String, dynamic> json) { ModuleTag.fromJson(Map<String, dynamic> json) {
text = json['text']; text = _parseString(json['text']);
} }
} }

View File

@@ -0,0 +1,20 @@
import 'package:PiliPlus/models_new/reply_interaction/interact_status.dart';
class ReplyInteractData {
InteractStatus upReplySelection;
InteractStatus upReply;
// InteractStatus upDm;
ReplyInteractData({
required this.upReplySelection,
required this.upReply,
// required this.upDm,
});
factory ReplyInteractData.fromJson(Map<String, dynamic> json) =>
ReplyInteractData(
upReplySelection: InteractStatus.fromJson(json["up_reply_selection"]),
upReply: InteractStatus.fromJson(json["up_reply"]),
// upDm: InteractStatus.fromJson(json["up_dm"]),
);
}

View File

@@ -0,0 +1,14 @@
class InteractStatus {
bool canModify;
int status;
InteractStatus({
required this.canModify,
required this.status,
});
factory InteractStatus.fromJson(Map<String, dynamic> json) => InteractStatus(
canModify: json["can_modify"],
status: json["status"],
);
}

View File

@@ -7,20 +7,28 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart';
import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'
show PicModel, FilePicModel, OpusPicModel;
import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart';
import 'package:PiliPlus/models_new/emote/emote.dart' as e; import 'package:PiliPlus/models_new/emote/emote.dart' as e;
import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart'; import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart';
import 'package:PiliPlus/pages/common/publish/common_publish_page.dart'; import 'package:PiliPlus/pages/common/publish/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics_mention/view.dart'; import 'package:PiliPlus/pages/dynamics_mention/view.dart';
import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart' show CancelToken; import 'package:dio/dio.dart' show CancelToken;
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart'; import 'package:image_cropper/image_cropper.dart';
@@ -31,19 +39,21 @@ abstract class CommonRichTextPubPage
const CommonRichTextPubPage({ const CommonRichTextPubPage({
super.key, super.key,
this.items, this.items,
this.pics,
super.onSave, super.onSave,
super.autofocus, super.autofocus,
super.imageLengthLimit, super.imageLengthLimit,
}); });
final List<RichTextItem>? items; final List<RichTextItem>? items;
final List<PicModel>? pics;
} }
abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage> abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
extends CommonPublishPageState<T> { extends CommonPublishPageState<T> {
final key = GlobalKey<RichTextFieldState>(); final key = GlobalKey<RichTextFieldState>();
late final imagePicker = ImagePicker(); late final imagePicker = ImagePicker();
late final RxList<String> pathList = <String>[].obs; late final RxList<PicModel> imageList;
int get limit => widget.imageLengthLimit ?? 9; int get limit => widget.imageLengthLimit ?? 9;
@override @override
@@ -58,13 +68,16 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
if (editController.rawText.trim().isNotEmpty) { if (editController.rawText.trim().isNotEmpty) {
enablePublish.value = true; enablePublish.value = true;
} }
imageList = RxList<PicModel>(widget.pics ?? <PicModel>[]);
} }
@override @override
void dispose() { void dispose() {
if (PlatformUtils.isMobile) { if (PlatformUtils.isMobile) {
for (final i in pathList) { for (final img in imageList) {
File(i).tryDel(); if (img is FilePicModel) {
File(img.path).tryDel();
}
} }
} }
super.dispose(); super.dispose();
@@ -82,15 +95,18 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
).colorScheme.secondaryContainer.withValues(alpha: 0.5); ).colorScheme.secondaryContainer.withValues(alpha: 0.5);
void onClear() { void onClear() {
final path = pathList.removeAt(index); final image = imageList.removeAt(index);
if (PlatformUtils.isMobile) { if (PlatformUtils.isMobile) {
File(path).tryDel(); if (image is FilePicModel) {
File(image.path).tryDel();
}
} }
if (pathList.isEmpty && editController.rawText.trim().isEmpty) { if (imageList.isEmpty && editController.rawText.trim().isEmpty) {
enablePublish.value = false; enablePublish.value = false;
} }
} }
final image = imageList[index];
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -98,12 +114,18 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
onTap: () async { onTap: () async {
controller.keepChatPanel(); controller.keepChatPanel();
await PageUtils.imageView( await PageUtils.imageView(
imgList: pathList imgList: imageList
.map( .map(
(path) => SourceModel( (img) => switch (img) {
url: path, FilePicModel e => SourceModel(
sourceType: SourceType.fileImage, url: e.path,
), sourceType: .fileImage,
),
OpusPicModel e => SourceModel(
url: e.url!,
sourceType: .networkImage,
),
},
) )
.toList(), .toList(),
initialPage: index, initialPage: index,
@@ -117,21 +139,35 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
onSecondaryTap: PlatformUtils.isMobile ? null : onClear, onSecondaryTap: PlatformUtils.isMobile ? null : onClear,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)), borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Image( child: ConstrainedBox(
height: height, constraints: const BoxConstraints(minWidth: 42),
fit: BoxFit.fitHeight, child: switch (image) {
filterQuality: FilterQuality.low, FilePicModel e => Image.file(
image: FileImage(File(pathList[index])), File(e.path),
height: height,
filterQuality: .low,
cacheHeight: height.cacheSize(context),
),
OpusPicModel e => CachedNetworkImage(
imageUrl: ImageUtils.thumbnailUrl(e.url!),
height: height,
filterQuality: .low,
memCacheHeight: height.cacheSize(context),
fadeInDuration: .zero,
fadeOutDuration: .zero,
placeholder: (_, _) => const SizedBox(width: 42),
),
},
), ),
), ),
), ),
if (PlatformUtils.isMobile) if (kDebugMode || PlatformUtils.isMobile)
Positioned( Positioned(
top: 34, top: 34,
right: 5, right: 5,
child: iconButton( child: iconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () => onCropImage(index), onPressed: () => onCropImage(index, image),
size: 24, size: 24,
iconSize: 14, iconSize: 14,
bgColor: color, bgColor: color,
@@ -152,11 +188,23 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
); );
} }
Future<void> onCropImage(int index) async { Future<void> onCropImage(int index, PicModel image) async {
String? path;
switch (image) {
case FilePicModel e:
path = e.path;
case OpusPicModel e:
SmartDialog.showLoading();
final file = (await DefaultCacheManager().getSingleFile(
e.url.http2https,
));
await SmartDialog.dismiss();
path = file.path;
}
if (!mounted || path.isEmpty) return;
late final colorScheme = ColorScheme.of(context); late final colorScheme = ColorScheme.of(context);
final sourcePath = pathList[index];
final croppedFile = await ImageCropper.platform.cropImage( final croppedFile = await ImageCropper.platform.cropImage(
sourcePath: sourcePath, sourcePath: path,
uiSettings: [ uiSettings: [
AndroidUiSettings( AndroidUiSettings(
toolbarTitle: '裁剪', toolbarTitle: '裁剪',
@@ -168,8 +216,10 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
], ],
); );
if (croppedFile != null) { if (croppedFile != null) {
File(sourcePath).tryDel(); if (image is FilePicModel) {
pathList[index] = croppedFile.path; File(image.path).tryDel();
}
imageList[index] = FilePicModel(path: croppedFile.path);
} }
} }
@@ -185,11 +235,11 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
); );
if (pickedFiles.isNotEmpty) { if (pickedFiles.isNotEmpty) {
for (int i = 0; i < pickedFiles.length; i++) { for (int i = 0; i < pickedFiles.length; i++) {
if (pathList.length == limit) { if (imageList.length == limit) {
SmartDialog.showToast('最多选择$limit张图片'); SmartDialog.showToast('最多选择$limit张图片');
break; break;
} else { } else {
pathList.add(pickedFiles[i].path); imageList.add(FilePicModel(path: pickedFiles[i].path));
} }
} }
callback?.call(); callback?.call();
@@ -465,25 +515,30 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
Future<void> onPublish() async { Future<void> onPublish() async {
feedBack(); feedBack();
List<Map<String, dynamic>>? pictures; List<Map<String, dynamic>>? pictures;
if (pathList.isNotEmpty) { if (imageList.isNotEmpty) {
SmartDialog.showLoading(msg: '正在上传图片...'); SmartDialog.showLoading(msg: '正在上传图片...');
final cancelToken = CancelToken(); final cancelToken = CancelToken();
try { try {
pictures = await Future.wait<Map<String, dynamic>>( pictures = await Future.wait<Map<String, dynamic>>(
pathList.map((path) async { imageList.map((img) async {
final result = await MsgHttp.uploadBfs( switch (img) {
path: path, case FilePicModel e:
category: 'daily', final result = await MsgHttp.uploadBfs(
biz: 'new_dyn', path: e.path,
cancelToken: cancelToken, category: 'daily',
); biz: 'new_dyn',
final data = result.data; cancelToken: cancelToken,
return { );
'img_width': data.imageWidth, final data = result.data;
'img_height': data.imageHeight, return {
'img_size': data.imgSize, 'img_width': data.imageWidth,
'img_src': data.imageUrl, 'img_height': data.imageHeight,
}; 'img_size': data.imgSize,
'img_src': data.imageUrl,
};
case OpusPicModel e:
return e.toJson();
}
}), }),
eagerError: true, eagerError: true,
); );

View File

@@ -5,6 +5,8 @@ import 'package:PiliPlus/common/widgets/dialog/report.dart';
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart'; import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
@@ -28,22 +30,27 @@ import 'package:get/get.dart';
class AuthorPanel extends StatelessWidget { class AuthorPanel extends StatelessWidget {
final DynamicItemModel item; final DynamicItemModel item;
final Function? addBannedList;
final bool isSave; final bool isSave;
final bool isDetail; final bool isDetail;
final ValueChanged? onRemove; final ValueChanged<Object>? onRemove;
final void Function(bool isTop, Object dynId)? onSetTop; final void Function(bool isTop, Object dynId)? onSetTop;
final VoidCallback? onBlock; final VoidCallback? onBlock;
final Future<LoadingState> Function(bool isPrivate, Object dynId)?
onSetPubSetting;
final VoidCallback? onEdit;
final ValueChanged<int>? onSetReplySubject;
const AuthorPanel({ const AuthorPanel({
super.key, super.key,
required this.item, required this.item,
this.addBannedList,
this.isDetail = false, this.isDetail = false,
this.onRemove, this.onRemove,
this.isSave = false, this.isSave = false,
this.onSetTop, this.onSetTop,
this.onBlock, this.onBlock,
this.onSetPubSetting,
this.onEdit,
this.onSetReplySubject,
}); });
Widget _buildAvatar(ModuleAuthorModel moduleAuthor) { Widget _buildAvatar(ModuleAuthorModel moduleAuthor) {
@@ -73,6 +80,33 @@ class AuthorPanel extends StatelessWidget {
) )
: DateFormatUtils.dateFormat(moduleAuthor.pubTs) : DateFormatUtils.dateFormat(moduleAuthor.pubTs)
: moduleAuthor.pubTime; : moduleAuthor.pubTime;
Widget? pubTs;
if (pubTime != null) {
pubTs = Text(
'$pubTime${moduleAuthor.pubAction != null ? ' ${moduleAuthor.pubAction}' : ''}',
style: TextStyle(
color: theme.colorScheme.outline,
fontSize: theme.textTheme.labelSmall!.fontSize,
),
);
if (moduleAuthor.badgeText case final badgeText?) {
pubTs = Row(
mainAxisSize: .min,
spacing: 5,
children: [
pubTs,
Text(
badgeText,
style: TextStyle(
color: theme.colorScheme.secondary,
fontSize: theme.textTheme.labelSmall!.fontSize,
),
),
],
);
}
}
final moduleTagText = !isDetail ? item.modules.moduleTag?.text : null;
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.center, alignment: Alignment.center,
@@ -88,10 +122,10 @@ class AuthorPanel extends StatelessWidget {
} }
: null, : null,
child: Row( child: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildAvatar(moduleAuthor), _buildAvatar(moduleAuthor),
const SizedBox(width: 10),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -107,14 +141,7 @@ class AuthorPanel extends StatelessWidget {
fontSize: theme.textTheme.titleSmall!.fontSize, fontSize: theme.textTheme.titleSmall!.fontSize,
), ),
), ),
if (pubTime != null) ?pubTs,
Text(
'$pubTime${moduleAuthor.pubAction != null ? ' ${moduleAuthor.pubAction}' : ''}',
style: TextStyle(
color: theme.colorScheme.outline,
fontSize: theme.textTheme.labelSmall!.fontSize,
),
),
], ],
), ),
], ],
@@ -123,7 +150,7 @@ class AuthorPanel extends StatelessWidget {
), ),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: !isDetail && item.modules.moduleTag?.text != null child: moduleTagText != null
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -142,7 +169,7 @@ class AuthorPanel extends StatelessWidget {
), ),
), ),
child: Text( child: Text(
item.modules.moduleTag!.text!, moduleTagText,
style: TextStyle( style: TextStyle(
height: 1, height: 1,
fontSize: 12, fontSize: 12,
@@ -399,18 +426,150 @@ class AuthorPanel extends StatelessWidget {
ListTile( ListTile(
onTap: () { onTap: () {
Get.back(); Get.back();
onSetTop!( onSetTop!(moduleAuthor.isTop ?? false, item.idStr);
item.modules.moduleTag?.text != null,
item.idStr,
);
}, },
minLeadingWidth: 0, minLeadingWidth: 0,
leading: const Icon(Icons.vertical_align_top, size: 19), leading: const Icon(Icons.vertical_align_top, size: 19),
title: Text( title: Text(
'${item.modules.moduleTag?.text != null ? '取消' : ''}置顶', '${moduleAuthor.isTop == true ? '取消' : ''}置顶',
style: theme.textTheme.titleSmall!, style: theme.textTheme.titleSmall!,
), ),
), ),
if (onSetReplySubject != null)
ListTile(
onTap: () async {
Get.back();
final res = await ReplyHttp.replyInteraction(
oid: item.basic!.commentIdStr!,
type: item.basic!.commentType!,
);
if (res case Success(:final response)) {
if (context.mounted) {
showDialog(
context: context,
builder: (context) {
final selection = response.upReplySelection;
final enableSelection = selection.status == 1;
final reply = response.upReply;
final enableReply = reply.status == 1;
return AlertDialog(
clipBehavior: .hardEdge,
contentPadding: const .symmetric(vertical: 12),
content: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
ListTile(
dense: true,
enabled: selection.canModify,
title: Text(
'${enableSelection ? '停止' : '开启'}评论精选',
style: const TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
onSetReplySubject!(
enableSelection ? 2 : 1,
);
},
),
ListTile(
dense: true,
enabled: reply.canModify,
title: Text(
'${enableReply ? '关闭' : '恢复'}评论',
style: const TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
onSetReplySubject!(enableReply ? 3 : 4);
},
),
],
),
);
},
);
}
} else {
res.toast();
}
},
minLeadingWidth: 0,
leading: const Icon(
Icons.mark_unread_chat_alt_outlined,
size: 19,
),
title: Text(
'互动设置',
style: theme.textTheme.titleSmall!,
),
),
if (onSetPubSetting != null)
ListTile(
onTap: () {
Get.back();
final isPrivate = moduleAuthor.badgeText != null;
Future<void> onTap() async {
Get.back();
if ((await onSetPubSetting!(
isPrivate,
item.idStr,
)).isSuccess) {
if (context.mounted) {
(context as Element).markNeedsBuild();
}
}
}
showDialog(
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const .symmetric(vertical: 12),
content: Column(
mainAxisSize: .min,
children: [
ListTile(
dense: true,
enabled: isPrivate,
title: const Text(
'所有用户可见',
style: TextStyle(fontSize: 14),
),
onTap: onTap,
),
ListTile(
dense: true,
enabled: !isPrivate,
title: const Text(
'仅自己可见',
style: TextStyle(fontSize: 14),
),
onTap: onTap,
),
],
),
),
);
},
minLeadingWidth: 0,
leading: const Icon(Icons.visibility, size: 19),
title: Text('可见范围', style: theme.textTheme.titleSmall!),
),
if (onEdit != null)
ListTile(
onTap: () {
Get.back();
onEdit!();
},
minLeadingWidth: 0,
leading: const Icon(Icons.edit_note, size: 19),
title: Text('编辑动态', style: theme.textTheme.titleSmall!),
),
if (onRemove != null) if (onRemove != null)
ListTile( ListTile(
onTap: () { onTap: () {

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/common/widgets/avatars.dart'; import 'package:PiliPlus/common/widgets/avatars.dart';
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart'; import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/action_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/action_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart';
@@ -15,12 +16,16 @@ class DynamicPanel extends StatelessWidget {
final DynamicItemModel item; final DynamicItemModel item;
final double maxWidth; final double maxWidth;
final bool isDetail; final bool isDetail;
final ValueChanged? onRemove; final ValueChanged<Object>? onRemove;
final bool isSave; final bool isSave;
final void Function(bool isTop, Object dynId)? onSetTop; final void Function(bool isTop, Object dynId)? onSetTop;
final VoidCallback? onBlock; final VoidCallback? onBlock;
final VoidCallback? onUnfold; final VoidCallback? onUnfold;
final bool isDetailPortraitW; final bool isDetailPortraitW;
final Future<LoadingState> Function(bool isPrivate, Object dynId)?
onSetPubSetting;
final VoidCallback? onEdit;
final ValueChanged<int>? onSetReplySubject;
const DynamicPanel({ const DynamicPanel({
super.key, super.key,
@@ -33,6 +38,9 @@ class DynamicPanel extends StatelessWidget {
this.onBlock, this.onBlock,
this.onUnfold, this.onUnfold,
this.isDetailPortraitW = true, this.isDetailPortraitW = true,
this.onSetPubSetting,
this.onEdit,
this.onSetReplySubject,
}); });
@override @override
@@ -48,6 +56,9 @@ class DynamicPanel extends StatelessWidget {
isSave: isSave, isSave: isSave,
onSetTop: onSetTop, onSetTop: onSetTop,
onBlock: onBlock, onBlock: onBlock,
onSetPubSetting: onSetPubSetting,
onEdit: onEdit,
onSetReplySubject: onSetReplySubject,
); );
void showMore() => _imageSaveDialog(context, authorWidget.morePanel); void showMore() => _imageSaveDialog(context, authorWidget.morePanel);

View File

@@ -35,10 +35,13 @@ TextSpan? richNode(
if (richTextNodes == null || richTextNodes.isEmpty) { if (richTextNodes == null || richTextNodes.isEmpty) {
return TextSpan(text: desc.text); return TextSpan(text: desc.text);
} }
} else if (moduleDynamic?.major?.opus case final opus?) { } else if (moduleDynamic?.major?.opus case DynamicOpusModel(
:final title,
:final summary,
)) {
// 动态页面 richTextNodes 层级可能与主页动态层级不同 // 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes = opus.summary?.richTextNodes; richTextNodes = summary?.richTextNodes;
if (opus.title case final title?) { if (title != null && title.isNotEmpty) {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: '$title\n', text: '$title\n',
@@ -76,7 +79,7 @@ TextSpan? richNode(
case 'RICH_TEXT_NODE_TYPE_TOPIC': case 'RICH_TEXT_NODE_TYPE_TOPIC':
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: i.origText!, text: i.origText,
style: style, style: style,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => Get.toNamed( ..onTap = () => Get.toNamed(

View File

@@ -10,8 +10,10 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/common/reply/reply_option_type.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart' show PicModel;
import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
@@ -29,7 +31,6 @@ import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart' hide DraggableScrollableSheet; import 'package:flutter/material.dart' hide DraggableScrollableSheet;
import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -39,48 +40,81 @@ class CreateDynPanel extends CommonRichTextPubPage {
const CreateDynPanel({ const CreateDynPanel({
super.key, super.key,
super.imageLengthLimit = 18, super.imageLengthLimit = 18,
super.items,
super.pics,
this.scrollController, this.scrollController,
this.topic, this.topic,
this.editConfig,
this.title,
this.isPrivate = false,
this.replyOption = .allow,
this.onSuccess,
}); });
final ScrollController? scrollController; final ScrollController? scrollController;
final String? title;
final Pair<int, String>? topic; final Pair<int, String>? topic;
final bool isPrivate;
final ReplyOptionType replyOption;
final ({Object dynId, Object? repostDynId})? editConfig;
final VoidCallback? onSuccess;
@override @override
State<CreateDynPanel> createState() => _CreateDynPanelState(); State<CreateDynPanel> createState() => _CreateDynPanelState();
static void onCreateDyn(BuildContext context, {Pair<int, String>? topic}) => static void onCreateDyn(
showModalBottomSheet( BuildContext context, {
context: context, String? title,
useSafeArea: true, bool isPrivate = false,
isScrollControlled: true, ReplyOptionType replyOption = .allow,
builder: (context) => dyn_sheet.DraggableScrollableSheet( List<RichTextItem>? items,
snap: true, List<PicModel>? pics,
expand: false, Pair<int, String>? topic,
initialChildSize: 1, ({Object dynId, Object? repostDynId})? editConfig,
minChildSize: 0, VoidCallback? onSuccess,
maxChildSize: 1, }) => showModalBottomSheet(
snapSizes: const [1], context: context,
builder: (context, scrollController) => CreateDynPanel( useSafeArea: true,
scrollController: scrollController, isScrollControlled: true,
topic: topic, builder: (context) => dyn_sheet.DraggableScrollableSheet(
), snap: true,
), expand: false,
); initialChildSize: 1,
minChildSize: 0,
maxChildSize: 1,
snapSizes: const [1],
builder: (context, scrollController) => CreateDynPanel(
scrollController: scrollController,
title: title,
items: items,
pics: pics,
topic: topic,
isPrivate: isPrivate,
editConfig: editConfig,
replyOption: replyOption,
onSuccess: onSuccess,
),
),
);
} }
class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> { class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
final RxBool _isPrivate = false.obs; late final bool _isEdit;
final Rx<DateTime?> _publishTime = Rx<DateTime?>(null); late final RxBool _isPrivate;
final Rx<ReplyOptionType> _replyOption = ReplyOptionType.allow.obs; late final Rx<Pair<int, String>?> _topic;
final _titleEditCtr = TextEditingController(); late final Rx<ReplyOptionType> _replyOption;
final Rx<Pair<int, String>?> topic = Rx<Pair<int, String>?>(null); late final TextEditingController _titleEditCtr;
late final Rx<DateTime?> _publishTime = Rx<DateTime?>(null);
final Rx<ReserveInfoData?> _reserveCard = Rx<ReserveInfoData?>(null); final Rx<ReserveInfoData?> _reserveCard = Rx<ReserveInfoData?>(null);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
topic.value = widget.topic; _isEdit = widget.editConfig != null;
_isPrivate = widget.isPrivate.obs;
_replyOption = widget.replyOption.obs;
_topic = Rx<Pair<int, String>?>(widget.topic);
_titleEditCtr = TextEditingController(text: widget.title);
} }
@override @override
@@ -110,7 +144,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Obx( child: Obx(
() { () {
final hasTopic = topic.value != null; final hasTopic = _topic.value != null;
return Row( return Row(
spacing: 10, spacing: 10,
children: [ children: [
@@ -158,7 +192,9 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
), ),
), ),
TextSpan( TextSpan(
text: hasTopic ? topic.value!.second : '选择话题', text: hasTopic
? _topic.value!.second
: '选择话题',
style: TextStyle( style: TextStyle(
color: hasTopic color: hasTopic
? null ? null
@@ -176,7 +212,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
bgColor: theme.colorScheme.onInverseSurface, bgColor: theme.colorScheme.onInverseSurface,
iconColor: theme.colorScheme.onSurfaceVariant, iconColor: theme.colorScheme.onSurfaceVariant,
onPressed: () => topic.value = null, onPressed: () => _topic.value = null,
), ),
], ],
); );
@@ -253,10 +289,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...List.generate( ...List.generate(
pathList.length, imageList.length,
(index) => buildImage(index, 100), (index) => buildImage(index, 100),
), ),
if (pathList.length != limit) if (imageList.length != limit)
Builder( Builder(
builder: (context) { builder: (context) {
const borderRadius = StyleString.mdRadius; const borderRadius = StyleString.mdRadius;
@@ -265,7 +301,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
child: InkWell( child: InkWell(
borderRadius: borderRadius, borderRadius: borderRadius,
onTap: () => onPickImage(() { onTap: () => onPickImage(() {
if (pathList.isNotEmpty && !enablePublish.value) { if (imageList.isNotEmpty && !enablePublish.value) {
enablePublish.value = true; enablePublish.value = true;
} }
}), }),
@@ -316,10 +352,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
), ),
), ),
), ),
const Center( Center(
child: Text( child: Text(
'发布动态', _isEdit ? '编辑动态' : '发布动态',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
), ),
), ),
Align( Align(
@@ -464,7 +500,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
), ),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
onPressed: _isPrivate.value onPressed: _isEdit || _isPrivate.value
? null ? null
: () async { : () async {
controller.keepChatPanel(); controller.keepChatPanel();
@@ -538,8 +574,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
children: [ children: [
emojiBtn, emojiBtn,
atBtn, atBtn,
voteBtn, if (!_isEdit) ...[
moreBtn, voteBtn,
moreBtn,
],
// if (kDebugMode) // if (kDebugMode)
// ToolbarIconButton( // ToolbarIconButton(
// onPressed: editController.clear, // onPressed: editController.clear,
@@ -707,8 +745,34 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
SmartDialog.showLoading(msg: '正在发布'); SmartDialog.showLoading(msg: '正在发布');
List<Map<String, dynamic>>? extraContent = getRichContent(); List<Map<String, dynamic>>? extraContent = getRichContent();
final hasRichText = extraContent != null; final hasRichText = extraContent != null;
if (_isEdit) {
final editConfig = widget.editConfig!;
final res = await DynamicsHttp.editDyn(
dynId: editConfig.dynId,
repostDynId: editConfig.repostDynId,
rawText: hasRichText ? null : editController.text,
pics: pictures,
replyOption: _replyOption.value,
privatePub: _isPrivate.value ? 1 : null,
title: _titleEditCtr.text,
topic: _topic.value,
extraContent: extraContent,
);
SmartDialog.dismiss();
if (res.isSuccess) {
hasPub = true;
Get.back();
SmartDialog.showToast('发布成功');
widget.onSuccess?.call();
} else {
res.toast();
}
return;
}
final reserveCard = _reserveCard.value; final reserveCard = _reserveCard.value;
final result = await DynamicsHttp.createDynamic( final res = await DynamicsHttp.createDynamic(
mid: Accounts.main.mid, mid: Accounts.main.mid,
rawText: hasRichText ? null : editController.text, rawText: hasRichText ? null : editController.text,
pics: pictures, pics: pictures,
@@ -718,7 +782,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
replyOption: _replyOption.value, replyOption: _replyOption.value,
privatePub: _isPrivate.value ? 1 : null, privatePub: _isPrivate.value ? 1 : null,
title: _titleEditCtr.text, title: _titleEditCtr.text,
topic: topic.value, topic: _topic.value,
extraContent: extraContent, extraContent: extraContent,
attachCard: reserveCard == null attachCard: reserveCard == null
? null ? null
@@ -732,11 +796,11 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
}, },
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (res case Success(:final response)) {
hasPub = true; hasPub = true;
Get.back(); Get.back();
SmartDialog.showToast('发布成功'); SmartDialog.showToast('发布成功');
final id = result['data']?['dyn_id']; final id = response?['dyn_id'];
RequestUtils.insertCreatedDyn(id); RequestUtils.insertCreatedDyn(id);
if (!_isPrivate.value) { if (!_isPrivate.value) {
RequestUtils.checkCreatedDyn( RequestUtils.checkCreatedDyn(
@@ -745,8 +809,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
); );
} }
} else { } else {
SmartDialog.showToast(result['msg']); res.toast();
if (kDebugMode) debugPrint('failed to publish: ${result['msg']}');
} }
} }
@@ -759,7 +822,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
onCachePos: (offset) => _topicOffset = offset, onCachePos: (offset) => _topicOffset = offset,
); );
if (res != null) { if (res != null) {
topic.value = Pair(first: res.id, second: res.name); _topic.value = Pair(first: res.id, second: res.name);
} }
controller.restoreChatPanel(); controller.restoreChatPanel();
} }

View File

@@ -1,9 +1,11 @@
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class DynamicDetailController extends CommonDynController { class DynamicDetailController extends CommonDynController {
@@ -45,4 +47,33 @@ class DynamicDetailController extends CommonDynController {
replyType = commentType; replyType = commentType;
queryData(); queryData();
} }
Future<LoadingState> onSetPubSetting(bool isPrivate, Object dynId) async {
final res = await DynamicsHttp.dynPrivatePubSetting(
dynId: dynId,
action: isPrivate ? 'public_pub' : 'private_pub',
);
if (res.isSuccess) {
dynItem.modules.moduleAuthor?.badgeText = isPrivate ? null : '仅自己可见';
SmartDialog.showToast('设置成功');
} else {
res.toast();
}
return res;
}
Future<void> onSetReplySubject(int action) async {
final res = await ReplyHttp.replySubjectModify(
oid: oid,
type: replyType,
action: action,
);
if (res.isSuccess) {
await Future.delayed(const Duration(milliseconds: 500), () {
if (!isClosed) {
onReload();
}
});
}
}
} }

View File

@@ -2,11 +2,17 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart';
import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_create/view.dart';
import 'package:PiliPlus/pages/dynamics_detail/controller.dart'; import 'package:PiliPlus/pages/dynamics_detail/controller.dart';
import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/pages/dynamics_repost/view.dart';
import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart';
@@ -14,6 +20,7 @@ import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -66,6 +73,142 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
); );
} }
void _onEdit() {
final item = controller.dynItem;
List<RichTextItem>? items;
final moduleDynamic = item.modules.moduleDynamic;
final desc = moduleDynamic?.desc;
final opus = moduleDynamic?.major?.opus;
Pair<int, String>? topic;
if (moduleDynamic?.topic case final t?) {
try {
topic = Pair(first: t.id!, second: t.name!);
} catch (_) {
if (kDebugMode) rethrow;
}
}
final richTextNodes = desc?.richTextNodes ?? opus?.summary?.richTextNodes;
if (richTextNodes != null && richTextNodes.isNotEmpty) {
items = <RichTextItem>[];
final buffer = StringBuffer();
try {
for (final e in richTextNodes) {
if (e.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
const placeHolder = '\uFFFC';
items.add(
RichTextItem(
text: placeHolder,
rawText: e.origText,
type: .emoji,
range: TextRange(
start: buffer.length,
end: buffer.length + placeHolder.length,
),
emote: Emote(
url: e.emoji!.url!,
width: 22,
),
),
);
buffer.write(placeHolder);
continue;
}
final range = TextRange(
start: buffer.length,
end: buffer.length + e.origText!.length,
);
final item = switch (e.type) {
'RICH_TEXT_NODE_TYPE_AT' => RichTextItem(
text: e.origText!,
type: .at,
range: range,
id: e.rid,
),
'RICH_TEXT_NODE_TYPE_BV' ||
'RICH_TEXT_NODE_TYPE_TOPIC' ||
'RICH_TEXT_NODE_TYPE_LOTTERY' ||
'RICH_TEXT_NODE_TYPE_VIEW_PICTURE' => RichTextItem(
text: e.origText!,
type: .common,
range: range,
id: e.rid,
),
'RICH_TEXT_NODE_TYPE_VOTE' => RichTextItem(
text: e.origText!,
type: .vote,
range: range,
id: e.rid,
),
_ => RichTextItem(
text: e.origText!,
range: range,
),
};
items.add(item);
buffer.write(e.origText!);
}
bool isValid = true;
int cursor = 0;
for (final e in items) {
final range = e.range;
if (range.start == cursor) {
cursor = range.end;
} else {
isValid = false;
break;
}
}
assert(isValid);
} catch (e) {
if (kDebugMode) rethrow;
}
} else {
final text = desc?.text ?? opus?.summary?.text;
if (text != null && text.isNotEmpty) {
items = [
RichTextItem.fromStart(text),
];
}
}
ReplyOptionType? replyOption;
if (controller.loadingState.value case Error(:final code)) {
if (code == 12061 || code == 12002) {
replyOption = .close;
}
}
CreateDynPanel.onCreateDyn(
context,
title: opus?.title,
items: items,
pics: opus?.pics,
topic: topic,
replyOption: replyOption ?? .allow,
isPrivate: item.modules.moduleAuthor?.badgeText != null,
editConfig: (
dynId: item.idStr,
repostDynId: item.orig?.idStr,
),
onSuccess: () {
Future.delayed(
const Duration(milliseconds: 500),
() async {
if (!mounted) return;
final res = await DynamicsHttp.dynamicDetail(id: item.idStr);
if (res case Success(:final response)) {
if (mounted) {
controller.dynItem = response;
setState(() {});
}
}
},
);
},
);
}
PreferredSizeWidget _buildAppBar() => AppBar( PreferredSizeWidget _buildAppBar() => AppBar(
title: Padding( title: Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
@@ -80,6 +223,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
child: AuthorPanel( child: AuthorPanel(
item: controller.dynItem, item: controller.dynItem,
isDetail: true, isDetail: true,
onSetPubSetting: controller.onSetPubSetting,
onEdit: _onEdit,
onSetReplySubject: controller.onSetReplySubject,
), ),
), ),
); );
@@ -88,10 +234,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
), ),
actions: isPortrait actions: isPortrait
? null ? null
: [ : [ratioWidget(maxWidth), const SizedBox(width: 16)],
ratioWidget(maxWidth),
const SizedBox(width: 16),
],
); );
Widget _buildBody(ThemeData theme) { Widget _buildBody(ThemeData theme) {
@@ -110,6 +253,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
isDetail: true, isDetail: true,
maxWidth: maxWidth - this.padding.horizontal - 2 * padding, maxWidth: maxWidth - this.padding.horizontal - 2 * padding,
isDetailPortraitW: isPortrait, isDetailPortraitW: isPortrait,
onSetPubSetting: controller.onSetPubSetting,
onEdit: _onEdit,
onSetReplySubject: controller.onSetReplySubject,
), ),
), ),
buildReplyHeader(theme), buildReplyHeader(theme),
@@ -143,6 +289,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
(flex / (flex + flex1)) - (flex / (flex + flex1)) -
padding, padding,
isDetailPortraitW: isPortrait, isDetailPortraitW: isPortrait,
onSetPubSetting: controller.onSetPubSetting,
onEdit: _onEdit,
onSetReplySubject: controller.onSetReplySubject,
), ),
), ),
), ),

View File

@@ -3,6 +3,7 @@ import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scroll
import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart'; import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
@@ -419,7 +420,7 @@ class _RepostPanelState extends CommonRichTextPubPageState<RepostPanel> {
if (hasRichText && repostContent != null) { if (hasRichText && repostContent != null) {
richContent.addAll(repostContent); richContent.addAll(repostContent);
} }
final result = await DynamicsHttp.createDynamic( final res = await DynamicsHttp.createDynamic(
mid: Accounts.main.mid, mid: Accounts.main.mid,
dynIdStr: widget.item?.idStr ?? widget.dynIdStr, dynIdStr: widget.item?.idStr ?? widget.dynIdStr,
rid: widget.rid, rid: widget.rid,
@@ -428,19 +429,19 @@ class _RepostPanelState extends CommonRichTextPubPageState<RepostPanel> {
extraContent: richContent ?? repostContent, extraContent: richContent ?? repostContent,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (res case Success(:final response)) {
hasPub = true; hasPub = true;
Get.back(); Get.back();
SmartDialog.showToast('转发成功'); SmartDialog.showToast('转发成功');
widget.onSuccess?.call(); widget.onSuccess?.call();
final id = result['data']?['dyn_id']; final id = response?['dyn_id'];
RequestUtils.insertCreatedDyn(id); RequestUtils.insertCreatedDyn(id);
RequestUtils.checkCreatedDyn( RequestUtils.checkCreatedDyn(
id: id, id: id,
dynText: editController.rawText, dynText: editController.rawText,
); );
} else { } else {
SmartDialog.showToast(result['msg']); res.toast();
} }
} }

View File

@@ -12,6 +12,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart' show FilePicModel;
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart'; import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
import 'package:PiliPlus/pages/dynamics_mention/controller.dart'; import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/pages/emote/view.dart';
@@ -110,7 +111,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
Widget buildImagePreview() { Widget buildImagePreview() {
return Obx( return Obx(
() { () {
if (pathList.isNotEmpty) { if (imageList.isNotEmpty) {
return SizedBox( return SizedBox(
height: 85, height: 85,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -120,7 +121,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
spacing: 10, spacing: 10,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate( children: List.generate(
pathList.length, imageList.length,
(index) => buildImage(index, 75), (index) => buildImage(index, 75),
), ),
), ),
@@ -368,7 +369,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
if (isRoot && widget.canUploadPic) if (isRoot && widget.canUploadPic)
item( item(
onTap: () async { onTap: () async {
if (pathList.length >= limit) { if (imageList.length >= limit) {
SmartDialog.showToast('最多选择$limit张图片'); SmartDialog.showToast('最多选择$limit张图片');
return; return;
} }
@@ -385,7 +386,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
'$tmpDirPath/${Utils.generateRandomString(8)}.png', '$tmpDirPath/${Utils.generateRandomString(8)}.png',
); );
await file.writeAsBytes(res); await file.writeAsBytes(res);
pathList.add(file.path); imageList.add(FilePicModel(path: file.path));
} else { } else {
debugPrint('null screenshot'); debugPrint('null screenshot');
} }