merge main
This commit is contained in:
@ -215,9 +215,8 @@ class VideoContent extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (videoItem.goto == 'bangumi')
|
if (videoItem.goto == 'bangumi')
|
||||||
_buildBadge(videoItem.bangumiBadge, 'line', 9),
|
_buildBadge(videoItem.bangumiBadge, 'line', 9),
|
||||||
if (videoItem.rcmdReason?.content != null &&
|
if (videoItem.rcmdReason != null)
|
||||||
videoItem.rcmdReason.content != '')
|
_buildBadge(videoItem.rcmdReason, 'color'),
|
||||||
_buildBadge(videoItem.rcmdReason.content, 'color'),
|
|
||||||
if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),
|
if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),
|
||||||
if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),
|
if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@ -487,6 +487,8 @@ class Api {
|
|||||||
static const getSeasonDetailApi =
|
static const getSeasonDetailApi =
|
||||||
'/x/polymer/web-space/seasons_archives_list';
|
'/x/polymer/web-space/seasons_archives_list';
|
||||||
|
|
||||||
|
static const getSeriesDetailApi = '/x/series/archives';
|
||||||
|
|
||||||
/// 获取未读动态数
|
/// 获取未读动态数
|
||||||
static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';
|
static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';
|
||||||
|
|
||||||
@ -562,4 +564,11 @@ class Api {
|
|||||||
|
|
||||||
/// 新建收藏夹
|
/// 新建收藏夹
|
||||||
static const String addFavFolder = '/x/v3/fav/folder/add';
|
static const String addFavFolder = '/x/v3/fav/folder/add';
|
||||||
|
|
||||||
|
/// 直播间弹幕信息
|
||||||
|
static const String getDanmuInfo =
|
||||||
|
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';
|
||||||
|
|
||||||
|
/// 直播间发送弹幕
|
||||||
|
static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class Request {
|
|||||||
late String systemProxyPort;
|
late String systemProxyPort;
|
||||||
static final RegExp spmPrefixExp =
|
static final RegExp spmPrefixExp =
|
||||||
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
|
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
|
||||||
|
static String? buvid;
|
||||||
|
|
||||||
/// 设置cookie
|
/// 设置cookie
|
||||||
static setCookie() async {
|
static setCookie() async {
|
||||||
@ -70,6 +71,7 @@ class Request {
|
|||||||
final String cookieString = cookie
|
final String cookieString = cookie
|
||||||
.map((Cookie cookie) => '${cookie.name}=${cookie.value}')
|
.map((Cookie cookie) => '${cookie.name}=${cookie.value}')
|
||||||
.join('; ');
|
.join('; ');
|
||||||
|
|
||||||
dio.options.headers['cookie'] = cookieString;
|
dio.options.headers['cookie'] = cookieString;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +86,30 @@ class Request {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> getBuvid() async {
|
||||||
|
if (buvid != null) {
|
||||||
|
return buvid!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Cookie> cookies = await cookieManager.cookieJar
|
||||||
|
.loadForRequest(Uri.parse(HttpString.baseUrl));
|
||||||
|
buvid = cookies.firstWhere((cookie) => cookie.name == 'buvid3').value;
|
||||||
|
if (buvid == null) {
|
||||||
|
try {
|
||||||
|
var result = await Request().get(
|
||||||
|
"${HttpString.apiBaseUrl}/x/frontend/finger/spi",
|
||||||
|
);
|
||||||
|
buvid = result["data"]["b_3"].toString();
|
||||||
|
} catch (e) {
|
||||||
|
// 处理请求错误
|
||||||
|
buvid = '';
|
||||||
|
print("Error fetching buvid: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buvid!;
|
||||||
|
}
|
||||||
|
|
||||||
static setOptionsHeaders(userInfo, bool status) {
|
static setOptionsHeaders(userInfo, bool status) {
|
||||||
if (status) {
|
if (status) {
|
||||||
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
|
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
|
||||||
|
|||||||
@ -65,4 +65,56 @@ class LiveHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取弹幕信息
|
||||||
|
static Future liveDanmakuInfo({roomId}) async {
|
||||||
|
var res = await Request().get(Api.getDanmuInfo, data: {
|
||||||
|
'id': roomId,
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送弹幕
|
||||||
|
static Future sendDanmaku({roomId, msg}) async {
|
||||||
|
var res = await Request().post(Api.sendLiveMsg, queryParameters: {
|
||||||
|
'bubble': 0,
|
||||||
|
'msg': msg,
|
||||||
|
'color': 16777215, // 颜色
|
||||||
|
'mode': 1, // 模式
|
||||||
|
'room_type': 0,
|
||||||
|
'jumpfrom': 71001, // 直播间来源
|
||||||
|
'reply_mid': 0,
|
||||||
|
'reply_attr': 0,
|
||||||
|
'replay_dmid': '',
|
||||||
|
'statistics': {"appId": 100, "platform": 5},
|
||||||
|
'fontsize': 25, // 字体大小
|
||||||
|
'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳
|
||||||
|
'roomid': roomId,
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
'csrf_token': await Request.getCsrf(),
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -520,4 +520,40 @@ class MemberHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future getSeriesDetail({
|
||||||
|
required int mid,
|
||||||
|
required int currentMid,
|
||||||
|
required int seriesId,
|
||||||
|
required int pn,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.getSeriesDetailApi,
|
||||||
|
data: {
|
||||||
|
'mid': mid,
|
||||||
|
'series_id': seriesId,
|
||||||
|
'only_normal': true,
|
||||||
|
'sort': 'desc',
|
||||||
|
'pn': pn,
|
||||||
|
'ps': 30,
|
||||||
|
'current_mid': currentMid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': MemberSeasonsDataModel.fromJson(res.data['data'])
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
print(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -298,7 +298,6 @@ class MsgHttp {
|
|||||||
});
|
});
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
try {
|
try {
|
||||||
print(res.data['data']['system_notify_list']);
|
|
||||||
return {
|
return {
|
||||||
'status': true,
|
'status': true,
|
||||||
'data': res.data['data']['system_notify_list']
|
'data': res.data['data']['system_notify_list']
|
||||||
@ -312,4 +311,23 @@ class MsgHttp {
|
|||||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统消息标记已读
|
||||||
|
static Future systemMarkRead(int cursor) async {
|
||||||
|
String csrf = await Request.getCsrf();
|
||||||
|
var res = await Request().get(Api.systemMarkRead, data: {
|
||||||
|
'csrf': csrf,
|
||||||
|
'cursor': cursor,
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,6 @@ class VideoHttp {
|
|||||||
// 添加额外的loginState变量模拟未登录状态
|
// 添加额外的loginState变量模拟未登录状态
|
||||||
static Future rcmdVideoListApp(
|
static Future rcmdVideoListApp(
|
||||||
{bool loginStatus = true, required int freshIdx}) async {
|
{bool loginStatus = true, required int freshIdx}) async {
|
||||||
try {
|
|
||||||
var res = await Request().get(
|
var res = await Request().get(
|
||||||
Api.recommendListApp,
|
Api.recommendListApp,
|
||||||
data: {
|
data: {
|
||||||
@ -83,8 +82,8 @@ class VideoHttp {
|
|||||||
'pull': freshIdx == 0 ? 'true' : 'false',
|
'pull': freshIdx == 0 ? 'true' : 'false',
|
||||||
'appkey': Constants.appKey,
|
'appkey': Constants.appKey,
|
||||||
'access_key': loginStatus
|
'access_key': loginStatus
|
||||||
? (localCache.get(LocalCacheKey.accessKey,
|
? (localCache
|
||||||
defaultValue: {})['value'] ??
|
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
|
||||||
'')
|
'')
|
||||||
: ''
|
: ''
|
||||||
},
|
},
|
||||||
@ -109,9 +108,6 @@ class VideoHttp {
|
|||||||
} else {
|
} else {
|
||||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
return {'status': false, 'data': [], 'msg': err.toString()};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最热视频
|
// 最热视频
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class RecVideoItemAppModel {
|
|||||||
String? title;
|
String? title;
|
||||||
int? isFollowed;
|
int? isFollowed;
|
||||||
RcmdOwner? owner;
|
RcmdOwner? owner;
|
||||||
RcmdReason? rcmdReason;
|
String? rcmdReason;
|
||||||
String? goto;
|
String? goto;
|
||||||
int? param;
|
int? param;
|
||||||
String? uri;
|
String? uri;
|
||||||
@ -62,25 +62,19 @@ class RecVideoItemAppModel {
|
|||||||
duration =
|
duration =
|
||||||
json['player_args'] != null ? json['player_args']['duration'] : -1;
|
json['player_args'] != null ? json['player_args']['duration'] : -1;
|
||||||
//duration = json['cover_right_text'];
|
//duration = json['cover_right_text'];
|
||||||
title = json['title'];
|
title = json['title'] ?? '获取标题失败';
|
||||||
owner = RcmdOwner.fromJson(json);
|
owner = RcmdOwner.fromJson(json);
|
||||||
rcmdReason = json['rcmd_reason_style'] != null
|
rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason'];
|
||||||
? RcmdReason.fromJson(json['rcmd_reason_style'])
|
|
||||||
: null;
|
|
||||||
// 由于app端api并不会直接返回与owner的关注状态
|
// 由于app端api并不会直接返回与owner的关注状态
|
||||||
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态,从而与web端接口等效
|
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态,从而与web端接口等效
|
||||||
RegExp regex = RegExp(r'已关注|新关注');
|
RegExp regex = RegExp(r'已关注|新关注');
|
||||||
isFollowed = rcmdReason != null &&
|
isFollowed = regex.hasMatch(rcmdReason ?? '') ? 1 : 0;
|
||||||
rcmdReason!.content != null &&
|
|
||||||
regex.hasMatch(rcmdReason!.content!)
|
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
// 如果是,就无需再显示推荐原因,交由view统一处理即可
|
// 如果是,就无需再显示推荐原因,交由view统一处理即可
|
||||||
if (isFollowed == 1) {
|
if (isFollowed == 1) {
|
||||||
rcmdReason = null;
|
rcmdReason = null;
|
||||||
}
|
}
|
||||||
goto = json['goto'];
|
goto = json['goto'];
|
||||||
param = int.parse(json['param']);
|
param = int.parse(json['param'] ?? '-1');
|
||||||
uri = json['uri'];
|
uri = json['uri'];
|
||||||
talkBack = json['talk_back'];
|
talkBack = json['talk_back'];
|
||||||
|
|
||||||
|
|||||||
101
lib/models/live/message.dart
Normal file
101
lib/models/live/message.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
class LiveMessageModel {
|
||||||
|
// 消息类型
|
||||||
|
final LiveMessageType type;
|
||||||
|
|
||||||
|
// 用户名
|
||||||
|
final String userName;
|
||||||
|
|
||||||
|
// 信息
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
final dynamic data;
|
||||||
|
|
||||||
|
final String? face;
|
||||||
|
final int? uid;
|
||||||
|
final Map<String, dynamic>? emots;
|
||||||
|
|
||||||
|
// 颜色
|
||||||
|
final LiveMessageColor color;
|
||||||
|
|
||||||
|
LiveMessageModel({
|
||||||
|
required this.type,
|
||||||
|
required this.userName,
|
||||||
|
required this.message,
|
||||||
|
required this.color,
|
||||||
|
this.data,
|
||||||
|
this.face,
|
||||||
|
this.uid,
|
||||||
|
this.emots,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveSuperChatMessage {
|
||||||
|
final String backgroundBottomColor;
|
||||||
|
final String backgroundColor;
|
||||||
|
final DateTime endTime;
|
||||||
|
final String face;
|
||||||
|
final String message;
|
||||||
|
final String price;
|
||||||
|
final DateTime startTime;
|
||||||
|
final String userName;
|
||||||
|
|
||||||
|
LiveSuperChatMessage({
|
||||||
|
required this.backgroundBottomColor,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.endTime,
|
||||||
|
required this.face,
|
||||||
|
required this.message,
|
||||||
|
required this.price,
|
||||||
|
required this.startTime,
|
||||||
|
required this.userName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LiveMessageType {
|
||||||
|
// 普通留言
|
||||||
|
chat,
|
||||||
|
// 醒目留言
|
||||||
|
superChat,
|
||||||
|
//
|
||||||
|
online,
|
||||||
|
// 加入
|
||||||
|
join,
|
||||||
|
// 关注
|
||||||
|
follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveMessageColor {
|
||||||
|
final int r, g, b;
|
||||||
|
LiveMessageColor(this.r, this.g, this.b);
|
||||||
|
static LiveMessageColor get white => LiveMessageColor(255, 255, 255);
|
||||||
|
static LiveMessageColor numberToColor(int intColor) {
|
||||||
|
var obj = intColor.toRadixString(16);
|
||||||
|
|
||||||
|
LiveMessageColor color = LiveMessageColor.white;
|
||||||
|
if (obj.length == 4) {
|
||||||
|
obj = "00$obj";
|
||||||
|
}
|
||||||
|
if (obj.length == 6) {
|
||||||
|
var R = int.parse(obj.substring(0, 2), radix: 16);
|
||||||
|
var G = int.parse(obj.substring(2, 4), radix: 16);
|
||||||
|
var B = int.parse(obj.substring(4, 6), radix: 16);
|
||||||
|
|
||||||
|
color = LiveMessageColor(R, G, B);
|
||||||
|
}
|
||||||
|
if (obj.length == 8) {
|
||||||
|
var R = int.parse(obj.substring(2, 4), radix: 16);
|
||||||
|
var G = int.parse(obj.substring(4, 6), radix: 16);
|
||||||
|
var B = int.parse(obj.substring(6, 8), radix: 16);
|
||||||
|
//var A = int.parse(obj.substring(0, 2), radix: 16);
|
||||||
|
color = LiveMessageColor(R, G, B);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "#${r.toRadixString(16).padLeft(2, '0')}${g.toRadixString(16).padLeft(2, '0')}${b.toRadixString(16).padLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,12 @@ class MemberSeasonsDataModel {
|
|||||||
MemberSeasonsDataModel({
|
MemberSeasonsDataModel({
|
||||||
this.page,
|
this.page,
|
||||||
this.seasonsList,
|
this.seasonsList,
|
||||||
|
this.seriesList,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map? page;
|
Map? page;
|
||||||
List<MemberSeasonsList>? seasonsList;
|
List<MemberSeasonsList>? seasonsList;
|
||||||
|
List<MemberArchiveItem>? seriesList;
|
||||||
|
|
||||||
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
|
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
page = json['page'];
|
page = json['page'];
|
||||||
@ -19,6 +21,11 @@ class MemberSeasonsDataModel {
|
|||||||
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
|
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
|
||||||
.toList()
|
.toList()
|
||||||
: [];
|
: [];
|
||||||
|
seriesList = json['archives'] != null
|
||||||
|
? json['archives']
|
||||||
|
.map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
: [];
|
||||||
|
|
||||||
seasonsList = [...tempList1, ...tempList2];
|
seasonsList = [...tempList1, ...tempList2];
|
||||||
}
|
}
|
||||||
@ -93,6 +100,8 @@ class MamberMeta {
|
|||||||
this.ptime,
|
this.ptime,
|
||||||
this.seasonId,
|
this.seasonId,
|
||||||
this.total,
|
this.total,
|
||||||
|
this.seriesId,
|
||||||
|
this.category,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? cover;
|
String? cover;
|
||||||
@ -102,6 +111,8 @@ class MamberMeta {
|
|||||||
int? ptime;
|
int? ptime;
|
||||||
int? seasonId;
|
int? seasonId;
|
||||||
int? total;
|
int? total;
|
||||||
|
int? seriesId;
|
||||||
|
int? category;
|
||||||
|
|
||||||
MamberMeta.fromJson(Map<String, dynamic> json) {
|
MamberMeta.fromJson(Map<String, dynamic> json) {
|
||||||
cover = json['cover'];
|
cover = json['cover'];
|
||||||
@ -111,5 +122,7 @@ class MamberMeta {
|
|||||||
ptime = json['ptime'];
|
ptime = json['ptime'];
|
||||||
seasonId = json['season_id'];
|
seasonId = json['season_id'];
|
||||||
total = json['total'];
|
total = json['total'];
|
||||||
|
seriesId = json['series_id'];
|
||||||
|
category = json['category'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
import 'package:hive/hive.dart';
|
|
||||||
|
|
||||||
part 'model_owner.g.dart';
|
|
||||||
|
|
||||||
@HiveType(typeId: 3)
|
|
||||||
class Owner {
|
class Owner {
|
||||||
Owner({
|
Owner({
|
||||||
this.mid,
|
this.mid,
|
||||||
this.name,
|
this.name,
|
||||||
this.face,
|
this.face,
|
||||||
});
|
});
|
||||||
@HiveField(0)
|
|
||||||
int? mid;
|
int? mid;
|
||||||
@HiveField(1)
|
|
||||||
String? name;
|
String? name;
|
||||||
@HiveField(2)
|
|
||||||
String? face;
|
String? face;
|
||||||
|
|
||||||
Owner.fromJson(Map<String, dynamic> json) {
|
Owner.fromJson(Map<String, dynamic> json) {
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'model_owner.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class OwnerAdapter extends TypeAdapter<Owner> {
|
|
||||||
@override
|
|
||||||
final int typeId = 3;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Owner read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return Owner(
|
|
||||||
mid: fields[0] as int?,
|
|
||||||
name: fields[1] as String?,
|
|
||||||
face: fields[2] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, Owner obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(3)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.mid)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.face);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is OwnerAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,5 @@
|
|||||||
import './model_owner.dart';
|
import './model_owner.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
|
|
||||||
part 'model_rec_video_item.g.dart';
|
|
||||||
|
|
||||||
@HiveType(typeId: 0)
|
|
||||||
class RecVideoItemModel {
|
class RecVideoItemModel {
|
||||||
RecVideoItemModel({
|
RecVideoItemModel({
|
||||||
this.id,
|
this.id,
|
||||||
@ -21,32 +17,19 @@ class RecVideoItemModel {
|
|||||||
this.rcmdReason,
|
this.rcmdReason,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
|
||||||
int? id = -1;
|
int? id = -1;
|
||||||
@HiveField(1)
|
|
||||||
String? bvid = '';
|
String? bvid = '';
|
||||||
@HiveField(2)
|
|
||||||
int? cid = -1;
|
int? cid = -1;
|
||||||
@HiveField(3)
|
|
||||||
String? goto = '';
|
String? goto = '';
|
||||||
@HiveField(4)
|
|
||||||
String? uri = '';
|
String? uri = '';
|
||||||
@HiveField(5)
|
|
||||||
String? pic = '';
|
String? pic = '';
|
||||||
@HiveField(6)
|
|
||||||
String? title = '';
|
String? title = '';
|
||||||
@HiveField(7)
|
|
||||||
int? duration = -1;
|
int? duration = -1;
|
||||||
@HiveField(8)
|
|
||||||
int? pubdate = -1;
|
int? pubdate = -1;
|
||||||
@HiveField(9)
|
|
||||||
Owner? owner;
|
Owner? owner;
|
||||||
@HiveField(10)
|
|
||||||
Stat? stat;
|
Stat? stat;
|
||||||
@HiveField(11)
|
|
||||||
int? isFollowed;
|
int? isFollowed;
|
||||||
@HiveField(12)
|
String? rcmdReason;
|
||||||
RcmdReason? rcmdReason;
|
|
||||||
|
|
||||||
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
|
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
id = json["id"];
|
id = json["id"];
|
||||||
@ -61,26 +44,20 @@ class RecVideoItemModel {
|
|||||||
owner = Owner.fromJson(json["owner"]);
|
owner = Owner.fromJson(json["owner"]);
|
||||||
stat = Stat.fromJson(json["stat"]);
|
stat = Stat.fromJson(json["stat"]);
|
||||||
isFollowed = json["is_followed"] ?? 0;
|
isFollowed = json["is_followed"] ?? 0;
|
||||||
rcmdReason = json["rcmd_reason"] != null
|
rcmdReason = json["rcmd_reason"]?['content'];
|
||||||
? RcmdReason.fromJson(json["rcmd_reason"])
|
|
||||||
: RcmdReason(content: '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 1)
|
|
||||||
class Stat {
|
class Stat {
|
||||||
Stat({
|
Stat({
|
||||||
this.view,
|
this.view,
|
||||||
this.like,
|
this.like,
|
||||||
this.danmu,
|
this.danmu,
|
||||||
});
|
});
|
||||||
@HiveField(0)
|
|
||||||
int? view;
|
|
||||||
@HiveField(1)
|
|
||||||
int? like;
|
|
||||||
@HiveField(2)
|
|
||||||
int? danmu;
|
|
||||||
|
|
||||||
|
int? view;
|
||||||
|
int? like;
|
||||||
|
int? danmu;
|
||||||
Stat.fromJson(Map<String, dynamic> json) {
|
Stat.fromJson(Map<String, dynamic> json) {
|
||||||
// 无需在model中转换以保留原始数据,在view层处理即可
|
// 无需在model中转换以保留原始数据,在view层处理即可
|
||||||
view = json["view"];
|
view = json["view"];
|
||||||
@ -88,20 +65,3 @@ class Stat {
|
|||||||
danmu = json['danmaku'];
|
danmu = json['danmaku'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 2)
|
|
||||||
class RcmdReason {
|
|
||||||
RcmdReason({
|
|
||||||
this.reasonType,
|
|
||||||
this.content,
|
|
||||||
});
|
|
||||||
@HiveField(0)
|
|
||||||
int? reasonType;
|
|
||||||
@HiveField(1)
|
|
||||||
String? content = '';
|
|
||||||
|
|
||||||
RcmdReason.fromJson(Map<String, dynamic> json) {
|
|
||||||
reasonType = json["reason_type"];
|
|
||||||
content = json["content"] ?? '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'model_rec_video_item.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RecVideoItemModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return RecVideoItemModel(
|
|
||||||
id: fields[0] as int?,
|
|
||||||
bvid: fields[1] as String?,
|
|
||||||
cid: fields[2] as int?,
|
|
||||||
goto: fields[3] as String?,
|
|
||||||
uri: fields[4] as String?,
|
|
||||||
pic: fields[5] as String?,
|
|
||||||
title: fields[6] as String?,
|
|
||||||
duration: fields[7] as int?,
|
|
||||||
pubdate: fields[8] as int?,
|
|
||||||
owner: fields[9] as Owner?,
|
|
||||||
stat: fields[10] as Stat?,
|
|
||||||
isFollowed: fields[11] as int?,
|
|
||||||
rcmdReason: fields[12] as RcmdReason?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, RecVideoItemModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(13)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.bvid)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.cid)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.goto)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.uri)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.pic)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.title)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.duration)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.pubdate)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.owner)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.stat)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.isFollowed)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.rcmdReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is RecVideoItemModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatAdapter extends TypeAdapter<Stat> {
|
|
||||||
@override
|
|
||||||
final int typeId = 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stat read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return Stat(
|
|
||||||
view: fields[0] as int?,
|
|
||||||
like: fields[1] as int?,
|
|
||||||
danmu: fields[2] as int?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, Stat obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(3)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.view)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.like)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.danmu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is StatAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
|
|
||||||
@override
|
|
||||||
final int typeId = 2;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RcmdReason read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return RcmdReason(
|
|
||||||
reasonType: fields[0] as int?,
|
|
||||||
content: fields[1] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, RcmdReason obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(2)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.reasonType)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is RcmdReasonAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@ -1,14 +1,8 @@
|
|||||||
import 'package:hive/hive.dart';
|
|
||||||
|
|
||||||
part 'hot.g.dart';
|
|
||||||
|
|
||||||
@HiveType(typeId: 6)
|
|
||||||
class HotSearchModel {
|
class HotSearchModel {
|
||||||
HotSearchModel({
|
HotSearchModel({
|
||||||
this.list,
|
this.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
|
||||||
List<HotSearchItem>? list;
|
List<HotSearchItem>? list;
|
||||||
|
|
||||||
HotSearchModel.fromJson(Map<String, dynamic> json) {
|
HotSearchModel.fromJson(Map<String, dynamic> json) {
|
||||||
@ -18,7 +12,6 @@ class HotSearchModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 7)
|
|
||||||
class HotSearchItem {
|
class HotSearchItem {
|
||||||
HotSearchItem({
|
HotSearchItem({
|
||||||
this.keyword,
|
this.keyword,
|
||||||
@ -27,14 +20,10 @@ class HotSearchItem {
|
|||||||
this.icon,
|
this.icon,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
|
||||||
String? keyword;
|
String? keyword;
|
||||||
@HiveField(1)
|
|
||||||
String? showName;
|
String? showName;
|
||||||
// 4/5热 11话题 8普通 7直播
|
// 4/5热 11话题 8普通 7直播
|
||||||
@HiveField(2)
|
|
||||||
int? wordType;
|
int? wordType;
|
||||||
@HiveField(3)
|
|
||||||
String? icon;
|
String? icon;
|
||||||
|
|
||||||
HotSearchItem.fromJson(Map<String, dynamic> json) {
|
HotSearchItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'hot.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class HotSearchModelAdapter extends TypeAdapter<HotSearchModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 6;
|
|
||||||
|
|
||||||
@override
|
|
||||||
HotSearchModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return HotSearchModel(
|
|
||||||
list: (fields[0] as List?)?.cast<HotSearchItem>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, HotSearchModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(1)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is HotSearchModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HotSearchItemAdapter extends TypeAdapter<HotSearchItem> {
|
|
||||||
@override
|
|
||||||
final int typeId = 7;
|
|
||||||
|
|
||||||
@override
|
|
||||||
HotSearchItem read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return HotSearchItem(
|
|
||||||
keyword: fields[0] as String?,
|
|
||||||
showName: fields[1] as String?,
|
|
||||||
wordType: fields[2] as int?,
|
|
||||||
icon: fields[3] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, HotSearchItem obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(4)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.keyword)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.showName)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.wordType)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is HotSearchItemAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,3 @@
|
|||||||
import 'package:hive/hive.dart';
|
|
||||||
|
|
||||||
part 'stat.g.dart';
|
|
||||||
|
|
||||||
@HiveType(typeId: 1)
|
|
||||||
class UserStat {
|
class UserStat {
|
||||||
UserStat({
|
UserStat({
|
||||||
this.following,
|
this.following,
|
||||||
@ -10,11 +5,8 @@ class UserStat {
|
|||||||
this.dynamicCount,
|
this.dynamicCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
|
||||||
int? following;
|
int? following;
|
||||||
@HiveField(1)
|
|
||||||
int? follower;
|
int? follower;
|
||||||
@HiveField(2)
|
|
||||||
int? dynamicCount;
|
int? dynamicCount;
|
||||||
|
|
||||||
UserStat.fromJson(Map<String, dynamic> json) {
|
UserStat.fromJson(Map<String, dynamic> json) {
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'stat.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class UserStatAdapter extends TypeAdapter<UserStat> {
|
|
||||||
@override
|
|
||||||
final int typeId = 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
UserStat read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return UserStat(
|
|
||||||
following: fields[0] as int?,
|
|
||||||
follower: fields[1] as int?,
|
|
||||||
dynamicCount: fields[2] as int?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, UserStat obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(3)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.following)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.follower)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.dynamicCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is UserStatAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@ -2,8 +2,9 @@ import 'package:pilipala/http/danmaku.dart';
|
|||||||
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
||||||
|
|
||||||
class PlDanmakuController {
|
class PlDanmakuController {
|
||||||
PlDanmakuController(this.cid);
|
PlDanmakuController(this.cid, this.type);
|
||||||
final int cid;
|
final int cid;
|
||||||
|
final String type;
|
||||||
Map<int, List<DanmakuElem>> dmSegMap = {};
|
Map<int, List<DanmakuElem>> dmSegMap = {};
|
||||||
// 已请求的段落标记
|
// 已请求的段落标记
|
||||||
List<bool> requestedSeg = [];
|
List<bool> requestedSeg = [];
|
||||||
|
|||||||
@ -12,11 +12,15 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
class PlDanmaku extends StatefulWidget {
|
class PlDanmaku extends StatefulWidget {
|
||||||
final int cid;
|
final int cid;
|
||||||
final PlPlayerController playerController;
|
final PlPlayerController playerController;
|
||||||
|
final String type;
|
||||||
|
final Function(DanmakuController)? createdController;
|
||||||
|
|
||||||
const PlDanmaku({
|
const PlDanmaku({
|
||||||
super.key,
|
super.key,
|
||||||
required this.cid,
|
required this.cid,
|
||||||
required this.playerController,
|
required this.playerController,
|
||||||
|
this.type = 'video',
|
||||||
|
this.createdController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -43,9 +47,9 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
enableShowDanmaku =
|
enableShowDanmaku =
|
||||||
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||||
_plDanmakuController = PlDanmakuController(widget.cid);
|
_plDanmakuController = PlDanmakuController(widget.cid, widget.type);
|
||||||
if (mounted) {
|
|
||||||
playerController = widget.playerController;
|
playerController = widget.playerController;
|
||||||
|
if (mounted && widget.type == 'video') {
|
||||||
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
|
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
|
||||||
_plDanmakuController.initiate(
|
_plDanmakuController.initiate(
|
||||||
playerController.duration.value.inMilliseconds,
|
playerController.duration.value.inMilliseconds,
|
||||||
@ -55,6 +59,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
..addStatusLister(playerListener)
|
..addStatusLister(playerListener)
|
||||||
..addPositionListener(videoPositionListen);
|
..addPositionListener(videoPositionListen);
|
||||||
}
|
}
|
||||||
|
if (widget.type == 'video') {
|
||||||
playerController.isOpenDanmu.listen((p0) {
|
playerController.isOpenDanmu.listen((p0) {
|
||||||
if (p0 && !_plDanmakuController.initiated) {
|
if (p0 && !_plDanmakuController.initiated) {
|
||||||
_plDanmakuController.initiate(
|
_plDanmakuController.initiate(
|
||||||
@ -62,6 +67,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
playerController.position.value.inMilliseconds);
|
playerController.position.value.inMilliseconds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
blockTypes = playerController.blockTypes;
|
blockTypes = playerController.blockTypes;
|
||||||
showArea = playerController.showArea;
|
showArea = playerController.showArea;
|
||||||
opacityVal = playerController.opacityVal;
|
opacityVal = playerController.opacityVal;
|
||||||
@ -128,6 +134,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
child: DanmakuView(
|
child: DanmakuView(
|
||||||
createdController: (DanmakuController e) async {
|
createdController: (DanmakuController e) async {
|
||||||
playerController.danmakuController = _controller = e;
|
playerController.danmakuController = _controller = e;
|
||||||
|
widget.createdController?.call(e);
|
||||||
},
|
},
|
||||||
option: DanmakuOption(
|
option: DanmakuOption(
|
||||||
fontSize: 15 * fontSizeVal,
|
fontSize: 15 * fontSizeVal,
|
||||||
@ -136,8 +143,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
hideTop: blockTypes.contains(5),
|
hideTop: blockTypes.contains(5),
|
||||||
hideScroll: blockTypes.contains(2),
|
hideScroll: blockTypes.contains(2),
|
||||||
hideBottom: blockTypes.contains(4),
|
hideBottom: blockTypes.contains(4),
|
||||||
duration:
|
duration: danmakuDurationVal / playerController.playbackSpeed,
|
||||||
danmakuDurationVal / playerController.playbackSpeed,
|
|
||||||
strokeWidth: strokeWidth,
|
strokeWidth: strokeWidth,
|
||||||
// initDuration /
|
// initDuration /
|
||||||
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
|
// (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 oid = replyItem.oid;
|
||||||
int rpid = replyItem.rpid!;
|
int rpid = replyItem.rpid!;
|
||||||
Get.to(
|
Get.to(
|
||||||
@ -125,6 +125,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
|||||||
source: 'dynamic',
|
source: 'dynamic',
|
||||||
replyType: ReplyType.values[replyType],
|
replyType: ReplyType.values[replyType],
|
||||||
firstFloor: replyItem,
|
firstFloor: replyItem,
|
||||||
|
loadMore: loadMore,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -324,8 +325,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
|||||||
replyItem: replyList[index],
|
replyItem: replyList[index],
|
||||||
showReplyRow: true,
|
showReplyRow: true,
|
||||||
replyLevel: '1',
|
replyLevel: '1',
|
||||||
replyReply: (replyItem, currentReply) =>
|
replyReply:
|
||||||
replyReply(replyItem, currentReply),
|
(replyItem, currentReply, loadMore) =>
|
||||||
|
replyReply(replyItem,
|
||||||
|
currentReply, loadMore),
|
||||||
replyType: ReplyType.values[replyType],
|
replyType: ReplyType.values[replyType],
|
||||||
addReply: (replyItem) {
|
addReply: (replyItem) {
|
||||||
replyList[index]
|
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:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.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/constants.dart';
|
||||||
|
import 'package:pilipala/http/init.dart';
|
||||||
import 'package:pilipala/http/live.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/quality.dart';
|
||||||
import 'package:pilipala/models/live/room_info.dart';
|
import 'package:pilipala/models/live/room_info.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/index.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 '../../models/live/room_info_h5.dart';
|
||||||
import '../../utils/storage.dart';
|
import '../../utils/storage.dart';
|
||||||
import '../../utils/video_utils.dart';
|
import '../../utils/video_utils.dart';
|
||||||
@ -24,6 +33,21 @@ class LiveRoomController extends GetxController {
|
|||||||
int? tempCurrentQn;
|
int? tempCurrentQn;
|
||||||
late List<Map<String, dynamic>> acceptQnList;
|
late List<Map<String, dynamic>> acceptQnList;
|
||||||
RxString currentQnDesc = ''.obs;
|
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
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -40,9 +64,18 @@ class LiveRoomController extends GetxController {
|
|||||||
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
|
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
|
||||||
cover = liveItem.cover;
|
cover = liveItem.cover;
|
||||||
}
|
}
|
||||||
|
Request.getBuvid().then((value) => buvid = value);
|
||||||
}
|
}
|
||||||
// CDN优化
|
// CDN优化
|
||||||
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
|
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 {
|
playerInit(source) async {
|
||||||
@ -61,6 +94,7 @@ class LiveRoomController extends GetxController {
|
|||||||
enableHA: true,
|
enableHA: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
);
|
);
|
||||||
|
plPlayerController.isOpenDanmu.value = danmakuSwitch.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future queryLiveInfo() async {
|
Future queryLiveInfo() async {
|
||||||
@ -127,4 +161,126 @@ class LiveRoomController extends GetxController {
|
|||||||
.description;
|
.description;
|
||||||
await queryLiveInfo();
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:floating/floating.dart';
|
import 'package:floating/floating.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.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 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
@ -16,15 +20,20 @@ class LiveRoomPage extends StatefulWidget {
|
|||||||
State<LiveRoomPage> createState() => _LiveRoomPageState();
|
State<LiveRoomPage> createState() => _LiveRoomPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LiveRoomPageState extends State<LiveRoomPage> {
|
class _LiveRoomPageState extends State<LiveRoomPage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
|
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
|
||||||
PlPlayerController? plPlayerController;
|
late PlPlayerController plPlayerController;
|
||||||
late Future? _futureBuilder;
|
late Future? _futureBuilder;
|
||||||
late Future? _futureBuilderFuture;
|
late Future? _futureBuilderFuture;
|
||||||
|
|
||||||
bool isShowCover = true;
|
bool isShowCover = true;
|
||||||
bool isPlay = true;
|
bool isPlay = true;
|
||||||
Floating? floating;
|
Floating? floating;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
late AnimationController fabAnimationCtr;
|
||||||
|
bool _shouldAutoScroll = true;
|
||||||
|
final int roomId = int.parse(Get.parameters['roomid']!);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -34,6 +43,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
}
|
}
|
||||||
videoSourceInit();
|
videoSourceInit();
|
||||||
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
|
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
|
||||||
|
// 监听滚动事件
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
fabAnimationCtr = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
value: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> videoSourceInit() async {
|
Future<void> videoSourceInit() async {
|
||||||
@ -41,12 +57,52 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
plPlayerController = _liveRoomController.plPlayerController;
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
plPlayerController!.dispose();
|
plPlayerController.dispose();
|
||||||
if (floating != null) {
|
if (floating != null) {
|
||||||
floating!.dispose();
|
floating!.dispose();
|
||||||
}
|
}
|
||||||
|
_scrollController.dispose();
|
||||||
|
fabAnimationCtr.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +112,9 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
future: _futureBuilderFuture,
|
future: _futureBuilderFuture,
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
if (snapshot.hasData && snapshot.data['status']) {
|
if (snapshot.hasData && snapshot.data['status']) {
|
||||||
|
plPlayerController = _liveRoomController.plPlayerController;
|
||||||
return PLVideoPlayer(
|
return PLVideoPlayer(
|
||||||
controller: plPlayerController!,
|
controller: plPlayerController,
|
||||||
bottomControl: BottomControl(
|
bottomControl: BottomControl(
|
||||||
controller: plPlayerController,
|
controller: plPlayerController,
|
||||||
liveRoomCtr: _liveRoomController,
|
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 {
|
} else {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
@ -80,20 +145,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
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(
|
Obx(
|
||||||
() => Positioned(
|
() => Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -106,7 +157,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
.roomInfoH5.value.roomInfo?.appBackground !=
|
.roomInfoH5.value.roomInfo?.appBackground !=
|
||||||
null
|
null
|
||||||
? Opacity(
|
? Opacity(
|
||||||
opacity: 0.8,
|
opacity: 0.6,
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
width: Get.width,
|
width: Get.width,
|
||||||
height: Get.height,
|
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(
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AppBar(
|
AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@ -179,10 +239,10 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopScope(
|
PopScope(
|
||||||
canPop: plPlayerController?.isFullScreen.value != true,
|
canPop: plPlayerController.isFullScreen.value != true,
|
||||||
onPopInvoked: (bool didPop) {
|
onPopInvoked: (bool didPop) {
|
||||||
if (plPlayerController?.isFullScreen.value == true) {
|
if (plPlayerController.isFullScreen.value == true) {
|
||||||
plPlayerController!.triggerFullScreen(status: false);
|
plPlayerController.triggerFullScreen(status: false);
|
||||||
}
|
}
|
||||||
if (MediaQuery.of(context).orientation ==
|
if (MediaQuery.of(context).orientation ==
|
||||||
Orientation.landscape) {
|
Orientation.landscape) {
|
||||||
@ -198,8 +258,160 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
|||||||
child: videoPlayerPanel,
|
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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => Get.toNamed(
|
onTap: () {
|
||||||
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}&seasonName=${item.meta!.name}'),
|
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(
|
title: Text(
|
||||||
item.meta!.name!,
|
item.meta!.name!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import 'package:pilipala/models/member/seasons.dart';
|
|||||||
class MemberSeasonsController extends GetxController {
|
class MemberSeasonsController extends GetxController {
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
late int mid;
|
late int mid;
|
||||||
late int seasonId;
|
int? seasonId;
|
||||||
|
int? seriesId;
|
||||||
|
late String category;
|
||||||
int pn = 1;
|
int pn = 1;
|
||||||
int ps = 30;
|
int ps = 30;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@ -17,17 +19,23 @@ class MemberSeasonsController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
mid = int.parse(Get.parameters['mid']!);
|
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 {
|
Future getSeasonDetail(type) async {
|
||||||
if (type == 'onRefresh') {
|
if (type == 'onRefresh') {
|
||||||
pn = 1;
|
pn = 1;
|
||||||
}
|
}
|
||||||
var res = await MemberHttp.getSeasonDetail(
|
var res = await MemberHttp.getSeasonDetail(
|
||||||
mid: mid,
|
mid: mid,
|
||||||
seasonId: seasonId,
|
seasonId: seasonId!,
|
||||||
pn: pn,
|
pn: pn,
|
||||||
ps: ps,
|
ps: ps,
|
||||||
sortReverse: false,
|
sortReverse: false,
|
||||||
@ -40,8 +48,32 @@ class MemberSeasonsController extends GetxController {
|
|||||||
return res;
|
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 {
|
Future onLoad() async {
|
||||||
|
if (category == '0') {
|
||||||
getSeasonDetail('onLoad');
|
getSeasonDetail('onLoad');
|
||||||
}
|
}
|
||||||
|
if (category == '1') {
|
||||||
|
getSeriesDetail('onLoad');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,15 @@ class _MemberSeasonsPageState extends State<MemberSeasonsPage> {
|
|||||||
Get.put(MemberSeasonsController());
|
Get.put(MemberSeasonsController());
|
||||||
late Future _futureBuilderFuture;
|
late Future _futureBuilderFuture;
|
||||||
late ScrollController scrollController;
|
late ScrollController scrollController;
|
||||||
|
late String category;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_futureBuilderFuture =
|
category = Get.parameters['category']!;
|
||||||
_memberSeasonsController.getSeasonDetail('onRefresh');
|
_futureBuilderFuture = category == '0'
|
||||||
|
? _memberSeasonsController.getSeasonDetail('onRefresh')
|
||||||
|
: _memberSeasonsController.getSeriesDetail('onRefresh');
|
||||||
scrollController = _memberSeasonsController.scrollController;
|
scrollController = _memberSeasonsController.scrollController;
|
||||||
scrollController.addListener(
|
scrollController.addListener(
|
||||||
() {
|
() {
|
||||||
|
|||||||
@ -8,8 +8,20 @@ class MessageSystemController extends GetxController {
|
|||||||
Future queryMessageSystem({String type = 'init'}) async {
|
Future queryMessageSystem({String type = 'init'}) async {
|
||||||
var res = await MsgHttp.messageSystem();
|
var res = await MsgHttp.messageSystem();
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
|
if (type == 'init') {
|
||||||
|
systemItems.value = res['data'];
|
||||||
|
} else {
|
||||||
systemItems.addAll(res['data']);
|
systemItems.addAll(res['data']);
|
||||||
}
|
}
|
||||||
|
if (systemItems.isNotEmpty) {
|
||||||
|
systemMarkRead(systemItems.first.cursor!);
|
||||||
|
}
|
||||||
|
}
|
||||||
return res;
|
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:bottom_sheet/bottom_sheet.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:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/http/constants.dart';
|
import 'package:pilipala/http/constants.dart';
|
||||||
@ -197,35 +198,29 @@ class VideoIntroController extends GetxController {
|
|||||||
SmartDialog.showToast('账号未登录');
|
SmartDialog.showToast('账号未登录');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasCoin.value) {
|
|
||||||
SmartDialog.showToast('已投过币了');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: Get.context!,
|
context: Get.context!,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('选择投币个数'),
|
title: const Text('选择投币个数'),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
|
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
|
||||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
content: Column(
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [1, 2]
|
children: [1, 2]
|
||||||
.map(
|
.map(
|
||||||
(e) => RadioListTile(
|
(e) => ListTile(
|
||||||
value: e,
|
title: Padding(
|
||||||
title: Text('$e枚'),
|
padding: const EdgeInsets.only(left: 20),
|
||||||
groupValue: _tempThemeValue,
|
child: Text('$e 枚'),
|
||||||
onChanged: (value) async {
|
),
|
||||||
_tempThemeValue = value!;
|
onTap: () async {
|
||||||
setState(() {});
|
var res =
|
||||||
var res = await VideoHttp.coinVideo(
|
await VideoHttp.coinVideo(bvid: bvid, multiply: e);
|
||||||
bvid: bvid, multiply: _tempThemeValue);
|
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
SmartDialog.showToast('投币成功');
|
SmartDialog.showToast('投币成功');
|
||||||
hasCoin.value = true;
|
hasCoin.value = true;
|
||||||
videoDetail.value.stat!.coin =
|
videoDetail.value.stat!.coin =
|
||||||
videoDetail.value.stat!.coin! + _tempThemeValue;
|
videoDetail.value.stat!.coin! + e;
|
||||||
} else {
|
} else {
|
||||||
SmartDialog.showToast(res['msg']);
|
SmartDialog.showToast(res['msg']);
|
||||||
}
|
}
|
||||||
@ -234,8 +229,7 @@ class VideoIntroController extends GetxController {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appscheme/appscheme.dart';
|
import 'package:appscheme/appscheme.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/gestures.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/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/reply_new/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_gallery/index.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_popup/index.dart';
|
||||||
import 'package:pilipala/utils/app_scheme.dart';
|
import 'package:pilipala/utils/app_scheme.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/id_utils.dart';
|
import 'package:pilipala/utils/id_utils.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:pilipala/utils/url_utils.dart';
|
import 'package:pilipala/utils/url_utils.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
import 'reply_save.dart';
|
||||||
import 'zan.dart';
|
import 'zan.dart';
|
||||||
|
|
||||||
Box setting = GStrorage.setting;
|
Box setting = GStrorage.setting;
|
||||||
@ -32,6 +36,7 @@ class ReplyItem extends StatelessWidget {
|
|||||||
this.showReplyRow = true,
|
this.showReplyRow = true,
|
||||||
this.replyReply,
|
this.replyReply,
|
||||||
this.replyType,
|
this.replyType,
|
||||||
|
this.replySave = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final ReplyItemModel? replyItem;
|
final ReplyItemModel? replyItem;
|
||||||
@ -40,6 +45,7 @@ class ReplyItem extends StatelessWidget {
|
|||||||
final bool? showReplyRow;
|
final bool? showReplyRow;
|
||||||
final Function? replyReply;
|
final Function? replyReply;
|
||||||
final ReplyType? replyType;
|
final ReplyType? replyType;
|
||||||
|
final bool? replySave;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -47,19 +53,28 @@ class ReplyItem extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
// 点击整个评论区 评论详情/回复
|
// 点击整个评论区 评论详情/回复
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (replySave!) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
feedBack();
|
feedBack();
|
||||||
if (replyReply != null) {
|
if (replyReply != null) {
|
||||||
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
|
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
|
if (replySave!) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
feedBack();
|
feedBack();
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) {
|
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! ||
|
if ((replyItem!.replyControl!.isShow! ||
|
||||||
replyItem!.replies!.isNotEmpty) &&
|
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;
|
ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||||
TextTheme textTheme = Theme.of(context).textTheme;
|
TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
return Row(
|
return Row(
|
||||||
@ -286,6 +301,7 @@ class ReplyItem extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
|
if (!replySave!) ...[
|
||||||
Icon(Icons.reply,
|
Icon(Icons.reply,
|
||||||
size: 18, color: colorScheme.outline.withOpacity(0.8)),
|
size: 18, color: colorScheme.outline.withOpacity(0.8)),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
@ -295,6 +311,15 @@ class ReplyItem extends StatelessWidget {
|
|||||||
fontSize: textTheme.labelMedium!.fontSize,
|
fontSize: textTheme.labelMedium!.fontSize,
|
||||||
color: colorScheme.outline,
|
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)
|
if (extraRow == 1)
|
||||||
InkWell(
|
InkWell(
|
||||||
// 一楼点击【共xx条回复】展开评论详情
|
// 一楼点击【共xx条回复】展开评论详情
|
||||||
onTap: () => replyReply!(replyItem),
|
onTap: () => replyReply?.call(replyItem, null, true),
|
||||||
|
onLongPress: () => {},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),
|
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>();
|
final MainController mainController = Get.find<MainController>();
|
||||||
mainController.imgPreviewStatus = true;
|
mainController.imgPreviewStatus = true;
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
@ -575,7 +601,7 @@ InlineSpan buildContent(
|
|||||||
},
|
},
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: picList[index],
|
tag: picList[index] + randomInt,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
imageUrl: picList[index],
|
imageUrl: picList[index],
|
||||||
@ -886,11 +912,12 @@ InlineSpan buildContent(
|
|||||||
pictureItem['img_width']))
|
pictureItem['img_width']))
|
||||||
.truncateToDouble();
|
.truncateToDouble();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
String randomInt = Random().nextInt(101).toString();
|
||||||
|
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: picList[0],
|
tag: picList[0] + randomInt,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => onPreviewImg(picList, 0),
|
onTap: () => onPreviewImg(picList, 0, randomInt),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||||
@ -927,13 +954,14 @@ InlineSpan buildContent(
|
|||||||
picList.add(content.pictures[i]['img_src']);
|
picList.add(content.pictures[i]['img_src']);
|
||||||
}
|
}
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
|
String randomInt = Random().nextInt(101).toString();
|
||||||
list.add(
|
list.add(
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, BoxConstraints box) {
|
builder: (context, BoxConstraints box) {
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: picList[i],
|
tag: picList[i] + randomInt,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => onPreviewImg(picList, i),
|
onTap: () => onPreviewImg(picList, i, randomInt),
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: picList[i],
|
src: picList[i],
|
||||||
width: box.maxWidth,
|
width: box.maxWidth,
|
||||||
@ -1004,7 +1032,12 @@ InlineSpan buildContent(
|
|||||||
|
|
||||||
class MorePanel extends StatelessWidget {
|
class MorePanel extends StatelessWidget {
|
||||||
final dynamic item;
|
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 {
|
Future<dynamic> menuActionHandler(String type) async {
|
||||||
String message = item.content.message ?? item.content;
|
String message = item.content.message ?? item.content;
|
||||||
@ -1026,6 +1059,13 @@ class MorePanel extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'save':
|
||||||
|
Get.back();
|
||||||
|
Navigator.push(
|
||||||
|
Get.context!,
|
||||||
|
PlPopupRoute(child: ReplySave(replyItem: item)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
// case 'block':
|
// case 'block':
|
||||||
// SmartDialog.showToast('加入黑名单');
|
// SmartDialog.showToast('加入黑名单');
|
||||||
// break;
|
// break;
|
||||||
@ -1076,6 +1116,13 @@ class MorePanel extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.copy_outlined, size: 19),
|
leading: const Icon(Icons.copy_outlined, size: 19),
|
||||||
title: Text('自由复制', style: textTheme.titleSmall),
|
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(
|
// ListTile(
|
||||||
// onTap: () async => await menuActionHandler('block'),
|
// onTap: () async => await menuActionHandler('block'),
|
||||||
// minLeadingWidth: 0,
|
// 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.replyType,
|
||||||
this.sheetHeight,
|
this.sheetHeight,
|
||||||
this.currentReply,
|
this.currentReply,
|
||||||
this.loadMore,
|
this.loadMore = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final int? oid;
|
final int? oid;
|
||||||
@ -32,7 +32,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
|||||||
final ReplyType? replyType;
|
final ReplyType? replyType;
|
||||||
final double? sheetHeight;
|
final double? sheetHeight;
|
||||||
final dynamic currentReply;
|
final dynamic currentReply;
|
||||||
final bool? loadMore;
|
final bool loadMore;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
||||||
@ -142,7 +142,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
widget.loadMore != null && widget.loadMore!
|
widget.loadMore
|
||||||
? FutureBuilder(
|
? FutureBuilder(
|
||||||
future: _futureBuilderFuture,
|
future: _futureBuilderFuture,
|
||||||
builder: (BuildContext context, snapshot) {
|
builder: (BuildContext context, snapshot) {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
late bool autoPlayEnable;
|
late bool autoPlayEnable;
|
||||||
late bool autoPiP;
|
late bool autoPiP;
|
||||||
late Floating floating;
|
late Floating floating;
|
||||||
bool isShowing = true;
|
RxBool isShowing = true.obs;
|
||||||
// 生命周期监听
|
// 生命周期监听
|
||||||
late final AppLifecycleListener _lifecycleListener;
|
late final AppLifecycleListener _lifecycleListener;
|
||||||
late double statusHeight;
|
late double statusHeight;
|
||||||
@ -183,6 +183,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
plPlayerController!.addStatusLister(playerListener);
|
plPlayerController!.addStatusLister(playerListener);
|
||||||
vdCtr.autoPlay.value = true;
|
vdCtr.autoPlay.value = true;
|
||||||
vdCtr.isShowCover.value = false;
|
vdCtr.isShowCover.value = false;
|
||||||
|
isShowing.value = true;
|
||||||
autoEnterPip(status: PlayerStatus.playing);
|
autoEnterPip(status: PlayerStatus.playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +259,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
plPlayerController!.pause();
|
plPlayerController!.pause();
|
||||||
vdCtr.clearSubtitleContent();
|
vdCtr.clearSubtitleContent();
|
||||||
}
|
}
|
||||||
setState(() => isShowing = false);
|
isShowing.value = false;
|
||||||
super.didPushNext();
|
super.didPushNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,10 +273,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
|
|
||||||
if (plPlayerController != null &&
|
if (plPlayerController != null &&
|
||||||
plPlayerController!.videoPlayerController != null) {
|
plPlayerController!.videoPlayerController != null) {
|
||||||
setState(() {
|
|
||||||
vdCtr.setSubtitleContent();
|
vdCtr.setSubtitleContent();
|
||||||
isShowing = true;
|
isShowing.value = true;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
vdCtr.isFirstTime = false;
|
vdCtr.isFirstTime = false;
|
||||||
final bool autoplay = autoPlayEnable;
|
final bool autoplay = autoPlayEnable;
|
||||||
@ -330,6 +329,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
plPlayerController?.danmakuController?.clear();
|
plPlayerController?.danmakuController?.clear();
|
||||||
break;
|
break;
|
||||||
case 'pause':
|
case 'pause':
|
||||||
|
if (autoPiP) {
|
||||||
vdCtr.hiddenReplyReplyPanel();
|
vdCtr.hiddenReplyReplyPanel();
|
||||||
if (vdCtr.videoType == SearchType.video) {
|
if (vdCtr.videoType == SearchType.video) {
|
||||||
videoIntroController.hiddenEpisodeBottomSheet();
|
videoIntroController.hiddenEpisodeBottomSheet();
|
||||||
@ -337,6 +337,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
if (vdCtr.videoType == SearchType.media_bangumi) {
|
if (vdCtr.videoType == SearchType.media_bangumi) {
|
||||||
bangumiIntroController.hiddenEpisodeBottomSheet();
|
bangumiIntroController.hiddenEpisodeBottomSheet();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -650,7 +651,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (isShowing) buildVideoPlayerPanel(),
|
Obx(
|
||||||
|
() => isShowing.value
|
||||||
|
? buildVideoPlayerPanel()
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
|
|
||||||
/// 关闭自动播放时 手动播放
|
/// 关闭自动播放时 手动播放
|
||||||
Obx(
|
Obx(
|
||||||
|
|||||||
@ -214,7 +214,7 @@ class SessionItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 content = sessionItem.lastMsg.content;
|
||||||
final msgStatus = sessionItem.lastMsg.msgStatus;
|
final msgStatus = sessionItem.lastMsg.msgStatus;
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget {
|
|||||||
'talkerId': sessionItem.talkerId.toString(),
|
'talkerId': sessionItem.talkerId.toString(),
|
||||||
'name': sessionItem.accountInfo.name,
|
'name': sessionItem.accountInfo.name,
|
||||||
'face': sessionItem.accountInfo.face,
|
'face': sessionItem.accountInfo.face,
|
||||||
'mid': sessionItem.accountInfo.mid.toString(),
|
'mid': (sessionItem.accountInfo?.mid ?? 0).toString(),
|
||||||
'heroTag': heroTag,
|
'heroTag': heroTag,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
107
lib/plugin/pl_socket/index.dart
Normal file
107
lib/plugin/pl_socket/index.dart
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:pilipala/utils/live.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
|
enum SocketStatus {
|
||||||
|
connected,
|
||||||
|
failed,
|
||||||
|
closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlSocket {
|
||||||
|
SocketStatus status = SocketStatus.closed;
|
||||||
|
// 链接
|
||||||
|
final String url;
|
||||||
|
// 心跳时间
|
||||||
|
final int heartTime;
|
||||||
|
// 监听初始化完成
|
||||||
|
final Function? onReadyCb;
|
||||||
|
// 监听关闭
|
||||||
|
final Function? onCloseCb;
|
||||||
|
// 监听异常
|
||||||
|
final Function? onErrorCb;
|
||||||
|
// 监听消息
|
||||||
|
final Function? onMessageCb;
|
||||||
|
// 请求头
|
||||||
|
final Map<String, dynamic>? headers;
|
||||||
|
|
||||||
|
PlSocket({
|
||||||
|
required this.url,
|
||||||
|
required this.heartTime,
|
||||||
|
this.onReadyCb,
|
||||||
|
this.onCloseCb,
|
||||||
|
this.onErrorCb,
|
||||||
|
this.onMessageCb,
|
||||||
|
this.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
WebSocketChannel? channel;
|
||||||
|
StreamSubscription<dynamic>? channelStreamSub;
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
Future connect() async {
|
||||||
|
// 连接之前关闭上次连接
|
||||||
|
onClose();
|
||||||
|
try {
|
||||||
|
channel = IOWebSocketChannel.connect(
|
||||||
|
url,
|
||||||
|
connectTimeout: const Duration(seconds: 15),
|
||||||
|
headers: null,
|
||||||
|
);
|
||||||
|
await channel?.ready;
|
||||||
|
onReady();
|
||||||
|
} catch (err) {
|
||||||
|
connect();
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化完成
|
||||||
|
void onReady() {
|
||||||
|
status = SocketStatus.connected;
|
||||||
|
onReadyCb?.call();
|
||||||
|
channelStreamSub = channel?.stream.listen((message) {
|
||||||
|
onMessageCb?.call(message);
|
||||||
|
}, onDone: () {
|
||||||
|
// 流被关闭
|
||||||
|
print('结束了');
|
||||||
|
}, onError: (err) {
|
||||||
|
onError(err);
|
||||||
|
});
|
||||||
|
// 每30s发送心跳
|
||||||
|
Timer.periodic(Duration(seconds: heartTime), (timer) {
|
||||||
|
if (status == SocketStatus.connected) {
|
||||||
|
sendMessage(LiveUtils.encodeData(
|
||||||
|
"",
|
||||||
|
2,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
void onClose() {
|
||||||
|
status = SocketStatus.closed;
|
||||||
|
onCloseCb?.call();
|
||||||
|
channelStreamSub?.cancel();
|
||||||
|
channel?.sink.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接异常
|
||||||
|
void onError(err) {
|
||||||
|
onErrorCb?.call(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
void onMessage() {}
|
||||||
|
|
||||||
|
void sendMessage(dynamic message) {
|
||||||
|
if (status == SocketStatus.connected) {
|
||||||
|
channel?.sink.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
lib/utils/binary_writer.dart
Normal file
117
lib/utils/binary_writer.dart
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
class BinaryWriter {
|
||||||
|
List<int> buffer;
|
||||||
|
int position = 0;
|
||||||
|
BinaryWriter(this.buffer);
|
||||||
|
int get length => buffer.length;
|
||||||
|
|
||||||
|
void writeBytes(List<int> list) {
|
||||||
|
buffer.addAll(list);
|
||||||
|
position += list.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeInt(int value, int len, {Endian endian = Endian.big}) {
|
||||||
|
var bytes = _createByteData(len);
|
||||||
|
switch (len) {
|
||||||
|
case 1:
|
||||||
|
bytes.setUint8(0, value.toUnsigned(8));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
bytes.setInt16(0, value, endian);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
bytes.setInt32(0, value, endian);
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
bytes.setInt64(0, value, endian);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid length for writeInt: $len');
|
||||||
|
}
|
||||||
|
_addBytesToBuffer(bytes, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeDouble(double value, int len, {Endian endian = Endian.big}) {
|
||||||
|
var bytes = _createByteData(len);
|
||||||
|
switch (len) {
|
||||||
|
case 4:
|
||||||
|
bytes.setFloat32(0, value, endian);
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
bytes.setFloat64(0, value, endian);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid length for writeDouble: $len');
|
||||||
|
}
|
||||||
|
_addBytesToBuffer(bytes, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteData _createByteData(int len) {
|
||||||
|
var b = Uint8List(len).buffer;
|
||||||
|
return ByteData.view(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addBytesToBuffer(ByteData bytes, int len) {
|
||||||
|
buffer.addAll(bytes.buffer.asUint8List());
|
||||||
|
position += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BinaryReader {
|
||||||
|
Uint8List buffer;
|
||||||
|
int position = 0;
|
||||||
|
BinaryReader(this.buffer);
|
||||||
|
int get length => buffer.length;
|
||||||
|
|
||||||
|
int read() {
|
||||||
|
return buffer[position++];
|
||||||
|
}
|
||||||
|
|
||||||
|
int readInt(int len, {Endian endian = Endian.big}) {
|
||||||
|
var bytes = _getBytes(len);
|
||||||
|
var data = ByteData.view(bytes.buffer);
|
||||||
|
switch (len) {
|
||||||
|
case 1:
|
||||||
|
return data.getUint8(0);
|
||||||
|
case 2:
|
||||||
|
return data.getInt16(0, endian);
|
||||||
|
case 4:
|
||||||
|
return data.getInt32(0, endian);
|
||||||
|
case 8:
|
||||||
|
return data.getInt64(0, endian);
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid length for readInt: $len');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int readByte({Endian endian = Endian.big}) => readInt(1, endian: endian);
|
||||||
|
int readShort({Endian endian = Endian.big}) => readInt(2, endian: endian);
|
||||||
|
int readInt32({Endian endian = Endian.big}) => readInt(4, endian: endian);
|
||||||
|
int readLong({Endian endian = Endian.big}) => readInt(8, endian: endian);
|
||||||
|
|
||||||
|
Uint8List readBytes(int len) {
|
||||||
|
var bytes = _getBytes(len);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
double readFloat(int len, {Endian endian = Endian.big}) {
|
||||||
|
var bytes = _getBytes(len);
|
||||||
|
var data = ByteData.view(bytes.buffer);
|
||||||
|
switch (len) {
|
||||||
|
case 4:
|
||||||
|
return data.getFloat32(0, endian);
|
||||||
|
case 8:
|
||||||
|
return data.getFloat64(0, endian);
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid length for readFloat: $len');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _getBytes(int len) {
|
||||||
|
var bytes =
|
||||||
|
Uint8List.fromList(buffer.getRange(position, position + len).toList());
|
||||||
|
position += len;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
196
lib/utils/live.dart
Normal file
196
lib/utils/live.dart
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:brotli/brotli.dart';
|
||||||
|
import 'package:pilipala/models/live/message.dart';
|
||||||
|
import 'package:pilipala/utils/binary_writer.dart';
|
||||||
|
|
||||||
|
class LiveUtils {
|
||||||
|
static List<int> encodeData(String msg, int action) {
|
||||||
|
var data = utf8.encode(msg);
|
||||||
|
//头部长度固定16
|
||||||
|
var length = data.length + 16;
|
||||||
|
var buffer = Uint8List(length);
|
||||||
|
|
||||||
|
var writer = BinaryWriter([]);
|
||||||
|
|
||||||
|
//数据包长度
|
||||||
|
writer.writeInt(buffer.length, 4);
|
||||||
|
//数据包头部长度,固定16
|
||||||
|
writer.writeInt(16, 2);
|
||||||
|
|
||||||
|
//协议版本,0=JSON,1=Int32,2=Buffer
|
||||||
|
writer.writeInt(0, 2);
|
||||||
|
|
||||||
|
//操作类型
|
||||||
|
writer.writeInt(action, 4);
|
||||||
|
|
||||||
|
//数据包头部长度,固定1
|
||||||
|
|
||||||
|
writer.writeInt(1, 4);
|
||||||
|
|
||||||
|
writer.writeBytes(data);
|
||||||
|
|
||||||
|
return writer.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LiveMessageModel>? decodeMessage(List<int> data) {
|
||||||
|
try {
|
||||||
|
//操作类型。3=心跳回应,内容为房间人气值;5=通知,弹幕、广播等全部信息;8=进房回应,空
|
||||||
|
int operation = readInt(data, 8, 4);
|
||||||
|
//内容
|
||||||
|
var body = data.skip(16).toList();
|
||||||
|
if (operation == 3) {
|
||||||
|
var online = readInt(body, 0, 4);
|
||||||
|
final LiveMessageModel liveMsg = LiveMessageModel(
|
||||||
|
type: LiveMessageType.online,
|
||||||
|
userName: '',
|
||||||
|
message: '',
|
||||||
|
color: LiveMessageColor.white,
|
||||||
|
data: online,
|
||||||
|
);
|
||||||
|
return [liveMsg];
|
||||||
|
} else if (operation == 5) {
|
||||||
|
//协议版本。0为JSON,可以直接解析;1为房间人气值,Body为4位Int32;2为压缩过Buffer,需要解压再处理
|
||||||
|
int protocolVersion = readInt(data, 6, 2);
|
||||||
|
if (protocolVersion == 2) {
|
||||||
|
body = zlib.decode(body);
|
||||||
|
} else if (protocolVersion == 3) {
|
||||||
|
body = brotli.decode(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = utf8.decode(body, allowMalformed: true);
|
||||||
|
|
||||||
|
var group =
|
||||||
|
text.split(RegExp(r"[\x00-\x1f]+", unicode: true, multiLine: true));
|
||||||
|
List<LiveMessageModel> messages = [];
|
||||||
|
for (var item
|
||||||
|
in group.where((x) => x.length > 2 && x.startsWith('{'))) {
|
||||||
|
if (parseMessage(item) is LiveMessageModel) {
|
||||||
|
messages.add(parseMessage(item)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LiveMessageModel? parseMessage(String jsonMessage) {
|
||||||
|
try {
|
||||||
|
var obj = json.decode(jsonMessage);
|
||||||
|
var cmd = obj["cmd"].toString();
|
||||||
|
if (cmd.contains("DANMU_MSG")) {
|
||||||
|
if (obj["info"] != null && obj["info"].length != 0) {
|
||||||
|
var message = obj["info"][1].toString();
|
||||||
|
var color = asT<int?>(obj["info"][0][3]) ?? 0;
|
||||||
|
if (obj["info"][2] != null && obj["info"][2].length != 0) {
|
||||||
|
var extra = obj["info"][0][15]['extra'];
|
||||||
|
var user = obj["info"][0][15]['user']['base'];
|
||||||
|
Map<String, dynamic> extraMap = jsonDecode(extra);
|
||||||
|
final int userId = obj["info"][2][0];
|
||||||
|
final LiveMessageModel liveMsg = LiveMessageModel(
|
||||||
|
type: LiveMessageType.chat,
|
||||||
|
userName: user['name'],
|
||||||
|
message: message,
|
||||||
|
color: color == 0
|
||||||
|
? LiveMessageColor.white
|
||||||
|
: LiveMessageColor.numberToColor(color),
|
||||||
|
face: user['face'],
|
||||||
|
uid: userId,
|
||||||
|
emots: extraMap['emots'],
|
||||||
|
);
|
||||||
|
return liveMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cmd == "SUPER_CHAT_MESSAGE") {
|
||||||
|
if (obj["data"] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final data = obj["data"];
|
||||||
|
final userInfo = data["user_info"];
|
||||||
|
final String backgroundBottomColor =
|
||||||
|
data["background_bottom_color"].toString();
|
||||||
|
final String backgroundColor = data["background_color"].toString();
|
||||||
|
final DateTime endTime =
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(data["end_time"] * 1000);
|
||||||
|
final String face = "${userInfo["face"]}@200w.jpg";
|
||||||
|
final String message = data["message"].toString();
|
||||||
|
final String price = data["price"];
|
||||||
|
final DateTime startTime =
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(data["start_time"] * 1000);
|
||||||
|
final String userName = userInfo["uname"].toString();
|
||||||
|
|
||||||
|
final LiveMessageModel liveMsg = LiveMessageModel(
|
||||||
|
type: LiveMessageType.superChat,
|
||||||
|
userName: "SUPER_CHAT_MESSAGE",
|
||||||
|
message: "SUPER_CHAT_MESSAGE",
|
||||||
|
color: LiveMessageColor.white,
|
||||||
|
data: {
|
||||||
|
"backgroundBottomColor": backgroundBottomColor,
|
||||||
|
"backgroundColor": backgroundColor,
|
||||||
|
"endTime": endTime,
|
||||||
|
"face": face,
|
||||||
|
"message": message,
|
||||||
|
"price": price,
|
||||||
|
"startTime": startTime,
|
||||||
|
"userName": userName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return liveMsg;
|
||||||
|
} else if (cmd == 'INTERACT_WORD') {
|
||||||
|
if (obj["data"] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final data = obj["data"];
|
||||||
|
final String userName = data['uname'];
|
||||||
|
final int msgType = data['msg_type'];
|
||||||
|
final LiveMessageModel liveMsg = LiveMessageModel(
|
||||||
|
type: msgType == 1 ? LiveMessageType.join : LiveMessageType.follow,
|
||||||
|
userName: userName,
|
||||||
|
message: msgType == 1 ? '进入直播间' : '关注了主播',
|
||||||
|
color: LiveMessageColor.white,
|
||||||
|
);
|
||||||
|
return liveMsg;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static T? asT<T>(dynamic value) {
|
||||||
|
if (value is T) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int readInt(List<int> buffer, int start, int len) {
|
||||||
|
var data = _getByteData(buffer, start, len);
|
||||||
|
return _readIntFromByteData(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ByteData _getByteData(List<int> buffer, int start, int len) {
|
||||||
|
var bytes =
|
||||||
|
Uint8List.fromList(buffer.getRange(start, start + len).toList());
|
||||||
|
return ByteData.view(bytes.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _readIntFromByteData(ByteData data, int len) {
|
||||||
|
switch (len) {
|
||||||
|
case 1:
|
||||||
|
return data.getUint8(0);
|
||||||
|
case 2:
|
||||||
|
return data.getInt16(0, Endian.big);
|
||||||
|
case 4:
|
||||||
|
return data.getInt32(0, Endian.big);
|
||||||
|
case 8:
|
||||||
|
return data.getInt64(0, Endian.big);
|
||||||
|
default:
|
||||||
|
throw ArgumentError('Invalid length: $len');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:pilipala/models/model_owner.dart';
|
|
||||||
import 'package:pilipala/models/search/hot.dart';
|
|
||||||
import 'package:pilipala/models/user/info.dart';
|
import 'package:pilipala/models/user/info.dart';
|
||||||
import '../models/common/gesture_mode.dart';
|
import '../models/common/gesture_mode.dart';
|
||||||
import 'global_data.dart';
|
import 'global_data.dart';
|
||||||
@ -54,11 +52,8 @@ class GStrorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void regAdapter() {
|
static void regAdapter() {
|
||||||
Hive.registerAdapter(OwnerAdapter());
|
|
||||||
Hive.registerAdapter(UserInfoDataAdapter());
|
Hive.registerAdapter(UserInfoDataAdapter());
|
||||||
Hive.registerAdapter(LevelInfoAdapter());
|
Hive.registerAdapter(LevelInfoAdapter());
|
||||||
Hive.registerAdapter(HotSearchModelAdapter());
|
|
||||||
Hive.registerAdapter(HotSearchItemAdapter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> close() async {
|
static Future<void> close() async {
|
||||||
|
|||||||
14
pubspec.lock
14
pubspec.lock
@ -129,6 +129,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.4"
|
||||||
|
brotli:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: brotli
|
||||||
|
sha256: "7f891558ed779aab2bed874f0a36b8123f9ff3f19cf6efbee89e18ed294945ae"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1672,13 +1680,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "0.5.1"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.5"
|
||||||
webview_cookie_manager:
|
webview_cookie_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -147,6 +147,8 @@ dependencies:
|
|||||||
# 二维码
|
# 二维码
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
bottom_sheet: ^4.0.4
|
bottom_sheet: ^4.0.4
|
||||||
|
web_socket_channel: ^2.4.5
|
||||||
|
brotli: ^0.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user