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.setStatusBar,
this.onClose,
this.isFile,
});
final bool? isFile;
final VoidCallback? onClose;
final bool? setStatusBar;
@@ -137,9 +140,11 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
}
}
if (widget.isFile != true) {
for (int index = 0; index < widget.sources.length; index++) {
CachedNetworkImageProvider(_getActualUrl(index)).evict();
}
}
super.dispose();
}
@@ -267,7 +272,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
_doubleTapLocalPosition = details.localPosition;
},
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onLongPress: widget.isFile == true ? null : onLongPress,
child: widget.itemBuilder != null
? widget.itemBuilder!(
context,
@@ -303,25 +308,34 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
),
)
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Stack(
alignment: Alignment.center,
children: [
IconButton(
Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: onClose,
),
widget.sources.length > 1
? Text(
),
if (widget.sources.length > 1)
Align(
alignment: Alignment.center,
child: Text(
"${currentIndex! + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
)
: const SizedBox(),
PopupMenuButton(
),
),
if (widget.isFile != true)
Align(
alignment: Alignment.centerRight,
child: PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
value: 0,
onTap: () => onShareImg(widget.sources[currentIndex!]),
onTap: () =>
onShareImg(widget.sources[currentIndex!]),
child: const Text("分享图片"),
),
PopupMenuItem(
@@ -347,7 +361,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
onTap: () {
DownloadUtils.downloadImg(
context,
widget.sources as List<String>,
widget.sources,
);
},
child: const Text("保存全部图片"),
@@ -356,6 +370,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
},
child: const Icon(Icons.more_horiz, color: Colors.white),
),
),
],
),
),
@@ -382,7 +397,12 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
return Center(
child: Hero(
tag: widget.sources[index],
child: CachedNetworkImage(
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),
@@ -391,7 +411,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
return Center(
child: SizedBox(
width: 150.0,
child: LinearProgressIndicator(value: progress.progress ?? 0),
child: LinearProgressIndicator(
value: progress.progress ?? 0),
),
);
},

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,
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;
},
);

View File

