mod: refine reply/publish page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-01-24 18:14:19 +08:00
parent 4d79f763ac
commit a115b5e91b
14 changed files with 1394 additions and 1537 deletions

View File

@@ -45,8 +45,11 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
this.onDismissed, this.onDismissed,
this.setStatusBar, this.setStatusBar,
this.onClose, this.onClose,
this.isFile,
}); });
final bool? isFile;
final VoidCallback? onClose; final VoidCallback? onClose;
final bool? setStatusBar; final bool? setStatusBar;
@@ -137,8 +140,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE); StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
} }
} }
for (int index = 0; index < widget.sources.length; index++) { if (widget.isFile != true) {
CachedNetworkImageProvider(_getActualUrl(index)).evict(); for (int index = 0; index < widget.sources.length; index++) {
CachedNetworkImageProvider(_getActualUrl(index)).evict();
}
} }
super.dispose(); super.dispose();
} }
@@ -267,7 +272,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
_doubleTapLocalPosition = details.localPosition; _doubleTapLocalPosition = details.localPosition;
}, },
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onLongPress: onLongPress, onLongPress: widget.isFile == true ? null : onLongPress,
child: widget.itemBuilder != null child: widget.itemBuilder != null
? widget.itemBuilder!( ? widget.itemBuilder!(
context, context,
@@ -303,59 +308,69 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
), ),
) )
: null, : null,
child: Row( child: Stack(
mainAxisAlignment: MainAxisAlignment.spaceBetween, alignment: Alignment.center,
children: [ children: [
IconButton( Align(
icon: const Icon(Icons.close, color: Colors.white), alignment: Alignment.centerLeft,
onPressed: onClose, child: IconButton(
), icon: const Icon(Icons.close, color: Colors.white),
widget.sources.length > 1 onPressed: onClose,
? 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),
), ),
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( return Center(
child: Hero( child: Hero(
tag: widget.sources[index], tag: widget.sources[index],
child: CachedNetworkImage( child: widget.isFile == true
fadeInDuration: const Duration(milliseconds: 0), ? Image(
fadeOutDuration: const Duration(milliseconds: 0), filterQuality: FilterQuality.low,
imageUrl: _getActualUrl(index), image: FileImage(File(widget.sources[index])),
// fit: BoxFit.contain, )
progressIndicatorBuilder: (context, url, progress) { : CachedNetworkImage(
return Center( fadeInDuration: const Duration(milliseconds: 0),
child: SizedBox( fadeOutDuration: const Duration(milliseconds: 0),
width: 150.0, imageUrl: _getActualUrl(index),
child: LinearProgressIndicator(value: progress.progress ?? 0), // 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;
// });
// });
// },
),
), ),
); );
} }

View 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());
}
});
}
}

View File

@@ -153,8 +153,8 @@ abstract class ReplyController extends CommonController {
? ReplyType.values[replyItem.type.toInt()] ? ReplyType.values[replyItem.type.toInt()]
: replyType, : replyType,
replyItem: replyItem, replyItem: replyItem,
savedReply: savedReplies[key], initialValue: savedReplies[key],
onSaveReply: (reply) { onSave: (reply) {
savedReplies[key] = reply; savedReplies[key] = reply;
}, },
) )
@@ -166,8 +166,8 @@ abstract class ReplyController extends CommonController {
? ReplyType.values[replyItem.type.toInt()] ? ReplyType.values[replyItem.type.toInt()]
: replyType, : replyType,
replyItem: replyItem, replyItem: replyItem,
savedReply: savedReplies[key], initialValue: savedReplies[key],
onSaveReply: (reply) { onSave: (reply) {
savedReplies[key] = reply; savedReplies[key] = reply;
}, },
); );

View File

