mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-02 00:58:19 +08:00
feat: new reply page
This commit is contained in:
@@ -9,6 +9,7 @@ allprojects {
|
|||||||
}
|
}
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ class _EmotePanelState extends State<EmotePanel>
|
|||||||
int type = e.type!;
|
int type = e.type!;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 6, 12, 0),
|
padding: const EdgeInsets.fromLTRB(12, 6, 12, 0),
|
||||||
child: type != 4
|
child: GridView.builder(
|
||||||
? GridView.builder(
|
|
||||||
gridDelegate:
|
gridDelegate:
|
||||||
SliverGridDelegateWithMaxCrossAxisExtent(
|
SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: (size == 1 ? 40 : 60),
|
maxCrossAxisExtent:
|
||||||
|
type == 4 ? 100 : (size == 1 ? 40 : 60),
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
mainAxisExtent: size == 1 ? 40 : 60,
|
mainAxisExtent: size == 1 ? 40 : 60,
|
||||||
@@ -67,15 +67,17 @@ class _EmotePanelState extends State<EmotePanel>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onChoose(e, e.emote![index]);
|
widget.onChoose(e, e.emote![index]);
|
||||||
},
|
},
|
||||||
child: Tooltip(
|
|
||||||
message: e.emote![index].text!
|
|
||||||
.substring(
|
|
||||||
1,
|
|
||||||
e.emote![index].text!.length -
|
|
||||||
1),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: NetworkImgLayer(
|
child: type == 4
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
e.emote![index].text!,
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: NetworkImgLayer(
|
||||||
src: e.emote![index].url!,
|
src: e.emote![index].url!,
|
||||||
width: size * 38,
|
width: size * 38,
|
||||||
height: size * 38,
|
height: size * 38,
|
||||||
@@ -85,40 +87,14 @@ class _EmotePanelState extends State<EmotePanel>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
|
||||||
: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: e.emote!
|
|
||||||
.map(
|
|
||||||
(item) => Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(8),
|
|
||||||
onTap: () {
|
|
||||||
widget.onChoose(e, item);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.all(6),
|
|
||||||
child: Text(item.text!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:PiliPalaX/pages/video/detail/reply_new/reply_page.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@@ -5,9 +6,9 @@ import 'package:get/get.dart';
|
|||||||
import 'package:PiliPalaX/common/skeleton/video_reply.dart';
|
import 'package:PiliPalaX/common/skeleton/video_reply.dart';
|
||||||
import 'package:PiliPalaX/models/common/reply_type.dart';
|
import 'package:PiliPalaX/models/common/reply_type.dart';
|
||||||
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
||||||
import 'package:PiliPalaX/pages/video/detail/reply_new/index.dart';
|
|
||||||
import 'package:PiliPalaX/utils/feed_back.dart';
|
import 'package:PiliPalaX/utils/feed_back.dart';
|
||||||
import 'package:PiliPalaX/utils/id_utils.dart';
|
import 'package:PiliPalaX/utils/id_utils.dart';
|
||||||
|
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'widgets/reply_item.dart';
|
import 'widgets/reply_item.dart';
|
||||||
|
|
||||||
@@ -35,7 +36,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
||||||
late VideoReplyController _videoReplyController;
|
late VideoReplyController _videoReplyController;
|
||||||
late AnimationController fabAnimationCtr;
|
late AnimationController fabAnimationCtr;
|
||||||
late ScrollController scrollController;
|
|
||||||
|
|
||||||
bool _isFabVisible = true;
|
bool _isFabVisible = true;
|
||||||
String replyLevel = '1';
|
String replyLevel = '1';
|
||||||
@@ -70,17 +70,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
fabAnimationCtr.dispose();
|
fabAnimationCtr.dispose();
|
||||||
scrollController.removeListener(() {});
|
_videoReplyController.scrollController.removeListener(() {});
|
||||||
scrollController.dispose();
|
_videoReplyController.scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void scrollListener() {
|
void scrollListener() {
|
||||||
scrollController = _videoReplyController.scrollController;
|
_videoReplyController.scrollController.addListener(
|
||||||
scrollController.addListener(
|
|
||||||
() {
|
() {
|
||||||
if (scrollController.position.pixels >=
|
if (_videoReplyController.scrollController.position.pixels >=
|
||||||
scrollController.position.maxScrollExtent - 300) {
|
_videoReplyController.scrollController.position.maxScrollExtent -
|
||||||
|
300) {
|
||||||
EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
|
EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
|
||||||
() {
|
() {
|
||||||
_videoReplyController.onLoad();
|
_videoReplyController.onLoad();
|
||||||
@@ -88,7 +88,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ScrollDirection direction =
|
final ScrollDirection direction =
|
||||||
scrollController.position.userScrollDirection;
|
_videoReplyController.scrollController.position.userScrollDirection;
|
||||||
if (direction == ScrollDirection.forward) {
|
if (direction == ScrollDirection.forward) {
|
||||||
_showFab();
|
_showFab();
|
||||||
} else if (direction == ScrollDirection.reverse) {
|
} else if (direction == ScrollDirection.reverse) {
|
||||||
@@ -134,7 +134,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
controller: scrollController,
|
controller: _videoReplyController.scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
key: const PageStorageKey<String>('评论'),
|
key: const PageStorageKey<String>('评论'),
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
@@ -243,11 +243,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
heroTag: null,
|
heroTag: null,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
feedBack();
|
feedBack();
|
||||||
showModalBottomSheet(
|
Navigator.of(context)
|
||||||
context: context,
|
.push(
|
||||||
isScrollControlled: true,
|
GetDialogRoute(
|
||||||
builder: (BuildContext context) {
|
pageBuilder:
|
||||||
return VideoReplyNewDialog(
|
(buildContext, animation, secondaryAnimation) {
|
||||||
|
return ReplyPage(
|
||||||
oid: _videoReplyController.aid ??
|
oid: _videoReplyController.aid ??
|
||||||
IdUtils.bv2av(Get.parameters['bvid']!),
|
IdUtils.bv2av(Get.parameters['bvid']!),
|
||||||
root: 0,
|
root: 0,
|
||||||
@@ -255,7 +256,24 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
replyType: ReplyType.video,
|
replyType: ReplyType.video,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).then(
|
transitionDuration: const Duration(milliseconds: 500),
|
||||||
|
transitionBuilder:
|
||||||
|
(context, animation, secondaryAnimation, child) {
|
||||||
|
const begin = Offset(0.0, 1.0);
|
||||||
|
const end = Offset.zero;
|
||||||
|
const curve = Curves.linear;
|
||||||
|
|
||||||
|
var tween = Tween(begin: begin, end: end)
|
||||||
|
.chain(CurveTween(curve: curve));
|
||||||
|
|
||||||
|
return SlideTransition(
|
||||||
|
position: animation.drive(tween),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
(value) => {
|
(value) => {
|
||||||
// 完成评论,数据添加
|
// 完成评论,数据添加
|
||||||
if (value != null && value['data'] != null)
|
if (value != null && value['data'] != null)
|
||||||
@@ -265,6 +283,28 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// showModalBottomSheet(
|
||||||
|
// context: context,
|
||||||
|
// isScrollControlled: true,
|
||||||
|
// builder: (BuildContext context) {
|
||||||
|
// return VideoReplyNewDialog(
|
||||||
|
// oid: _videoReplyController.aid ??
|
||||||
|
// IdUtils.bv2av(Get.parameters['bvid']!),
|
||||||
|
// root: 0,
|
||||||
|
// parent: 0,
|
||||||
|
// replyType: ReplyType.video,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ).then(
|
||||||
|
// (value) => {
|
||||||
|
// // 完成评论,数据添加
|
||||||
|
// if (value != null && value['data'] != null)
|
||||||
|
// {
|
||||||
|
// _videoReplyController.replyList
|
||||||
|
// .insert(0, value['data'])
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// );
|
||||||
},
|
},
|
||||||
tooltip: '发表评论',
|
tooltip: '发表评论',
|
||||||
child: const Icon(Icons.reply),
|
child: const Icon(Icons.reply),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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';
|
||||||
|
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:PiliPalaX/common/widgets/badge.dart';
|
import 'package:PiliPalaX/common/widgets/badge.dart';
|
||||||
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
|
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
|
||||||
@@ -11,12 +12,12 @@ import 'package:PiliPalaX/models/common/reply_type.dart';
|
|||||||
import 'package:PiliPalaX/models/video/reply/item.dart';
|
import 'package:PiliPalaX/models/video/reply/item.dart';
|
||||||
import 'package:PiliPalaX/pages/preview/index.dart';
|
import 'package:PiliPalaX/pages/preview/index.dart';
|
||||||
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
||||||
import 'package:PiliPalaX/pages/video/detail/reply_new/index.dart';
|
|
||||||
import 'package:PiliPalaX/utils/feed_back.dart';
|
import 'package:PiliPalaX/utils/feed_back.dart';
|
||||||
import 'package:PiliPalaX/utils/storage.dart';
|
import 'package:PiliPalaX/utils/storage.dart';
|
||||||
import 'package:PiliPalaX/utils/url_utils.dart';
|
import 'package:PiliPalaX/utils/url_utils.dart';
|
||||||
import 'package:PiliPalaX/utils/utils.dart';
|
import 'package:PiliPalaX/utils/utils.dart';
|
||||||
import '../../../../../utils/app_scheme.dart';
|
import '../../../../../utils/app_scheme.dart';
|
||||||
|
import '../../reply_new/reply_page.dart';
|
||||||
import 'zan.dart';
|
import 'zan.dart';
|
||||||
|
|
||||||
Box setting = GStorage.setting;
|
Box setting = GStorage.setting;
|
||||||
@@ -302,11 +303,12 @@ class ReplyItem extends StatelessWidget {
|
|||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
feedBack();
|
feedBack();
|
||||||
showModalBottomSheet(
|
Navigator.of(context)
|
||||||
context: context,
|
.push(
|
||||||
isScrollControlled: true,
|
GetDialogRoute(
|
||||||
builder: (builder) {
|
pageBuilder:
|
||||||
return VideoReplyNewDialog(
|
(buildContext, animation, secondaryAnimation) {
|
||||||
|
return ReplyPage(
|
||||||
oid: replyItem!.oid,
|
oid: replyItem!.oid,
|
||||||
root: replyItem!.rpid,
|
root: replyItem!.rpid,
|
||||||
parent: replyItem!.rpid,
|
parent: replyItem!.rpid,
|
||||||
@@ -314,7 +316,24 @@ class ReplyItem extends StatelessWidget {
|
|||||||
replyItem: replyItem,
|
replyItem: replyItem,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).then((value) => {
|
transitionDuration: const Duration(milliseconds: 500),
|
||||||
|
transitionBuilder:
|
||||||
|
(context, animation, secondaryAnimation, child) {
|
||||||
|
const begin = Offset(0.0, 1.0);
|
||||||
|
const end = Offset.zero;
|
||||||
|
const curve = Curves.linear;
|
||||||
|
|
||||||
|
var tween = Tween(begin: begin, end: end)
|
||||||
|
.chain(CurveTween(curve: curve));
|
||||||
|
|
||||||
|
return SlideTransition(
|
||||||
|
position: animation.drive(tween),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((value) => {
|
||||||
// 完成评论,数据添加
|
// 完成评论,数据添加
|
||||||
if (value != null &&
|
if (value != null &&
|
||||||
value['data'] != null &&
|
value['data'] != null &&
|
||||||
@@ -324,6 +343,28 @@ class ReplyItem extends StatelessWidget {
|
|||||||
// replyControl.replies.add(value['data']),
|
// replyControl.replies.add(value['data']),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// showModalBottomSheet(
|
||||||
|
// context: context,
|
||||||
|
// isScrollControlled: true,
|
||||||
|
// builder: (builder) {
|
||||||
|
// return VideoReplyNewDialog(
|
||||||
|
// oid: replyItem!.oid,
|
||||||
|
// root: replyItem!.rpid,
|
||||||
|
// parent: replyItem!.rpid,
|
||||||
|
// replyType: replyType,
|
||||||
|
// replyItem: replyItem,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ).then((value) => {
|
||||||
|
// // 完成评论,数据添加
|
||||||
|
// if (value != null &&
|
||||||
|
// value['data'] != null &&
|
||||||
|
// addReply != null)
|
||||||
|
// {
|
||||||
|
// addReply?.call(value['data'])
|
||||||
|
// // replyControl.replies.add(value['data']),
|
||||||
|
// }
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Icon(Icons.reply,
|
Icon(Icons.reply,
|
||||||
|
|||||||
377
lib/pages/video/detail/reply_new/reply_page.dart
Normal file
377
lib/pages/video/detail/reply_new/reply_page.dart
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:PiliPalaX/common/widgets/no_splash_factory.dart';
|
||||||
|
import 'package:PiliPalaX/http/video.dart';
|
||||||
|
import 'package:PiliPalaX/models/common/reply_type.dart';
|
||||||
|
import 'package:PiliPalaX/models/video/reply/emote.dart';
|
||||||
|
import 'package:PiliPalaX/models/video/reply/item.dart';
|
||||||
|
import 'package:PiliPalaX/pages/emote/index.dart';
|
||||||
|
import 'package:PiliPalaX/utils/feed_back.dart';
|
||||||
|
import 'package:PiliPalaX/pages/emote/view.dart';
|
||||||
|
import 'package:PiliPalaX/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||||
|
|
||||||
|
enum PanelType { none, keyboard, emoji }
|
||||||
|
|
||||||
|
class ReplyPage extends StatefulWidget {
|
||||||
|
final int? oid;
|
||||||
|
final int? root;
|
||||||
|
final int? parent;
|
||||||
|
final ReplyType? replyType;
|
||||||
|
final ReplyItemModel? replyItem;
|
||||||
|
|
||||||
|
const ReplyPage({
|
||||||
|
super.key,
|
||||||
|
this.oid,
|
||||||
|
this.root,
|
||||||
|
this.parent,
|
||||||
|
this.replyType,
|
||||||
|
this.replyItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReplyPage> createState() => _ReplyPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReplyPageState extends State<ReplyPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final _focusNode = FocusNode();
|
||||||
|
late final _controller = ChatBottomPanelContainerController<PanelType>();
|
||||||
|
final TextEditingController _replyContentController = TextEditingController();
|
||||||
|
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();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
() async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
if (mounted) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() async {
|
||||||
|
_publishStream.close();
|
||||||
|
_readOnlyStream.close();
|
||||||
|
_enableSend.close();
|
||||||
|
_focusNode.dispose();
|
||||||
|
_replyContentController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQuery.removePadding(
|
||||||
|
removeTop: true,
|
||||||
|
context: context,
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
splashFactory: NoSplashFactory(),
|
||||||
|
onTap: Get.back,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildInputView(),
|
||||||
|
_buildPanelContainer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPanelContainer() {
|
||||||
|
return ChatBottomPanelContainer<PanelType>(
|
||||||
|
controller: _controller,
|
||||||
|
inputFocusNode: _focusNode,
|
||||||
|
otherPanelWidget: (type) {
|
||||||
|
if (type == null) return const SizedBox.shrink();
|
||||||
|
switch (type) {
|
||||||
|
case PanelType.emoji:
|
||||||
|
return _buildEmojiPickerPanel();
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanelTypeChange: (panelType, data) {
|
||||||
|
debugPrint('panelType: $panelType');
|
||||||
|
switch (panelType) {
|
||||||
|
case ChatBottomPanelType.none:
|
||||||
|
_currentPanelType = PanelType.none;
|
||||||
|
break;
|
||||||
|
case ChatBottomPanelType.keyboard:
|
||||||
|
_currentPanelType = PanelType.keyboard;
|
||||||
|
break;
|
||||||
|
case ChatBottomPanelType.other:
|
||||||
|
if (data == null) return;
|
||||||
|
switch (data) {
|
||||||
|
case PanelType.emoji:
|
||||||
|
_currentPanelType = PanelType.emoji;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_currentPanelType = PanelType.none;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
panelBgColor: Theme.of(context).colorScheme.surface,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiPickerPanel() {
|
||||||
|
double height = 200;
|
||||||
|
final keyboardHeight = _controller.keyboardHeight;
|
||||||
|
if (keyboardHeight != 0) {
|
||||||
|
height = max(height, keyboardHeight);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: EmotePanel(onChoose: onChooseEmote),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInputView() {
|
||||||
|
return Container(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 12, right: 15, left: 15, bottom: 10),
|
||||||
|
child: Form(
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: Listener(
|
||||||
|
onPointerUp: (event) {
|
||||||
|
if (_readOnly) {
|
||||||
|
updatePanelType(PanelType.keyboard);
|
||||||
|
if (!_selectKeyboard) {
|
||||||
|
_selectKeyboard = true;
|
||||||
|
_keyboardStream.add(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: StreamBuilder(
|
||||||
|
initialData: false,
|
||||||
|
stream: _readOnlyStream.stream,
|
||||||
|
builder: (context, snapshot) => TextField(
|
||||||
|
controller: _replyContentController,
|
||||||
|
minLines: 4,
|
||||||
|
maxLines: 8,
|
||||||
|
autofocus: false,
|
||||||
|
readOnly: snapshot.data ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isNotEmpty && !_enablePublish) {
|
||||||
|
_enablePublish = true;
|
||||||
|
_publishStream.add(true);
|
||||||
|
} else if (value.isEmpty && _enablePublish) {
|
||||||
|
_enablePublish = false;
|
||||||
|
_publishStream.add(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusNode: _focusNode,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "输入回复内容",
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintStyle: TextStyle(fontSize: 14)),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 52,
|
||||||
|
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
StreamBuilder(
|
||||||
|
initialData: true,
|
||||||
|
stream: _keyboardStream.stream,
|
||||||
|
builder: (_, snapshot) => ToolbarIconButton(
|
||||||
|
tooltip: '输入',
|
||||||
|
onPressed: () {
|
||||||
|
if (!_selectKeyboard) {
|
||||||
|
_selectKeyboard = true;
|
||||||
|
_keyboardStream.add(true);
|
||||||
|
}
|
||||||
|
updatePanelType(PanelType.keyboard);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.keyboard, size: 22),
|
||||||
|
selected: snapshot.data!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
StreamBuilder(
|
||||||
|
initialData: true,
|
||||||
|
stream: _keyboardStream.stream,
|
||||||
|
builder: (_, snapshot) => ToolbarIconButton(
|
||||||
|
tooltip: '表情',
|
||||||
|
onPressed: () {
|
||||||
|
updatePanelType(
|
||||||
|
PanelType.emoji == _currentPanelType
|
||||||
|
? PanelType.keyboard
|
||||||
|
: PanelType.emoji,
|
||||||
|
);
|
||||||
|
if (_selectKeyboard) {
|
||||||
|
_selectKeyboard = false;
|
||||||
|
_keyboardStream.add(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||||
|
selected: !snapshot.data!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
StreamBuilder(
|
||||||
|
initialData: false,
|
||||||
|
stream: _publishStream.stream,
|
||||||
|
builder: (_, snapshot) => FilledButton.tonal(
|
||||||
|
onPressed: snapshot.data == true ? submitReplyAdd : null,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 10),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -2,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('发送'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePanelType(PanelType type) async {
|
||||||
|
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||||
|
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||||
|
bool isUpdated = false;
|
||||||
|
switch (type) {
|
||||||
|
case PanelType.keyboard:
|
||||||
|
updateInputView(isReadOnly: false);
|
||||||
|
break;
|
||||||
|
case PanelType.emoji:
|
||||||
|
isUpdated = updateInputView(isReadOnly: true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePanelTypeFunc() {
|
||||||
|
_controller.updatePanelType(
|
||||||
|
isSwitchToKeyboard
|
||||||
|
? ChatBottomPanelType.keyboard
|
||||||
|
: ChatBottomPanelType.other,
|
||||||
|
data: type,
|
||||||
|
forceHandleFocus: isSwitchToEmojiPanel
|
||||||
|
? ChatBottomHandleFocus.requestFocus
|
||||||
|
: ChatBottomHandleFocus.none,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUpdated) {
|
||||||
|
// Waiting for the input view to update.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
updatePanelTypeFunc();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updatePanelTypeFunc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePanel() async {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
updateInputView(isReadOnly: false);
|
||||||
|
if (ChatBottomPanelType.none == _controller.currentPanelType) return;
|
||||||
|
_controller.updatePanelType(ChatBottomPanelType.none);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool updateInputView({
|
||||||
|
required bool isReadOnly,
|
||||||
|
}) {
|
||||||
|
if (_readOnly != isReadOnly) {
|
||||||
|
_readOnly = isReadOnly;
|
||||||
|
_readOnlyStream.add(_readOnly);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future submitReplyAdd() async {
|
||||||
|
feedBack();
|
||||||
|
String message = _replyContentController.text;
|
||||||
|
var result = await VideoHttp.replyAdd(
|
||||||
|
type: widget.replyType ?? ReplyType.video,
|
||||||
|
oid: widget.oid!,
|
||||||
|
root: widget.root!,
|
||||||
|
parent: widget.parent!,
|
||||||
|
message: widget.replyItem != null && widget.replyItem!.root != 0
|
||||||
|
? ' 回复 @${widget.replyItem!.member!.uname!} : $message'
|
||||||
|
: message,
|
||||||
|
);
|
||||||
|
if (result['status']) {
|
||||||
|
SmartDialog.showToast(result['data']['success_toast']);
|
||||||
|
Get.back(result: {
|
||||||
|
'data': ReplyItemModel.fromJson(result['data']['reply'], ''),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||||||
class ToolbarIconButton extends StatelessWidget {
|
class ToolbarIconButton extends StatelessWidget {
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
final Icon icon;
|
final Icon icon;
|
||||||
final String toolbarType;
|
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final String tooltip;
|
final String tooltip;
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ class ToolbarIconButton extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.toolbarType,
|
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.tooltip,
|
required this.tooltip,
|
||||||
});
|
});
|
||||||
@@ -30,8 +28,8 @@ class ToolbarIconButton extends StatelessWidget {
|
|||||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||||
: Theme.of(context).colorScheme.outline,
|
: Theme.of(context).colorScheme.outline,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||||
return selected
|
return selected
|
||||||
? Theme.of(context).colorScheme.secondaryContainer
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
|||||||
FocusScope.of(context).requestFocus(replyContentFocusNode);
|
FocusScope.of(context).requestFocus(replyContentFocusNode);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.keyboard, size: 22),
|
icon: const Icon(Icons.keyboard, size: 22),
|
||||||
toolbarType: toolbarType,
|
// toolbarType: toolbarType,
|
||||||
selected: toolbarType == 'input',
|
selected: toolbarType == 'input',
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@@ -229,7 +229,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||||
toolbarType: toolbarType,
|
// toolbarType: toolbarType,
|
||||||
selected: toolbarType == 'emote',
|
selected: toolbarType == 'emote',
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -255,6 +255,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
chat_bottom_container:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chat_bottom_container
|
||||||
|
sha256: fc2e690892699604524bf4a4c7ba9b3281ae060c90154b55b38116ce7812c3d8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1421,6 +1429,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.0"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.2"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ dependencies:
|
|||||||
marquee: ^2.2.3
|
marquee: ^2.2.3
|
||||||
#富文本
|
#富文本
|
||||||
extended_text: ^14.0.1
|
extended_text: ^14.0.1
|
||||||
|
chat_bottom_container: ^0.2.0
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
mime:
|
mime:
|
||||||
git: https://github.com/orz12/mime.git
|
git: https://github.com/orz12/mime.git
|
||||||
|
|||||||
Reference in New Issue
Block a user