@@ -1,115 +1,142 @@
import 'dart:io';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics/view.dart';
import 'package:PiliPlus/pages/emote/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
class CreateDynPanel extends StatefulWidget {
const CreateDynPanel({super.key});
class CreateDynPanel extends CommonPublishPage {
const CreateDynPanel({
super.key,
super.imageLengthLimit = 18,
});
@override
State<CreateDynPanel> createState() => _CreateDynPanelState();
}
class _CreateDynPanelState extends State<CreateDynPanel> {
final _ctr = TextEditingController();
late final _imagePicker = ImagePicker();
late final int _limit = 18;
final RxBool _isEnablePub = false.obs;
late final RxList<String> _pathList = <String>[].obs;
class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
bool _isPrivate = false;
DateTime? _publishTime;
ReplyOption _replyOption = ReplyOption.allow;
@override
void dispose() {
_ctr.dispose();
try {
Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose();
}
Future _onCreate() async {
// if (_pathList.isEmpty) {
// dynamic result = await MsgHttp.createTextDynamic(_ctr.text);
// if (result['status']) {
// Get.back();
// SmartDialog.showToast('发布成功');
// } else {
// SmartDialog.showToast(result['msg']);
// }
// } else {
List? pics;
if (_pathList.isNotEmpty) {
pics = [];
for (int i = 0; i < _pathList.length; i++) {
SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}');
dynamic result = await MsgHttp.uploadBfs(
path: _pathList[i],
category: 'daily',
biz: 'new_dyn',
);
if (result['status']) {
int imageSize = await File(_pathList[i]).length();
pics.add({
'img_width': result['data']['image_width'],
'img_height': result['data']['image_height'],
'img_size': imageSize / 1024,
'img_src': result['data']['image_url'],
});
} else {
SmartDialog.dismiss();
SmartDialog.showToast(result['msg']);
return;
}
if (i == _pathList.length - 1) {
SmartDialog.dismiss();
}
}
}
SmartDialog.showLoading(msg: '正在发布');
dynamic result = await MsgHttp.createDynamic(
mid: GStorage.userInfo.get('userInfoCache')?.mid,
rawText: _ctr.text,
pics: pics,
publishTime: _publishTime != null
? _publishTime!.millisecondsSinceEpoch ~/ 1000
: null,
replyOption: _replyOption,
privatePub: _isPrivate ? 1 : null,
);
if (result['status']) {
Get.back();
SmartDialog.dismiss();
SmartDialog.showToast('发布成功');
} else {
SmartDialog.dismiss();
SmartDialog.showToast(result['msg']);
}
// }
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
resizeToAvoidBottomInset: false,
appBar: _buildAppBar,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildEditWidget,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildPubtimeWidget,
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReplyOptionWidget,
const SizedBox(height: 5),
_buildPrivateWidget,
],
),
],
),
),
const SizedBox(height: 10),
_buildImageList,
const SizedBox(height: 2),
],
),
),
_buildToolbar,
buildPanelContainer(Colors.transparent),
],
),
);
}
Widget get _buildImageList => Obx(
() => SizedBox(
height: 100,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: pathList.length == limit ? limit : pathList.length + 1,
itemBuilder: (context, index) {
if (pathList.length != limit && index == pathList.length) {
return Material(
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
onPickImage(() {
if (pathList.isNotEmpty && !enablePublish.value) {
enablePublish.value = true;
}
});
},
child: Ink(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: Center(child: Icon(Icons.add, size: 35)),
),
),
);
} else {
return buildImage(index, 100);
}
},
separatorBuilder: (context, index) => const SizedBox(width: 10),
),
),
);
PreferredSizeWidget get _buildAppBar => PreferredSize(
preferredSize: Size.fromHeight(66),
child: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 0,
left: 0,
bottom: 0,
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 34,
height: 34,
@@ -140,12 +167,11 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
),
Positioned(
top: 0,
right: 0,
Align(
alignment: Alignment.centerRight,
child: Obx(
() => FilledButton.tonal(
onPressed: _isEnablePub.value ? _onCreate : null,
onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.symmetric(
@@ -164,147 +190,72 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
],
),
),
),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
controller: _ctr,
minLines: 4,
maxLines: 8,
autofocus: true,
onChanged: (value) {
bool isEmpty = value.trim().isEmpty && _pathList.isEmpty;
if (!isEmpty && !_isEnablePub.value) {
_isEnablePub.value = true;
} else if (isEmpty && _isEnablePub.value) {
_isEnablePub.value = false;
}
);
Widget get _buildPrivateWidget => PopupMenuButton(
initialValue: _isPrivate,
onOpened: controller.keepChatPanel,
onSelected: (value) {
setState(() {
_isPrivate = value;
});
},
decoration: const InputDecoration(
hintText: '说点什么吧',
border: OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.zero,
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (context) => List.generate(
2,
(index) => PopupMenuItem<bool>(
enabled: _publishTime != null && index == 1 ? false : true,
value: index == 0 ? false : true,
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(
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;
@@ -361,167 +312,185 @@ class _CreateDynPanelState extends State<CreateDynPanel> {
],
),
),
);
Widget get _buildPubtimeWidget => _publishTime == null
? FilledButton.tonal(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
const SizedBox(height: 5),
PopupMenuButton(
initialValue: _isPrivate,
onSelected: (value) {
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;
}
}
}
setState(() {
_isPrivate = value;
_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;
});
},
itemBuilder: (context) => List.generate(
2,
(index) => PopupMenuItem<bool>(
enabled: _publishTime != null && index == 1
? false
: true,
value: index == 0 ? false : true,
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(
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(height: 10),
Obx(
() => SizedBox(
height: 100,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _pathList.length == _limit
? _limit
: _pathList.length + 1,
itemBuilder: (context, index) {
if (_pathList.length != _limit &&
index == _pathList.length) {
return Material(
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
EasyThrottle.throttle('imagePicker',
const Duration(milliseconds: 500), () async {
try {
List<XFile> pickedFiles =
await _imagePicker.pickMultiImage(
limit: _limit,
imageQuality: 100,
() => ToolbarIconButton(
onPressed: () {
selectKeyboard.value = PanelType.emoji == currentPanelType;
updatePanelType(
PanelType.emoji == currentPanelType
? PanelType.keyboard
: PanelType.emoji,
);
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;
}
}
} catch (e) {
SmartDialog.showToast(e.toString());
}
});
},
child: Ink(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context)
.colorScheme
.secondaryContainer,
icon: const Icon(Icons.emoji_emotions, size: 22),
tooltip: '表情',
selected: !selectKeyboard.value,
),
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),
),
),
),
SizedBox(
height: MediaQuery.paddingOf(context).bottom + 25,
),
],
),
);
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/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,37 +25,9 @@ class RepostPanel extends StatefulWidget {
State<RepostPanel> createState() => _RepostPanelState();
}
class _RepostPanelState extends State<RepostPanel> {
class _RepostPanelState extends CommonPublishPageState<RepostPanel> {
bool _isMax = false;
final _ctr = TextEditingController();
final _focusNode = FocusNode();
Future _onRepost() async {
dynamic result = await MsgHttp.createDynamic(
mid: GStorage.userInfo.get('userInfoCache')?.mid,
dynIdStr: widget.item.idStr,
rawText: _ctr.text,
);
if (result['status']) {
Get.back();
SmartDialog.showToast('转发成功');
widget.callback();
} else {
SmartDialog.showToast(result['msg']);
}
}
@override
void dispose() {
_ctr.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
dynamic pic = (widget.item as DynamicItemModel?)
late final dynamic _pic = (widget.item as DynamicItemModel?)
?.modules
?.moduleDynamic
?.major
@@ -70,6 +47,38 @@ class _RepostPanelState extends State<RepostPanel> {
?.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() {
try {
Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedSize(
alignment: Alignment.topCenter,
curve: Curves.ease,
@@ -79,97 +88,28 @@ class _RepostPanelState extends State<RepostPanel> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: _isMax ? 16 : 10),
if (!_isMax)
Row(
children: [
const SizedBox(width: 16),
const Text(
'转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton(
onPressed: _onRepost,
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),
_buildAppBar,
if (_isMax) Expanded(child: _buildEditPanel) else _buildEditPanel,
if (_isMax.not)
..._biuldDismiss
else ...[
_buildToolbar,
buildPanelContainer(Colors.transparent),
]
],
),
if (_isMax)
SizedBox(
height: 34,
child: Stack(
);
}
Widget get _buildEditPanel => Column(
mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min,
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(
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
width: double.infinity,
decoration: !_isMax
decoration: _isMax.not
? BoxDecoration(
border: Border(
left: BorderSide(
@@ -179,42 +119,16 @@ class _RepostPanelState extends State<RepostPanel> {
),
)
: 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),
),
child: _isMax.not ? _buildEditPlaceHolder : _buildEditWidget,
),
),
),
const SizedBox(height: 10),
Container(
_buildRefWidget,
],
);
Widget get _buildRefWidget => Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -226,12 +140,12 @@ class _RepostPanelState extends State<RepostPanel> {
),
child: Row(
children: [
if (pic != null) ...[
if (_pic != null) ...[
NetworkImgLayer(
radius: 8,
width: 40,
height: 40,
src: pic,
src: _pic,
),
const SizedBox(width: 10),
],
@@ -248,31 +162,7 @@ class _RepostPanelState extends State<RepostPanel> {
),
),
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 ??
'',
_text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@@ -281,23 +171,197 @@ class _RepostPanelState extends State<RepostPanel> {
),
],
),
);
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,
),
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,
),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
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,
),
),
],
),
);
List<Widget> get _biuldDismiss => [
const SizedBox(height: 10),
if (!_isMax)
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),
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
Future<LoadingState> customGetData() =>
ReplyHttp.getEmoteList(business: 'reply');
@override
void onClose() {
tabController.dispose();
super.onClose();
}
}

