From a115b5e91b42e79cd20b2f32fe67652de2560a2c Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Fri, 24 Jan 2025 18:14:19 +0800 Subject: [PATCH] mod: refine reply/publish page Signed-off-by: bggRGjQaUbCoE --- .../interactiveviewer_gallery.dart | 169 ++-- lib/pages/common/common_publish_page.dart | 373 ++++++++ lib/pages/common/reply_controller.dart | 8 +- lib/pages/dynamics/create_dyn_panel.dart | 829 +++++++++--------- lib/pages/dynamics/repost_dyn_panel.dart | 566 ++++++------ lib/pages/emote/controller.dart | 6 + .../member/new/widget/edit_profile_page.dart | 5 +- lib/pages/video/detail/controller.dart | 4 +- .../introduction/widgets/create_fav_page.dart | 5 +- .../video/detail/reply_new/reply_page.dart | 398 ++------- lib/pages/video/detail/reply_reply/view.dart | 4 +- .../detail/widgets/send_danmaku_panel.dart | 352 +++----- lib/pages/whisper_detail/view.dart | 210 +---- lib/utils/extension.dart | 2 + 14 files changed, 1394 insertions(+), 1537 deletions(-) create mode 100644 lib/pages/common/common_publish_page.dart diff --git a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart index feb1bb6d..bf6cb61f 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart @@ -45,8 +45,11 @@ class InteractiveviewerGallery 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 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 _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 ), ) : 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, - ); - }, - 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 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; - // }); - // }); - // }, - ), ), ); } diff --git a/lib/pages/common/common_publish_page.dart b/lib/pages/common/common_publish_page.dart new file mode 100644 index 00000000..f12d7e44 --- /dev/null +++ b/lib/pages/common/common_publish_page.dart @@ -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? onSave; + final bool autofocus; + + @override + State createState(); +} + +abstract class CommonPublishPageState + extends State with WidgetsBindingObserver { + late final focusNode = FocusNode(); + late final controller = ChatBottomPanelContainerController(); + 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 pathList = [].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( + 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 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()); + } + }); + } +} diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index 1f099db6..451deee1 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -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; }, ); diff --git a/lib/pages/dynamics/create_dyn_panel.dart b/lib/pages/dynamics/create_dyn_panel.dart index ee49e279..f68b90ad 100644 --- a/lib/pages/dynamics/create_dyn_panel.dart +++ b/lib/pages/dynamics/create_dyn_panel.dart @@ -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 createState() => _CreateDynPanelState(); } -class _CreateDynPanelState extends State { - final _ctr = TextEditingController(); - late final _imagePicker = ImagePicker(); - late final int _limit = 18; - - final RxBool _isEnablePub = false.obs; - late final RxList _pathList = [].obs; - +class _CreateDynPanelState extends CommonPublishPageState { bool _isPrivate = false; DateTime? _publishTime; ReplyOption _replyOption = ReplyOption.allow; @override void dispose() { - _ctr.dispose(); + try { + Get.delete(); + } 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 { 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 { ], ), ), - ), - 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( + 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( + 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( - 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( - 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 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']}'); + } } } diff --git a/lib/pages/dynamics/repost_dyn_panel.dart b/lib/pages/dynamics/repost_dyn_panel.dart index 5ed74667..5a72877c 100644 --- a/lib/pages/dynamics/repost_dyn_panel.dart +++ b/lib/pages/dynamics/repost_dyn_panel.dart @@ -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 createState() => _RepostPanelState(); } -class _RepostPanelState extends State { +class _RepostPanelState extends CommonPublishPageState { 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(); + } 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 { 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 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']); + } } } diff --git a/lib/pages/emote/controller.dart b/lib/pages/emote/controller.dart index 1d859ad6..7fb2790a 100644 --- a/lib/pages/emote/controller.dart +++ b/lib/pages/emote/controller.dart @@ -26,4 +26,10 @@ class EmotePanelController extends CommonController @override Future customGetData() => ReplyHttp.getEmoteList(business: 'reply'); + + @override + void onClose() { + tabController.dispose(); + super.onClose(); + } } diff --git a/lib/pages/member/new/widget/edit_profile_page.dart b/lib/pages/member/new/widget/edit_profile_page.dart index 13de9f1d..e559bd69 100644 --- a/lib/pages/member/new/widget/edit_profile_page.dart +++ b/lib/pages/member/new/widget/edit_profile_page.dart @@ -458,8 +458,9 @@ class _EditProfilePageState extends State { 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(), ], diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 0d89c04f..3e98fffe 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -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); diff --git a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart index c98de29b..170428d5 100644 --- a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart +++ b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart @@ -125,8 +125,9 @@ class _CreateFavPageState extends State { 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, ], diff --git a/lib/pages/video/detail/reply_new/reply_page.dart b/lib/pages/video/detail/reply_new/reply_page.dart index d67bf1c1..7f756766 100644 --- a/lib/pages/video/detail/reply_new/reply_page.dart +++ b/lib/pages/video/detail/reply_new/reply_page.dart @@ -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 createState() => _ReplyPageState(); } -class _ReplyPageState extends State - with SingleTickerProviderStateMixin, WidgetsBindingObserver { - late final _focusNode = FocusNode(); - late final _controller = ChatBottomPanelContainerController(); - late final TextEditingController _replyContentController = - TextEditingController(text: widget.savedReply); - // PanelType _currentPanelType = PanelType.none; - bool _readOnly = false; - final _readOnlyStream = StreamController(); - late final _enableSend = StreamController(); - bool _enablePublish = false; - final _publishStream = StreamController(); - bool _selectKeyboard = true; - final _keyboardStream = StreamController.broadcast(); - late final _imagePicker = ImagePicker(); - late final _pathStream = StreamController>(); - late final _pathList = []; - 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 { @override Widget build(BuildContext context) { return MediaQuery.removePadding( @@ -140,9 +55,9 @@ class _ReplyPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - _buildInputView(), - _buildImagePreview(), - _buildPanelContainer(), + buildInputView(), + buildImagePreview(), + buildPanelContainer(), ], ), ), @@ -154,63 +69,16 @@ class _ReplyPageState extends State ); } - Widget _buildPanelContainer() { - return ChatBottomPanelContainer( - 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 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 ); } - 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 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 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 tooltip: '图片', selected: false, icon: const Icon(Icons.image, size: 22), - onPressed: () { - EasyThrottle.throttle( - 'imagePicker', const Duration(milliseconds: 500), - () async { - try { - List 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 ); } - 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 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); - } } diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 5b6d14be..71b19a10 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -317,8 +317,8 @@ class _VideoReplyReplyPanelState extends State parent: root, replyType: widget.replyType, replyItem: item, - savedReply: _savedReplies[key], - onSaveReply: (reply) { + initialValue: _savedReplies[key], + onSave: (reply) { _savedReplies[key] = reply; }, ); diff --git a/lib/pages/video/detail/widgets/send_danmaku_panel.dart b/lib/pages/video/detail/widgets/send_danmaku_panel.dart index 9a27bad2..21ab33ab 100644 --- a/lib/pages/video/detail/widgets/send_danmaku_panel.dart +++ b/lib/pages/video/detail/widgets/send_danmaku_panel.dart @@ -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? onSaveDanmaku; final ValueChanged 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 createState() => _SendDanmakuPanelState(); } -class _SendDanmakuPanelState extends State - with SingleTickerProviderStateMixin, WidgetsBindingObserver { - late final _focusNode = FocusNode(); - late final _controller = ChatBottomPanelContainerController(); - 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 { final RxInt _mode = 1.obs; final RxInt _fontsize = 25.obs; final Rx _color = Colors.white.obs; @@ -119,57 +107,6 @@ class _SendDanmakuPanelState extends State ), ); - @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 mainAxisAlignment: MainAxisAlignment.end, children: [ _buildInputView(), - _buildPanelContainer(), + buildPanelContainer(), ], ), ), @@ -204,80 +141,57 @@ class _SendDanmakuPanelState extends State ); } - Widget _buildPanelContainer() { - return ChatBottomPanelContainer( - 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 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 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 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 ); } - 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 ), ); } + + @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']}'); + } + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 98788f53..9374375c 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -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 createState() => _WhisperDetailPageState(); } -class _WhisperDetailPageState extends State { +class _WhisperDetailPageState + extends CommonPublishPageState { final _whisperDetailController = Get.put(WhisperDetailController()); - late final _controller = ChatBottomPanelContainerController(); - late final _focusNode = FocusNode(); - PanelType _currentPanelType = PanelType.none; - bool _readOnly = false; - final _readOnlyStream = StreamController(); - late final _enableSend = StreamController(); - 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 { children: [ Expanded(child: _buildList()), _buildInputView(), - _buildPanelContainer(), + buildPanelContainer(Theme.of(context).colorScheme.onInverseSurface), ], ), ); @@ -177,66 +141,6 @@ class _WhisperDetailPageState extends State { 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 { IconButton( onPressed: () async { updatePanelType( - PanelType.emoji == _currentPanelType + PanelType.emoji == currentPanelType ? PanelType.keyboard : PanelType.emoji, ); @@ -265,27 +169,23 @@ class _WhisperDetailPageState extends State { 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 { ), ), ), - 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 { } } }, - 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 { ); } - 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( - 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(); } } diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index dfdf947b..93c3647f 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -75,11 +75,13 @@ extension BuildContextExt on BuildContext { int? initialPage, required List imgList, ValueChanged? onDismissed, + bool? isFile, }) { Navigator.of(this).push( HeroDialogRoute( builder: (context) => InteractiveviewerGallery( sources: imgList, + isFile: isFile, initIndex: initialPage ?? 0, onPageChanged: (int pageIndex) {}, onDismissed: onDismissed,