mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
mod: refine reply/publish page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -45,8 +45,11 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
this.onDismissed,
|
||||
this.setStatusBar,
|
||||
this.onClose,
|
||||
this.isFile,
|
||||
});
|
||||
|
||||
final bool? isFile;
|
||||
|
||||
final VoidCallback? onClose;
|
||||
|
||||
final bool? setStatusBar;
|
||||
@@ -137,8 +140,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||
if (widget.isFile != true) {
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@@ -267,7 +272,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
onLongPress: widget.isFile == true ? null : onLongPress,
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
@@ -303,59 +308,69 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: onClose,
|
||||
),
|
||||
widget.sources.length > 1
|
||||
? Text(
|
||||
"${currentIndex! + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)
|
||||
: const SizedBox(),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
onTap: () => onShareImg(widget.sources[currentIndex!]),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
onTap: () {
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources as List<String>,
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"${currentIndex! + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
if (widget.isFile != true)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
onTap: () =>
|
||||
onShareImg(widget.sources[currentIndex!]),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
onTap: () {
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources,
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -382,27 +397,33 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
return Center(
|
||||
child: Hero(
|
||||
tag: widget.sources[index],
|
||||
child: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: _getActualUrl(index),
|
||||
// fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(value: progress.progress ?? 0),
|
||||
child: widget.isFile == true
|
||||
? Image(
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(widget.sources[index])),
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: _getActualUrl(index),
|
||||
// fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.progress ?? 0),
|
||||
),
|
||||
);
|
||||
},
|
||||
// errorListener: (value) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// setState(() {
|
||||
// _thumbList[index] = false;
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
),
|
||||
);
|
||||
},
|
||||
// errorListener: (value) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// setState(() {
|
||||
// _thumbList[index] = false;
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
373
lib/pages/common/common_publish_page.dart
Normal file
373
lib/pages/common/common_publish_page.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPlus/models/video/reply/emote.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
enum PanelType { none, keyboard, emoji }
|
||||
|
||||
abstract class CommonPublishPage extends StatefulWidget {
|
||||
const CommonPublishPage({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
this.imageLengthLimit,
|
||||
this.onSave,
|
||||
this.autofocus = true,
|
||||
});
|
||||
|
||||
final String? initialValue;
|
||||
final int? imageLengthLimit;
|
||||
final ValueChanged<String>? onSave;
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
State<CommonPublishPage> createState();
|
||||
}
|
||||
|
||||
abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
extends State<T> with WidgetsBindingObserver {
|
||||
late final focusNode = FocusNode();
|
||||
late final controller = ChatBottomPanelContainerController<PanelType>();
|
||||
late final editController = TextEditingController(text: widget.initialValue);
|
||||
|
||||
PanelType currentPanelType = PanelType.none;
|
||||
late final RxBool readOnly = false.obs;
|
||||
late final RxBool enablePublish = false.obs;
|
||||
late final RxBool selectKeyboard = true.obs;
|
||||
|
||||
late final imagePicker = ImagePicker();
|
||||
late final RxList<String> pathList = <String>[].obs;
|
||||
int get limit => widget.imageLengthLimit ?? 9;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
if (widget.initialValue.isNullOrEmpty.not) {
|
||||
enablePublish.value = true;
|
||||
}
|
||||
|
||||
if (widget.autofocus) {
|
||||
Future.delayed(const Duration(milliseconds: 300)).then((_) {
|
||||
if (mounted) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
focusNode.dispose();
|
||||
editController.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestFocus() async {
|
||||
await Future.delayed(const Duration(microseconds: 200));
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted && widget.autofocus && selectKeyboard.value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
_requestFocus();
|
||||
} else {
|
||||
_requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
controller.keepChatPanel();
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePanelType(PanelType type) async {
|
||||
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||
bool isUpdated = false;
|
||||
switch (type) {
|
||||
case PanelType.keyboard:
|
||||
updateInputView(isReadOnly: false);
|
||||
break;
|
||||
case PanelType.emoji:
|
||||
isUpdated = updateInputView(isReadOnly: true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
updatePanelTypeFunc() {
|
||||
controller.updatePanelType(
|
||||
isSwitchToKeyboard
|
||||
? ChatBottomPanelType.keyboard
|
||||
: ChatBottomPanelType.other,
|
||||
data: type,
|
||||
forceHandleFocus: isSwitchToEmojiPanel
|
||||
? ChatBottomHandleFocus.requestFocus
|
||||
: ChatBottomHandleFocus.none,
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
// Waiting for the input view to update.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
updatePanelTypeFunc();
|
||||
});
|
||||
} else {
|
||||
updatePanelTypeFunc();
|
||||
}
|
||||
}
|
||||
|
||||
hidePanel() async {
|
||||
if (focusNode.hasFocus) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
focusNode.unfocus();
|
||||
}
|
||||
updateInputView(isReadOnly: false);
|
||||
if (ChatBottomPanelType.none == controller.currentPanelType) return;
|
||||
controller.updatePanelType(ChatBottomPanelType.none);
|
||||
}
|
||||
|
||||
bool updateInputView({
|
||||
required bool isReadOnly,
|
||||
}) {
|
||||
if (readOnly.value != isReadOnly) {
|
||||
readOnly.value = isReadOnly;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future onPublish() async {
|
||||
feedBack();
|
||||
List? pictures;
|
||||
if (pathList.isNotEmpty) {
|
||||
pictures = [];
|
||||
for (int i = 0; i < pathList.length; i++) {
|
||||
SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${pathList.length}');
|
||||
dynamic result = await MsgHttp.uploadBfs(
|
||||
path: pathList[i],
|
||||
category: 'daily',
|
||||
biz: 'new_dyn',
|
||||
);
|
||||
if (result['status']) {
|
||||
int imageSize = await File(pathList[i]).length();
|
||||
pictures.add({
|
||||
'img_width': result['data']['image_width'],
|
||||
'img_height': result['data']['image_height'],
|
||||
'img_size': imageSize / 1024,
|
||||
'img_src': result['data']['image_url'],
|
||||
});
|
||||
} else {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(result['msg']);
|
||||
return;
|
||||
}
|
||||
if (i == pathList.length - 1) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
onCustomPublish(message: editController.text, pictures: pictures);
|
||||
}
|
||||
|
||||
Future onCustomPublish({required String message, List? pictures});
|
||||
|
||||
void onChooseEmote(Packages package, Emote emote) {
|
||||
enablePublish.value = true;
|
||||
final int cursorPosition = editController.selection.baseOffset;
|
||||
final String currentText = editController.text;
|
||||
final String newText = currentText.substring(0, cursorPosition) +
|
||||
emote.text! +
|
||||
currentText.substring(cursorPosition);
|
||||
editController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection:
|
||||
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
|
||||
);
|
||||
widget.onSave?.call(editController.text);
|
||||
}
|
||||
|
||||
Widget? customPanel(double height) => null;
|
||||
|
||||
Widget buildEmojiPickerPanel() {
|
||||
double height = 170;
|
||||
final keyboardHeight = controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(height, keyboardHeight);
|
||||
}
|
||||
return customPanel(height) ?? SizedBox(height: height);
|
||||
}
|
||||
|
||||
Widget buildPanelContainer([Color? panelBgColor]) {
|
||||
return ChatBottomPanelContainer<PanelType>(
|
||||
controller: controller,
|
||||
inputFocusNode: focusNode,
|
||||
otherPanelWidget: (type) {
|
||||
if (type == null) return const SizedBox.shrink();
|
||||
switch (type) {
|
||||
case PanelType.emoji:
|
||||
return buildEmojiPickerPanel();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
onPanelTypeChange: (panelType, data) {
|
||||
debugPrint('panelType: $panelType');
|
||||
switch (panelType) {
|
||||
case ChatBottomPanelType.none:
|
||||
currentPanelType = PanelType.none;
|
||||
break;
|
||||
case ChatBottomPanelType.keyboard:
|
||||
currentPanelType = PanelType.keyboard;
|
||||
break;
|
||||
case ChatBottomPanelType.other:
|
||||
if (data == null) return;
|
||||
switch (data) {
|
||||
case PanelType.emoji:
|
||||
currentPanelType = PanelType.emoji;
|
||||
break;
|
||||
default:
|
||||
currentPanelType = PanelType.none;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
panelBgColor: panelBgColor ?? Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildImage(int index, double height) {
|
||||
void onClear() {
|
||||
pathList.removeAt(index);
|
||||
if (pathList.isEmpty && editController.text.trim().isEmpty) {
|
||||
enablePublish.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.keepChatPanel();
|
||||
context.imageView(
|
||||
isFile: true,
|
||||
imgList: pathList,
|
||||
initialPage: index,
|
||||
);
|
||||
},
|
||||
onLongPress: onClear,
|
||||
child: ClipRRect(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Image(
|
||||
height: height,
|
||||
fit: BoxFit.fitHeight,
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(pathList[index])),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 34,
|
||||
right: 5,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
onCropImage(index);
|
||||
},
|
||||
size: 24,
|
||||
iconSize: 14,
|
||||
bgColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 5,
|
||||
right: 5,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: onClear,
|
||||
size: 24,
|
||||
iconSize: 14,
|
||||
bgColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onCropImage(int index) async {
|
||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: pathList[index],
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: '裁剪',
|
||||
toolbarColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
toolbarWidgetColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: '裁剪',
|
||||
),
|
||||
],
|
||||
);
|
||||
if (croppedFile != null) {
|
||||
pathList[index] = croppedFile.path;
|
||||
}
|
||||
}
|
||||
|
||||
void onPickImage([VoidCallback? callback]) {
|
||||
EasyThrottle.throttle('imagePicker', const Duration(milliseconds: 500),
|
||||
() async {
|
||||
try {
|
||||
List<XFile> pickedFiles = await imagePicker.pickMultiImage(
|
||||
limit: limit,
|
||||
imageQuality: 100,
|
||||
);
|
||||
if (pickedFiles.isNotEmpty) {
|
||||
for (int i = 0; i < pickedFiles.length; i++) {
|
||||
if (pathList.length == limit) {
|
||||
SmartDialog.showToast('最多选择$limit张图片');
|
||||
break;
|
||||
} else {
|
||||
pathList.add(pickedFiles[i].path);
|
||||
}
|
||||
}
|
||||
callback?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -153,8 +153,8 @@ abstract class ReplyController extends CommonController {
|
||||
? ReplyType.values[replyItem.type.toInt()]
|
||||
: replyType,
|
||||
replyItem: replyItem,
|
||||
savedReply: savedReplies[key],
|
||||
onSaveReply: (reply) {
|
||||
initialValue: savedReplies[key],
|
||||
onSave: (reply) {
|
||||
savedReplies[key] = reply;
|
||||
},
|
||||
)
|
||||
@@ -166,8 +166,8 @@ abstract class ReplyController extends CommonController {
|
||||
? ReplyType.values[replyItem.type.toInt()]
|
||||
: replyType,
|
||||
replyItem: replyItem,
|
||||
savedReply: savedReplies[key],
|
||||
onSaveReply: (reply) {
|
||||
initialValue: savedReplies[key],
|
||||
onSave: (reply) {
|
||||
savedReplies[key] = reply;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,115 +1,142 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/pages/common/common_publish_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/view.dart';
|
||||
import 'package:PiliPlus/pages/emote/controller.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CreateDynPanel extends StatefulWidget {
|
||||
const CreateDynPanel({super.key});
|
||||
class CreateDynPanel extends CommonPublishPage {
|
||||
const CreateDynPanel({
|
||||
super.key,
|
||||
super.imageLengthLimit = 18,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateDynPanel> createState() => _CreateDynPanelState();
|
||||
}
|
||||
|
||||
class _CreateDynPanelState extends State<CreateDynPanel> {
|
||||
final _ctr = TextEditingController();
|
||||
late final _imagePicker = ImagePicker();
|
||||
late final int _limit = 18;
|
||||
|
||||
final RxBool _isEnablePub = false.obs;
|
||||
late final RxList<String> _pathList = <String>[].obs;
|
||||
|
||||
class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
|
||||
bool _isPrivate = false;
|
||||
DateTime? _publishTime;
|
||||
ReplyOption _replyOption = ReplyOption.allow;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctr.dispose();
|
||||
try {
|
||||
Get.delete<EmotePanelController>();
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future _onCreate() async {
|
||||
// if (_pathList.isEmpty) {
|
||||
// dynamic result = await MsgHttp.createTextDynamic(_ctr.text);
|
||||
// if (result['status']) {
|
||||
// Get.back();
|
||||
// SmartDialog.showToast('发布成功');
|
||||
// } else {
|
||||
// SmartDialog.showToast(result['msg']);
|
||||
// }
|
||||
// } else {
|
||||
List? pics;
|
||||
if (_pathList.isNotEmpty) {
|
||||
pics = [];
|
||||
for (int i = 0; i < _pathList.length; i++) {
|
||||
SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}');
|
||||
dynamic result = await MsgHttp.uploadBfs(
|
||||
path: _pathList[i],
|
||||
category: 'daily',
|
||||
biz: 'new_dyn',
|
||||
);
|
||||
if (result['status']) {
|
||||
int imageSize = await File(_pathList[i]).length();
|
||||
pics.add({
|
||||
'img_width': result['data']['image_width'],
|
||||
'img_height': result['data']['image_height'],
|
||||
'img_size': imageSize / 1024,
|
||||
'img_src': result['data']['image_url'],
|
||||
});
|
||||
} else {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(result['msg']);
|
||||
return;
|
||||
}
|
||||
if (i == _pathList.length - 1) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
SmartDialog.showLoading(msg: '正在发布');
|
||||
dynamic result = await MsgHttp.createDynamic(
|
||||
mid: GStorage.userInfo.get('userInfoCache')?.mid,
|
||||
rawText: _ctr.text,
|
||||
pics: pics,
|
||||
publishTime: _publishTime != null
|
||||
? _publishTime!.millisecondsSinceEpoch ~/ 1000
|
||||
: null,
|
||||
replyOption: _replyOption,
|
||||
privatePub: _isPrivate ? 1 : null,
|
||||
);
|
||||
if (result['status']) {
|
||||
Get.back();
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('发布成功');
|
||||
} else {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: PreferredSize(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: _buildAppBar,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildEditWidget,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildPubtimeWidget,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyOptionWidget,
|
||||
const SizedBox(height: 5),
|
||||
_buildPrivateWidget,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildImageList,
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildToolbar,
|
||||
buildPanelContainer(Colors.transparent),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildImageList => Obx(
|
||||
() => SizedBox(
|
||||
height: 100,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: pathList.length == limit ? limit : pathList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (pathList.length != limit && index == pathList.length) {
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
onPickImage(() {
|
||||
if (pathList.isNotEmpty && !enablePublish.value) {
|
||||
enablePublish.value = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Ink(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: Center(child: Icon(Icons.add, size: 35)),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return buildImage(index, 100);
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
PreferredSizeWidget get _buildAppBar => PreferredSize(
|
||||
preferredSize: Size.fromHeight(66),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
@@ -140,12 +167,11 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Obx(
|
||||
() => FilledButton.tonal(
|
||||
onPressed: _isEnablePub.value ? _onCreate : null,
|
||||
onPressed: enablePublish.value ? onPublish : null,
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -164,364 +190,307 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
controller: _ctr,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
autofocus: true,
|
||||
onChanged: (value) {
|
||||
bool isEmpty = value.trim().isEmpty && _pathList.isEmpty;
|
||||
if (!isEmpty && !_isEnablePub.value) {
|
||||
_isEnablePub.value = true;
|
||||
} else if (isEmpty && _isEnablePub.value) {
|
||||
_isEnablePub.value = false;
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: '说点什么吧',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
gapPadding: 0,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
|
||||
Widget get _buildPrivateWidget => PopupMenuButton(
|
||||
initialValue: _isPrivate,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_isPrivate = value;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => List.generate(
|
||||
2,
|
||||
(index) => PopupMenuItem<bool>(
|
||||
enabled: _publishTime != null && index == 1 ? false : true,
|
||||
value: index == 0 ? false : true,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
index == 0 ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(index == 0 ? '所有人可见' : '仅自己可见'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
_isPrivate ? Icons.visibility_off : Icons.visibility,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_isPrivate ? '仅自己可见' : '所有人可见',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildReplyOptionWidget => PopupMenuButton(
|
||||
initialValue: _replyOption,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (item) {
|
||||
setState(() {
|
||||
_replyOption = item;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => ReplyOption.values
|
||||
.map(
|
||||
(item) => PopupMenuItem<ReplyOption>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
item.iconData,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(item.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_publishTime == null
|
||||
? FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: _isPrivate
|
||||
? null
|
||||
: () {
|
||||
DateTime nowDate = DateTime.now();
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: nowDate,
|
||||
firstDate: nowDate,
|
||||
lastDate: DateTime(
|
||||
nowDate.year,
|
||||
nowDate.month,
|
||||
nowDate.day + 7,
|
||||
),
|
||||
).then(
|
||||
(selectedDate) {
|
||||
if (selectedDate != null &&
|
||||
context.mounted) {
|
||||
TimeOfDay nowTime = TimeOfDay.now();
|
||||
showTimePicker(
|
||||
context: context,
|
||||
initialTime: nowTime.replacing(
|
||||
hour: nowTime.minute + 6 >= 60
|
||||
? (nowTime.hour + 1) % 24
|
||||
: nowTime.hour,
|
||||
minute: (nowTime.minute + 6) % 60,
|
||||
),
|
||||
).then((selectedTime) {
|
||||
if (selectedTime != null) {
|
||||
if (selectedDate.day ==
|
||||
nowDate.day) {
|
||||
if (selectedTime.hour <
|
||||
nowTime.hour) {
|
||||
SmartDialog.showToast(
|
||||
'时间设置错误,至少选择6分钟之后');
|
||||
return;
|
||||
} else if (selectedTime.hour ==
|
||||
nowTime.hour) {
|
||||
if (selectedTime.minute <
|
||||
nowTime.minute + 6) {
|
||||
if (selectedDate.day ==
|
||||
nowDate.day) {
|
||||
SmartDialog.showToast(
|
||||
'时间设置错误,至少选择6分钟之后');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_publishTime = DateTime(
|
||||
selectedDate.year,
|
||||
selectedDate.month,
|
||||
selectedDate.day,
|
||||
selectedTime.hour,
|
||||
selectedTime.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('定时发布'),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_publishTime = null;
|
||||
});
|
||||
},
|
||||
label: Text(DateFormat('yyyy-MM-dd HH:mm')
|
||||
.format(_publishTime!)),
|
||||
icon: Icon(Icons.clear, size: 20),
|
||||
iconAlignment: IconAlignment.end,
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
initialValue: _replyOption,
|
||||
onSelected: (item) {
|
||||
setState(() {
|
||||
_replyOption = item;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => ReplyOption.values
|
||||
.map(
|
||||
(item) => PopupMenuItem<ReplyOption>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
item.iconData,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(item.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
_replyOption.iconData,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_replyOption.title,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
PopupMenuButton(
|
||||
initialValue: _isPrivate,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_isPrivate = value;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => List.generate(
|
||||
2,
|
||||
(index) => PopupMenuItem<bool>(
|
||||
enabled: _publishTime != null && index == 1
|
||||
? false
|
||||
: true,
|
||||
value: index == 0 ? false : true,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
index == 0
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(index == 0 ? '所有人可见' : '仅自己可见'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
_isPrivate
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_isPrivate ? '仅自己可见' : '所有人可见',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: _isPrivate
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 19,
|
||||
_replyOption.iconData,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_replyOption.title,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: _replyOption == ReplyOption.close
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildPubtimeWidget => _publishTime == null
|
||||
? FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
height: 100,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _pathList.length == _limit
|
||||
? _limit
|
||||
: _pathList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (_pathList.length != _limit &&
|
||||
index == _pathList.length) {
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
EasyThrottle.throttle('imagePicker',
|
||||
const Duration(milliseconds: 500), () async {
|
||||
try {
|
||||
List<XFile> pickedFiles =
|
||||
await _imagePicker.pickMultiImage(
|
||||
limit: _limit,
|
||||
imageQuality: 100,
|
||||
);
|
||||
if (pickedFiles.isNotEmpty) {
|
||||
for (int i = 0; i < pickedFiles.length; i++) {
|
||||
if (_pathList.length == _limit) {
|
||||
SmartDialog.showToast('最多选择$_limit张图片');
|
||||
break;
|
||||
} else {
|
||||
_pathList.add(pickedFiles[i].path);
|
||||
}
|
||||
}
|
||||
if (_pathList.isNotEmpty &&
|
||||
!_isEnablePub.value) {
|
||||
_isEnablePub.value = true;
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: _isPrivate
|
||||
? null
|
||||
: () {
|
||||
DateTime nowDate = DateTime.now();
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: nowDate,
|
||||
firstDate: nowDate,
|
||||
lastDate: DateTime(
|
||||
nowDate.year,
|
||||
nowDate.month,
|
||||
nowDate.day + 7,
|
||||
),
|
||||
).then(
|
||||
(selectedDate) {
|
||||
if (selectedDate != null && mounted) {
|
||||
TimeOfDay nowTime = TimeOfDay.now();
|
||||
showTimePicker(
|
||||
context: context,
|
||||
initialTime: nowTime.replacing(
|
||||
hour: nowTime.minute + 6 >= 60
|
||||
? (nowTime.hour + 1) % 24
|
||||
: nowTime.hour,
|
||||
minute: (nowTime.minute + 6) % 60,
|
||||
),
|
||||
).then((selectedTime) {
|
||||
if (selectedTime != null) {
|
||||
if (selectedDate.day == nowDate.day) {
|
||||
if (selectedTime.hour < nowTime.hour) {
|
||||
SmartDialog.showToast('时间设置错误,至少选择6分钟之后');
|
||||
return;
|
||||
} else if (selectedTime.hour == nowTime.hour) {
|
||||
if (selectedTime.minute < nowTime.minute + 6) {
|
||||
if (selectedDate.day == nowDate.day) {
|
||||
SmartDialog.showToast('时间设置错误,至少选择6分钟之后');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_publishTime = DateTime(
|
||||
selectedDate.year,
|
||||
selectedDate.month,
|
||||
selectedDate.day,
|
||||
selectedTime.hour,
|
||||
selectedTime.minute,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Ink(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
),
|
||||
child: Center(child: Icon(Icons.add, size: 35)),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_pathList.removeAt(index);
|
||||
if (_pathList.isEmpty && _ctr.text.trim().isEmpty) {
|
||||
_isEnablePub.value = false;
|
||||
}
|
||||
},
|
||||
child: Image(
|
||||
height: 100,
|
||||
fit: BoxFit.fitHeight,
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(_pathList[index])),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 10),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('定时发布'),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.paddingOf(context).bottom + 25,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_publishTime = null;
|
||||
});
|
||||
},
|
||||
label: Text(DateFormat('yyyy-MM-dd HH:mm').format(_publishTime!)),
|
||||
icon: Icon(Icons.clear, size: 20),
|
||||
iconAlignment: IconAlignment.end,
|
||||
);
|
||||
|
||||
Widget get _buildToolbar => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
onPressed: () {
|
||||
selectKeyboard.value = PanelType.emoji == currentPanelType;
|
||||
updatePanelType(
|
||||
PanelType.emoji == currentPanelType
|
||||
? PanelType.keyboard
|
||||
: PanelType.emoji,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
tooltip: '表情',
|
||||
selected: !selectKeyboard.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildEditWidget => Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Listener(
|
||||
onPointerUp: (event) {
|
||||
if (readOnly.value) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
selectKeyboard.value = true;
|
||||
}
|
||||
},
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
controller: editController,
|
||||
minLines: 4,
|
||||
maxLines: null,
|
||||
focusNode: focusNode,
|
||||
readOnly: readOnly.value,
|
||||
onChanged: (value) {
|
||||
bool isEmpty = value.trim().isEmpty && pathList.isEmpty;
|
||||
if (!isEmpty && !enablePublish.value) {
|
||||
enablePublish.value = true;
|
||||
} else if (isEmpty && enablePublish.value) {
|
||||
enablePublish.value = false;
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: '说点什么吧',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
gapPadding: 0,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget? customPanel(double height) => SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(onChoose: onChooseEmote),
|
||||
);
|
||||
|
||||
@override
|
||||
Future onCustomPublish({required String message, List? pictures}) async {
|
||||
SmartDialog.showLoading(msg: '正在发布');
|
||||
dynamic result = await MsgHttp.createDynamic(
|
||||
mid: GStorage.userInfo.get('userInfoCache')?.mid,
|
||||
rawText: editController.text,
|
||||
pics: pictures,
|
||||
publishTime: _publishTime != null
|
||||
? _publishTime!.millisecondsSinceEpoch ~/ 1000
|
||||
: null,
|
||||
replyOption: _replyOption,
|
||||
privatePub: _isPrivate ? 1 : null,
|
||||
);
|
||||
if (result['status']) {
|
||||
Get.back();
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('发布成功');
|
||||
} else {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(result['msg']);
|
||||
debugPrint('failed to publish: ${result['msg']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/common/common_publish_page.dart';
|
||||
import 'package:PiliPlus/pages/emote/controller.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RepostPanel extends StatefulWidget {
|
||||
class RepostPanel extends CommonPublishPage {
|
||||
const RepostPanel({
|
||||
super.key,
|
||||
required this.item,
|
||||
@@ -20,56 +25,60 @@ class RepostPanel extends StatefulWidget {
|
||||
State<RepostPanel> createState() => _RepostPanelState();
|
||||
}
|
||||
|
||||
class _RepostPanelState extends State<RepostPanel> {
|
||||
class _RepostPanelState extends CommonPublishPageState<RepostPanel> {
|
||||
bool _isMax = false;
|
||||
|
||||
final _ctr = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
Future _onRepost() async {
|
||||
dynamic result = await MsgHttp.createDynamic(
|
||||
mid: GStorage.userInfo.get('userInfoCache')?.mid,
|
||||
dynIdStr: widget.item.idStr,
|
||||
rawText: _ctr.text,
|
||||
);
|
||||
if (result['status']) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('转发成功');
|
||||
widget.callback();
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
late final dynamic _pic = (widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.archive
|
||||
?.cover ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.pgc
|
||||
?.cover ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.opus
|
||||
?.pics
|
||||
?.firstOrNull
|
||||
?.url;
|
||||
late final _text = (widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.opus
|
||||
?.summary
|
||||
?.text ??
|
||||
(widget.item as DynamicItemModel?)?.modules?.moduleDynamic?.desc?.text ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.archive
|
||||
?.title ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.pgc
|
||||
?.title ??
|
||||
'';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctr.dispose();
|
||||
_focusNode.dispose();
|
||||
try {
|
||||
Get.delete<EmotePanelController>();
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
dynamic pic = (widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.archive
|
||||
?.cover ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.pgc
|
||||
?.cover ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.opus
|
||||
?.pics
|
||||
?.firstOrNull
|
||||
?.url;
|
||||
return AnimatedSize(
|
||||
alignment: Alignment.topCenter,
|
||||
curve: Curves.ease,
|
||||
@@ -79,225 +88,280 @@ class _RepostPanelState extends State<RepostPanel> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: _isMax ? 16 : 10),
|
||||
if (!_isMax)
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
'转发动态',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
_buildAppBar,
|
||||
if (_isMax) Expanded(child: _buildEditPanel) else _buildEditPanel,
|
||||
if (_isMax.not)
|
||||
..._biuldDismiss
|
||||
else ...[
|
||||
_buildToolbar,
|
||||
buildPanelContainer(Colors.transparent),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildEditPanel => Column(
|
||||
mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: _isMax.not
|
||||
? BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: _isMax.not ? _buildEditPlaceHolder : _buildEditWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildRefWidget,
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _buildRefWidget => Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh ==
|
||||
Theme.of(context).colorScheme.surface
|
||||
? Theme.of(context).colorScheme.onInverseSurface
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_pic != null) ...[
|
||||
NetworkImgLayer(
|
||||
radius: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
src: _pic,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'@${(widget.item as DynamicItemModel?)?.modules?.moduleAuthor?.name}',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildEditPlaceHolder => GestureDetector(
|
||||
onTap: () async {
|
||||
setState(() => _isMax = true);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'说点什么吧',
|
||||
style: TextStyle(
|
||||
height: 1.75,
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildEditWidget => Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Listener(
|
||||
onPointerUp: (event) {
|
||||
if (readOnly.value) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
selectKeyboard.value = true;
|
||||
}
|
||||
},
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
controller: editController,
|
||||
minLines: 4,
|
||||
maxLines: null,
|
||||
focusNode: focusNode,
|
||||
readOnly: readOnly.value,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '说点什么吧',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
gapPadding: 0,
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: _onRepost,
|
||||
style: TextButton.styleFrom(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildAppBar => _isMax.not
|
||||
? Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
'转发动态',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: onPublish,
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text('立即转发'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: '返回',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context).colorScheme.secondaryContainer;
|
||||
}),
|
||||
),
|
||||
onPressed: Get.back,
|
||||
icon: Icon(
|
||||
Icons.arrow_back_outlined,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: const Text(
|
||||
'转发动态',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.tonal(
|
||||
onPressed: onPublish,
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text('立即转发'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
if (_isMax)
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 16,
|
||||
top: 0,
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: '返回',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer;
|
||||
}),
|
||||
),
|
||||
onPressed: Get.back,
|
||||
icon: Icon(
|
||||
Icons.arrow_back_outlined,
|
||||
size: 18,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: const Text(
|
||||
'转发动态',
|
||||
style:
|
||||
TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
top: 0,
|
||||
child: FilledButton.tonal(
|
||||
onPressed: _onRepost,
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text('转发'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: !_isMax
|
||||
? BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: !_isMax
|
||||
? GestureDetector(
|
||||
onTap: () async {
|
||||
setState(() => _isMax = true);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted && context.mounted) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'说点什么吧',
|
||||
style: TextStyle(
|
||||
height: 1.75,
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextField(
|
||||
controller: _ctr,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
focusNode: _focusNode,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '说点什么吧',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
gapPadding: 0,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh ==
|
||||
Theme.of(context).colorScheme.surface
|
||||
? Theme.of(context).colorScheme.onInverseSurface
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (pic != null) ...[
|
||||
NetworkImgLayer(
|
||||
radius: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
src: pic,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'@${(widget.item as DynamicItemModel?)?.modules?.moduleAuthor?.name}',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.opus
|
||||
?.summary
|
||||
?.text ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.desc
|
||||
?.text ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.archive
|
||||
?.title ??
|
||||
(widget.item as DynamicItemModel?)
|
||||
?.modules
|
||||
?.moduleDynamic
|
||||
?.major
|
||||
?.pgc
|
||||
?.title ??
|
||||
'',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (!_isMax)
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: Get.back,
|
||||
title: Center(
|
||||
child: Text(
|
||||
'取消',
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
child: const Text('转发'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _buildToolbar => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
onPressed: () {
|
||||
selectKeyboard.value = PanelType.emoji == currentPanelType;
|
||||
updatePanelType(
|
||||
PanelType.emoji == currentPanelType
|
||||
? PanelType.keyboard
|
||||
: PanelType.emoji,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
tooltip: '表情',
|
||||
selected: !selectKeyboard.value,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 + MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> get _biuldDismiss => [
|
||||
const SizedBox(height: 10),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: Get.back,
|
||||
title: Center(
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 + MediaQuery.of(context).padding.bottom),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget? customPanel(double height) => SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(onChoose: onChooseEmote),
|
||||
);
|
||||
|
||||
@override
|
||||
Future onCustomPublish({required String message, List? pictures}) async {
|
||||
dynamic result = await MsgHttp.createDynamic(
|
||||
mid: GStorage.userInfo.get('userInfoCache')?.mid,
|
||||
dynIdStr: widget.item.idStr,
|
||||
rawText: editController.text,
|
||||
);
|
||||
if (result['status']) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('转发成功');
|
||||
widget.callback();
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,10 @@ class EmotePanelController extends CommonController
|
||||
@override
|
||||
Future<LoadingState> customGetData() =>
|
||||
ReplyHttp.getEmoteList(business: 'reply');
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,8 +458,9 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: '裁剪',
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Colors.white,
|
||||
toolbarColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
toolbarWidgetColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
aspectRatioPresets: [
|
||||
CropAspectRatioPresetCustom(),
|
||||
],
|
||||
|
||||
@@ -870,8 +870,8 @@ class VideoDetailController extends GetxController
|
||||
cid: cid.value,
|
||||
bvid: bvid,
|
||||
progress: plPlayerController.position.value.inMilliseconds,
|
||||
savedDanmaku: savedDanmaku,
|
||||
onSaveDanmaku: (danmaku) => savedDanmaku = danmaku,
|
||||
initialValue: savedDanmaku,
|
||||
onSave: (danmaku) => savedDanmaku = danmaku,
|
||||
callback: (danmakuModel) async {
|
||||
savedDanmaku = null;
|
||||
plPlayerController.danmakuController?.addDanmaku(danmakuModel);
|
||||
|
||||
@@ -125,8 +125,9 @@ class _CreateFavPageState extends State<CreateFavPage> {
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: '裁剪',
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Colors.white,
|
||||
toolbarColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
toolbarWidgetColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
aspectRatioPresets: [
|
||||
CropAspectRatioPreset.ratio16x9,
|
||||
],
|
||||
|
||||
@@ -1,124 +1,39 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/pages/common/common_publish_page.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/reply_type.dart';
|
||||
import 'package:PiliPlus/models/video/reply/emote.dart';
|
||||
import 'package:PiliPlus/pages/emote/index.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
enum PanelType { none, keyboard, emoji }
|
||||
|
||||
class ReplyPage extends StatefulWidget {
|
||||
class ReplyPage extends CommonPublishPage {
|
||||
final int? oid;
|
||||
final int? root;
|
||||
final int? parent;
|
||||
final ReplyType? replyType;
|
||||
final dynamic replyItem;
|
||||
final String? savedReply;
|
||||
final Function(String reply)? onSaveReply;
|
||||
|
||||
const ReplyPage({
|
||||
super.key,
|
||||
super.initialValue,
|
||||
super.imageLengthLimit,
|
||||
super.onSave,
|
||||
this.oid,
|
||||
this.root,
|
||||
this.parent,
|
||||
this.replyType,
|
||||
this.replyItem,
|
||||
this.savedReply,
|
||||
this.onSaveReply,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReplyPage> createState() => _ReplyPageState();
|
||||
}
|
||||
|
||||
class _ReplyPageState extends State<ReplyPage>
|
||||
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
late final _focusNode = FocusNode();
|
||||
late final _controller = ChatBottomPanelContainerController<PanelType>();
|
||||
late final TextEditingController _replyContentController =
|
||||
TextEditingController(text: widget.savedReply);
|
||||
// PanelType _currentPanelType = PanelType.none;
|
||||
bool _readOnly = false;
|
||||
final _readOnlyStream = StreamController<bool>();
|
||||
late final _enableSend = StreamController<bool>();
|
||||
bool _enablePublish = false;
|
||||
final _publishStream = StreamController<bool>();
|
||||
bool _selectKeyboard = true;
|
||||
final _keyboardStream = StreamController<bool>.broadcast();
|
||||
late final _imagePicker = ImagePicker();
|
||||
late final _pathStream = StreamController<List<String>>();
|
||||
late final _pathList = <String>[];
|
||||
late final _limit = 9;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
if (widget.savedReply != null && widget.savedReply!.isNotEmpty) {
|
||||
_enablePublish = true;
|
||||
}
|
||||
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
_keyboardStream.close();
|
||||
_pathStream.close();
|
||||
_publishStream.close();
|
||||
_readOnlyStream.close();
|
||||
_enableSend.close();
|
||||
_focusNode.dispose();
|
||||
_replyContentController.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestFocus() async {
|
||||
await Future.delayed(const Duration(microseconds: 200));
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted && _selectKeyboard) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
_requestFocus();
|
||||
} else {
|
||||
_requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
_controller.keepChatPanel();
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
@@ -140,9 +55,9 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_buildInputView(),
|
||||
_buildImagePreview(),
|
||||
_buildPanelContainer(),
|
||||
buildInputView(),
|
||||
buildImagePreview(),
|
||||
buildPanelContainer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -154,63 +69,16 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelContainer() {
|
||||
return ChatBottomPanelContainer<PanelType>(
|
||||
controller: _controller,
|
||||
inputFocusNode: _focusNode,
|
||||
otherPanelWidget: (type) {
|
||||
if (type == null) return const SizedBox.shrink();
|
||||
switch (type) {
|
||||
case PanelType.emoji:
|
||||
return _buildEmojiPickerPanel();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
// onPanelTypeChange: (panelType, data) {
|
||||
// debugPrint('panelType: $panelType');
|
||||
// switch (panelType) {
|
||||
// case ChatBottomPanelType.none:
|
||||
// _currentPanelType = PanelType.none;
|
||||
// break;
|
||||
// case ChatBottomPanelType.keyboard:
|
||||
// _currentPanelType = PanelType.keyboard;
|
||||
// break;
|
||||
// case ChatBottomPanelType.other:
|
||||
// if (data == null) return;
|
||||
// switch (data) {
|
||||
// case PanelType.emoji:
|
||||
// _currentPanelType = PanelType.emoji;
|
||||
// break;
|
||||
// default:
|
||||
// _currentPanelType = PanelType.none;
|
||||
// break;
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
panelBgColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
@override
|
||||
Widget? customPanel(double height) => SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(onChoose: onChooseEmote),
|
||||
);
|
||||
|
||||
Widget _buildEmojiPickerPanel() {
|
||||
double height = 170;
|
||||
final keyboardHeight = _controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(height, keyboardHeight);
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(onChoose: onChooseEmote),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImagePreview() {
|
||||
return StreamBuilder(
|
||||
initialData: const [],
|
||||
stream: _pathStream.stream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||
Widget buildImagePreview() {
|
||||
return Obx(
|
||||
() {
|
||||
if (pathList.isNotEmpty) {
|
||||
return Container(
|
||||
height: 85,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
@@ -221,19 +89,8 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
itemCount: _pathList.length,
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
_pathList.removeAt(index);
|
||||
_pathStream.add(_pathList);
|
||||
},
|
||||
child: Image(
|
||||
height: 75,
|
||||
fit: BoxFit.fitHeight,
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(_pathList[index])),
|
||||
),
|
||||
),
|
||||
itemCount: pathList.length,
|
||||
itemBuilder: (context, index) => buildImage(index, 75),
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 10),
|
||||
),
|
||||
);
|
||||
@@ -244,7 +101,7 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputView() {
|
||||
Widget buildInputView() {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
@@ -265,35 +122,28 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Listener(
|
||||
onPointerUp: (event) {
|
||||
if (_readOnly) {
|
||||
if (readOnly.value) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
if (!_selectKeyboard) {
|
||||
_selectKeyboard = true;
|
||||
_keyboardStream.add(true);
|
||||
}
|
||||
selectKeyboard.value = true;
|
||||
}
|
||||
},
|
||||
child: StreamBuilder(
|
||||
initialData: false,
|
||||
stream: _readOnlyStream.stream,
|
||||
builder: (context, snapshot) => TextField(
|
||||
controller: _replyContentController,
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
controller: editController,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
autofocus: false,
|
||||
readOnly: snapshot.data ?? false,
|
||||
readOnly: readOnly.value,
|
||||
onChanged: (value) {
|
||||
bool isEmpty = value.trim().isEmpty;
|
||||
if (!isEmpty && !_enablePublish) {
|
||||
_enablePublish = true;
|
||||
_publishStream.add(true);
|
||||
} else if (isEmpty && _enablePublish) {
|
||||
_enablePublish = false;
|
||||
_publishStream.add(false);
|
||||
if (!isEmpty && !enablePublish.value) {
|
||||
enablePublish.value = true;
|
||||
} else if (isEmpty && enablePublish.value) {
|
||||
enablePublish.value = false;
|
||||
}
|
||||
widget.onSaveReply?.call(value);
|
||||
widget.onSave?.call(value);
|
||||
},
|
||||
focusNode: _focusNode,
|
||||
focusNode: focusNode,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "输入回复内容",
|
||||
border: InputBorder.none,
|
||||
@@ -314,37 +164,31 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
StreamBuilder(
|
||||
initialData: true,
|
||||
stream: _keyboardStream.stream,
|
||||
builder: (context, snapshot) => ToolbarIconButton(
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '输入',
|
||||
onPressed: () {
|
||||
if (!_selectKeyboard) {
|
||||
_selectKeyboard = true;
|
||||
_keyboardStream.add(true);
|
||||
if (!selectKeyboard.value) {
|
||||
selectKeyboard.value = true;
|
||||
updatePanelType(PanelType.keyboard);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.keyboard, size: 22),
|
||||
selected: snapshot.data!,
|
||||
selected: selectKeyboard.value,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
StreamBuilder(
|
||||
initialData: true,
|
||||
stream: _keyboardStream.stream,
|
||||
builder: (context, snapshot) => ToolbarIconButton(
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '表情',
|
||||
onPressed: () {
|
||||
if (_selectKeyboard) {
|
||||
_selectKeyboard = false;
|
||||
_keyboardStream.add(false);
|
||||
if (selectKeyboard.value) {
|
||||
selectKeyboard.value = false;
|
||||
updatePanelType(PanelType.emoji);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
selected: !snapshot.data!,
|
||||
selected: !selectKeyboard.value,
|
||||
),
|
||||
),
|
||||
if (widget.root == 0) ...[
|
||||
@@ -353,46 +197,13 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
tooltip: '图片',
|
||||
selected: false,
|
||||
icon: const Icon(Icons.image, size: 22),
|
||||
onPressed: () {
|
||||
EasyThrottle.throttle(
|
||||
'imagePicker', const Duration(milliseconds: 500),
|
||||
() async {
|
||||
try {
|
||||
List<XFile> pickedFiles =
|
||||
await _imagePicker.pickMultiImage(
|
||||
limit: _limit,
|
||||
imageQuality: 100,
|
||||
);
|
||||
if (pickedFiles.isNotEmpty) {
|
||||
for (int i = 0; i < pickedFiles.length; i++) {
|
||||
if (_pathList.length == _limit) {
|
||||
SmartDialog.showToast('最多选择$_limit张图片');
|
||||
if (i != 0) {
|
||||
_pathStream.add(_pathList);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
_pathList.add(pickedFiles[i].path);
|
||||
if (i == pickedFiles.length - 1) {
|
||||
SmartDialog.dismiss();
|
||||
_pathStream.add(_pathList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: onPickImage,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
StreamBuilder(
|
||||
initialData: _enablePublish,
|
||||
stream: _publishStream.stream,
|
||||
builder: (context, snapshot) => FilledButton.tonal(
|
||||
onPressed: snapshot.data == true ? submitReplyAdd : null,
|
||||
Obx(
|
||||
() => FilledButton.tonal(
|
||||
onPressed: enablePublish.value ? onPublish : null,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
@@ -412,95 +223,8 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
);
|
||||
}
|
||||
|
||||
updatePanelType(PanelType type) async {
|
||||
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||
bool isUpdated = false;
|
||||
switch (type) {
|
||||
case PanelType.keyboard:
|
||||
updateInputView(isReadOnly: false);
|
||||
break;
|
||||
case PanelType.emoji:
|
||||
isUpdated = updateInputView(isReadOnly: true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
updatePanelTypeFunc() {
|
||||
_controller.updatePanelType(
|
||||
isSwitchToKeyboard
|
||||
? ChatBottomPanelType.keyboard
|
||||
: ChatBottomPanelType.other,
|
||||
data: type,
|
||||
forceHandleFocus: isSwitchToEmojiPanel
|
||||
? ChatBottomHandleFocus.requestFocus
|
||||
: ChatBottomHandleFocus.none,
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
// Waiting for the input view to update.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
updatePanelTypeFunc();
|
||||
});
|
||||
} else {
|
||||
updatePanelTypeFunc();
|
||||
}
|
||||
}
|
||||
|
||||
hidePanel() async {
|
||||
if (_focusNode.hasFocus) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
updateInputView(isReadOnly: false);
|
||||
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
|
||||
_controller.updatePanelType(ChatBottomPanelType.none);
|
||||
}
|
||||
|
||||
bool updateInputView({
|
||||
required bool isReadOnly,
|
||||
}) {
|
||||
if (_readOnly != isReadOnly) {
|
||||
_readOnly = isReadOnly;
|
||||
_readOnlyStream.add(_readOnly);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future submitReplyAdd() async {
|
||||
feedBack();
|
||||
List? pictures;
|
||||
if (_pathList.isNotEmpty) {
|
||||
pictures = [];
|
||||
for (int i = 0; i < _pathList.length; i++) {
|
||||
SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}');
|
||||
dynamic result = await MsgHttp.uploadBfs(
|
||||
path: _pathList[i],
|
||||
category: 'daily',
|
||||
biz: 'new_dyn',
|
||||
);
|
||||
if (result['status']) {
|
||||
int imageSize = await File(_pathList[i]).length();
|
||||
pictures.add({
|
||||
'img_width': result['data']['image_width'],
|
||||
'img_height': result['data']['image_height'],
|
||||
'img_size': imageSize / 1024,
|
||||
'img_src': result['data']['image_url'],
|
||||
});
|
||||
} else {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(result['msg']);
|
||||
return;
|
||||
}
|
||||
if (i == _pathList.length - 1) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
String message = _replyContentController.text;
|
||||
@override
|
||||
Future onCustomPublish({required String message, List? pictures}) async {
|
||||
var result = await VideoHttp.replyAdd(
|
||||
type: widget.replyType ?? ReplyType.video,
|
||||
oid: widget.oid!,
|
||||
@@ -518,22 +242,4 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
void onChooseEmote(Packages package, Emote emote) {
|
||||
if (!_enablePublish) {
|
||||
_enablePublish = true;
|
||||
_publishStream.add(true);
|
||||
}
|
||||
final int cursorPosition = _replyContentController.selection.baseOffset;
|
||||
final String currentText = _replyContentController.text;
|
||||
final String newText = currentText.substring(0, cursorPosition) +
|
||||
emote.text! +
|
||||
currentText.substring(cursorPosition);
|
||||
_replyContentController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection:
|
||||
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
|
||||
);
|
||||
widget.onSaveReply?.call(_replyContentController.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,8 +317,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
|
||||
parent: root,
|
||||
replyType: widget.replyType,
|
||||
replyItem: item,
|
||||
savedReply: _savedReplies[key],
|
||||
onSaveReply: (reply) {
|
||||
initialValue: _savedReplies[key],
|
||||
onSave: (reply) {
|
||||
_savedReplies[key] = reply;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/http/danmaku.dart';
|
||||
import 'package:PiliPlus/pages/common/common_publish_page.dart';
|
||||
import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart'
|
||||
show PanelType;
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:canvas_danmaku/models/danmaku_content_item.dart';
|
||||
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class SendDanmakuPanel extends StatefulWidget {
|
||||
class SendDanmakuPanel extends CommonPublishPage {
|
||||
final dynamic cid;
|
||||
final dynamic bvid;
|
||||
final dynamic progress;
|
||||
final String? savedDanmaku;
|
||||
final ValueChanged<String>? onSaveDanmaku;
|
||||
final ValueChanged<DanmakuContentItem> callback;
|
||||
|
||||
const SendDanmakuPanel({
|
||||
super.key,
|
||||
super.initialValue,
|
||||
super.onSave,
|
||||
required this.cid,
|
||||
required this.bvid,
|
||||
required this.progress,
|
||||
required this.savedDanmaku,
|
||||
required this.onSaveDanmaku,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
@@ -36,14 +31,7 @@ class SendDanmakuPanel extends StatefulWidget {
|
||||
State<SendDanmakuPanel> createState() => _SendDanmakuPanelState();
|
||||
}
|
||||
|
||||
class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
late final _focusNode = FocusNode();
|
||||
late final _controller = ChatBottomPanelContainerController<PanelType>();
|
||||
late final _textController = TextEditingController(text: widget.savedDanmaku);
|
||||
final RxBool _readOnly = false.obs;
|
||||
final RxBool _enablePublish = false.obs;
|
||||
final RxBool _selectKeyboard = true.obs;
|
||||
class _SendDanmakuPanelState extends CommonPublishPageState<SendDanmakuPanel> {
|
||||
final RxInt _mode = 1.obs;
|
||||
final RxInt _fontsize = 25.obs;
|
||||
final Rx<Color> _color = Colors.white.obs;
|
||||
@@ -119,57 +107,6 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
if (widget.savedDanmaku.isNullOrEmpty.not) {
|
||||
_enablePublish.value = true;
|
||||
}
|
||||
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
_focusNode.dispose();
|
||||
_textController.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestFocus() async {
|
||||
await Future.delayed(const Duration(microseconds: 200));
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted && _selectKeyboard.value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
_requestFocus();
|
||||
} else {
|
||||
_requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
_controller.keepChatPanel();
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
@@ -192,7 +129,7 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_buildInputView(),
|
||||
_buildPanelContainer(),
|
||||
buildPanelContainer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -204,80 +141,57 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelContainer() {
|
||||
return ChatBottomPanelContainer<PanelType>(
|
||||
controller: _controller,
|
||||
inputFocusNode: _focusNode,
|
||||
otherPanelWidget: (type) {
|
||||
if (type == null) return const SizedBox.shrink();
|
||||
switch (type) {
|
||||
case PanelType.emoji:
|
||||
return _buildEmojiPickerPanel();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
panelBgColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiPickerPanel() {
|
||||
double height = 170;
|
||||
final keyboardHeight = _controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(height, keyboardHeight);
|
||||
}
|
||||
return Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
@override
|
||||
Widget? customPanel(double height) => Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text('弹幕字号', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildFontSizeItem(18, '小'),
|
||||
const SizedBox(width: 5),
|
||||
_buildFontSizeItem(25, '标准'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text('弹幕样式', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildPositionItem(1, '滚动'),
|
||||
const SizedBox(width: 5),
|
||||
_buildPositionItem(5, '顶部'),
|
||||
const SizedBox(width: 5),
|
||||
_buildPositionItem(4, '底部'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('弹幕颜色', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildColorPanel,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text('弹幕字号', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildFontSizeItem(18, '小'),
|
||||
const SizedBox(width: 5),
|
||||
_buildFontSizeItem(25, '标准'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text('弹幕样式', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildPositionItem(1, '滚动'),
|
||||
const SizedBox(width: 5),
|
||||
_buildPositionItem(5, '顶部'),
|
||||
const SizedBox(width: 5),
|
||||
_buildPositionItem(4, '底部'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('弹幕颜色', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(width: 16),
|
||||
_buildColorPanel,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildColorItem(Color color) {
|
||||
return GestureDetector(
|
||||
@@ -392,18 +306,18 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
context: context,
|
||||
tooltip: '弹幕样式',
|
||||
onPressed: () {
|
||||
if (_selectKeyboard.value) {
|
||||
_selectKeyboard.value = false;
|
||||
if (selectKeyboard.value) {
|
||||
selectKeyboard.value = false;
|
||||
updatePanelType(PanelType.emoji);
|
||||
} else {
|
||||
_selectKeyboard.value = true;
|
||||
selectKeyboard.value = true;
|
||||
updatePanelType(PanelType.keyboard);
|
||||
}
|
||||
},
|
||||
bgColor: Colors.transparent,
|
||||
iconSize: 24,
|
||||
icon: Icons.text_format,
|
||||
iconColor: _selectKeyboard.value.not
|
||||
iconColor: selectKeyboard.value.not
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -414,35 +328,35 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Listener(
|
||||
onPointerUp: (event) {
|
||||
if (_readOnly.value) {
|
||||
if (readOnly.value) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
_selectKeyboard.value = true;
|
||||
selectKeyboard.value = true;
|
||||
}
|
||||
},
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
controller: _textController,
|
||||
controller: editController,
|
||||
autofocus: false,
|
||||
readOnly: _readOnly.value,
|
||||
readOnly: readOnly.value,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(100),
|
||||
],
|
||||
onChanged: (value) {
|
||||
bool isEmpty = value.trim().isEmpty;
|
||||
if (!isEmpty && !_enablePublish.value) {
|
||||
_enablePublish.value = true;
|
||||
} else if (isEmpty && _enablePublish.value) {
|
||||
_enablePublish.value = false;
|
||||
if (!isEmpty && !enablePublish.value) {
|
||||
enablePublish.value = true;
|
||||
} else if (isEmpty && enablePublish.value) {
|
||||
enablePublish.value = false;
|
||||
}
|
||||
widget.onSaveDanmaku?.call(value);
|
||||
widget.onSave?.call(value);
|
||||
},
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
onSendDanmaku();
|
||||
onPublish();
|
||||
}
|
||||
},
|
||||
focusNode: _focusNode,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: "输入弹幕内容",
|
||||
border: InputBorder.none,
|
||||
@@ -464,10 +378,10 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
tooltip: '发送',
|
||||
bgColor: Colors.transparent,
|
||||
iconSize: 22,
|
||||
iconColor: _enablePublish.value
|
||||
iconColor: enablePublish.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
onPressed: _enablePublish.value ? onSendDanmaku : null,
|
||||
onPressed: enablePublish.value ? onPublish : null,
|
||||
icon: Icons.send,
|
||||
),
|
||||
)
|
||||
@@ -476,97 +390,8 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
);
|
||||
}
|
||||
|
||||
updatePanelType(PanelType type) async {
|
||||
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||
bool isUpdated = false;
|
||||
switch (type) {
|
||||
case PanelType.keyboard:
|
||||
updateInputView(isReadOnly: false);
|
||||
break;
|
||||
case PanelType.emoji:
|
||||
isUpdated = updateInputView(isReadOnly: true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
updatePanelTypeFunc() {
|
||||
_controller.updatePanelType(
|
||||
isSwitchToKeyboard
|
||||
? ChatBottomPanelType.keyboard
|
||||
: ChatBottomPanelType.other,
|
||||
data: type,
|
||||
forceHandleFocus: isSwitchToEmojiPanel
|
||||
? ChatBottomHandleFocus.requestFocus
|
||||
: ChatBottomHandleFocus.none,
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
// Waiting for the input view to update.
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
updatePanelTypeFunc();
|
||||
});
|
||||
} else {
|
||||
updatePanelTypeFunc();
|
||||
}
|
||||
}
|
||||
|
||||
hidePanel() async {
|
||||
if (_focusNode.hasFocus) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
updateInputView(isReadOnly: false);
|
||||
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
|
||||
_controller.updatePanelType(ChatBottomPanelType.none);
|
||||
}
|
||||
|
||||
bool updateInputView({
|
||||
required bool isReadOnly,
|
||||
}) {
|
||||
if (_readOnly.value != isReadOnly) {
|
||||
_readOnly.value = isReadOnly;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future onSendDanmaku() async {
|
||||
SmartDialog.showLoading(msg: '发送中...');
|
||||
final dynamic res = await DanmakaHttp.shootDanmaku(
|
||||
oid: widget.cid,
|
||||
bvid: widget.bvid,
|
||||
progress: widget.progress,
|
||||
msg: _textController.text,
|
||||
mode: _mode.value,
|
||||
fontsize: _fontsize.value,
|
||||
color: _color.value.value & 0xFFFFFF,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (res['status']) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('发送成功');
|
||||
widget.callback(
|
||||
DanmakuContentItem(
|
||||
_textController.text,
|
||||
color: _color.value,
|
||||
type: switch (_mode.value) {
|
||||
5 => DanmakuItemType.top,
|
||||
4 => DanmakuItemType.bottom,
|
||||
_ => DanmakuItemType.scroll,
|
||||
},
|
||||
selfSend: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
SmartDialog.showToast('发送失败: ${res['msg']}');
|
||||
}
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
_controller.keepChatPanel();
|
||||
controller.keepChatPanel();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -611,4 +436,37 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future onCustomPublish({required String message, List? pictures}) async {
|
||||
SmartDialog.showLoading(msg: '发送中...');
|
||||
final dynamic res = await DanmakaHttp.shootDanmaku(
|
||||
oid: widget.cid,
|
||||
bvid: widget.bvid,
|
||||
progress: widget.progress,
|
||||
msg: editController.text,
|
||||
mode: _mode.value,
|
||||
fontsize: _fontsize.value,
|
||||
color: _color.value.value & 0xFFFFFF,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (res['status']) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('发送成功');
|
||||
widget.callback(
|
||||
DanmakuContentItem(
|
||||
editController.text,
|
||||
color: _color.value,
|
||||
type: switch (_mode.value) {
|
||||
5 => DanmakuItemType.top,
|
||||
4 => DanmakuItemType.bottom,
|
||||
_ => DanmakuItemType.scroll,
|
||||
},
|
||||
selfSend: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
SmartDialog.showToast('发送失败: ${res['msg']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/pages/common/common_publish_page.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:chat_bottom_container/panel_container.dart';
|
||||
import 'package:chat_bottom_container/typedef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -16,55 +13,22 @@ import 'package:mime/mime.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/pages/whisper_detail/controller.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:PiliPlus/models/video/reply/emote.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'widget/chat_item.dart';
|
||||
|
||||
class WhisperDetailPage extends StatefulWidget {
|
||||
const WhisperDetailPage({super.key});
|
||||
class WhisperDetailPage extends CommonPublishPage {
|
||||
const WhisperDetailPage({
|
||||
super.key,
|
||||
super.autofocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WhisperDetailPage> createState() => _WhisperDetailPageState();
|
||||
}
|
||||
|
||||
class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
class _WhisperDetailPageState
|
||||
extends CommonPublishPageState<WhisperDetailPage> {
|
||||
final _whisperDetailController = Get.put(WhisperDetailController());
|
||||
late final _controller = ChatBottomPanelContainerController<PanelType>();
|
||||
late final _focusNode = FocusNode();
|
||||
PanelType _currentPanelType = PanelType.none;
|
||||
bool _readOnly = false;
|
||||
final _readOnlyStream = StreamController<bool>();
|
||||
late final _enableSend = StreamController<bool>();
|
||||
late bool _visibleSend = false;
|
||||
late final _imagePicker = ImagePicker();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_readOnlyStream.close();
|
||||
_enableSend.close();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onChooseEmote(Packages package, Emote emote) {
|
||||
if (!_visibleSend) {
|
||||
_visibleSend = true;
|
||||
_enableSend.add(true);
|
||||
}
|
||||
int cursorPosition =
|
||||
_whisperDetailController.replyContentController.selection.baseOffset;
|
||||
if (cursorPosition == -1) cursorPosition = 0;
|
||||
final String currentText =
|
||||
_whisperDetailController.replyContentController.text;
|
||||
final String newText = currentText.substring(0, cursorPosition) +
|
||||
emote.text! +
|
||||
currentText.substring(cursorPosition);
|
||||
_whisperDetailController.replyContentController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection:
|
||||
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -135,7 +99,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
children: [
|
||||
Expanded(child: _buildList()),
|
||||
_buildInputView(),
|
||||
_buildPanelContainer(),
|
||||
buildPanelContainer(Theme.of(context).colorScheme.onInverseSurface),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -177,66 +141,6 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
return resultWidget;
|
||||
}
|
||||
|
||||
hidePanel() async {
|
||||
if (_focusNode.hasFocus) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
updateInputView(isReadOnly: false);
|
||||
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
|
||||
_controller.updatePanelType(ChatBottomPanelType.none);
|
||||
}
|
||||
|
||||
bool updateInputView({
|
||||
required bool isReadOnly,
|
||||
}) {
|
||||
if (_readOnly != isReadOnly) {
|
||||
_readOnly = isReadOnly;
|
||||
_readOnlyStream.add(_readOnly);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updatePanelType(PanelType type) async {
|
||||
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||
bool isUpdated = false;
|
||||
switch (type) {
|
||||
case PanelType.keyboard:
|
||||
updateInputView(isReadOnly: false);
|
||||
break;
|
||||
case PanelType.emoji:
|
||||
isUpdated = updateInputView(isReadOnly: true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
updatePanelTypeFunc() {
|
||||
_controller.updatePanelType(
|
||||
isSwitchToKeyboard
|
||||
? ChatBottomPanelType.keyboard
|
||||
: ChatBottomPanelType.other,
|
||||
data: type,
|
||||
forceHandleFocus: isSwitchToEmojiPanel
|
||||
? ChatBottomHandleFocus.requestFocus
|
||||
: ChatBottomHandleFocus.none,
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
// Waiting for the input view to update.
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(timeStamp) {
|
||||
updatePanelTypeFunc();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
updatePanelTypeFunc();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInputView() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
@@ -253,7 +157,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
updatePanelType(
|
||||
PanelType.emoji == _currentPanelType
|
||||
PanelType.emoji == currentPanelType
|
||||
? PanelType.keyboard
|
||||
: PanelType.emoji,
|
||||
);
|
||||
@@ -265,27 +169,23 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
child: Listener(
|
||||
onPointerUp: (event) {
|
||||
// Currently it may be emojiPanel.
|
||||
if (_readOnly) {
|
||||
if (readOnly.value) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
}
|
||||
},
|
||||
child: StreamBuilder(
|
||||
initialData: false,
|
||||
stream: _readOnlyStream.stream,
|
||||
builder: (context, snapshot) => TextField(
|
||||
readOnly: snapshot.data ?? false,
|
||||
focusNode: _focusNode,
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
readOnly: readOnly.value,
|
||||
focusNode: focusNode,
|
||||
controller: _whisperDetailController.replyContentController,
|
||||
minLines: 1,
|
||||
maxLines: 4,
|
||||
onChanged: (value) {
|
||||
bool isNotEmpty = value.trim().isNotEmpty;
|
||||
if (isNotEmpty && !_visibleSend) {
|
||||
_visibleSend = true;
|
||||
_enableSend.add(true);
|
||||
} else if (!isNotEmpty && _visibleSend) {
|
||||
_visibleSend = false;
|
||||
_enableSend.add(false);
|
||||
if (isNotEmpty && !enablePublish.value) {
|
||||
enablePublish.value = true;
|
||||
} else if (!isNotEmpty && enablePublish.value) {
|
||||
enablePublish.value = false;
|
||||
}
|
||||
},
|
||||
textInputAction: TextInputAction.newline,
|
||||
@@ -304,16 +204,15 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _enableSend.stream,
|
||||
builder: (context, snapshot) {
|
||||
Obx(
|
||||
() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
if (snapshot.data == true) {
|
||||
if (enablePublish.value) {
|
||||
_whisperDetailController.sendMsg();
|
||||
} else {
|
||||
try {
|
||||
XFile? pickedFile = await _imagePicker.pickImage(
|
||||
XFile? pickedFile = await imagePicker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 100,
|
||||
);
|
||||
@@ -351,10 +250,10 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: Icon(snapshot.data == true
|
||||
icon: Icon(enablePublish.value
|
||||
? Icons.send
|
||||
: Icons.add_photo_alternate_outlined),
|
||||
tooltip: snapshot.data == true ? '发送' : '图片',
|
||||
tooltip: enablePublish.value ? '发送' : '图片',
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -363,57 +262,14 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiPickerPanel() {
|
||||
double height = 300;
|
||||
final keyboardHeight = _controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(200, keyboardHeight);
|
||||
}
|
||||
@override
|
||||
Widget? customPanel(double height) => SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(onChoose: onChooseEmote),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: EmotePanel(
|
||||
onChoose: onChooseEmote,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelContainer() {
|
||||
return ChatBottomPanelContainer<PanelType>(
|
||||
controller: _controller,
|
||||
inputFocusNode: _focusNode,
|
||||
otherPanelWidget: (type) {
|
||||
if (type == null) return const SizedBox.shrink();
|
||||
switch (type) {
|
||||
case PanelType.emoji:
|
||||
return _buildEmojiPickerPanel();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
onPanelTypeChange: (panelType, data) {
|
||||
// debugPrint('panelType: $panelType');
|
||||
switch (panelType) {
|
||||
case ChatBottomPanelType.none:
|
||||
_currentPanelType = PanelType.none;
|
||||
break;
|
||||
case ChatBottomPanelType.keyboard:
|
||||
_currentPanelType = PanelType.keyboard;
|
||||
break;
|
||||
case ChatBottomPanelType.other:
|
||||
if (data == null) return;
|
||||
switch (data) {
|
||||
case PanelType.emoji:
|
||||
_currentPanelType = PanelType.emoji;
|
||||
break;
|
||||
default:
|
||||
_currentPanelType = PanelType.none;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
panelBgColor: Theme.of(context).colorScheme.onInverseSurface,
|
||||
);
|
||||
@override
|
||||
Future onCustomPublish({required String message, List? pictures}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +75,13 @@ extension BuildContextExt on BuildContext {
|
||||
int? initialPage,
|
||||
required List<String> imgList,
|
||||
ValueChanged<int>? onDismissed,
|
||||
bool? isFile,
|
||||
}) {
|
||||
Navigator.of(this).push(
|
||||
HeroDialogRoute(
|
||||
builder: (context) => InteractiveviewerGallery(
|
||||
sources: imgList,
|
||||
isFile: isFile,
|
||||
initIndex: initialPage ?? 0,
|
||||
onPageChanged: (int pageIndex) {},
|
||||
onDismissed: onDismissed,
|
||||
|
||||
Reference in New Issue
Block a user