View File

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

View File

@@ -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);

View File

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

View File

@@ -1,124 +1,39 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:chat_bottom_container/chat_bottom_container.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/models/video/reply/emote.dart';
import 'package:PiliPlus/pages/emote/index.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:image_picker/image_picker.dart';
enum PanelType { none, keyboard, emoji }
class ReplyPage extends StatefulWidget {
class ReplyPage extends CommonPublishPage {
final int? oid;
final int? root;
final int? parent;
final ReplyType? replyType;
final dynamic replyItem;
final String? savedReply;
final Function(String reply)? onSaveReply;
const ReplyPage({
super.key,
super.initialValue,
super.imageLengthLimit,
super.onSave,
this.oid,
this.root,
this.parent,
this.replyType,
this.replyItem,
this.savedReply,
this.onSaveReply,
});
@override
State<ReplyPage> createState() => _ReplyPageState();
}
class _ReplyPageState extends State<ReplyPage>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late final _focusNode = FocusNode();
late final _controller = ChatBottomPanelContainerController<PanelType>();
late final TextEditingController _replyContentController =
TextEditingController(text: widget.savedReply);
// PanelType _currentPanelType = PanelType.none;
bool _readOnly = false;
final _readOnlyStream = StreamController<bool>();
late final _enableSend = StreamController<bool>();
bool _enablePublish = false;
final _publishStream = StreamController<bool>();
bool _selectKeyboard = true;
final _keyboardStream = StreamController<bool>.broadcast();
late final _imagePicker = ImagePicker();
late final _pathStream = StreamController<List<String>>();
late final _pathList = <String>[];
late final _limit = 9;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (widget.savedReply != null && widget.savedReply!.isNotEmpty) {
_enablePublish = true;
}
() async {
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) {
_focusNode.requestFocus();
}
}();
}
@override
void dispose() async {
_keyboardStream.close();
_pathStream.close();
_publishStream.close();
_readOnlyStream.close();
_enableSend.close();
_focusNode.dispose();
_replyContentController.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void _requestFocus() async {
await Future.delayed(const Duration(microseconds: 200));
_focusNode.requestFocus();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
if (mounted && _selectKeyboard) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
_requestFocus();
} else {
_requestFocus();
}
});
}
} else if (state == AppLifecycleState.paused) {
_controller.keepChatPanel();
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
}
}
class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
@@ -140,9 +55,9 @@ class _ReplyPageState extends State<ReplyPage>
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildInputView(),
_buildImagePreview(),
_buildPanelContainer(),
buildInputView(),
buildImagePreview(),
buildPanelContainer(),
],
),
),
@@ -154,63 +69,16 @@ class _ReplyPageState extends State<ReplyPage>
);
}
Widget _buildPanelContainer() {
return ChatBottomPanelContainer<PanelType>(
controller: _controller,
inputFocusNode: _focusNode,
otherPanelWidget: (type) {
if (type == null) return const SizedBox.shrink();
switch (type) {
case PanelType.emoji:
return _buildEmojiPickerPanel();
default:
return const SizedBox.shrink();
}
},
// onPanelTypeChange: (panelType, data) {
// debugPrint('panelType: $panelType');
// switch (panelType) {
// case ChatBottomPanelType.none:
// _currentPanelType = PanelType.none;
// break;
// case ChatBottomPanelType.keyboard:
// _currentPanelType = PanelType.keyboard;
// break;
// case ChatBottomPanelType.other:
// if (data == null) return;
// switch (data) {
// case PanelType.emoji:
// _currentPanelType = PanelType.emoji;
// break;
// default:
// _currentPanelType = PanelType.none;
// break;
// }
// break;
// }
// },
panelBgColor: Theme.of(context).colorScheme.surface,
);
}
Widget _buildEmojiPickerPanel() {
double height = 170;
final keyboardHeight = _controller.keyboardHeight;
if (keyboardHeight != 0) {
height = max(height, keyboardHeight);
}
return SizedBox(
@override
Widget? customPanel(double height) => SizedBox(
height: height,
child: EmotePanel(onChoose: onChooseEmote),
);
}
Widget _buildImagePreview() {
return StreamBuilder(
initialData: const [],
stream: _pathStream.stream,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
Widget buildImagePreview() {
return Obx(
() {
if (pathList.isNotEmpty) {
return Container(
height: 85,
color: Theme.of(context).colorScheme.surface,
@@ -221,19 +89,8 @@ class _ReplyPageState extends State<ReplyPage>
parent: BouncingScrollPhysics(),
),
padding: const EdgeInsets.symmetric(horizontal: 15),
itemCount: _pathList.length,
itemBuilder: (context, index) => GestureDetector(
onTap: () {
_pathList.removeAt(index);
_pathStream.add(_pathList);
},
child: Image(
height: 75,
fit: BoxFit.fitHeight,
filterQuality: FilterQuality.low,
image: FileImage(File(_pathList[index])),
),
),
itemCount: pathList.length,
itemBuilder: (context, index) => buildImage(index, 75),
separatorBuilder: (context, index) => const SizedBox(width: 10),
),
);
@@ -244,7 +101,7 @@ class _ReplyPageState extends State<ReplyPage>
);
}
Widget _buildInputView() {
Widget buildInputView() {
return Container(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
@@ -265,35 +122,28 @@ class _ReplyPageState extends State<ReplyPage>
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener(
onPointerUp: (event) {
if (_readOnly) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
if (!_selectKeyboard) {
_selectKeyboard = true;
_keyboardStream.add(true);
}
selectKeyboard.value = true;
}
},
child: StreamBuilder(
initialData: false,
stream: _readOnlyStream.stream,
builder: (context, snapshot) => TextField(
controller: _replyContentController,
child: Obx(
() => TextField(
controller: editController,
minLines: 4,
maxLines: 8,
autofocus: false,
readOnly: snapshot.data ?? false,
readOnly: readOnly.value,
onChanged: (value) {
bool isEmpty = value.trim().isEmpty;
if (!isEmpty && !_enablePublish) {
_enablePublish = true;
_publishStream.add(true);
} else if (isEmpty && _enablePublish) {
_enablePublish = false;
_publishStream.add(false);
if (!isEmpty && !enablePublish.value) {
enablePublish.value = true;
} else if (isEmpty && enablePublish.value) {
enablePublish.value = false;
}
widget.onSaveReply?.call(value);
widget.onSave?.call(value);
},
focusNode: _focusNode,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: "输入回复内容",
border: InputBorder.none,
@@ -314,37 +164,31 @@ class _ReplyPageState extends State<ReplyPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamBuilder(
initialData: true,
stream: _keyboardStream.stream,
builder: (context, snapshot) => ToolbarIconButton(
Obx(
() => ToolbarIconButton(
tooltip: '输入',
onPressed: () {
if (!_selectKeyboard) {
_selectKeyboard = true;
_keyboardStream.add(true);
if (!selectKeyboard.value) {
selectKeyboard.value = true;
updatePanelType(PanelType.keyboard);
}
},
icon: const Icon(Icons.keyboard, size: 22),
selected: snapshot.data!,
selected: selectKeyboard.value,
),
),
const SizedBox(width: 20),
StreamBuilder(
initialData: true,
stream: _keyboardStream.stream,
builder: (context, snapshot) => ToolbarIconButton(
Obx(
() => ToolbarIconButton(
tooltip: '表情',
onPressed: () {
if (_selectKeyboard) {
_selectKeyboard = false;
_keyboardStream.add(false);
if (selectKeyboard.value) {
selectKeyboard.value = false;
updatePanelType(PanelType.emoji);
}
},
icon: const Icon(Icons.emoji_emotions, size: 22),
selected: !snapshot.data!,
selected: !selectKeyboard.value,
),
),
if (widget.root == 0) ...[
@@ -353,46 +197,13 @@ class _ReplyPageState extends State<ReplyPage>
tooltip: '图片',
selected: false,
icon: const Icon(Icons.image, size: 22),
onPressed: () {
EasyThrottle.throttle(
'imagePicker', const Duration(milliseconds: 500),
() async {
try {
List<XFile> pickedFiles =
await _imagePicker.pickMultiImage(
limit: _limit,
imageQuality: 100,
);
if (pickedFiles.isNotEmpty) {
for (int i = 0; i < pickedFiles.length; i++) {
if (_pathList.length == _limit) {
SmartDialog.showToast('最多选择$_limit张图片');
if (i != 0) {
_pathStream.add(_pathList);
}
break;
} else {
_pathList.add(pickedFiles[i].path);
if (i == pickedFiles.length - 1) {
SmartDialog.dismiss();
_pathStream.add(_pathList);
}
}
}
}
} catch (e) {
SmartDialog.showToast(e.toString());
}
});
},
onPressed: onPickImage,
),
],
const Spacer(),
StreamBuilder(
initialData: _enablePublish,
stream: _publishStream.stream,
builder: (context, snapshot) => FilledButton.tonal(
onPressed: snapshot.data == true ? submitReplyAdd : null,
Obx(
() => FilledButton.tonal(
onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
@@ -412,95 +223,8 @@ class _ReplyPageState extends State<ReplyPage>
);
}
updatePanelType(PanelType type) async {
final isSwitchToKeyboard = PanelType.keyboard == type;
final isSwitchToEmojiPanel = PanelType.emoji == type;
bool isUpdated = false;
switch (type) {
case PanelType.keyboard:
updateInputView(isReadOnly: false);
break;
case PanelType.emoji:
isUpdated = updateInputView(isReadOnly: true);
break;
default:
break;
}
updatePanelTypeFunc() {
_controller.updatePanelType(
isSwitchToKeyboard
? ChatBottomPanelType.keyboard
: ChatBottomPanelType.other,
data: type,
forceHandleFocus: isSwitchToEmojiPanel
? ChatBottomHandleFocus.requestFocus
: ChatBottomHandleFocus.none,
);
}
if (isUpdated) {
// Waiting for the input view to update.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
updatePanelTypeFunc();
});
} else {
updatePanelTypeFunc();
}
}
hidePanel() async {
if (_focusNode.hasFocus) {
await Future.delayed(const Duration(milliseconds: 100));
_focusNode.unfocus();
}
updateInputView(isReadOnly: false);
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
_controller.updatePanelType(ChatBottomPanelType.none);
}
bool updateInputView({
required bool isReadOnly,
}) {
if (_readOnly != isReadOnly) {
_readOnly = isReadOnly;
_readOnlyStream.add(_readOnly);
return true;
}
return false;
}
Future submitReplyAdd() async {
feedBack();
List? pictures;
if (_pathList.isNotEmpty) {
pictures = [];
for (int i = 0; i < _pathList.length; i++) {
SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}');
dynamic result = await MsgHttp.uploadBfs(
path: _pathList[i],
category: 'daily',
biz: 'new_dyn',
);
if (result['status']) {
int imageSize = await File(_pathList[i]).length();
pictures.add({
'img_width': result['data']['image_width'],
'img_height': result['data']['image_height'],
'img_size': imageSize / 1024,
'img_src': result['data']['image_url'],
});
} else {
SmartDialog.dismiss();
SmartDialog.showToast(result['msg']);
return;
}
if (i == _pathList.length - 1) {
SmartDialog.dismiss();
}
}
}
String message = _replyContentController.text;
@override
Future onCustomPublish({required String message, List? pictures}) async {
var result = await VideoHttp.replyAdd(
type: widget.replyType ?? ReplyType.video,
oid: widget.oid!,
@@ -518,22 +242,4 @@ class _ReplyPageState extends State<ReplyPage>
SmartDialog.showToast(result['msg']);
}
}
void onChooseEmote(Packages package, Emote emote) {
if (!_enablePublish) {
_enablePublish = true;
_publishStream.add(true);
}
final int cursorPosition = _replyContentController.selection.baseOffset;
final String currentText = _replyContentController.text;
final String newText = currentText.substring(0, cursorPosition) +
emote.text! +
currentText.substring(cursorPosition);
_replyContentController.value = TextEditingValue(
text: newText,
selection:
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
);
widget.onSaveReply?.call(_replyContentController.text);
}
}

View File

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

View File

@@ -1,34 +1,29 @@
import 'dart:async';
import 'dart:math';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/http/danmaku.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart'
show PanelType;
import 'package:PiliPlus/utils/extension.dart';
import 'package:canvas_danmaku/models/danmaku_content_item.dart';
import 'package:chat_bottom_container/chat_bottom_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class SendDanmakuPanel extends StatefulWidget {
class SendDanmakuPanel extends CommonPublishPage {
final dynamic cid;
final dynamic bvid;
final dynamic progress;
final String? savedDanmaku;
final ValueChanged<String>? onSaveDanmaku;
final ValueChanged<DanmakuContentItem> callback;
const SendDanmakuPanel({
super.key,
super.initialValue,
super.onSave,
required this.cid,
required this.bvid,
required this.progress,
required this.savedDanmaku,
required this.onSaveDanmaku,
required this.callback,
});
@@ -36,14 +31,7 @@ class SendDanmakuPanel extends StatefulWidget {
State<SendDanmakuPanel> createState() => _SendDanmakuPanelState();
}
class _SendDanmakuPanelState extends State<SendDanmakuPanel>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late final _focusNode = FocusNode();
late final _controller = ChatBottomPanelContainerController<PanelType>();
late final _textController = TextEditingController(text: widget.savedDanmaku);
final RxBool _readOnly = false.obs;
final RxBool _enablePublish = false.obs;
final RxBool _selectKeyboard = true.obs;
class _SendDanmakuPanelState extends CommonPublishPageState<SendDanmakuPanel> {
final RxInt _mode = 1.obs;
final RxInt _fontsize = 25.obs;
final Rx<Color> _color = Colors.white.obs;
@@ -119,57 +107,6 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
),
);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (widget.savedDanmaku.isNullOrEmpty.not) {
_enablePublish.value = true;
}
() async {
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) {
_focusNode.requestFocus();
}
}();
}
@override
void dispose() async {
_focusNode.dispose();
_textController.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void _requestFocus() async {
await Future.delayed(const Duration(microseconds: 200));
_focusNode.requestFocus();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
if (mounted && _selectKeyboard.value) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
_requestFocus();
} else {
_requestFocus();
}
});
}
} else if (state == AppLifecycleState.paused) {
_controller.keepChatPanel();
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
}
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
@@ -192,7 +129,7 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildInputView(),
_buildPanelContainer(),
buildPanelContainer(),
],
),
),
@@ -204,30 +141,8 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
);
}
Widget _buildPanelContainer() {
return ChatBottomPanelContainer<PanelType>(
controller: _controller,
inputFocusNode: _focusNode,
otherPanelWidget: (type) {
if (type == null) return const SizedBox.shrink();
switch (type) {
case PanelType.emoji:
return _buildEmojiPickerPanel();
default:
return const SizedBox.shrink();
}
},
panelBgColor: Theme.of(context).colorScheme.surface,
);
}
Widget _buildEmojiPickerPanel() {
double height = 170;
final keyboardHeight = _controller.keyboardHeight;
if (keyboardHeight != 0) {
height = max(height, keyboardHeight);
}
return Container(
@override
Widget? customPanel(double height) => Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -277,7 +192,6 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
),
),
);
}
Widget _buildColorItem(Color color) {
return GestureDetector(
@@ -392,18 +306,18 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
context: context,
tooltip: '弹幕样式',
onPressed: () {
if (_selectKeyboard.value) {
_selectKeyboard.value = false;
if (selectKeyboard.value) {
selectKeyboard.value = false;
updatePanelType(PanelType.emoji);
} else {
_selectKeyboard.value = true;
selectKeyboard.value = true;
updatePanelType(PanelType.keyboard);
}
},
bgColor: Colors.transparent,
iconSize: 24,
icon: Icons.text_format,
iconColor: _selectKeyboard.value.not
iconColor: selectKeyboard.value.not
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
@@ -414,35 +328,35 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener(
onPointerUp: (event) {
if (_readOnly.value) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
_selectKeyboard.value = true;
selectKeyboard.value = true;
}
},
child: Obx(
() => TextField(
controller: _textController,
controller: editController,
autofocus: false,
readOnly: _readOnly.value,
readOnly: readOnly.value,
inputFormatters: [
LengthLimitingTextInputFormatter(100),
],
onChanged: (value) {
bool isEmpty = value.trim().isEmpty;
if (!isEmpty && !_enablePublish.value) {
_enablePublish.value = true;
} else if (isEmpty && _enablePublish.value) {
_enablePublish.value = false;
if (!isEmpty && !enablePublish.value) {
enablePublish.value = true;
} else if (isEmpty && enablePublish.value) {
enablePublish.value = false;
}
widget.onSaveDanmaku?.call(value);
widget.onSave?.call(value);
},
textInputAction: TextInputAction.send,
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
onSendDanmaku();
onPublish();
}
},
focusNode: _focusNode,
focusNode: focusNode,
decoration: InputDecoration(
hintText: "输入弹幕内容",
border: InputBorder.none,
@@ -464,10 +378,10 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
tooltip: '发送',
bgColor: Colors.transparent,
iconSize: 22,
iconColor: _enablePublish.value
iconColor: enablePublish.value
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
onPressed: _enablePublish.value ? onSendDanmaku : null,
onPressed: enablePublish.value ? onPublish : null,
icon: Icons.send,
),
)
@@ -476,97 +390,8 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
);
}
updatePanelType(PanelType type) async {
final isSwitchToKeyboard = PanelType.keyboard == type;
final isSwitchToEmojiPanel = PanelType.emoji == type;
bool isUpdated = false;
switch (type) {
case PanelType.keyboard:
updateInputView(isReadOnly: false);
break;
case PanelType.emoji:
isUpdated = updateInputView(isReadOnly: true);
break;
default:
break;
}
updatePanelTypeFunc() {
_controller.updatePanelType(
isSwitchToKeyboard
? ChatBottomPanelType.keyboard
: ChatBottomPanelType.other,
data: type,
forceHandleFocus: isSwitchToEmojiPanel
? ChatBottomHandleFocus.requestFocus
: ChatBottomHandleFocus.none,
);
}
if (isUpdated) {
// Waiting for the input view to update.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
updatePanelTypeFunc();
});
} else {
updatePanelTypeFunc();
}
}
hidePanel() async {
if (_focusNode.hasFocus) {
await Future.delayed(const Duration(milliseconds: 100));
_focusNode.unfocus();
}
updateInputView(isReadOnly: false);
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
_controller.updatePanelType(ChatBottomPanelType.none);
}
bool updateInputView({
required bool isReadOnly,
}) {
if (_readOnly.value != isReadOnly) {
_readOnly.value = isReadOnly;
return true;
}
return false;
}
Future onSendDanmaku() async {
SmartDialog.showLoading(msg: '发送中...');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: widget.cid,
bvid: widget.bvid,
progress: widget.progress,
msg: _textController.text,
mode: _mode.value,
fontsize: _fontsize.value,
color: _color.value.value & 0xFFFFFF,
);
SmartDialog.dismiss();
if (res['status']) {
Get.back();
SmartDialog.showToast('发送成功');
widget.callback(
DanmakuContentItem(
_textController.text,
color: _color.value,
type: switch (_mode.value) {
5 => DanmakuItemType.top,
4 => DanmakuItemType.bottom,
_ => DanmakuItemType.scroll,
},
selfSend: true,
),
);
} else {
SmartDialog.showToast('发送失败: ${res['msg']}');
}
}
void _showColorPicker() {
_controller.keepChatPanel();
controller.keepChatPanel();
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -611,4 +436,37 @@ class _SendDanmakuPanelState extends State<SendDanmakuPanel>
),
);
}
@override
Future onCustomPublish({required String message, List? pictures}) async {
SmartDialog.showLoading(msg: '发送中...');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: widget.cid,
bvid: widget.bvid,
progress: widget.progress,
msg: editController.text,
mode: _mode.value,
fontsize: _fontsize.value,
color: _color.value.value & 0xFFFFFF,
);
SmartDialog.dismiss();
if (res['status']) {
Get.back();
SmartDialog.showToast('发送成功');
widget.callback(
DanmakuContentItem(
editController.text,
color: _color.value,
type: switch (_mode.value) {
5 => DanmakuItemType.top,
4 => DanmakuItemType.bottom,
_ => DanmakuItemType.scroll,
},
selfSend: true,
),
);
} else {
SmartDialog.showToast('发送失败: ${res['msg']}');
}
}
}

