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

@@ -7,20 +7,28 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models/common/image_preview_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/emote/emote.dart' as e;
import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart';
import 'package:PiliPlus/pages/common/publish/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics_mention/view.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/feed_back.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_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:easy_debounce/easy_throttle.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.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:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
@@ -31,19 +39,21 @@ abstract class CommonRichTextPubPage
const CommonRichTextPubPage({
super.key,
this.items,
this.pics,
super.onSave,
super.autofocus,
super.imageLengthLimit,
});
final List<RichTextItem>? items;
final List<PicModel>? pics;
}
abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
extends CommonPublishPageState<T> {
final key = GlobalKey<RichTextFieldState>();
late final imagePicker = ImagePicker();
late final RxList<String> pathList = <String>[].obs;
late final RxList<PicModel> imageList;
int get limit => widget.imageLengthLimit ?? 9;
@override
@@ -58,13 +68,16 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
if (editController.rawText.trim().isNotEmpty) {
enablePublish.value = true;
}
imageList = RxList<PicModel>(widget.pics ?? <PicModel>[]);
}
@override
void dispose() {
if (PlatformUtils.isMobile) {
for (final i in pathList) {
File(i).tryDel();
for (final img in imageList) {
if (img is FilePicModel) {
File(img.path).tryDel();
}
}
}
super.dispose();
@@ -82,15 +95,18 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
).colorScheme.secondaryContainer.withValues(alpha: 0.5);
void onClear() {
final path = pathList.removeAt(index);
final image = imageList.removeAt(index);
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;
}
}
final image = imageList[index];
return Stack(
clipBehavior: Clip.none,
children: [
@@ -98,12 +114,18 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
onTap: () async {
controller.keepChatPanel();
await PageUtils.imageView(
imgList: pathList
imgList: imageList
.map(
(path) => SourceModel(
url: path,
sourceType: SourceType.fileImage,
),
(img) => switch (img) {
FilePicModel e => SourceModel(
url: e.path,
sourceType: .fileImage,
),
OpusPicModel e => SourceModel(
url: e.url!,
sourceType: .networkImage,
),
},
)
.toList(),
initialPage: index,
@@ -117,21 +139,35 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
onSecondaryTap: PlatformUtils.isMobile ? null : onClear,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Image(
height: height,
fit: BoxFit.fitHeight,
filterQuality: FilterQuality.low,
image: FileImage(File(pathList[index])),
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 42),
child: switch (image) {
FilePicModel e => Image.file(
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(
top: 34,
right: 5,
child: iconButton(
icon: const Icon(Icons.edit),
onPressed: () => onCropImage(index),
onPressed: () => onCropImage(index, image),
size: 24,
iconSize: 14,
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);
final sourcePath = pathList[index];
final croppedFile = await ImageCropper.platform.cropImage(
sourcePath: sourcePath,
sourcePath: path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: '裁剪',
@@ -168,8 +216,10 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
],
);
if (croppedFile != null) {
File(sourcePath).tryDel();
pathList[index] = croppedFile.path;
if (image is FilePicModel) {
File(image.path).tryDel();
}
imageList[index] = FilePicModel(path: croppedFile.path);
}
}
@@ -185,11 +235,11 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
);
if (pickedFiles.isNotEmpty) {
for (int i = 0; i < pickedFiles.length; i++) {
if (pathList.length == limit) {
if (imageList.length == limit) {
SmartDialog.showToast('最多选择$limit张图片');
break;
} else {
pathList.add(pickedFiles[i].path);
imageList.add(FilePicModel(path: pickedFiles[i].path));
}
}
callback?.call();
@@ -465,25 +515,30 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
Future<void> onPublish() async {
feedBack();
List<Map<String, dynamic>>? pictures;
if (pathList.isNotEmpty) {
if (imageList.isNotEmpty) {
SmartDialog.showLoading(msg: '正在上传图片...');
final cancelToken = CancelToken();
try {
pictures = await Future.wait<Map<String, dynamic>>(
pathList.map((path) async {
final result = await MsgHttp.uploadBfs(
path: path,
category: 'daily',
biz: 'new_dyn',
cancelToken: cancelToken,
);
final data = result.data;
return {
'img_width': data.imageWidth,
'img_height': data.imageHeight,
'img_size': data.imgSize,
'img_src': data.imageUrl,
};
imageList.map((img) async {
switch (img) {
case FilePicModel e:
final result = await MsgHttp.uploadBfs(
path: e.path,
category: 'daily',
biz: 'new_dyn',
cancelToken: cancelToken,
);
final data = result.data;
return {
'img_width': data.imageWidth,
'img_height': data.imageHeight,
'img_size': data.imgSize,
'img_src': data.imageUrl,
};
case OpusPicModel e:
return e.toJson();
}
}),
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/pendant_avatar.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/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
@@ -28,22 +30,27 @@ import 'package:get/get.dart';
class AuthorPanel extends StatelessWidget {
final DynamicItemModel item;
final Function? addBannedList;
final bool isSave;
final bool isDetail;
final ValueChanged? onRemove;
final ValueChanged<Object>? onRemove;
final void Function(bool isTop, Object dynId)? onSetTop;
final VoidCallback? onBlock;
final Future<LoadingState> Function(bool isPrivate, Object dynId)?
onSetPubSetting;
final VoidCallback? onEdit;
final ValueChanged<int>? onSetReplySubject;
const AuthorPanel({
super.key,
required this.item,
this.addBannedList,
this.isDetail = false,
this.onRemove,
this.isSave = false,
this.onSetTop,
this.onBlock,
this.onSetPubSetting,
this.onEdit,
this.onSetReplySubject,
});
Widget _buildAvatar(ModuleAuthorModel moduleAuthor) {
@@ -73,6 +80,33 @@ class AuthorPanel extends StatelessWidget {
)
: DateFormatUtils.dateFormat(moduleAuthor.pubTs)
: 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(
clipBehavior: Clip.none,
alignment: Alignment.center,
@@ -88,10 +122,10 @@ class AuthorPanel extends StatelessWidget {
}
: null,
child: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [
_buildAvatar(moduleAuthor),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -107,14 +141,7 @@ class AuthorPanel extends StatelessWidget {
fontSize: theme.textTheme.titleSmall!.fontSize,
),
),
if (pubTime != null)
Text(
'$pubTime${moduleAuthor.pubAction != null ? ' ${moduleAuthor.pubAction}' : ''}',
style: TextStyle(
color: theme.colorScheme.outline,
fontSize: theme.textTheme.labelSmall!.fontSize,
),
),
?pubTs,
],
),
],
@@ -123,7 +150,7 @@ class AuthorPanel extends StatelessWidget {
),
Align(
alignment: Alignment.centerRight,
child: !isDetail && item.modules.moduleTag?.text != null
child: moduleTagText != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -142,7 +169,7 @@ class AuthorPanel extends StatelessWidget {
),
),
child: Text(
item.modules.moduleTag!.text!,
moduleTagText,
style: TextStyle(
height: 1,
fontSize: 12,
@@ -399,18 +426,150 @@ class AuthorPanel extends StatelessWidget {
ListTile(
onTap: () {
Get.back();
onSetTop!(
item.modules.moduleTag?.text != null,
item.idStr,
);
onSetTop!(moduleAuthor.isTop ?? false, item.idStr);
},
minLeadingWidth: 0,
leading: const Icon(Icons.vertical_align_top, size: 19),
title: Text(
'${item.modules.moduleTag?.text != null ? '取消' : ''}置顶',
'${moduleAuthor.isTop == true ? '取消' : ''}置顶',
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)
ListTile(
onTap: () {

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class DynamicDetailController extends CommonDynController {
@@ -45,4 +47,33 @@ class DynamicDetailController extends CommonDynController {
replyType = commentType;
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/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/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/pages/common/dyn/common_dyn_page.dart';
import 'package:PiliPlus/pages/dynamics/widgets/author_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_repost/view.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/request_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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(
title: Padding(
padding: const EdgeInsets.only(right: 12),
@@ -80,6 +223,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
child: AuthorPanel(
item: controller.dynItem,
isDetail: true,
onSetPubSetting: controller.onSetPubSetting,
onEdit: _onEdit,
onSetReplySubject: controller.onSetReplySubject,
),
),
);
@@ -88,10 +234,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
),
actions: isPortrait
? null
: [
ratioWidget(maxWidth),
const SizedBox(width: 16),
],
: [ratioWidget(maxWidth), const SizedBox(width: 16)],
);
Widget _buildBody(ThemeData theme) {
@@ -110,6 +253,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
isDetail: true,
maxWidth: maxWidth - this.padding.horizontal - 2 * padding,
isDetailPortraitW: isPortrait,
onSetPubSetting: controller.onSetPubSetting,
onEdit: _onEdit,
onSetReplySubject: controller.onSetReplySubject,
),
),
buildReplyHeader(theme),
@@ -143,6 +289,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
(flex / (flex + flex1)) -
padding,
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/image/network_img_layer.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/dynamics/result.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) {
richContent.addAll(repostContent);
}
final result = await DynamicsHttp.createDynamic(
final res = await DynamicsHttp.createDynamic(
mid: Accounts.main.mid,
dynIdStr: widget.item?.idStr ?? widget.dynIdStr,
rid: widget.rid,
@@ -428,19 +429,19 @@ class _RepostPanelState extends CommonRichTextPubPageState<RepostPanel> {
extraContent: richContent ?? repostContent,
);
SmartDialog.dismiss();
if (result['status']) {
if (res case Success(:final response)) {
hasPub = true;
Get.back();
SmartDialog.showToast('转发成功');
widget.onSuccess?.call();
final id = result['data']?['dyn_id'];
final id = response?['dyn_id'];
RequestUtils.insertCreatedDyn(id);
RequestUtils.checkCreatedDyn(
id: id,
dynText: editController.rawText,
);
} 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/main.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/dynamics_mention/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart';
@@ -110,7 +111,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
Widget buildImagePreview() {
return Obx(
() {
if (pathList.isNotEmpty) {
if (imageList.isNotEmpty) {
return SizedBox(
height: 85,
child: SingleChildScrollView(
@@ -120,7 +121,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
spacing: 10,
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
pathList.length,
imageList.length,
(index) => buildImage(index, 75),
),
),
@@ -368,7 +369,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
if (isRoot && widget.canUploadPic)
item(
onTap: () async {
if (pathList.length >= limit) {
if (imageList.length >= limit) {
SmartDialog.showToast('最多选择$limit张图片');
return;
}
@@ -385,7 +386,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
'$tmpDirPath/${Utils.generateRandomString(8)}.png',
);
await file.writeAsBytes(res);
pathList.add(file.path);
imageList.add(FilePicModel(path: file.path));
} else {
debugPrint('null screenshot');
}