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