View File

@@ -1,13 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:chat_bottom_container/panel_container.dart';
import 'package:chat_bottom_container/typedef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -16,55 +13,22 @@ import 'package:mime/mime.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/pages/whisper_detail/controller.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/models/video/reply/emote.dart';
import 'package:image_picker/image_picker.dart';
import 'widget/chat_item.dart';
class WhisperDetailPage extends StatefulWidget {
const WhisperDetailPage({super.key});
class WhisperDetailPage extends CommonPublishPage {
const WhisperDetailPage({
super.key,
super.autofocus = false,
});
@override
State<WhisperDetailPage> createState() => _WhisperDetailPageState();
}
class _WhisperDetailPageState extends State<WhisperDetailPage> {
class _WhisperDetailPageState
extends CommonPublishPageState<WhisperDetailPage> {
final _whisperDetailController = Get.put(WhisperDetailController());
late final _controller = ChatBottomPanelContainerController<PanelType>();
late final _focusNode = FocusNode();
PanelType _currentPanelType = PanelType.none;
bool _readOnly = false;
final _readOnlyStream = StreamController<bool>();
late final _enableSend = StreamController<bool>();
late bool _visibleSend = false;
late final _imagePicker = ImagePicker();
@override
void dispose() {
_readOnlyStream.close();
_enableSend.close();
_focusNode.dispose();
super.dispose();
}
void onChooseEmote(Packages package, Emote emote) {
if (!_visibleSend) {
_visibleSend = true;
_enableSend.add(true);
}
int cursorPosition =
_whisperDetailController.replyContentController.selection.baseOffset;
if (cursorPosition == -1) cursorPosition = 0;
final String currentText =
_whisperDetailController.replyContentController.text;
final String newText = currentText.substring(0, cursorPosition) +
emote.text! +
currentText.substring(cursorPosition);
_whisperDetailController.replyContentController.value = TextEditingValue(
text: newText,
selection:
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
);
}
@override
Widget build(BuildContext context) {
@@ -135,7 +99,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
children: [
Expanded(child: _buildList()),
_buildInputView(),
_buildPanelContainer(),
buildPanelContainer(Theme.of(context).colorScheme.onInverseSurface),
],
),
);
@@ -177,66 +141,6 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
return resultWidget;
}
hidePanel() async {
if (_focusNode.hasFocus) {
await Future.delayed(const Duration(milliseconds: 100));
_focusNode.unfocus();
}
updateInputView(isReadOnly: false);
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
_controller.updatePanelType(ChatBottomPanelType.none);
}
bool updateInputView({
required bool isReadOnly,
}) {
if (_readOnly != isReadOnly) {
_readOnly = isReadOnly;
_readOnlyStream.add(_readOnly);
return true;
}
return false;
}
updatePanelType(PanelType type) async {
final isSwitchToKeyboard = PanelType.keyboard == type;
final isSwitchToEmojiPanel = PanelType.emoji == type;
bool isUpdated = false;
switch (type) {
case PanelType.keyboard:
updateInputView(isReadOnly: false);
break;
case PanelType.emoji:
isUpdated = updateInputView(isReadOnly: true);
break;
default:
break;
}
updatePanelTypeFunc() {
_controller.updatePanelType(
isSwitchToKeyboard
? ChatBottomPanelType.keyboard
: ChatBottomPanelType.other,
data: type,
forceHandleFocus: isSwitchToEmojiPanel
? ChatBottomHandleFocus.requestFocus
: ChatBottomHandleFocus.none,
);
}
if (isUpdated) {
// Waiting for the input view to update.
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) {
updatePanelTypeFunc();
},
);
} else {
updatePanelTypeFunc();
}
}
Widget _buildInputView() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -253,7 +157,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
IconButton(
onPressed: () async {
updatePanelType(
PanelType.emoji == _currentPanelType
PanelType.emoji == currentPanelType
? PanelType.keyboard
: PanelType.emoji,
);
@@ -265,27 +169,23 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
child: Listener(
onPointerUp: (event) {
// Currently it may be emojiPanel.
if (_readOnly) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
}
},
child: StreamBuilder(
initialData: false,
stream: _readOnlyStream.stream,
builder: (context, snapshot) => TextField(
readOnly: snapshot.data ?? false,
focusNode: _focusNode,
child: Obx(
() => TextField(
readOnly: readOnly.value,
focusNode: focusNode,
controller: _whisperDetailController.replyContentController,
minLines: 1,
maxLines: 4,
onChanged: (value) {
bool isNotEmpty = value.trim().isNotEmpty;
if (isNotEmpty && !_visibleSend) {
_visibleSend = true;
_enableSend.add(true);
} else if (!isNotEmpty && _visibleSend) {
_visibleSend = false;
_enableSend.add(false);
if (isNotEmpty && !enablePublish.value) {
enablePublish.value = true;
} else if (!isNotEmpty && enablePublish.value) {
enablePublish.value = false;
}
},
textInputAction: TextInputAction.newline,
@@ -304,16 +204,15 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
),
),
),
StreamBuilder(
stream: _enableSend.stream,
builder: (context, snapshot) {
Obx(
() {
return IconButton(
onPressed: () async {
if (snapshot.data == true) {
if (enablePublish.value) {
_whisperDetailController.sendMsg();
} else {
try {
XFile? pickedFile = await _imagePicker.pickImage(
XFile? pickedFile = await imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 100,
);
@@ -351,10 +250,10 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
}
}
},
icon: Icon(snapshot.data == true
icon: Icon(enablePublish.value
? Icons.send
: Icons.add_photo_alternate_outlined),
tooltip: snapshot.data == true ? '发送' : '图片',
tooltip: enablePublish.value ? '发送' : '图片',
);
},
),
@@ -363,57 +262,14 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
);
}
Widget _buildEmojiPickerPanel() {
double height = 300;
final keyboardHeight = _controller.keyboardHeight;
if (keyboardHeight != 0) {
height = max(200, keyboardHeight);
}
return SizedBox(
@override
Widget? customPanel(double height) => SizedBox(
height: height,
child: EmotePanel(
onChoose: onChooseEmote,
),
child: EmotePanel(onChoose: onChooseEmote),
);
}
Widget _buildPanelContainer() {
return ChatBottomPanelContainer<PanelType>(
controller: _controller,
inputFocusNode: _focusNode,
otherPanelWidget: (type) {
if (type == null) return const SizedBox.shrink();
switch (type) {
case PanelType.emoji:
return _buildEmojiPickerPanel();
default:
return const SizedBox.shrink();
}
},
onPanelTypeChange: (panelType, data) {
// debugPrint('panelType: $panelType');
switch (panelType) {
case ChatBottomPanelType.none:
_currentPanelType = PanelType.none;
break;
case ChatBottomPanelType.keyboard:
_currentPanelType = PanelType.keyboard;
break;
case ChatBottomPanelType.other:
if (data == null) return;
switch (data) {
case PanelType.emoji:
_currentPanelType = PanelType.emoji;
break;
default:
_currentPanelType = PanelType.none;
break;
}
break;
}
},
panelBgColor: Theme.of(context).colorScheme.onInverseSurface,
);
@override
Future onCustomPublish({required String message, List? pictures}) {
throw UnimplementedError();
}
}

View File

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