@@ -1,115 +1,142 @@
import 'dart:io';
import 'package:PiliPlus/http/msg.dart'; 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/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:PiliPlus/utils/storage.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class CreateDynPanel extends StatefulWidget { class CreateDynPanel extends CommonPublishPage {
const CreateDynPanel({super.key}); const CreateDynPanel({
super.key,
super.imageLengthLimit = 18,
});
@override @override
State<CreateDynPanel> createState() => _CreateDynPanelState(); State<CreateDynPanel> createState() => _CreateDynPanelState();
} }
class _CreateDynPanelState extends State<CreateDynPanel> { class _CreateDynPanelState extends CommonPublishPageState<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;
bool _isPrivate = false; bool _isPrivate = false;
DateTime? _publishTime; DateTime? _publishTime;
ReplyOption _replyOption = ReplyOption.allow; ReplyOption _replyOption = ReplyOption.allow;
@override @override
void dispose() { void dispose() {
_ctr.dispose(); try {
Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: false,
appBar: PreferredSize( 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), preferredSize: Size.fromHeight(66),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Stack( child: Stack(
alignment: Alignment.center,
children: [ children: [
Positioned( Align(
top: 0, alignment: Alignment.centerLeft,
left: 0,
bottom: 0,
child: SizedBox( child: SizedBox(
width: 34, width: 34,
height: 34, height: 34,
@@ -140,12 +167,11 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
), ),
), ),
Positioned( Align(
top: 0, alignment: Alignment.centerRight,
right: 0,
child: Obx( child: Obx(
() => FilledButton.tonal( () => FilledButton.tonal(
onPressed: _isEnablePub.value ? _onCreate : null, onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -164,364 +190,307 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
], ],
), ),
), ),
), );
body: SingleChildScrollView(
child: Column( Widget get _buildPrivateWidget => PopupMenuButton(
mainAxisSize: MainAxisSize.max, initialValue: _isPrivate,
crossAxisAlignment: CrossAxisAlignment.start, onOpened: controller.keepChatPanel,
children: [ onSelected: (value) {
Padding( setState(() {
padding: const EdgeInsets.symmetric(horizontal: 16), _isPrivate = value;
child: TextField( });
controller: _ctr, },
minLines: 4, itemBuilder: (context) => List.generate(
maxLines: 8, 2,
autofocus: true, (index) => PopupMenuItem<bool>(
onChanged: (value) { enabled: _publishTime != null && index == 1 ? false : true,
bool isEmpty = value.trim().isEmpty && _pathList.isEmpty; value: index == 0 ? false : true,
if (!isEmpty && !_isEnablePub.value) { child: Row(
_isEnablePub.value = true; mainAxisSize: MainAxisSize.min,
} else if (isEmpty && _isEnablePub.value) { children: [
_isEnablePub.value = false; Icon(
} size: 19,
}, index == 0 ? Icons.visibility : Icons.visibility_off,
decoration: const InputDecoration( ),
hintText: '说点什么吧', const SizedBox(width: 4),
border: OutlineInputBorder( Text(index == 0 ? '所有人可见' : '仅自己可见'),
borderSide: BorderSide.none, ],
gapPadding: 0, ),
), ),
contentPadding: EdgeInsets.zero, ),
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), .toList(),
Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 2),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min,
children: [ children: [
_publishTime == null Icon(
? FilledButton.tonal( size: 19,
style: FilledButton.styleFrom( _replyOption.iconData,
padding: const EdgeInsets.symmetric( color: _replyOption == ReplyOption.close
horizontal: 16, ? Theme.of(context).colorScheme.error
vertical: 10, : Theme.of(context).colorScheme.secondary,
),
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,
),
],
),
),
),
],
),
],
), ),
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), visualDensity: const VisualDensity(
Obx( horizontal: -2,
() => SizedBox( vertical: -2,
height: 100, ),
child: ListView.separated( ),
scrollDirection: Axis.horizontal, onPressed: _isPrivate
physics: const AlwaysScrollableScrollPhysics( ? null
parent: BouncingScrollPhysics(), : () {
), DateTime nowDate = DateTime.now();
padding: const EdgeInsets.symmetric(horizontal: 16), showDatePicker(
itemCount: _pathList.length == _limit context: context,
? _limit initialDate: nowDate,
: _pathList.length + 1, firstDate: nowDate,
itemBuilder: (context, index) { lastDate: DateTime(
if (_pathList.length != _limit && nowDate.year,
index == _pathList.length) { nowDate.month,
return Material( nowDate.day + 7,
borderRadius: BorderRadius.circular(12), ),
child: InkWell( ).then(
borderRadius: BorderRadius.circular(12), (selectedDate) {
onTap: () { if (selectedDate != null && mounted) {
EasyThrottle.throttle('imagePicker', TimeOfDay nowTime = TimeOfDay.now();
const Duration(milliseconds: 500), () async { showTimePicker(
try { context: context,
List<XFile> pickedFiles = initialTime: nowTime.replacing(
await _imagePicker.pickMultiImage( hour: nowTime.minute + 6 >= 60
limit: _limit, ? (nowTime.hour + 1) % 24
imageQuality: 100, : nowTime.hour,
); minute: (nowTime.minute + 6) % 60,
if (pickedFiles.isNotEmpty) { ),
for (int i = 0; i < pickedFiles.length; i++) { ).then((selectedTime) {
if (_pathList.length == _limit) { if (selectedTime != null) {
SmartDialog.showToast('最多选择$_limit张图片'); if (selectedDate.day == nowDate.day) {
break; if (selectedTime.hour < nowTime.hour) {
} else { SmartDialog.showToast('时间设置错误至少选择6分钟之后');
_pathList.add(pickedFiles[i].path); return;
} } else if (selectedTime.hour == nowTime.hour) {
} if (selectedTime.minute < nowTime.minute + 6) {
if (_pathList.isNotEmpty && if (selectedDate.day == nowDate.day) {
!_isEnablePub.value) { SmartDialog.showToast('时间设置错误至少选择6分钟之后');
_isEnablePub.value = true;
} }
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])), child: const Text('定时发布'),
), )
); : OutlinedButton.icon(
} style: OutlinedButton.styleFrom(
}, padding: const EdgeInsets.symmetric(
separatorBuilder: (context, index) => horizontal: 16,
const SizedBox(width: 10), vertical: 10,
),
),
), ),
SizedBox( visualDensity: const VisualDensity(
height: MediaQuery.paddingOf(context).bottom + 25, 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']}');
}
} }
} }

View File

@@ -1,12 +1,17 @@
import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models/dynamics/result.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:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class RepostPanel extends StatefulWidget { class RepostPanel extends CommonPublishPage {
const RepostPanel({ const RepostPanel({
super.key, super.key,
required this.item, required this.item,
@@ -20,56 +25,60 @@ class RepostPanel extends StatefulWidget {
State<RepostPanel> createState() => _RepostPanelState(); State<RepostPanel> createState() => _RepostPanelState();
} }
class _RepostPanelState extends State<RepostPanel> { class _RepostPanelState extends CommonPublishPageState<RepostPanel> {
bool _isMax = false; bool _isMax = false;
late final dynamic _pic = (widget.item as DynamicItemModel?)
final _ctr = TextEditingController(); ?.modules
final _focusNode = FocusNode(); ?.moduleDynamic
?.major
Future _onRepost() async { ?.archive
dynamic result = await MsgHttp.createDynamic( ?.cover ??
mid: GStorage.userInfo.get('userInfoCache')?.mid, (widget.item as DynamicItemModel?)
dynIdStr: widget.item.idStr, ?.modules
rawText: _ctr.text, ?.moduleDynamic
); ?.major
if (result['status']) { ?.pgc
Get.back(); ?.cover ??
SmartDialog.showToast('转发成功'); (widget.item as DynamicItemModel?)
widget.callback(); ?.modules
} else { ?.moduleDynamic
SmartDialog.showToast(result['msg']); ?.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 @override
void dispose() { void dispose() {
_ctr.dispose(); try {
_focusNode.dispose(); Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { 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( return AnimatedSize(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
curve: Curves.ease, curve: Curves.ease,
@@ -79,225 +88,280 @@ class _RepostPanelState extends State<RepostPanel> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: _isMax ? 16 : 10), SizedBox(height: _isMax ? 16 : 10),
if (!_isMax) _buildAppBar,
Row( if (_isMax) Expanded(child: _buildEditPanel) else _buildEditPanel,
children: [ if (_isMax.not)
const SizedBox(width: 16), ..._biuldDismiss
const Text( else ...[
'转发动态', _buildToolbar,
style: TextStyle(fontWeight: FontWeight.bold), 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(), contentPadding: EdgeInsets.symmetric(vertical: 10),
TextButton( ),
onPressed: _onRepost, ),
style: TextButton.styleFrom( ),
),
);
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( padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10), horizontal: 20,
vertical: 10,
),
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
horizontal: -2, horizontal: -2,
vertical: -2, vertical: -2,
), ),
), ),
child: const Text('立即转发'), 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),
), ),
), ),
],
),
);
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']);
}
} }
} }

View File

@@ -26,4 +26,10 @@ class EmotePanelController extends CommonController
@override @override
Future<LoadingState> customGetData() => Future<LoadingState> customGetData() =>
ReplyHttp.getEmoteList(business: 'reply'); ReplyHttp.getEmoteList(business: 'reply');
@override
void onClose() {
tabController.dispose();
super.onClose();
}
} }

View File

@@ -458,8 +458,9 @@ class _EditProfilePageState extends State<EditProfilePage> {
uiSettings: [ uiSettings: [
AndroidUiSettings( AndroidUiSettings(
toolbarTitle: '裁剪', toolbarTitle: '裁剪',
toolbarColor: Theme.of(context).colorScheme.primary, toolbarColor: Theme.of(context).colorScheme.secondaryContainer,
toolbarWidgetColor: Colors.white, toolbarWidgetColor:
Theme.of(context).colorScheme.onSecondaryContainer,
aspectRatioPresets: [ aspectRatioPresets: [
CropAspectRatioPresetCustom(), CropAspectRatioPresetCustom(),
], ],

View File

@@ -870,8 +870,8 @@ class VideoDetailController extends GetxController
cid: cid.value, cid: cid.value,
bvid: bvid, bvid: bvid,
progress: plPlayerController.position.value.inMilliseconds, progress: plPlayerController.position.value.inMilliseconds,
savedDanmaku: savedDanmaku, initialValue: savedDanmaku,
onSaveDanmaku: (danmaku) => savedDanmaku = danmaku, onSave: (danmaku) => savedDanmaku = danmaku,
callback: (danmakuModel) async { callback: (danmakuModel) async {
savedDanmaku = null; savedDanmaku = null;
plPlayerController.danmakuController?.addDanmaku(danmakuModel); plPlayerController.danmakuController?.addDanmaku(danmakuModel);

View File

@@ -125,8 +125,9 @@ class _CreateFavPageState extends State<CreateFavPage> {
uiSettings: [ uiSettings: [
AndroidUiSettings( AndroidUiSettings(
toolbarTitle: '裁剪', toolbarTitle: '裁剪',
toolbarColor: Theme.of(context).colorScheme.primary, toolbarColor: Theme.of(context).colorScheme.secondaryContainer,
toolbarWidgetColor: Colors.white, toolbarWidgetColor:
Theme.of(context).colorScheme.onSecondaryContainer,
aspectRatioPresets: [ aspectRatioPresets: [
CropAspectRatioPreset.ratio16x9, CropAspectRatioPreset.ratio16x9,
], ],

View File

@@ -1,124 +1,39 @@
import 'dart:async'; 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: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/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/reply_type.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 CommonPublishPage {
class ReplyPage extends StatefulWidget {
final int? oid; final int? oid;
final int? root; final int? root;
final int? parent; final int? parent;
final ReplyType? replyType; final ReplyType? replyType;
final dynamic replyItem; final dynamic replyItem;
final String? savedReply;
final Function(String reply)? onSaveReply;
const ReplyPage({ const ReplyPage({
super.key, super.key,
super.initialValue,
super.imageLengthLimit,
super.onSave,
this.oid, this.oid,
this.root, this.root,
this.parent, this.parent,
this.replyType, this.replyType,
this.replyItem, this.replyItem,
this.savedReply,
this.onSaveReply,
}); });
@override @override
State<ReplyPage> createState() => _ReplyPageState(); State<ReplyPage> createState() => _ReplyPageState();
} }
class _ReplyPageState extends State<ReplyPage> class _ReplyPageState extends CommonPublishPageState<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();
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQuery.removePadding( return MediaQuery.removePadding(
@@ -140,9 +55,9 @@ class _ReplyPageState extends State<ReplyPage>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
_buildInputView(), buildInputView(),
_buildImagePreview(), buildImagePreview(),
_buildPanelContainer(), buildPanelContainer(),
], ],
), ),
), ),
@@ -154,63 +69,16 @@ class _ReplyPageState extends State<ReplyPage>
); );
} }
Widget _buildPanelContainer() { @override
return ChatBottomPanelContainer<PanelType>( Widget? customPanel(double height) => SizedBox(
controller: _controller, height: height,
inputFocusNode: _focusNode, child: EmotePanel(onChoose: onChooseEmote),
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,
);
}
Widget _buildEmojiPickerPanel() { Widget buildImagePreview() {
double height = 170; return Obx(
final keyboardHeight = _controller.keyboardHeight; () {
if (keyboardHeight != 0) { if (pathList.isNotEmpty) {
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) {
return Container( return Container(
height: 85, height: 85,
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
@@ -221,19 +89,8 @@ class _ReplyPageState extends State<ReplyPage>
parent: BouncingScrollPhysics(), parent: BouncingScrollPhysics(),
), ),
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
itemCount: _pathList.length, itemCount: pathList.length,
itemBuilder: (context, index) => GestureDetector( itemBuilder: (context, index) => buildImage(index, 75),
onTap: () {
_pathList.removeAt(index);
_pathStream.add(_pathList);
},
child: Image(
height: 75,
fit: BoxFit.fitHeight,
filterQuality: FilterQuality.low,
image: FileImage(File(_pathList[index])),
),
),
separatorBuilder: (context, index) => const SizedBox(width: 10), separatorBuilder: (context, index) => const SizedBox(width: 10),
), ),
); );
@@ -244,7 +101,7 @@ class _ReplyPageState extends State<ReplyPage>
); );
} }
Widget _buildInputView() { Widget buildInputView() {
return Container( return Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top), margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
@@ -265,35 +122,28 @@ class _ReplyPageState extends State<ReplyPage>
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener( child: Listener(
onPointerUp: (event) { onPointerUp: (event) {
if (_readOnly) { if (readOnly.value) {
updatePanelType(PanelType.keyboard); updatePanelType(PanelType.keyboard);
if (!_selectKeyboard) { selectKeyboard.value = true;
_selectKeyboard = true;
_keyboardStream.add(true);
}
} }
}, },
child: StreamBuilder( child: Obx(
initialData: false, () => TextField(
stream: _readOnlyStream.stream, controller: editController,
builder: (context, snapshot) => TextField(
controller: _replyContentController,
minLines: 4, minLines: 4,
maxLines: 8, maxLines: 8,
autofocus: false, autofocus: false,
readOnly: snapshot.data ?? false, readOnly: readOnly.value,
onChanged: (value) { onChanged: (value) {
bool isEmpty = value.trim().isEmpty; bool isEmpty = value.trim().isEmpty;
if (!isEmpty && !_enablePublish) { if (!isEmpty && !enablePublish.value) {
_enablePublish = true; enablePublish.value = true;
_publishStream.add(true); } else if (isEmpty && enablePublish.value) {
} else if (isEmpty && _enablePublish) { enablePublish.value = false;
_enablePublish = false;
_publishStream.add(false);
} }
widget.onSaveReply?.call(value); widget.onSave?.call(value);
}, },
focusNode: _focusNode, focusNode: focusNode,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "输入回复内容", hintText: "输入回复内容",
border: InputBorder.none, border: InputBorder.none,
@@ -314,37 +164,31 @@ class _ReplyPageState extends State<ReplyPage>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
StreamBuilder( Obx(
initialData: true, () => ToolbarIconButton(
stream: _keyboardStream.stream,
builder: (context, snapshot) => ToolbarIconButton(
tooltip: '输入', tooltip: '输入',
onPressed: () { onPressed: () {
if (!_selectKeyboard) { if (!selectKeyboard.value) {
_selectKeyboard = true; selectKeyboard.value = true;
_keyboardStream.add(true);
updatePanelType(PanelType.keyboard); updatePanelType(PanelType.keyboard);
} }
}, },
icon: const Icon(Icons.keyboard, size: 22), icon: const Icon(Icons.keyboard, size: 22),
selected: snapshot.data!, selected: selectKeyboard.value,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
StreamBuilder( Obx(
initialData: true, () => ToolbarIconButton(
stream: _keyboardStream.stream,
builder: (context, snapshot) => ToolbarIconButton(
tooltip: '表情', tooltip: '表情',
onPressed: () { onPressed: () {
if (_selectKeyboard) { if (selectKeyboard.value) {
_selectKeyboard = false; selectKeyboard.value = false;
_keyboardStream.add(false);
updatePanelType(PanelType.emoji); updatePanelType(PanelType.emoji);
} }
}, },
icon: const Icon(Icons.emoji_emotions, size: 22), icon: const Icon(Icons.emoji_emotions, size: 22),
selected: !snapshot.data!, selected: !selectKeyboard.value,
), ),
), ),
if (widget.root == 0) ...[ if (widget.root == 0) ...[
@@ -353,46 +197,13 @@ class _ReplyPageState extends State<ReplyPage>
tooltip: '图片', tooltip: '图片',
selected: false, selected: false,
icon: const Icon(Icons.image, size: 22), icon: const Icon(Icons.image, size: 22),
onPressed: () { onPressed: onPickImage,
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());
}
});
},
), ),
], ],
const Spacer(), const Spacer(),
StreamBuilder( Obx(
initialData: _enablePublish, () => FilledButton.tonal(
stream: _publishStream.stream, onPressed: enablePublish.value ? onPublish : null,
builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? submitReplyAdd : null,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10), horizontal: 20, vertical: 10),
@@ -412,95 +223,8 @@ class _ReplyPageState extends State<ReplyPage>
); );
} }
updatePanelType(PanelType type) async { @override
final isSwitchToKeyboard = PanelType.keyboard == type; Future onCustomPublish({required String message, List? pictures}) async {
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;
var result = await VideoHttp.replyAdd( var result = await VideoHttp.replyAdd(
type: widget.replyType ?? ReplyType.video, type: widget.replyType ?? ReplyType.video,
oid: widget.oid!, oid: widget.oid!,
@@ -518,22 +242,4 @@ class _ReplyPageState extends State<ReplyPage>
SmartDialog.showToast(result['msg']); 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);
}
} }

View File

@@ -317,8 +317,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
parent: root, parent: root,
replyType: widget.replyType, replyType: widget.replyType,
replyItem: item, replyItem: item,
savedReply: _savedReplies[key], initialValue: _savedReplies[key],
onSaveReply: (reply) { onSave: (reply) {
_savedReplies[key] = reply; _savedReplies[key] = reply;
}, },
); );

View File

@@ -1,34 +1,29 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/http/danmaku.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/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:PiliPlus/utils/extension.dart';
import 'package:canvas_danmaku/models/danmaku_content_item.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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class SendDanmakuPanel extends StatefulWidget { class SendDanmakuPanel extends CommonPublishPage {
final dynamic cid; final dynamic cid;
final dynamic bvid; final dynamic bvid;
final dynamic progress; final dynamic progress;
final String? savedDanmaku;
final ValueChanged<String>? onSaveDanmaku;
final ValueChanged<DanmakuContentItem> callback; final ValueChanged<DanmakuContentItem> callback;
const SendDanmakuPanel({ const SendDanmakuPanel({
super.key, super.key,
super.initialValue,
super.onSave,
required this.cid, required this.cid,
required this.bvid, required this.bvid,
required this.progress, required this.progress,
required this.savedDanmaku,
required this.onSaveDanmaku,
required this.callback, required this.callback,
}); });
@@ -36,14 +31,7 @@ class SendDanmakuPanel extends StatefulWidget {
State<SendDanmakuPanel> createState() => _SendDanmakuPanelState(); State<SendDanmakuPanel> createState() => _SendDanmakuPanelState();
} }
class _SendDanmakuPanelState extends State<SendDanmakuPanel> class _SendDanmakuPanelState extends CommonPublishPageState<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;
final RxInt _mode = 1.obs; final RxInt _mode = 1.obs;
final RxInt _fontsize = 25.obs; final RxInt _fontsize = 25.obs;
final Rx<Color> _color = Colors.white.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQuery.removePadding( return MediaQuery.removePadding(
@@ -192,7 +129,7 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
_buildInputView(), _buildInputView(),
_buildPanelContainer(), buildPanelContainer(),
], ],
), ),
), ),
@@ -204,80 +141,57 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
); );
} }
Widget _buildPanelContainer() { @override
return ChatBottomPanelContainer<PanelType>( Widget? customPanel(double height) => Container(
controller: _controller, height: height,
inputFocusNode: _focusNode, padding: const EdgeInsets.symmetric(horizontal: 16),
otherPanelWidget: (type) { decoration: BoxDecoration(
if (type == null) return const SizedBox.shrink(); border: Border(
switch (type) { top: BorderSide(
case PanelType.emoji: color: Theme.of(context).colorScheme.outline.withOpacity(0.1),
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),
), ),
), ),
), child: SingleChildScrollView(
child: SingleChildScrollView( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const SizedBox(height: 12),
const SizedBox(height: 12), Row(
Row( children: [
children: [ const Text('弹幕字号', style: TextStyle(fontSize: 15)),
const Text('弹幕字号', style: TextStyle(fontSize: 15)), const SizedBox(width: 16),
const SizedBox(width: 16), _buildFontSizeItem(18, ''),
_buildFontSizeItem(18, ''), const SizedBox(width: 5),
const SizedBox(width: 5), _buildFontSizeItem(25, '标准'),
_buildFontSizeItem(25, '标准'), ],
], ),
), const SizedBox(height: 12),
const SizedBox(height: 12), Row(
Row( children: [
children: [ const Text('弹幕样式', style: TextStyle(fontSize: 15)),
const Text('弹幕样式', style: TextStyle(fontSize: 15)), const SizedBox(width: 16),
const SizedBox(width: 16), _buildPositionItem(1, '滚动'),
_buildPositionItem(1, '滚动'), const SizedBox(width: 5),
const SizedBox(width: 5), _buildPositionItem(5, '顶部'),
_buildPositionItem(5, '顶部'), const SizedBox(width: 5),
const SizedBox(width: 5), _buildPositionItem(4, '底部'),
_buildPositionItem(4, '底部'), ],
], ),
), const SizedBox(height: 12),
const SizedBox(height: 12), Row(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const Text('弹幕颜色', style: TextStyle(fontSize: 15)),
const Text('弹幕颜色', style: TextStyle(fontSize: 15)), const SizedBox(width: 16),
const SizedBox(width: 16), _buildColorPanel,
_buildColorPanel, ],
], ),
), const SizedBox(height: 12),
const SizedBox(height: 12), ],
], ),
), ),
), );
);
}
Widget _buildColorItem(Color color) { Widget _buildColorItem(Color color) {
return GestureDetector( return GestureDetector(
@@ -392,18 +306,18 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
context: context, context: context,
tooltip: '弹幕样式', tooltip: '弹幕样式',
onPressed: () { onPressed: () {
if (_selectKeyboard.value) { if (selectKeyboard.value) {
_selectKeyboard.value = false; selectKeyboard.value = false;
updatePanelType(PanelType.emoji); updatePanelType(PanelType.emoji);
} else { } else {
_selectKeyboard.value = true; selectKeyboard.value = true;
updatePanelType(PanelType.keyboard); updatePanelType(PanelType.keyboard);
} }
}, },
bgColor: Colors.transparent, bgColor: Colors.transparent,
iconSize: 24, iconSize: 24,
icon: Icons.text_format, icon: Icons.text_format,
iconColor: _selectKeyboard.value.not iconColor: selectKeyboard.value.not
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant, : Theme.of(context).colorScheme.onSurfaceVariant,
), ),
@@ -414,35 +328,35 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener( child: Listener(
onPointerUp: (event) { onPointerUp: (event) {
if (_readOnly.value) { if (readOnly.value) {
updatePanelType(PanelType.keyboard); updatePanelType(PanelType.keyboard);
_selectKeyboard.value = true; selectKeyboard.value = true;
} }
}, },
child: Obx( child: Obx(
() => TextField( () => TextField(
controller: _textController, controller: editController,
autofocus: false, autofocus: false,
readOnly: _readOnly.value, readOnly: readOnly.value,
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(100), LengthLimitingTextInputFormatter(100),
], ],
onChanged: (value) { onChanged: (value) {
bool isEmpty = value.trim().isEmpty; bool isEmpty = value.trim().isEmpty;
if (!isEmpty && !_enablePublish.value) { if (!isEmpty && !enablePublish.value) {
_enablePublish.value = true; enablePublish.value = true;
} else if (isEmpty && _enablePublish.value) { } else if (isEmpty && enablePublish.value) {
_enablePublish.value = false; enablePublish.value = false;
} }
widget.onSaveDanmaku?.call(value); widget.onSave?.call(value);
}, },
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onSubmitted: (value) { onSubmitted: (value) {
if (value.trim().isNotEmpty) { if (value.trim().isNotEmpty) {
onSendDanmaku(); onPublish();
} }
}, },
focusNode: _focusNode, focusNode: focusNode,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "输入弹幕内容", hintText: "输入弹幕内容",
border: InputBorder.none, border: InputBorder.none,
@@ -464,10 +378,10 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
tooltip: '发送', tooltip: '发送',
bgColor: Colors.transparent, bgColor: Colors.transparent,
iconSize: 22, iconSize: 22,
iconColor: _enablePublish.value iconColor: enablePublish.value
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
onPressed: _enablePublish.value ? onSendDanmaku : null, onPressed: enablePublish.value ? onPublish : null,
icon: Icons.send, 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() { void _showColorPicker() {
_controller.keepChatPanel(); controller.keepChatPanel();
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( 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']}');
}
}
} }

View File

@@ -1,13 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/msg.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/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
import 'package:PiliPlus/utils/extension.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/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -16,55 +13,22 @@ import 'package:mime/mime.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/pages/whisper_detail/controller.dart'; import 'package:PiliPlus/pages/whisper_detail/controller.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/models/video/reply/emote.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'widget/chat_item.dart'; import 'widget/chat_item.dart';
class WhisperDetailPage extends StatefulWidget { class WhisperDetailPage extends CommonPublishPage {
const WhisperDetailPage({super.key}); const WhisperDetailPage({
super.key,
super.autofocus = false,
});
@override @override
State<WhisperDetailPage> createState() => _WhisperDetailPageState(); State<WhisperDetailPage> createState() => _WhisperDetailPageState();
} }
class _WhisperDetailPageState extends State<WhisperDetailPage> { class _WhisperDetailPageState
extends CommonPublishPageState<WhisperDetailPage> {
final _whisperDetailController = Get.put(WhisperDetailController()); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -135,7 +99,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
children: [ children: [
Expanded(child: _buildList()), Expanded(child: _buildList()),
_buildInputView(), _buildInputView(),
_buildPanelContainer(), buildPanelContainer(Theme.of(context).colorScheme.onInverseSurface),
], ],
), ),
); );
@@ -177,66 +141,6 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
return resultWidget; 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() { Widget _buildInputView() {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@@ -253,7 +157,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
IconButton( IconButton(
onPressed: () async { onPressed: () async {
updatePanelType( updatePanelType(
PanelType.emoji == _currentPanelType PanelType.emoji == currentPanelType
? PanelType.keyboard ? PanelType.keyboard
: PanelType.emoji, : PanelType.emoji,
); );
@@ -265,27 +169,23 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
child: Listener( child: Listener(
onPointerUp: (event) { onPointerUp: (event) {
// Currently it may be emojiPanel. // Currently it may be emojiPanel.
if (_readOnly) { if (readOnly.value) {
updatePanelType(PanelType.keyboard); updatePanelType(PanelType.keyboard);
} }
}, },
child: StreamBuilder( child: Obx(
initialData: false, () => TextField(
stream: _readOnlyStream.stream, readOnly: readOnly.value,
builder: (context, snapshot) => TextField( focusNode: focusNode,
readOnly: snapshot.data ?? false,
focusNode: _focusNode,
controller: _whisperDetailController.replyContentController, controller: _whisperDetailController.replyContentController,
minLines: 1, minLines: 1,
maxLines: 4, maxLines: 4,
onChanged: (value) { onChanged: (value) {
bool isNotEmpty = value.trim().isNotEmpty; bool isNotEmpty = value.trim().isNotEmpty;
if (isNotEmpty && !_visibleSend) { if (isNotEmpty && !enablePublish.value) {
_visibleSend = true; enablePublish.value = true;
_enableSend.add(true); } else if (!isNotEmpty && enablePublish.value) {
} else if (!isNotEmpty && _visibleSend) { enablePublish.value = false;
_visibleSend = false;
_enableSend.add(false);
} }
}, },
textInputAction: TextInputAction.newline, textInputAction: TextInputAction.newline,
@@ -304,16 +204,15 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
), ),
), ),
), ),
StreamBuilder( Obx(
stream: _enableSend.stream, () {
builder: (context, snapshot) {
return IconButton( return IconButton(
onPressed: () async { onPressed: () async {
if (snapshot.data == true) { if (enablePublish.value) {
_whisperDetailController.sendMsg(); _whisperDetailController.sendMsg();
} else { } else {
try { try {
XFile? pickedFile = await _imagePicker.pickImage( XFile? pickedFile = await imagePicker.pickImage(
source: ImageSource.gallery, source: ImageSource.gallery,
imageQuality: 100, imageQuality: 100,
); );
@@ -351,10 +250,10 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
} }
} }
}, },
icon: Icon(snapshot.data == true icon: Icon(enablePublish.value
? Icons.send ? Icons.send
: Icons.add_photo_alternate_outlined), : Icons.add_photo_alternate_outlined),
tooltip: snapshot.data == true ? '发送' : '图片', tooltip: enablePublish.value ? '发送' : '图片',
); );
}, },
), ),
@@ -363,57 +262,14 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
); );
} }
Widget _buildEmojiPickerPanel() { @override
double height = 300; Widget? customPanel(double height) => SizedBox(
final keyboardHeight = _controller.keyboardHeight; height: height,
if (keyboardHeight != 0) { child: EmotePanel(onChoose: onChooseEmote),
height = max(200, keyboardHeight); );
}
return SizedBox( @override
height: height, Future onCustomPublish({required String message, List? pictures}) {
child: EmotePanel( throw UnimplementedError();
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,
);
} }
} }

View File

@@ -75,11 +75,13 @@ extension BuildContextExt on BuildContext {
int? initialPage, int? initialPage,
required List<String> imgList, required List<String> imgList,
ValueChanged<int>? onDismissed, ValueChanged<int>? onDismissed,
bool? isFile,
}) { }) {
Navigator.of(this).push( Navigator.of(this).push(
HeroDialogRoute( HeroDialogRoute(
builder: (context) => InteractiveviewerGallery( builder: (context) => InteractiveviewerGallery(
sources: imgList, sources: imgList,
isFile: isFile,
initIndex: initialPage ?? 0, initIndex: initialPage ?? 0,
onPageChanged: (int pageIndex) {}, onPageChanged: (int pageIndex) {},
onDismissed: onDismissed, onDismissed: onDismissed,