merge main
This commit is contained in:
@ -2,8 +2,9 @@ import 'package:pilipala/http/danmaku.dart';
|
||||
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
||||
|
||||
class PlDanmakuController {
|
||||
PlDanmakuController(this.cid);
|
||||
PlDanmakuController(this.cid, this.type);
|
||||
final int cid;
|
||||
final String type;
|
||||
Map<int, List<DanmakuElem>> dmSegMap = {};
|
||||
// 已请求的段落标记
|
||||
List<bool> requestedSeg = [];
|
||||
|
||||
@ -12,11 +12,15 @@ import 'package:pilipala/utils/storage.dart';
|
||||
class PlDanmaku extends StatefulWidget {
|
||||
final int cid;
|
||||
final PlPlayerController playerController;
|
||||
final String type;
|
||||
final Function(DanmakuController)? createdController;
|
||||
|
||||
const PlDanmaku({
|
||||
super.key,
|
||||
required this.cid,
|
||||
required this.playerController,
|
||||
this.type = 'video',
|
||||
this.createdController,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -43,9 +47,9 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
super.initState();
|
||||
enableShowDanmaku =
|
||||
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||
_plDanmakuController = PlDanmakuController(widget.cid);
|
||||
if (mounted) {
|
||||
playerController = widget.playerController;
|
||||
_plDanmakuController = PlDanmakuController(widget.cid, widget.type);
|
||||
playerController = widget.playerController;
|
||||
if (mounted && widget.type == 'video') {
|
||||
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
|
||||
_plDanmakuController.initiate(
|
||||
playerController.duration.value.inMilliseconds,
|
||||
@ -55,13 +59,15 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
..addStatusLister(playerListener)
|
||||
..addPositionListener(videoPositionListen);
|
||||
}
|
||||
playerController.isOpenDanmu.listen((p0) {
|
||||
if (p0 && !_plDanmakuController.initiated) {
|
||||
_plDanmakuController.initiate(
|
||||
playerController.duration.value.inMilliseconds,
|
||||
playerController.position.value.inMilliseconds);
|
||||
}
|
||||
});
|
||||
if (widget.type == 'video') {
|
||||
playerController.isOpenDanmu.listen((p0) {
|
||||
if (p0 && !_plDanmakuController.initiated) {
|
||||
_plDanmakuController.initiate(
|
||||
playerController.duration.value.inMilliseconds,
|
||||
playerController.position.value.inMilliseconds);
|
||||
}
|
||||
});
|
||||
}
|
||||
blockTypes = playerController.blockTypes;
|
||||
showArea = playerController.showArea;
|
||||
opacityVal = playerController.opacityVal;
|
||||
@ -128,6 +134,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
child: DanmakuView(
|
||||
createdController: (DanmakuController e) async {
|
||||
playerController.danmakuController = _controller = e;
|
||||
widget.createdController?.call(e);
|
||||
},
|
||||
option: DanmakuOption(
|
||||
fontSize: 15 * fontSizeVal,
|
||||
@ -136,8 +143,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
duration:
|
||||
danmakuDurationVal / playerController.playbackSpeed,
|
||||
duration: danmakuDurationVal / playerController.playbackSpeed,
|
||||
strokeWidth: strokeWidth,
|
||||
// initDuration /
|
||||
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
|
||||
|
||||
@ -106,7 +106,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
}
|
||||
|
||||
// 查看二级评论
|
||||
void replyReply(replyItem, currentReply) {
|
||||
void replyReply(replyItem, currentReply, loadMore) {
|
||||
int oid = replyItem.oid;
|
||||
int rpid = replyItem.rpid!;
|
||||
Get.to(
|
||||
@ -125,6 +125,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
source: 'dynamic',
|
||||
replyType: ReplyType.values[replyType],
|
||||
firstFloor: replyItem,
|
||||
loadMore: loadMore,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -324,8 +325,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
replyItem: replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem, currentReply) =>
|
||||
replyReply(replyItem, currentReply),
|
||||
replyReply:
|
||||
(replyItem, currentReply, loadMore) =>
|
||||
replyReply(replyItem,
|
||||
currentReply, loadMore),
|
||||
replyType: ReplyType.values[replyType],
|
||||
addReply: (replyItem) {
|
||||
replyList[index]
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/http/live.dart';
|
||||
import 'package:pilipala/models/live/message.dart';
|
||||
import 'package:pilipala/models/live/quality.dart';
|
||||
import 'package:pilipala/models/live/room_info.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_socket/index.dart';
|
||||
import 'package:pilipala/utils/live.dart';
|
||||
import '../../models/live/room_info_h5.dart';
|
||||
import '../../utils/storage.dart';
|
||||
import '../../utils/video_utils.dart';
|
||||
@ -24,6 +33,21 @@ class LiveRoomController extends GetxController {
|
||||
int? tempCurrentQn;
|
||||
late List<Map<String, dynamic>> acceptQnList;
|
||||
RxString currentQnDesc = ''.obs;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
int userId = 0;
|
||||
PlSocket? plSocket;
|
||||
List<String> danmuHostList = [];
|
||||
String token = '';
|
||||
// 弹幕消息列表
|
||||
RxList<LiveMessageModel> messageList = <LiveMessageModel>[].obs;
|
||||
DanmakuController? danmakuController;
|
||||
// 输入控制器
|
||||
TextEditingController inputController = TextEditingController();
|
||||
// 加入直播间提示
|
||||
RxMap<String, String> joinRoomTip = {'userName': '', 'message': ''}.obs;
|
||||
// 直播间弹幕开关 默认打开
|
||||
RxBool danmakuSwitch = true.obs;
|
||||
late String buvid;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -40,9 +64,18 @@ class LiveRoomController extends GetxController {
|
||||
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
|
||||
cover = liveItem.cover;
|
||||
}
|
||||
Request.getBuvid().then((value) => buvid = value);
|
||||
}
|
||||
// CDN优化
|
||||
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
|
||||
final userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null && userInfo.mid != null) {
|
||||
userId = userInfo.mid;
|
||||
}
|
||||
liveDanmakuInfo().then((value) => initSocket());
|
||||
danmakuSwitch.listen((p0) {
|
||||
plPlayerController.isOpenDanmu.value = p0;
|
||||
});
|
||||
}
|
||||
|
||||
playerInit(source) async {
|
||||
@ -61,6 +94,7 @@ class LiveRoomController extends GetxController {
|
||||
enableHA: true,
|
||||
autoplay: true,
|
||||
);
|
||||
plPlayerController.isOpenDanmu.value = danmakuSwitch.value;
|
||||
}
|
||||
|
||||
Future queryLiveInfo() async {
|
||||
@ -127,4 +161,126 @@ class LiveRoomController extends GetxController {
|
||||
.description;
|
||||
await queryLiveInfo();
|
||||
}
|
||||
|
||||
Future liveDanmakuInfo() async {
|
||||
var res = await LiveHttp.liveDanmakuInfo(roomId: roomId);
|
||||
if (res['status']) {
|
||||
danmuHostList = (res["data"]["host_list"] as List)
|
||||
.map<String>((e) => '${e["host"]}:${e['wss_port']}')
|
||||
.toList();
|
||||
token = res["data"]["token"];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// 建立socket
|
||||
void initSocket() async {
|
||||
final wsUrl = danmuHostList.isNotEmpty
|
||||
? danmuHostList.first
|
||||
: "broadcastlv.chat.bilibili.com";
|
||||
plSocket = PlSocket(
|
||||
url: 'wss://$wsUrl/sub',
|
||||
heartTime: 30,
|
||||
onReadyCb: () {
|
||||
joinRoom();
|
||||
},
|
||||
onMessageCb: (message) {
|
||||
final List<LiveMessageModel>? liveMsg =
|
||||
LiveUtils.decodeMessage(message);
|
||||
if (liveMsg != null && liveMsg.isNotEmpty) {
|
||||
if (liveMsg.first.type == LiveMessageType.online) {
|
||||
print('当前直播间人气:${liveMsg.first.data}');
|
||||
} else if (liveMsg.first.type == LiveMessageType.join ||
|
||||
liveMsg.first.type == LiveMessageType.follow) {
|
||||
// 每隔一秒依次liveMsg中的每一项赋给activeUserName
|
||||
|
||||
int index = 0;
|
||||
Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||
if (index < liveMsg.length) {
|
||||
if (liveMsg[index].type == LiveMessageType.join ||
|
||||
liveMsg[index].type == LiveMessageType.follow) {
|
||||
joinRoomTip.value = {
|
||||
'userName': liveMsg[index].userName,
|
||||
'message': liveMsg[index].message!,
|
||||
};
|
||||
}
|
||||
index++;
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
// 过滤出聊天消息
|
||||
var chatMessages =
|
||||
liveMsg.where((msg) => msg.type == LiveMessageType.chat).toList();
|
||||
|
||||
// 添加到 messageList
|
||||
messageList.addAll(chatMessages);
|
||||
|
||||
// 将 chatMessages 转换为 danmakuItems 列表
|
||||
List<DanmakuItem> danmakuItems = chatMessages.map<DanmakuItem>((e) {
|
||||
return DanmakuItem(
|
||||
e.message ?? '',
|
||||
color: Color.fromARGB(
|
||||
255,
|
||||
e.color.r,
|
||||
e.color.g,
|
||||
e.color.b,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// 添加到 danmakuController
|
||||
if (danmakuSwitch.value) {
|
||||
danmakuController?.addItems(danmakuItems);
|
||||
}
|
||||
}
|
||||
},
|
||||
onErrorCb: (e) {
|
||||
print('error: $e');
|
||||
},
|
||||
);
|
||||
await plSocket?.connect();
|
||||
}
|
||||
|
||||
void joinRoom() async {
|
||||
var joinData = LiveUtils.encodeData(
|
||||
json.encode({
|
||||
"uid": userId,
|
||||
"roomid": roomId,
|
||||
"protover": 3,
|
||||
"buvid": buvid,
|
||||
"platform": "web",
|
||||
"type": 2,
|
||||
"key": token,
|
||||
}),
|
||||
7,
|
||||
);
|
||||
plSocket?.sendMessage(joinData);
|
||||
}
|
||||
|
||||
// 发送弹幕
|
||||
void sendMsg() async {
|
||||
final msg = inputController.text;
|
||||
if (msg.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final res = await LiveHttp.sendDanmaku(
|
||||
roomId: roomId,
|
||||
msg: msg,
|
||||
);
|
||||
if (res['status']) {
|
||||
inputController.clear();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
plSocket?.onClose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/live/message.dart';
|
||||
import 'package:pilipala/pages/danmaku/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
@ -16,15 +20,20 @@ class LiveRoomPage extends StatefulWidget {
|
||||
State<LiveRoomPage> createState() => _LiveRoomPageState();
|
||||
}
|
||||
|
||||
class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
class _LiveRoomPageState extends State<LiveRoomPage>
|
||||
with TickerProviderStateMixin {
|
||||
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
|
||||
PlPlayerController? plPlayerController;
|
||||
late PlPlayerController plPlayerController;
|
||||
late Future? _futureBuilder;
|
||||
late Future? _futureBuilderFuture;
|
||||
|
||||
bool isShowCover = true;
|
||||
bool isPlay = true;
|
||||
Floating? floating;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late AnimationController fabAnimationCtr;
|
||||
bool _shouldAutoScroll = true;
|
||||
final int roomId = int.parse(Get.parameters['roomid']!);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -34,6 +43,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
}
|
||||
videoSourceInit();
|
||||
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
|
||||
// 监听滚动事件
|
||||
_scrollController.addListener(_onScroll);
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> videoSourceInit() async {
|
||||
@ -41,12 +57,52 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
plPlayerController = _liveRoomController.plPlayerController;
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
// 反向时,展示按钮
|
||||
if (_scrollController.position.userScrollDirection ==
|
||||
ScrollDirection.forward) {
|
||||
_shouldAutoScroll = false;
|
||||
fabAnimationCtr.forward();
|
||||
} else {
|
||||
_shouldAutoScroll = true;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// 监听messageList的变化,自动滚动到底部
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_liveRoomController.messageList.listen((_) {
|
||||
if (_shouldAutoScroll) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController
|
||||
.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
)
|
||||
.then((value) {
|
||||
_shouldAutoScroll = true;
|
||||
// fabAnimationCtr.forward();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
plPlayerController!.dispose();
|
||||
plPlayerController.dispose();
|
||||
if (floating != null) {
|
||||
floating!.dispose();
|
||||
}
|
||||
_scrollController.dispose();
|
||||
fabAnimationCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -56,8 +112,9 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData && snapshot.data['status']) {
|
||||
plPlayerController = _liveRoomController.plPlayerController;
|
||||
return PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
controller: plPlayerController,
|
||||
bottomControl: BottomControl(
|
||||
controller: plPlayerController,
|
||||
liveRoomCtr: _liveRoomController,
|
||||
@ -68,6 +125,14 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
});
|
||||
},
|
||||
),
|
||||
danmuWidget: PlDanmaku(
|
||||
cid: roomId,
|
||||
playerController: plPlayerController,
|
||||
type: 'live',
|
||||
createdController: (e) {
|
||||
_liveRoomController.danmakuController = e;
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
@ -80,20 +145,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: Image.asset(
|
||||
'assets/images/live/default_bg.webp',
|
||||
fit: BoxFit.cover,
|
||||
// width: Get.width,
|
||||
// height: Get.height,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Positioned(
|
||||
left: 0,
|
||||
@ -106,7 +157,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
.roomInfoH5.value.roomInfo?.appBackground !=
|
||||
null
|
||||
? Opacity(
|
||||
opacity: 0.8,
|
||||
opacity: 0.6,
|
||||
child: NetworkImgLayer(
|
||||
width: Get.width,
|
||||
height: Get.height,
|
||||
@ -116,10 +167,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
'',
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
: Opacity(
|
||||
opacity: 0.6,
|
||||
child: Image.asset(
|
||||
'assets/images/live/default_bg.webp',
|
||||
fit: BoxFit.cover,
|
||||
// width: Get.width,
|
||||
// height: Get.height,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppBar(
|
||||
centerTitle: false,
|
||||
@ -179,10 +239,10 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
),
|
||||
),
|
||||
PopScope(
|
||||
canPop: plPlayerController?.isFullScreen.value != true,
|
||||
canPop: plPlayerController.isFullScreen.value != true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
if (plPlayerController?.isFullScreen.value == true) {
|
||||
plPlayerController!.triggerFullScreen(status: false);
|
||||
if (plPlayerController.isFullScreen.value == true) {
|
||||
plPlayerController.triggerFullScreen(status: false);
|
||||
}
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape) {
|
||||
@ -198,8 +258,160 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
child: videoPlayerPanel,
|
||||
),
|
||||
),
|
||||
// 显示消息的列表
|
||||
buildMessageListUI(
|
||||
context,
|
||||
_liveRoomController,
|
||||
_scrollController,
|
||||
),
|
||||
// Container(
|
||||
// padding: const EdgeInsets.only(
|
||||
// left: 14, right: 14, top: 4, bottom: 4),
|
||||
// margin: const EdgeInsets.only(
|
||||
// bottom: 6,
|
||||
// left: 14,
|
||||
// ),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.grey.withOpacity(0.1),
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
// ),
|
||||
// child: Obx(
|
||||
// () => AnimatedSwitcher(
|
||||
// duration: const Duration(milliseconds: 300),
|
||||
// transitionBuilder:
|
||||
// (Widget child, Animation<double> animation) {
|
||||
// return FadeTransition(opacity: animation, child: child);
|
||||
// },
|
||||
// child: Text.rich(
|
||||
// key:
|
||||
// ValueKey(_liveRoomController.joinRoomTip['userName']),
|
||||
// TextSpan(
|
||||
// style: const TextStyle(color: Colors.white),
|
||||
// children: [
|
||||
// TextSpan(
|
||||
// text:
|
||||
// '${_liveRoomController.joinRoomTip['userName']} ',
|
||||
// style: TextStyle(
|
||||
// color: Colors.white.withOpacity(0.6),
|
||||
// ),
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text:
|
||||
// '${_liveRoomController.joinRoomTip['message']}',
|
||||
// style: const TextStyle(color: Colors.white),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 10),
|
||||
// 弹幕输入框
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 14,
|
||||
right: 14,
|
||||
top: 4,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: Obx(
|
||||
() => IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor: MaterialStateProperty.resolveWith(
|
||||
(Set<MaterialState> states) {
|
||||
return Colors.grey.withOpacity(0.1);
|
||||
}),
|
||||
),
|
||||
onPressed: () {
|
||||
_liveRoomController.danmakuSwitch.value =
|
||||
!_liveRoomController.danmakuSwitch.value;
|
||||
},
|
||||
icon: Icon(
|
||||
_liveRoomController.danmakuSwitch.value
|
||||
? Icons.subtitles_outlined
|
||||
: Icons.subtitles_off_outlined,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _liveRoomController.inputController,
|
||||
style:
|
||||
const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: '发送弹幕',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => _liveRoomController.sendMsg(),
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 定位 快速滑动到底部
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 4),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_scrollToBottom();
|
||||
},
|
||||
icon: const Icon(Icons.keyboard_arrow_down), // 图标
|
||||
label: const Text('新消息'), // 文字
|
||||
style: ElevatedButton.styleFrom(
|
||||
// primary: Colors.blue, // 按钮背景颜色
|
||||
// onPrimary: Colors.white, // 按钮文字颜色
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -214,3 +426,138 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMessageListUI(
|
||||
BuildContext context,
|
||||
LiveRoomController liveRoomController,
|
||||
ScrollController scrollController,
|
||||
) {
|
||||
return Expanded(
|
||||
child: Obx(
|
||||
() => MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
removeBottom: true,
|
||||
child: ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.black,
|
||||
],
|
||||
stops: const [0.01, 0.05, 0.2],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// 键盘失去焦点
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: liveRoomController.messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final LiveMessageModel liveMsgItem =
|
||||
liveRoomController.messageList[index];
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
top: index == 0 ? 20.0 : 0.0,
|
||||
bottom: 6.0,
|
||||
left: 14.0,
|
||||
right: 14.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 3.0,
|
||||
horizontal: 10.0,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${liveMsgItem.userName}: ',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 处理点击事件
|
||||
print('Text clicked');
|
||||
},
|
||||
),
|
||||
TextSpan(
|
||||
children: [
|
||||
...buildMessageTextSpan(context, liveMsgItem)
|
||||
],
|
||||
// text: liveMsgItem.message,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<InlineSpan> buildMessageTextSpan(
|
||||
BuildContext context,
|
||||
LiveMessageModel liveMsgItem,
|
||||
) {
|
||||
final List<InlineSpan> inlineSpanList = [];
|
||||
|
||||
// 是否包含表情包
|
||||
if (liveMsgItem.emots == null) {
|
||||
// 没有表情包的消息
|
||||
inlineSpanList.add(
|
||||
TextSpan(text: liveMsgItem.message ?? ''),
|
||||
);
|
||||
} else {
|
||||
// 有表情包的消息 使用正则匹配 表情包用图片渲染
|
||||
final List<String> emotsKeys = liveMsgItem.emots!.keys.toList();
|
||||
final RegExp pattern = RegExp(emotsKeys.map(RegExp.escape).join('|'));
|
||||
|
||||
liveMsgItem.message?.splitMapJoin(
|
||||
pattern,
|
||||
onMatch: (Match match) {
|
||||
final emoteItem = liveMsgItem.emots![match.group(0)];
|
||||
if (emoteItem != null) {
|
||||
inlineSpanList.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
width: emoteItem['width'].toDouble(),
|
||||
height: emoteItem['height'].toDouble(),
|
||||
type: 'emote',
|
||||
src: emoteItem['url'],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String nonMatch) {
|
||||
inlineSpanList.add(
|
||||
TextSpan(text: nonMatch),
|
||||
);
|
||||
return nonMatch;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return inlineSpanList;
|
||||
}
|
||||
|
||||
@ -24,8 +24,28 @@ class MemberSeasonsPanel extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () => Get.toNamed(
|
||||
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}&seasonName=${item.meta!.name}'),
|
||||
onTap: () {
|
||||
final int category = item.meta!.category!;
|
||||
Map<String, String> parameters = {};
|
||||
if (category == 0) {
|
||||
parameters = {
|
||||
'category': '0',
|
||||
'mid': item.meta!.mid.toString(),
|
||||
'seasonId': item.meta!.seasonId.toString(),
|
||||
'seasonName': item.meta!.name!,
|
||||
};
|
||||
}
|
||||
// 2为直播回放
|
||||
if (category == 1 || category == 2) {
|
||||
parameters = {
|
||||
'category': '1',
|
||||
'mid': item.meta!.mid.toString(),
|
||||
'seriesId': item.meta!.seriesId.toString(),
|
||||
'seasonName': item.meta!.name!,
|
||||
};
|
||||
}
|
||||
Get.toNamed('/memberSeasons', parameters: parameters);
|
||||
},
|
||||
title: Text(
|
||||
item.meta!.name!,
|
||||
maxLines: 1,
|
||||
|
||||
@ -6,7 +6,9 @@ import 'package:pilipala/models/member/seasons.dart';
|
||||
class MemberSeasonsController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late int mid;
|
||||
late int seasonId;
|
||||
int? seasonId;
|
||||
int? seriesId;
|
||||
late String category;
|
||||
int pn = 1;
|
||||
int ps = 30;
|
||||
int count = 0;
|
||||
@ -17,17 +19,23 @@ class MemberSeasonsController extends GetxController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
seasonId = int.parse(Get.parameters['seasonId']!);
|
||||
category = Get.parameters['category']!;
|
||||
if (category == '0') {
|
||||
seasonId = int.parse(Get.parameters['seriesId']!);
|
||||
}
|
||||
if (category == '1') {
|
||||
seriesId = int.parse(Get.parameters['seriesId']!);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取专栏详情
|
||||
// 获取专栏详情 0: 专栏 1: 系列
|
||||
Future getSeasonDetail(type) async {
|
||||
if (type == 'onRefresh') {
|
||||
pn = 1;
|
||||
}
|
||||
var res = await MemberHttp.getSeasonDetail(
|
||||
mid: mid,
|
||||
seasonId: seasonId,
|
||||
seasonId: seasonId!,
|
||||
pn: pn,
|
||||
ps: ps,
|
||||
sortReverse: false,
|
||||
@ -40,8 +48,32 @@ class MemberSeasonsController extends GetxController {
|
||||
return res;
|
||||
}
|
||||
|
||||
// 获取系列详情 0: 专栏 1: 系列
|
||||
Future getSeriesDetail(type) async {
|
||||
if (type == 'onRefresh') {
|
||||
pn = 1;
|
||||
}
|
||||
var res = await MemberHttp.getSeriesDetail(
|
||||
mid: mid,
|
||||
seriesId: seriesId!,
|
||||
pn: pn,
|
||||
currentMid: 17340771,
|
||||
);
|
||||
if (res['status']) {
|
||||
seasonsList.addAll(res['data'].seriesList);
|
||||
page = res['data'].page;
|
||||
pn += 1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
getSeasonDetail('onLoad');
|
||||
if (category == '0') {
|
||||
getSeasonDetail('onLoad');
|
||||
}
|
||||
if (category == '1') {
|
||||
getSeriesDetail('onLoad');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,12 +17,15 @@ class _MemberSeasonsPageState extends State<MemberSeasonsPage> {
|
||||
Get.put(MemberSeasonsController());
|
||||
late Future _futureBuilderFuture;
|
||||
late ScrollController scrollController;
|
||||
late String category;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture =
|
||||
_memberSeasonsController.getSeasonDetail('onRefresh');
|
||||
category = Get.parameters['category']!;
|
||||
_futureBuilderFuture = category == '0'
|
||||
? _memberSeasonsController.getSeasonDetail('onRefresh')
|
||||
: _memberSeasonsController.getSeriesDetail('onRefresh');
|
||||
scrollController = _memberSeasonsController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
|
||||
@ -8,8 +8,20 @@ class MessageSystemController extends GetxController {
|
||||
Future queryMessageSystem({String type = 'init'}) async {
|
||||
var res = await MsgHttp.messageSystem();
|
||||
if (res['status']) {
|
||||
systemItems.addAll(res['data']);
|
||||
if (type == 'init') {
|
||||
systemItems.value = res['data'];
|
||||
} else {
|
||||
systemItems.addAll(res['data']);
|
||||
}
|
||||
if (systemItems.isNotEmpty) {
|
||||
systemMarkRead(systemItems.first.cursor!);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 标记已读
|
||||
void systemMarkRead(int cursor) async {
|
||||
await MsgHttp.systemMarkRead(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
@ -197,45 +198,38 @@ class VideoIntroController extends GetxController {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
if (hasCoin.value) {
|
||||
SmartDialog.showToast('已投过币了');
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('选择投币个数'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
|
||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [1, 2]
|
||||
.map(
|
||||
(e) => RadioListTile(
|
||||
value: e,
|
||||
title: Text('$e枚'),
|
||||
groupValue: _tempThemeValue,
|
||||
onChanged: (value) async {
|
||||
_tempThemeValue = value!;
|
||||
setState(() {});
|
||||
var res = await VideoHttp.coinVideo(
|
||||
bvid: bvid, multiply: _tempThemeValue);
|
||||
if (res['status']) {
|
||||
SmartDialog.showToast('投币成功');
|
||||
hasCoin.value = true;
|
||||
videoDetail.value.stat!.coin =
|
||||
videoDetail.value.stat!.coin! + _tempThemeValue;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [1, 2]
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Text('$e 枚'),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
onTap: () async {
|
||||
var res =
|
||||
await VideoHttp.coinVideo(bvid: bvid, multiply: e);
|
||||
if (res['status']) {
|
||||
SmartDialog.showToast('投币成功');
|
||||
hasCoin.value = true;
|
||||
videoDetail.value.stat!.coin =
|
||||
videoDetail.value.stat!.coin! + e;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appscheme/appscheme.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -14,12 +16,14 @@ import 'package:pilipala/pages/main/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
|
||||
import 'package:pilipala/plugin/pl_gallery/index.dart';
|
||||
import 'package:pilipala/plugin/pl_popup/index.dart';
|
||||
import 'package:pilipala/utils/app_scheme.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/url_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'reply_save.dart';
|
||||
import 'zan.dart';
|
||||
|
||||
Box setting = GStrorage.setting;
|
||||
@ -32,6 +36,7 @@ class ReplyItem extends StatelessWidget {
|
||||
this.showReplyRow = true,
|
||||
this.replyReply,
|
||||
this.replyType,
|
||||
this.replySave = false,
|
||||
super.key,
|
||||
});
|
||||
final ReplyItemModel? replyItem;
|
||||
@ -40,6 +45,7 @@ class ReplyItem extends StatelessWidget {
|
||||
final bool? showReplyRow;
|
||||
final Function? replyReply;
|
||||
final ReplyType? replyType;
|
||||
final bool? replySave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -47,19 +53,28 @@ class ReplyItem extends StatelessWidget {
|
||||
child: InkWell(
|
||||
// 点击整个评论区 评论详情/回复
|
||||
onTap: () {
|
||||
if (replySave!) {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
if (replyReply != null) {
|
||||
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (replySave!) {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return MorePanel(item: replyItem);
|
||||
return MorePanel(
|
||||
item: replyItem,
|
||||
mainFloor: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -232,7 +247,7 @@ class ReplyItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
// 操作区域
|
||||
bottonAction(context, replyItem!.replyControl),
|
||||
bottonAction(context, replyItem!.replyControl, replySave),
|
||||
// 一楼的评论
|
||||
if ((replyItem!.replyControl!.isShow! ||
|
||||
replyItem!.replies!.isNotEmpty) &&
|
||||
@ -253,7 +268,7 @@ class ReplyItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
// 感谢、回复、复制
|
||||
Widget bottonAction(BuildContext context, replyControl) {
|
||||
Widget bottonAction(BuildContext context, replyControl, replySave) {
|
||||
ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return Row(
|
||||
@ -286,16 +301,26 @@ class ReplyItem extends StatelessWidget {
|
||||
});
|
||||
},
|
||||
child: Row(children: [
|
||||
Icon(Icons.reply,
|
||||
size: 18, color: colorScheme.outline.withOpacity(0.8)),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'回复',
|
||||
style: TextStyle(
|
||||
fontSize: textTheme.labelMedium!.fontSize,
|
||||
color: colorScheme.outline,
|
||||
if (!replySave!) ...[
|
||||
Icon(Icons.reply,
|
||||
size: 18, color: colorScheme.outline.withOpacity(0.8)),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'回复',
|
||||
style: TextStyle(
|
||||
fontSize: textTheme.labelMedium!.fontSize,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
)
|
||||
],
|
||||
if (replySave!)
|
||||
Text(
|
||||
IdUtils.av2bv(replyItem!.oid!),
|
||||
style: TextStyle(
|
||||
fontSize: textTheme.labelMedium!.fontSize,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
@ -436,7 +461,8 @@ class ReplyItemRow extends StatelessWidget {
|
||||
if (extraRow == 1)
|
||||
InkWell(
|
||||
// 一楼点击【共xx条回复】展开评论详情
|
||||
onTap: () => replyReply!(replyItem),
|
||||
onTap: () => replyReply?.call(replyItem, null, true),
|
||||
onLongPress: () => {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),
|
||||
@ -549,7 +575,7 @@ InlineSpan buildContent(
|
||||
);
|
||||
}
|
||||
|
||||
void onPreviewImg(picList, initIndex) {
|
||||
void onPreviewImg(picList, initIndex, randomInt) {
|
||||
final MainController mainController = Get.find<MainController>();
|
||||
mainController.imgPreviewStatus = true;
|
||||
Navigator.of(context).push(
|
||||
@ -575,7 +601,7 @@ InlineSpan buildContent(
|
||||
},
|
||||
child: Center(
|
||||
child: Hero(
|
||||
tag: picList[index],
|
||||
tag: picList[index] + randomInt,
|
||||
child: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: picList[index],
|
||||
@ -886,11 +912,12 @@ InlineSpan buildContent(
|
||||
pictureItem['img_width']))
|
||||
.truncateToDouble();
|
||||
} catch (_) {}
|
||||
String randomInt = Random().nextInt(101).toString();
|
||||
|
||||
return Hero(
|
||||
tag: picList[0],
|
||||
tag: picList[0] + randomInt,
|
||||
child: GestureDetector(
|
||||
onTap: () => onPreviewImg(picList, 0),
|
||||
onTap: () => onPreviewImg(picList, 0, randomInt),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
@ -927,13 +954,14 @@ InlineSpan buildContent(
|
||||
picList.add(content.pictures[i]['img_src']);
|
||||
}
|
||||
for (var i = 0; i < len; i++) {
|
||||
String randomInt = Random().nextInt(101).toString();
|
||||
list.add(
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return Hero(
|
||||
tag: picList[i],
|
||||
tag: picList[i] + randomInt,
|
||||
child: GestureDetector(
|
||||
onTap: () => onPreviewImg(picList, i),
|
||||
onTap: () => onPreviewImg(picList, i, randomInt),
|
||||
child: NetworkImgLayer(
|
||||
src: picList[i],
|
||||
width: box.maxWidth,
|
||||
@ -1004,7 +1032,12 @@ InlineSpan buildContent(
|
||||
|
||||
class MorePanel extends StatelessWidget {
|
||||
final dynamic item;
|
||||
const MorePanel({super.key, required this.item});
|
||||
final bool mainFloor;
|
||||
const MorePanel({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.mainFloor = false,
|
||||
});
|
||||
|
||||
Future<dynamic> menuActionHandler(String type) async {
|
||||
String message = item.content.message ?? item.content;
|
||||
@ -1026,6 +1059,13 @@ class MorePanel extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'save':
|
||||
Get.back();
|
||||
Navigator.push(
|
||||
Get.context!,
|
||||
PlPopupRoute(child: ReplySave(replyItem: item)),
|
||||
);
|
||||
break;
|
||||
// case 'block':
|
||||
// SmartDialog.showToast('加入黑名单');
|
||||
// break;
|
||||
@ -1076,6 +1116,13 @@ class MorePanel extends StatelessWidget {
|
||||
leading: const Icon(Icons.copy_outlined, size: 19),
|
||||
title: Text('自由复制', style: textTheme.titleSmall),
|
||||
),
|
||||
if (mainFloor && item.content.pictures.isEmpty)
|
||||
ListTile(
|
||||
onTap: () async => await menuActionHandler('save'),
|
||||
minLeadingWidth: 0,
|
||||
leading: const Icon(Icons.save_alt_rounded, size: 19),
|
||||
title: Text('本地保存', style: textTheme.titleSmall),
|
||||
),
|
||||
// ListTile(
|
||||
// onTap: () async => await menuActionHandler('block'),
|
||||
// minLeadingWidth: 0,
|
||||
|
||||
148
lib/pages/video/detail/reply/widgets/reply_save.dart
Normal file
148
lib/pages/video/detail/reply/widgets/reply_save.dart
Normal file
@ -0,0 +1,148 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:saver_gallery/saver_gallery.dart';
|
||||
|
||||
class ReplySave extends StatefulWidget {
|
||||
final ReplyItemModel? replyItem;
|
||||
const ReplySave({required this.replyItem, super.key});
|
||||
|
||||
@override
|
||||
State<ReplySave> createState() => _ReplySaveState();
|
||||
}
|
||||
|
||||
class _ReplySaveState extends State<ReplySave> {
|
||||
final _boundaryKey = GlobalKey();
|
||||
|
||||
void _generatePicWidget() async {
|
||||
SmartDialog.showLoading(msg: '保存中');
|
||||
try {
|
||||
RenderRepaintBoundary boundary = _boundaryKey.currentContext!
|
||||
.findRenderObject() as RenderRepaintBoundary;
|
||||
var image = await boundary.toImage(pixelRatio: 3);
|
||||
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||
String picName =
|
||||
"plpl_reply_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}";
|
||||
final result = await SaverGallery.saveImage(
|
||||
Uint8List.fromList(pngBytes),
|
||||
name: '$picName.png',
|
||||
androidRelativePath: "Pictures/PiliPala",
|
||||
androidExistNotSave: false,
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
SmartDialog.showToast('保存成功');
|
||||
}
|
||||
} catch (err) {
|
||||
print(err);
|
||||
} finally {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _createWidgets(int count, Widget Function() builder) {
|
||||
return List<Widget>.generate(count, (_) => Expanded(child: builder()));
|
||||
}
|
||||
|
||||
List<Widget> _createColumnWidgets() {
|
||||
return _createWidgets(3, () => Row(children: _createRowWidgets()));
|
||||
}
|
||||
|
||||
List<Widget> _createRowWidgets() {
|
||||
return _createWidgets(
|
||||
4,
|
||||
() => Center(
|
||||
child: Transform.rotate(
|
||||
angle: pi / 10,
|
||||
child: const Text(
|
||||
'PiliPala',
|
||||
style: TextStyle(
|
||||
color: Color(0x08000000),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
|
||||
child: Container(
|
||||
width: Get.width,
|
||||
height: Get.height,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0,
|
||||
MediaQuery.of(context).padding.top + 4,
|
||||
0,
|
||||
MediaQuery.of(context).padding.bottom + 4,
|
||||
),
|
||||
color: Colors.black54,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: RepaintBoundary(
|
||||
key: _boundaryKey,
|
||||
child: IntrinsicHeight(
|
||||
child: Stack(
|
||||
children: [
|
||||
ReplyItem(
|
||||
replyItem: widget.replyItem,
|
||||
showReplyRow: false,
|
||||
replySave: true,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: _createColumnWidgets(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 40),
|
||||
FilledButton(
|
||||
onPressed: _generatePicWidget,
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
||||
this.replyType,
|
||||
this.sheetHeight,
|
||||
this.currentReply,
|
||||
this.loadMore,
|
||||
this.loadMore = true,
|
||||
super.key,
|
||||
});
|
||||
final int? oid;
|
||||
@ -32,7 +32,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
||||
final ReplyType? replyType;
|
||||
final double? sheetHeight;
|
||||
final dynamic currentReply;
|
||||
final bool? loadMore;
|
||||
final bool loadMore;
|
||||
|
||||
@override
|
||||
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
||||
@ -142,7 +142,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
),
|
||||
),
|
||||
],
|
||||
widget.loadMore != null && widget.loadMore!
|
||||
widget.loadMore
|
||||
? FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
|
||||
@ -63,7 +63,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
late bool autoPlayEnable;
|
||||
late bool autoPiP;
|
||||
late Floating floating;
|
||||
bool isShowing = true;
|
||||
RxBool isShowing = true.obs;
|
||||
// 生命周期监听
|
||||
late final AppLifecycleListener _lifecycleListener;
|
||||
late double statusHeight;
|
||||
@ -183,6 +183,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController!.addStatusLister(playerListener);
|
||||
vdCtr.autoPlay.value = true;
|
||||
vdCtr.isShowCover.value = false;
|
||||
isShowing.value = true;
|
||||
autoEnterPip(status: PlayerStatus.playing);
|
||||
}
|
||||
|
||||
@ -258,7 +259,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController!.pause();
|
||||
vdCtr.clearSubtitleContent();
|
||||
}
|
||||
setState(() => isShowing = false);
|
||||
isShowing.value = false;
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@ -272,10 +273,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
if (plPlayerController != null &&
|
||||
plPlayerController!.videoPlayerController != null) {
|
||||
setState(() {
|
||||
vdCtr.setSubtitleContent();
|
||||
isShowing = true;
|
||||
});
|
||||
vdCtr.setSubtitleContent();
|
||||
isShowing.value = true;
|
||||
}
|
||||
vdCtr.isFirstTime = false;
|
||||
final bool autoplay = autoPlayEnable;
|
||||
@ -330,12 +329,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController?.danmakuController?.clear();
|
||||
break;
|
||||
case 'pause':
|
||||
vdCtr.hiddenReplyReplyPanel();
|
||||
if (vdCtr.videoType == SearchType.video) {
|
||||
videoIntroController.hiddenEpisodeBottomSheet();
|
||||
}
|
||||
if (vdCtr.videoType == SearchType.media_bangumi) {
|
||||
bangumiIntroController.hiddenEpisodeBottomSheet();
|
||||
if (autoPiP) {
|
||||
vdCtr.hiddenReplyReplyPanel();
|
||||
if (vdCtr.videoType == SearchType.video) {
|
||||
videoIntroController.hiddenEpisodeBottomSheet();
|
||||
}
|
||||
if (vdCtr.videoType == SearchType.media_bangumi) {
|
||||
bangumiIntroController.hiddenEpisodeBottomSheet();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -650,7 +651,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
tag: heroTag,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (isShowing) buildVideoPlayerPanel(),
|
||||
Obx(
|
||||
() => isShowing.value
|
||||
? buildVideoPlayerPanel()
|
||||
: const SizedBox(),
|
||||
),
|
||||
|
||||
/// 关闭自动播放时 手动播放
|
||||
Obx(
|
||||
|
||||
@ -214,7 +214,7 @@ class SessionItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid);
|
||||
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0);
|
||||
final content = sessionItem.lastMsg.content;
|
||||
final msgStatus = sessionItem.lastMsg.msgStatus;
|
||||
|
||||
@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget {
|
||||
'talkerId': sessionItem.talkerId.toString(),
|
||||
'name': sessionItem.accountInfo.name,
|
||||
'face': sessionItem.accountInfo.face,
|
||||
'mid': sessionItem.accountInfo.mid.toString(),
|
||||
'mid': (sessionItem.accountInfo?.mid ?? 0).toString(),
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user