From 75859fc9cf687a3c86a1ba9060d741f8e56bed14 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 18 Jul 2024 01:11:26 +0800 Subject: [PATCH 01/22] =?UTF-8?q?mod:=20=E5=8F=96=E6=B6=88=E6=8A=95?= =?UTF-8?q?=E5=B8=81=E9=99=90=E5=88=B6&=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../video/detail/introduction/controller.dart | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 50aac4cd..cb9ad3c1 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/constants.dart'; @@ -196,45 +197,38 @@ class VideoIntroController extends GetxController { SmartDialog.showToast('账号未登录'); return; } - if (hasCoin.value) { - SmartDialog.showToast('已投过币了'); - return; - } showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('选择投币个数'), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24), - content: StatefulBuilder(builder: (context, StateSetter setState) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [1, 2] - .map( - (e) => RadioListTile( - value: e, - title: Text('$e枚'), - groupValue: _tempThemeValue, - onChanged: (value) async { - _tempThemeValue = value!; - setState(() {}); - var res = await VideoHttp.coinVideo( - bvid: bvid, multiply: _tempThemeValue); - if (res['status']) { - SmartDialog.showToast('投币成功'); - hasCoin.value = true; - videoDetail.value.stat!.coin = - videoDetail.value.stat!.coin! + _tempThemeValue; - } else { - SmartDialog.showToast(res['msg']); - } - Get.back(); - }, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [1, 2] + .map( + (e) => ListTile( + title: Padding( + padding: const EdgeInsets.only(left: 20), + child: Text('$e 枚'), ), - ) - .toList(), - ); - }), + onTap: () async { + var res = + await VideoHttp.coinVideo(bvid: bvid, multiply: e); + if (res['status']) { + SmartDialog.showToast('投币成功'); + hasCoin.value = true; + videoDetail.value.stat!.coin = + videoDetail.value.stat!.coin! + e; + } else { + SmartDialog.showToast(res['msg']); + } + Get.back(); + }, + ), + ) + .toList(), + ), ); }); } From 38b6b6b4e09cf8e2db9b969f8e525b9abd43ae92 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 21 Jul 2024 18:50:36 +0800 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20=E6=B6=88=E6=81=AFmid=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/whisper/view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 1814c274..1f69fa64 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -214,7 +214,7 @@ class SessionItem extends StatelessWidget { @override Widget build(BuildContext context) { - final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid); + final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0); final content = sessionItem.lastMsg.content; final msgStatus = sessionItem.lastMsg.msgStatus; From bc36493e60eb254c9e19e76a07f8aec4e8e37259 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Tue, 23 Jul 2024 23:25:06 +0800 Subject: [PATCH 03/22] =?UTF-8?q?fix:=20=E9=A6=96=E9=A1=B5=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E6=95=B0=E6=8D=AE=E6=A0=BC=E5=BC=8F=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_v.dart | 5 +- lib/http/video.dart | 72 ++++++------ lib/models/home/rcmd/result.dart | 12 +- lib/models/model_owner.dart | 9 +- lib/models/model_owner.g.dart | 47 -------- lib/models/model_rec_video_item.dart | 50 +------- lib/models/model_rec_video_item.g.dart | 154 ------------------------- lib/models/search/hot.dart | 11 -- lib/models/search/hot.g.dart | 84 -------------- lib/models/user/stat.dart | 8 -- lib/models/user/stat.g.dart | 47 -------- lib/utils/storage.dart | 5 - 12 files changed, 45 insertions(+), 459 deletions(-) delete mode 100644 lib/models/model_owner.g.dart delete mode 100644 lib/models/model_rec_video_item.g.dart delete mode 100644 lib/models/search/hot.g.dart delete mode 100644 lib/models/user/stat.g.dart diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 14476cdf..d8e1bb2c 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -215,9 +215,8 @@ class VideoContent extends StatelessWidget { children: [ if (videoItem.goto == 'bangumi') _buildBadge(videoItem.bangumiBadge, 'line', 9), - if (videoItem.rcmdReason?.content != null && - videoItem.rcmdReason.content != '') - _buildBadge(videoItem.rcmdReason.content, 'color'), + if (videoItem.rcmdReason != null) + _buildBadge(videoItem.rcmdReason, 'color'), if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9), if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'), Expanded( diff --git a/lib/http/video.dart b/lib/http/video.dart index 834a0102..160f5db2 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -70,47 +70,43 @@ class VideoHttp { // 添加额外的loginState变量模拟未登录状态 static Future rcmdVideoListApp( {bool loginStatus = true, required int freshIdx}) async { - try { - var res = await Request().get( - Api.recommendListApp, - data: { - 'idx': freshIdx, - 'flush': '5', - 'column': '4', - 'device': 'pad', - 'device_type': 0, - 'device_name': 'vivo', - 'pull': freshIdx == 0 ? 'true' : 'false', - 'appkey': Constants.appKey, - 'access_key': loginStatus - ? (localCache.get(LocalCacheKey.accessKey, - defaultValue: {})['value'] ?? - '') - : '' - }, - ); - if (res.data['code'] == 0) { - List list = []; - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); - for (var i in res.data['data']['items']) { - // 屏蔽推广和拉黑用户 - if (i['card_goto'] != 'ad_av' && - (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && - (i['args'] != null && - !blackMidsList.contains(i['args']['up_mid']))) { - RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i); - if (!RecommendFilter.filter(videoItem)) { - list.add(videoItem); - } + var res = await Request().get( + Api.recommendListApp, + data: { + 'idx': freshIdx, + 'flush': '5', + 'column': '4', + 'device': 'pad', + 'device_type': 0, + 'device_name': 'vivo', + 'pull': freshIdx == 0 ? 'true' : 'false', + 'appkey': Constants.appKey, + 'access_key': loginStatus + ? (localCache + .get(LocalCacheKey.accessKey, defaultValue: {})['value'] ?? + '') + : '' + }, + ); + if (res.data['code'] == 0) { + List list = []; + List blackMidsList = + setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + for (var i in res.data['data']['items']) { + // 屏蔽推广和拉黑用户 + if (i['card_goto'] != 'ad_av' && + (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && + (i['args'] != null && + !blackMidsList.contains(i['args']['up_mid']))) { + RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i); + if (!RecommendFilter.filter(videoItem)) { + list.add(videoItem); } } - return {'status': true, 'data': list}; - } else { - return {'status': false, 'data': [], 'msg': res.data['message']}; } - } catch (err) { - return {'status': false, 'data': [], 'msg': err.toString()}; + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; } } diff --git a/lib/models/home/rcmd/result.dart b/lib/models/home/rcmd/result.dart index 0098fe95..98beda59 100644 --- a/lib/models/home/rcmd/result.dart +++ b/lib/models/home/rcmd/result.dart @@ -34,7 +34,7 @@ class RecVideoItemAppModel { String? title; int? isFollowed; RcmdOwner? owner; - RcmdReason? rcmdReason; + String? rcmdReason; String? goto; int? param; String? uri; @@ -64,17 +64,11 @@ class RecVideoItemAppModel { //duration = json['cover_right_text']; title = json['title']; owner = RcmdOwner.fromJson(json); - rcmdReason = json['rcmd_reason_style'] != null - ? RcmdReason.fromJson(json['rcmd_reason_style']) - : null; + rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason']; // 由于app端api并不会直接返回与owner的关注状态 // 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态,从而与web端接口等效 RegExp regex = RegExp(r'已关注|新关注'); - isFollowed = rcmdReason != null && - rcmdReason!.content != null && - regex.hasMatch(rcmdReason!.content!) - ? 1 - : 0; + isFollowed = regex.hasMatch(rcmdReason ?? '') ? 1 : 0; // 如果是,就无需再显示推荐原因,交由view统一处理即可 if (isFollowed == 1) { rcmdReason = null; diff --git a/lib/models/model_owner.dart b/lib/models/model_owner.dart index 70396cbb..6ef425eb 100644 --- a/lib/models/model_owner.dart +++ b/lib/models/model_owner.dart @@ -1,19 +1,12 @@ -import 'package:hive/hive.dart'; - -part 'model_owner.g.dart'; - -@HiveType(typeId: 3) class Owner { Owner({ this.mid, this.name, this.face, }); - @HiveField(0) + int? mid; - @HiveField(1) String? name; - @HiveField(2) String? face; Owner.fromJson(Map json) { diff --git a/lib/models/model_owner.g.dart b/lib/models/model_owner.g.dart deleted file mode 100644 index de452713..00000000 --- a/lib/models/model_owner.g.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'model_owner.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class OwnerAdapter extends TypeAdapter { - @override - final int typeId = 3; - - @override - Owner read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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; -} diff --git a/lib/models/model_rec_video_item.dart b/lib/models/model_rec_video_item.dart index 1503f192..b82510db 100644 --- a/lib/models/model_rec_video_item.dart +++ b/lib/models/model_rec_video_item.dart @@ -1,9 +1,5 @@ import './model_owner.dart'; -import 'package:hive/hive.dart'; -part 'model_rec_video_item.g.dart'; - -@HiveType(typeId: 0) class RecVideoItemModel { RecVideoItemModel({ this.id, @@ -21,32 +17,19 @@ class RecVideoItemModel { this.rcmdReason, }); - @HiveField(0) int? id = -1; - @HiveField(1) String? bvid = ''; - @HiveField(2) int? cid = -1; - @HiveField(3) String? goto = ''; - @HiveField(4) String? uri = ''; - @HiveField(5) String? pic = ''; - @HiveField(6) String? title = ''; - @HiveField(7) int? duration = -1; - @HiveField(8) int? pubdate = -1; - @HiveField(9) Owner? owner; - @HiveField(10) Stat? stat; - @HiveField(11) int? isFollowed; - @HiveField(12) - RcmdReason? rcmdReason; + String? rcmdReason; RecVideoItemModel.fromJson(Map json) { id = json["id"]; @@ -61,26 +44,20 @@ class RecVideoItemModel { owner = Owner.fromJson(json["owner"]); stat = Stat.fromJson(json["stat"]); isFollowed = json["is_followed"] ?? 0; - rcmdReason = json["rcmd_reason"] != null - ? RcmdReason.fromJson(json["rcmd_reason"]) - : RcmdReason(content: ''); + rcmdReason = json["rcmd_reason"]?['content']; } } -@HiveType(typeId: 1) class Stat { Stat({ this.view, this.like, this.danmu, }); - @HiveField(0) - int? view; - @HiveField(1) - int? like; - @HiveField(2) - int? danmu; + int? view; + int? like; + int? danmu; Stat.fromJson(Map json) { // 无需在model中转换以保留原始数据,在view层处理即可 view = json["view"]; @@ -88,20 +65,3 @@ class Stat { danmu = json['danmaku']; } } - -@HiveType(typeId: 2) -class RcmdReason { - RcmdReason({ - this.reasonType, - this.content, - }); - @HiveField(0) - int? reasonType; - @HiveField(1) - String? content = ''; - - RcmdReason.fromJson(Map json) { - reasonType = json["reason_type"]; - content = json["content"] ?? ''; - } -} diff --git a/lib/models/model_rec_video_item.g.dart b/lib/models/model_rec_video_item.g.dart deleted file mode 100644 index dc614354..00000000 --- a/lib/models/model_rec_video_item.g.dart +++ /dev/null @@ -1,154 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'model_rec_video_item.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class RecVideoItemModelAdapter extends TypeAdapter { - @override - final int typeId = 0; - - @override - RecVideoItemModel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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 { - @override - final int typeId = 1; - - @override - Stat read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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 { - @override - final int typeId = 2; - - @override - RcmdReason read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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; -} diff --git a/lib/models/search/hot.dart b/lib/models/search/hot.dart index ce09b2ea..59d7e749 100644 --- a/lib/models/search/hot.dart +++ b/lib/models/search/hot.dart @@ -1,14 +1,8 @@ -import 'package:hive/hive.dart'; - -part 'hot.g.dart'; - -@HiveType(typeId: 6) class HotSearchModel { HotSearchModel({ this.list, }); - @HiveField(0) List? list; HotSearchModel.fromJson(Map json) { @@ -18,7 +12,6 @@ class HotSearchModel { } } -@HiveType(typeId: 7) class HotSearchItem { HotSearchItem({ this.keyword, @@ -27,14 +20,10 @@ class HotSearchItem { this.icon, }); - @HiveField(0) String? keyword; - @HiveField(1) String? showName; // 4/5热 11话题 8普通 7直播 - @HiveField(2) int? wordType; - @HiveField(3) String? icon; HotSearchItem.fromJson(Map json) { diff --git a/lib/models/search/hot.g.dart b/lib/models/search/hot.g.dart deleted file mode 100644 index a06dd475..00000000 --- a/lib/models/search/hot.g.dart +++ /dev/null @@ -1,84 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'hot.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class HotSearchModelAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - HotSearchModel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HotSearchModel( - list: (fields[0] as List?)?.cast(), - ); - } - - @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 { - @override - final int typeId = 7; - - @override - HotSearchItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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; -} diff --git a/lib/models/user/stat.dart b/lib/models/user/stat.dart index 3b09acb1..0b56a499 100644 --- a/lib/models/user/stat.dart +++ b/lib/models/user/stat.dart @@ -1,8 +1,3 @@ -import 'package:hive/hive.dart'; - -part 'stat.g.dart'; - -@HiveType(typeId: 1) class UserStat { UserStat({ this.following, @@ -10,11 +5,8 @@ class UserStat { this.dynamicCount, }); - @HiveField(0) int? following; - @HiveField(1) int? follower; - @HiveField(2) int? dynamicCount; UserStat.fromJson(Map json) { diff --git a/lib/models/user/stat.g.dart b/lib/models/user/stat.g.dart deleted file mode 100644 index 290fe026..00000000 --- a/lib/models/user/stat.g.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'stat.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class UserStatAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - UserStat read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - 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; -} diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index dca5a158..a132c5da 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,8 +1,6 @@ import 'dart:io'; import 'package:hive_flutter/hive_flutter.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 '../models/common/gesture_mode.dart'; import 'global_data.dart'; @@ -54,11 +52,8 @@ class GStrorage { } static void regAdapter() { - Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(LevelInfoAdapter()); - Hive.registerAdapter(HotSearchModelAdapter()); - Hive.registerAdapter(HotSearchItemAdapter()); } static Future close() async { From 5b78810ba43cd795b2bff953b08f41e0b4e680d3 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Tue, 23 Jul 2024 23:29:53 +0800 Subject: [PATCH 04/22] =?UTF-8?q?fix:=20=E6=B6=88=E6=81=AFmid=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/whisper/view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 1f69fa64..7082619f 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget { 'talkerId': sessionItem.talkerId.toString(), 'name': sessionItem.accountInfo.name, 'face': sessionItem.accountInfo.face, - 'mid': sessionItem.accountInfo.mid.toString(), + 'mid': (sessionItem.accountInfo?.mid ?? 0).toString(), 'heroTag': heroTag, }, ); From a0bc0314bf51c1da040e4a6f0326ab96737fbd28 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 25 Jul 2024 00:08:18 +0800 Subject: [PATCH 05/22] =?UTF-8?q?fix:=20=E8=AF=84=E8=AE=BA=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/reply/widgets/reply_item.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index bba5ddbb..5b7b0659 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -16,7 +16,6 @@ import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/plugin/pl_gallery/index.dart'; import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/feed_back.dart'; -import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/url_utils.dart'; import 'package:pilipala/utils/utils.dart'; @@ -436,7 +435,8 @@ class ReplyItemRow extends StatelessWidget { if (extraRow == 1) InkWell( // 一楼点击【共xx条回复】展开评论详情 - onTap: () => replyReply!(replyItem), + onTap: () => replyReply?.call(replyItem, null, true), + onLongPress: () => {}, child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(8, 5, 8, 8), From 80d0b15ac88d4b9bdcb576d45519fc296c036b2a Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 26 Jul 2024 22:47:14 +0800 Subject: [PATCH 06/22] =?UTF-8?q?fix:=20=E5=8A=A8=E6=80=81=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E6=9F=A5=E7=9C=8B=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/dynamics/detail/view.dart | 9 ++++++--- lib/pages/video/detail/reply_reply/view.dart | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index 6b3d969d..56af68fc 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -106,7 +106,7 @@ class _DynamicDetailPageState extends State } // 查看二级评论 - void replyReply(replyItem, currentReply) { + void replyReply(replyItem, currentReply, loadMore) { int oid = replyItem.oid; int rpid = replyItem.rpid!; Get.to( @@ -125,6 +125,7 @@ class _DynamicDetailPageState extends State source: 'dynamic', replyType: ReplyType.values[replyType], firstFloor: replyItem, + loadMore: loadMore, ), ), ); @@ -324,8 +325,10 @@ class _DynamicDetailPageState extends State replyItem: replyList[index], showReplyRow: true, replyLevel: '1', - replyReply: (replyItem, currentReply) => - replyReply(replyItem, currentReply), + replyReply: + (replyItem, currentReply, loadMore) => + replyReply(replyItem, + currentReply, loadMore), replyType: ReplyType.values[replyType], addReply: (replyItem) { replyList[index] diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 439f5d1d..06a40cd6 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -21,7 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget { this.replyType, this.sheetHeight, this.currentReply, - this.loadMore, + this.loadMore = true, super.key, }); final int? oid; @@ -32,7 +32,7 @@ class VideoReplyReplyPanel extends StatefulWidget { final ReplyType? replyType; final double? sheetHeight; final dynamic currentReply; - final bool? loadMore; + final bool loadMore; @override State createState() => _VideoReplyReplyPanelState(); @@ -142,7 +142,7 @@ class _VideoReplyReplyPanelState extends State { ), ), ], - widget.loadMore != null && widget.loadMore! + widget.loadMore ? FutureBuilder( future: _futureBuilderFuture, builder: (BuildContext context, snapshot) { From 5e6f082ade33223e30fc522ec6fb78f428a713a3 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 27 Jul 2024 11:00:19 +0800 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20=E8=AF=84=E8=AE=BA=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E4=B8=BA=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/reply/widgets/reply_item.dart | 28 +++- .../detail/reply/widgets/reply_save.dart | 140 ++++++++++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/pages/video/detail/reply/widgets/reply_save.dart diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 5b7b0659..9fc8b11e 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -14,11 +14,13 @@ import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/plugin/pl_gallery/index.dart'; +import 'package:pilipala/plugin/pl_popup/index.dart'; import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/url_utils.dart'; import 'package:pilipala/utils/utils.dart'; +import 'reply_save.dart'; import 'zan.dart'; Box setting = GStrorage.setting; @@ -58,7 +60,10 @@ class ReplyItem extends StatelessWidget { useRootNavigator: true, isScrollControlled: true, builder: (context) { - return MorePanel(item: replyItem); + return MorePanel( + item: replyItem, + mainFloor: true, + ); }, ); }, @@ -1004,7 +1009,12 @@ InlineSpan buildContent( class MorePanel extends StatelessWidget { final dynamic item; - const MorePanel({super.key, required this.item}); + final bool mainFloor; + const MorePanel({ + super.key, + required this.item, + this.mainFloor = false, + }); Future menuActionHandler(String type) async { String message = item.content.message ?? item.content; @@ -1026,6 +1036,13 @@ class MorePanel extends StatelessWidget { }, ); break; + case 'save': + Get.back(); + Navigator.push( + Get.context!, + PlPopupRoute(child: ReplySave(replyItem: item)), + ); + break; // case 'block': // SmartDialog.showToast('加入黑名单'); // break; @@ -1076,6 +1093,13 @@ class MorePanel extends StatelessWidget { leading: const Icon(Icons.copy_outlined, size: 19), title: Text('自由复制', style: textTheme.titleSmall), ), + if (mainFloor) + ListTile( + onTap: () async => await menuActionHandler('save'), + minLeadingWidth: 0, + leading: const Icon(Icons.save_alt_rounded, size: 19), + title: Text('本地保存', style: textTheme.titleSmall), + ), // ListTile( // onTap: () async => await menuActionHandler('block'), // minLeadingWidth: 0, diff --git a/lib/pages/video/detail/reply/widgets/reply_save.dart b/lib/pages/video/detail/reply/widgets/reply_save.dart new file mode 100644 index 00000000..c083806f --- /dev/null +++ b/lib/pages/video/detail/reply/widgets/reply_save.dart @@ -0,0 +1,140 @@ +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 createState() => _ReplySaveState(); +} + +class _ReplySaveState extends State { + 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 _createWidgets(int count, Widget Function() builder) { + return List.generate(count, (_) => Expanded(child: builder())); + } + + List _createColumnWidgets() { + return _createWidgets(3, () => Row(children: _createRowWidgets())); + } + + List _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 Container( + width: Get.width, + height: Get.height, + margin: EdgeInsets.fromLTRB( + 0, + MediaQuery.of(context).padding.top + 4, + 0, + MediaQuery.of(context).padding.bottom + 4, + ), + color: Colors.transparent, + 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, + ), + 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('保存'), + ), + ], + ), + ], + ), + ); + } +} From a2cbcca9091ce10dd041bb99fe6aba2f303df1eb Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 27 Jul 2024 11:02:42 +0800 Subject: [PATCH 08/22] =?UTF-8?q?fix:=20=E9=A6=96=E9=A1=B5=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/home/rcmd/result.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/home/rcmd/result.dart b/lib/models/home/rcmd/result.dart index 98beda59..88657b33 100644 --- a/lib/models/home/rcmd/result.dart +++ b/lib/models/home/rcmd/result.dart @@ -62,7 +62,7 @@ class RecVideoItemAppModel { duration = json['player_args'] != null ? json['player_args']['duration'] : -1; //duration = json['cover_right_text']; - title = json['title']; + title = json['title'] ?? '获取标题失败'; owner = RcmdOwner.fromJson(json); rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason']; // 由于app端api并不会直接返回与owner的关注状态 @@ -74,7 +74,7 @@ class RecVideoItemAppModel { rcmdReason = null; } goto = json['goto']; - param = int.parse(json['param']); + param = int.parse(json['param'] ?? '-1'); uri = json['uri']; talkBack = json['talk_back']; From 8e0fbb2a54391dd84d536ab6ef6d78f8bfbf850f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 27 Jul 2024 22:08:12 +0800 Subject: [PATCH 09/22] =?UTF-8?q?opt:=20=E9=9D=9Epip=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E8=87=B3=E5=90=8E=E5=8F=B0=E4=B8=8D=E5=85=B3?= =?UTF-8?q?=E9=97=ADBottomSheet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/view.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 331986dd..0388e962 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -330,12 +330,14 @@ class _VideoDetailPageState extends State plPlayerController?.danmakuController?.clear(); break; case 'pause': - vdCtr.hiddenReplyReplyPanel(); - if (vdCtr.videoType == SearchType.video) { - videoIntroController.hiddenEpisodeBottomSheet(); - } - if (vdCtr.videoType == SearchType.media_bangumi) { - bangumiIntroController.hiddenEpisodeBottomSheet(); + if (autoPiP) { + vdCtr.hiddenReplyReplyPanel(); + if (vdCtr.videoType == SearchType.video) { + videoIntroController.hiddenEpisodeBottomSheet(); + } + if (vdCtr.videoType == SearchType.media_bangumi) { + bangumiIntroController.hiddenEpisodeBottomSheet(); + } } break; } From 66edac428b99b1ff45860f9c6ed5b2ae66e51925 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 4 Aug 2024 00:35:42 +0800 Subject: [PATCH 10/22] =?UTF-8?q?mod:=20=E8=A1=A5=E5=85=85=E5=90=88?= =?UTF-8?q?=E9=9B=86=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 2 ++ lib/http/member.dart | 36 ++++++++++++++++++++ lib/models/member/seasons.dart | 13 ++++++++ lib/pages/member/widgets/seasons.dart | 23 +++++++++++-- lib/pages/member_seasons/controller.dart | 42 +++++++++++++++++++++--- lib/pages/member_seasons/view.dart | 7 ++-- 6 files changed, 114 insertions(+), 9 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index 46bbb6ac..31e5a38b 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -487,6 +487,8 @@ class Api { static const getSeasonDetailApi = '/x/polymer/web-space/seasons_archives_list'; + static const getSeriesDetailApi = '/x/series/archives'; + /// 获取未读动态数 static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; diff --git a/lib/http/member.dart b/lib/http/member.dart index 0fb010b0..e87aa42e 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -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'], + }; + } + } } diff --git a/lib/models/member/seasons.dart b/lib/models/member/seasons.dart index 88b93c78..275466a6 100644 --- a/lib/models/member/seasons.dart +++ b/lib/models/member/seasons.dart @@ -2,10 +2,12 @@ class MemberSeasonsDataModel { MemberSeasonsDataModel({ this.page, this.seasonsList, + this.seriesList, }); Map? page; List? seasonsList; + List? seriesList; MemberSeasonsDataModel.fromJson(Map json) { page = json['page']; @@ -19,6 +21,11 @@ class MemberSeasonsDataModel { .map((e) => MemberSeasonsList.fromJson(e)) .toList() : []; + seriesList = json['archives'] != null + ? json['archives'] + .map((e) => MemberArchiveItem.fromJson(e)) + .toList() + : []; seasonsList = [...tempList1, ...tempList2]; } @@ -93,6 +100,8 @@ class MamberMeta { this.ptime, this.seasonId, this.total, + this.seriesId, + this.category, }); String? cover; @@ -102,6 +111,8 @@ class MamberMeta { int? ptime; int? seasonId; int? total; + int? seriesId; + int? category; MamberMeta.fromJson(Map json) { cover = json['cover']; @@ -111,5 +122,7 @@ class MamberMeta { ptime = json['ptime']; seasonId = json['season_id']; total = json['total']; + seriesId = json['series_id']; + category = json['category']; } } diff --git a/lib/pages/member/widgets/seasons.dart b/lib/pages/member/widgets/seasons.dart index 1367d6bd..1749ff45 100644 --- a/lib/pages/member/widgets/seasons.dart +++ b/lib/pages/member/widgets/seasons.dart @@ -24,8 +24,27 @@ class MemberSeasonsPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - onTap: () => Get.toNamed( - '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}&seasonName=${item.meta!.name}'), + onTap: () { + final int category = item.meta!.category!; + Map parameters = {}; + if (category == 0) { + parameters = { + 'category': '0', + 'mid': item.meta!.mid.toString(), + 'seasonId': item.meta!.seasonId.toString(), + 'seasonName': item.meta!.name!, + }; + } + if (category == 1) { + parameters = { + 'category': '1', + 'mid': item.meta!.mid.toString(), + 'seriesId': item.meta!.seriesId.toString(), + 'seasonName': item.meta!.name!, + }; + } + Get.toNamed('/memberSeasons', parameters: parameters); + }, title: Text( item.meta!.name!, maxLines: 1, diff --git a/lib/pages/member_seasons/controller.dart b/lib/pages/member_seasons/controller.dart index 82ef0af0..58a9035f 100644 --- a/lib/pages/member_seasons/controller.dart +++ b/lib/pages/member_seasons/controller.dart @@ -6,7 +6,9 @@ import 'package:pilipala/models/member/seasons.dart'; class MemberSeasonsController extends GetxController { final ScrollController scrollController = ScrollController(); late int mid; - late int seasonId; + int? seasonId; + int? seriesId; + late String category; int pn = 1; int ps = 30; int count = 0; @@ -17,17 +19,23 @@ class MemberSeasonsController extends GetxController { void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); - seasonId = int.parse(Get.parameters['seasonId']!); + category = Get.parameters['category']!; + if (category == '0') { + seasonId = int.parse(Get.parameters['seriesId']!); + } + if (category == '1') { + seriesId = int.parse(Get.parameters['seriesId']!); + } } - // 获取专栏详情 + // 获取专栏详情 0: 专栏 1: 系列 Future getSeasonDetail(type) async { if (type == 'onRefresh') { pn = 1; } var res = await MemberHttp.getSeasonDetail( mid: mid, - seasonId: seasonId, + seasonId: seasonId!, pn: pn, ps: ps, sortReverse: false, @@ -40,8 +48,32 @@ class MemberSeasonsController extends GetxController { return res; } + // 获取系列详情 0: 专栏 1: 系列 + Future getSeriesDetail(type) async { + if (type == 'onRefresh') { + pn = 1; + } + var res = await MemberHttp.getSeriesDetail( + mid: mid, + seriesId: seriesId!, + pn: pn, + currentMid: 17340771, + ); + if (res['status']) { + seasonsList.addAll(res['data'].seriesList); + page = res['data'].page; + pn += 1; + } + return res; + } + // 上拉加载 Future onLoad() async { - getSeasonDetail('onLoad'); + if (category == '0') { + getSeasonDetail('onLoad'); + } + if (category == '1') { + getSeriesDetail('onLoad'); + } } } diff --git a/lib/pages/member_seasons/view.dart b/lib/pages/member_seasons/view.dart index 556e2ec5..b8c0407d 100644 --- a/lib/pages/member_seasons/view.dart +++ b/lib/pages/member_seasons/view.dart @@ -17,12 +17,15 @@ class _MemberSeasonsPageState extends State { Get.put(MemberSeasonsController()); late Future _futureBuilderFuture; late ScrollController scrollController; + late String category; @override void initState() { super.initState(); - _futureBuilderFuture = - _memberSeasonsController.getSeasonDetail('onRefresh'); + category = Get.parameters['category']!; + _futureBuilderFuture = category == '0' + ? _memberSeasonsController.getSeasonDetail('onRefresh') + : _memberSeasonsController.getSeriesDetail('onRefresh'); scrollController = _memberSeasonsController.scrollController; scrollController.addListener( () { From 61337338bd42d0e43ffcee264a952b5d52318c6a Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 10 Aug 2024 23:11:21 +0800 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20=E5=9B=BE=E7=89=87=E9=A2=84?= =?UTF-8?q?=E8=A7=88Hero=20tag=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../video/detail/reply/widgets/reply_item.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 5b7b0659..5f53b398 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:appscheme/appscheme.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/gestures.dart'; @@ -549,7 +551,7 @@ InlineSpan buildContent( ); } - void onPreviewImg(picList, initIndex) { + void onPreviewImg(picList, initIndex, randomInt) { final MainController mainController = Get.find(); mainController.imgPreviewStatus = true; Navigator.of(context).push( @@ -575,7 +577,7 @@ InlineSpan buildContent( }, child: Center( child: Hero( - tag: picList[index], + tag: picList[index] + randomInt, child: CachedNetworkImage( fadeInDuration: const Duration(milliseconds: 0), imageUrl: picList[index], @@ -886,11 +888,12 @@ InlineSpan buildContent( pictureItem['img_width'])) .truncateToDouble(); } catch (_) {} + String randomInt = Random().nextInt(101).toString(); return Hero( - tag: picList[0], + tag: picList[0] + randomInt, child: GestureDetector( - onTap: () => onPreviewImg(picList, 0), + onTap: () => onPreviewImg(picList, 0, randomInt), child: Container( padding: const EdgeInsets.only(top: 4), constraints: BoxConstraints(maxHeight: maxHeight), @@ -927,13 +930,14 @@ InlineSpan buildContent( picList.add(content.pictures[i]['img_src']); } for (var i = 0; i < len; i++) { + String randomInt = Random().nextInt(101).toString(); list.add( LayoutBuilder( builder: (context, BoxConstraints box) { return Hero( - tag: picList[i], + tag: picList[i] + randomInt, child: GestureDetector( - onTap: () => onPreviewImg(picList, i), + onTap: () => onPreviewImg(picList, i, randomInt), child: NetworkImgLayer( src: picList[i], width: box.maxWidth, From b188675faf2607e9e5d3eb5c60da0ae3863203fa Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 11 Aug 2024 18:43:21 +0800 Subject: [PATCH 12/22] opt --- .../detail/reply/widgets/reply_item.dart | 43 +++++-- .../detail/reply/widgets/reply_save.dart | 106 ++++++++++-------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 9fc8b11e..ae691f97 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -17,6 +17,7 @@ import 'package:pilipala/plugin/pl_gallery/index.dart'; import 'package:pilipala/plugin/pl_popup/index.dart'; import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/url_utils.dart'; import 'package:pilipala/utils/utils.dart'; @@ -33,6 +34,7 @@ class ReplyItem extends StatelessWidget { this.showReplyRow = true, this.replyReply, this.replyType, + this.replySave = false, super.key, }); final ReplyItemModel? replyItem; @@ -41,6 +43,7 @@ class ReplyItem extends StatelessWidget { final bool? showReplyRow; final Function? replyReply; final ReplyType? replyType; + final bool? replySave; @override Widget build(BuildContext context) { @@ -48,12 +51,18 @@ class ReplyItem extends StatelessWidget { child: InkWell( // 点击整个评论区 评论详情/回复 onTap: () { + if (replySave!) { + return; + } feedBack(); if (replyReply != null) { replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty); } }, onLongPress: () { + if (replySave!) { + return; + } feedBack(); showModalBottomSheet( context: context, @@ -236,7 +245,7 @@ class ReplyItem extends StatelessWidget { ), ), // 操作区域 - bottonAction(context, replyItem!.replyControl), + bottonAction(context, replyItem!.replyControl, replySave), // 一楼的评论 if ((replyItem!.replyControl!.isShow! || replyItem!.replies!.isNotEmpty) && @@ -257,7 +266,7 @@ class ReplyItem extends StatelessWidget { } // 感谢、回复、复制 - Widget bottonAction(BuildContext context, replyControl) { + Widget bottonAction(BuildContext context, replyControl, replySave) { ColorScheme colorScheme = Theme.of(context).colorScheme; TextTheme textTheme = Theme.of(context).textTheme; return Row( @@ -290,16 +299,26 @@ class ReplyItem extends StatelessWidget { }); }, child: Row(children: [ - Icon(Icons.reply, - size: 18, color: colorScheme.outline.withOpacity(0.8)), - const SizedBox(width: 3), - Text( - '回复', - style: TextStyle( - fontSize: textTheme.labelMedium!.fontSize, - color: colorScheme.outline, + if (!replySave!) ...[ + Icon(Icons.reply, + size: 18, color: colorScheme.outline.withOpacity(0.8)), + const SizedBox(width: 3), + Text( + '回复', + style: TextStyle( + fontSize: textTheme.labelMedium!.fontSize, + color: colorScheme.outline, + ), + ) + ], + if (replySave!) + Text( + IdUtils.av2bv(replyItem!.oid!), + style: TextStyle( + fontSize: textTheme.labelMedium!.fontSize, + color: colorScheme.outline, + ), ), - ), ]), ), ), @@ -1093,7 +1112,7 @@ class MorePanel extends StatelessWidget { leading: const Icon(Icons.copy_outlined, size: 19), title: Text('自由复制', style: textTheme.titleSmall), ), - if (mainFloor) + if (mainFloor && item.content.pictures.isEmpty) ListTile( onTap: () async => await menuActionHandler('save'), minLeadingWidth: 0, diff --git a/lib/pages/video/detail/reply/widgets/reply_save.dart b/lib/pages/video/detail/reply/widgets/reply_save.dart index c083806f..9eed4923 100644 --- a/lib/pages/video/detail/reply/widgets/reply_save.dart +++ b/lib/pages/video/detail/reply/widgets/reply_save.dart @@ -76,64 +76,72 @@ class _ReplySaveState extends State { @override Widget build(BuildContext context) { - return Container( - width: Get.width, - height: Get.height, - margin: EdgeInsets.fromLTRB( - 0, - MediaQuery.of(context).padding.top + 4, - 0, - MediaQuery.of(context).padding.bottom + 4, - ), - color: Colors.transparent, - 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, + 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(), + ), + ), + ], ), - 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('保存'), + 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('保存'), + ), + ], ), ], ), - ], + ), ), ); } From 1012b5d00907e05616833a4f71f53300913d31ed Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 18 Aug 2024 23:30:51 +0800 Subject: [PATCH 13/22] =?UTF-8?q?feat:=20=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 + lib/http/init.dart | 3 + lib/http/live.dart | 19 ++ lib/models/live/message.dart | 101 +++++++++++ lib/pages/live_room/controller.dart | 78 ++++++++ lib/pages/live_room/view.dart | 269 ++++++++++++++++++++++++++-- lib/plugin/pl_socket/index.dart | 107 +++++++++++ lib/utils/binary_writer.dart | 117 ++++++++++++ lib/utils/live.dart | 196 ++++++++++++++++++++ pubspec.lock | 14 +- pubspec.yaml | 2 + 11 files changed, 890 insertions(+), 20 deletions(-) create mode 100644 lib/models/live/message.dart create mode 100644 lib/plugin/pl_socket/index.dart create mode 100644 lib/utils/binary_writer.dart create mode 100644 lib/utils/live.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 31e5a38b..08a20382 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -554,4 +554,8 @@ class Api { /// 系统通知 static const String messageSystemAPi = '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify'; + + /// 直播间弹幕信息 + static const String getDanmuInfo = + '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo'; } diff --git a/lib/http/init.dart b/lib/http/init.dart index cb9d6f39..faa57dd5 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -29,6 +29,7 @@ class Request { late String systemProxyPort; static final RegExp spmPrefixExp = RegExp(r''); + static late String buvid; /// 设置cookie static setCookie() async { @@ -70,6 +71,8 @@ class Request { final String cookieString = cookie .map((Cookie cookie) => '${cookie.name}=${cookie.value}') .join('; '); + + buvid = cookie.firstWhere((e) => e.name == 'buvid3').value; dio.options.headers['cookie'] = cookieString; } diff --git a/lib/http/live.dart b/lib/http/live.dart index e624120e..a405fd58 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -65,4 +65,23 @@ 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'], + }; + } + } } diff --git a/lib/models/live/message.dart b/lib/models/live/message.dart new file mode 100644 index 00000000..cd0f4b75 --- /dev/null +++ b/lib/models/live/message.dart @@ -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? 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')}"; + } +} diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 4e67fa2c..fa95ce63 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,10 +1,16 @@ +import 'dart:convert'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/constants.dart'; +import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/live.dart'; +import 'package:pilipala/models/live/message.dart'; import 'package:pilipala/models/live/quality.dart'; import 'package:pilipala/models/live/room_info.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/plugin/pl_socket/index.dart'; +import 'package:pilipala/utils/live.dart'; import '../../models/live/room_info_h5.dart'; import '../../utils/storage.dart'; import '../../utils/video_utils.dart'; @@ -24,6 +30,13 @@ class LiveRoomController extends GetxController { int? tempCurrentQn; late List> acceptQnList; RxString currentQnDesc = ''.obs; + Box userInfoCache = GStrorage.userInfo; + int userId = 0; + PlSocket? plSocket; + List danmuHostList = []; + String token = ''; + // 弹幕消息列表 + RxList messageList = [].obs; @override void onInit() { @@ -43,6 +56,11 @@ class LiveRoomController extends GetxController { } // CDN优化 enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true); + final userInfo = userInfoCache.get('userInfoCache'); + if (userInfo != null && userInfo.mid != null) { + userId = userInfo.mid; + } + liveDanmakuInfo().then((value) => initSocket()); } playerInit(source) async { @@ -127,4 +145,64 @@ class LiveRoomController extends GetxController { .description; await queryLiveInfo(); } + + Future liveDanmakuInfo() async { + var res = await LiveHttp.liveDanmakuInfo(roomId: roomId); + if (res['status']) { + danmuHostList = (res["data"]["host_list"] as List) + .map((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? liveMsg = + LiveUtils.decodeMessage(message); + if (liveMsg != null) { + messageList.addAll(liveMsg + .where((msg) => msg.type == LiveMessageType.chat) + .toList()); + } + }, + onErrorCb: (e) { + print('error: $e'); + }, + ); + await plSocket?.connect(); + } + + void joinRoom() async { + var joinData = LiveUtils.encodeData( + json.encode({ + "uid": userId, + "roomid": roomId, + "protover": 3, + "buvid": Request.buvid, + "platform": "web", + "type": 2, + "key": token, + }), + 7, + ); + plSocket?.sendMessage(joinData); + } + + @override + void onClose() { + plSocket?.onClose(); + super.onClose(); + } } diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 37981b1d..d9b316e9 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:floating/floating.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/live/message.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'controller.dart'; @@ -16,7 +19,8 @@ class LiveRoomPage extends StatefulWidget { State createState() => _LiveRoomPageState(); } -class _LiveRoomPageState extends State { +class _LiveRoomPageState extends State + with TickerProviderStateMixin { final LiveRoomController _liveRoomController = Get.put(LiveRoomController()); PlPlayerController? plPlayerController; late Future? _futureBuilder; @@ -25,6 +29,9 @@ class _LiveRoomPageState extends State { bool isShowCover = true; bool isPlay = true; Floating? floating; + final ScrollController _scrollController = ScrollController(); + late AnimationController fabAnimationCtr; + bool _shouldAutoScroll = true; @override void initState() { @@ -34,6 +41,13 @@ class _LiveRoomPageState extends State { } videoSourceInit(); _futureBuilderFuture = _liveRoomController.queryLiveInfo(); + // 监听滚动事件 + _scrollController.addListener(_onScroll); + fabAnimationCtr = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + value: 0.0, + ); } Future videoSourceInit() async { @@ -41,12 +55,52 @@ class _LiveRoomPageState extends State { plPlayerController = _liveRoomController.plPlayerController; } + void _onScroll() { + // 反向时,展示按钮 + if (_scrollController.position.userScrollDirection == + ScrollDirection.forward) { + _shouldAutoScroll = false; + fabAnimationCtr.forward(); + } else { + _shouldAutoScroll = true; + fabAnimationCtr.reverse(); + } + } + + // 监听messageList的变化,自动滚动到底部 + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _liveRoomController.messageList.listen((_) { + if (_shouldAutoScroll) { + _scrollToBottom(); + } + }); + } + + void _scrollToBottom() { + if (_scrollController.hasClients) { + _scrollController + .animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .then((value) { + _shouldAutoScroll = true; + // fabAnimationCtr.forward(); + }); + } + } + @override void dispose() { plPlayerController!.dispose(); if (floating != null) { floating!.dispose(); } + _scrollController.dispose(); + fabAnimationCtr.dispose(); super.dispose(); } @@ -80,20 +134,6 @@ class _LiveRoomPageState extends State { backgroundColor: Colors.black, body: Stack( children: [ - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Opacity( - opacity: 0.8, - child: Image.asset( - 'assets/images/live/default_bg.webp', - fit: BoxFit.cover, - // width: Get.width, - // height: Get.height, - ), - ), - ), Obx( () => Positioned( left: 0, @@ -106,7 +146,7 @@ class _LiveRoomPageState extends State { .roomInfoH5.value.roomInfo?.appBackground != null ? Opacity( - opacity: 0.8, + opacity: 0.6, child: NetworkImgLayer( width: Get.width, height: Get.height, @@ -116,7 +156,15 @@ class _LiveRoomPageState extends State { '', ), ) - : 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( @@ -198,8 +246,45 @@ class _LiveRoomPageState extends State { child: videoPlayerPanel, ), ), + const SizedBox(height: 20), + // 显示消息的列表 + buildMessageListUI( + context, + _liveRoomController, + _scrollController, + ), + // 底部安全距离 + SizedBox( + height: MediaQuery.of(context).padding.bottom + 20, + ) ], ), + // 定位 快速滑动到底部 + Positioned( + right: 20, + bottom: MediaQuery.of(context).padding.bottom + 20, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + 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 +299,153 @@ class _LiveRoomPageState extends State { } } } + +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 const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black, + Colors.black, + ], + stops: [0.0, 0.1, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: ListView.builder( + controller: scrollController, + itemCount: liveRoomController.messageList.length, + itemBuilder: (context, index) { + final LiveMessageModel liveMsgItem = + liveRoomController.messageList[index]; + return Padding( + padding: EdgeInsets.only( + top: index == 0 ? 40.0 : 4.0, + bottom: 4.0, + left: 20.0, + right: 20.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 buildMessageTextSpan( + BuildContext context, + LiveMessageModel liveMsgItem, +) { + final List inlineSpanList = []; + + // 是否包含表情包 + if (liveMsgItem.emots == null) { + // 没有表情包的消息 + inlineSpanList.add( + TextSpan( + text: liveMsgItem.message ?? '', + style: const TextStyle( + shadows: [ + Shadow( + offset: Offset(2.0, 2.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(-1.0, -1.0), + blurRadius: 3.0, + color: Colors.black, + ), + ], + ), + ), + ); + } else { + // 有表情包的消息 使用正则匹配 表情包用图片渲染 + final List 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, + style: const TextStyle( + shadows: [ + Shadow( + offset: Offset(2.0, 2.0), + blurRadius: 3.0, + color: Colors.black, + ), + Shadow( + offset: Offset(-1.0, -1.0), + blurRadius: 3.0, + color: Colors.black, + ), + ], + ), + ), + ); + return nonMatch; + }, + ); + } + + return inlineSpanList; +} diff --git a/lib/plugin/pl_socket/index.dart b/lib/plugin/pl_socket/index.dart new file mode 100644 index 00000000..1ad6af94 --- /dev/null +++ b/lib/plugin/pl_socket/index.dart @@ -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? headers; + + PlSocket({ + required this.url, + required this.heartTime, + this.onReadyCb, + this.onCloseCb, + this.onErrorCb, + this.onMessageCb, + this.headers, + }); + + WebSocketChannel? channel; + StreamSubscription? 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); + } + } +} diff --git a/lib/utils/binary_writer.dart b/lib/utils/binary_writer.dart new file mode 100644 index 00000000..929bc573 --- /dev/null +++ b/lib/utils/binary_writer.dart @@ -0,0 +1,117 @@ +import 'dart:typed_data'; + +class BinaryWriter { + List buffer; + int position = 0; + BinaryWriter(this.buffer); + int get length => buffer.length; + + void writeBytes(List 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; + } +} diff --git a/lib/utils/live.dart b/lib/utils/live.dart new file mode 100644 index 00000000..dd56616e --- /dev/null +++ b/lib/utils/live.dart @@ -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 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? decodeMessage(List 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 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(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 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(dynamic value) { + if (value is T) { + return value; + } + return null; + } + + static int readInt(List buffer, int start, int len) { + var data = _getByteData(buffer, start, len); + return _readIntFromByteData(data, len); + } + + static ByteData _getByteData(List 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'); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index a46127f9..2dfaa961 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" + brotli: + dependency: "direct main" + description: + name: brotli + sha256: "7f891558ed779aab2bed874f0a36b8123f9ff3f19cf6efbee89e18ed294945ae" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" build: dependency: transitive description: @@ -1656,13 +1664,13 @@ packages: source: hosted version: "0.5.1" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.0" + version: "2.4.5" webview_cookie_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b5d45714..23c1426c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -146,6 +146,8 @@ dependencies: lottie: ^3.1.2 # 二维码 qr_flutter: ^4.1.0 + web_socket_channel: ^2.4.5 + brotli: ^0.6.0 dev_dependencies: flutter_test: From 869e49bec2369e85a3ade02768888a7620b90924 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 21 Aug 2024 00:09:26 +0800 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/danmaku/controller.dart | 3 ++- lib/pages/danmaku/view.dart | 32 +++++++++++++++++------------ lib/pages/live_room/controller.dart | 28 ++++++++++++++++++++++--- lib/pages/live_room/view.dart | 15 ++++++++++++-- 4 files changed, 59 insertions(+), 19 deletions(-) diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index 52c423d7..ed259b96 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -2,8 +2,9 @@ import 'package:pilipala/http/danmaku.dart'; import 'package:pilipala/models/danmaku/dm.pb.dart'; class PlDanmakuController { - PlDanmakuController(this.cid); + PlDanmakuController(this.cid, this.type); final int cid; + final String type; Map> dmSegMap = {}; // 已请求的段落标记 List requestedSeg = []; diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 109f0206..e669b881 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -12,11 +12,15 @@ import 'package:pilipala/utils/storage.dart'; class PlDanmaku extends StatefulWidget { final int cid; final PlPlayerController playerController; + final String type; + final Function(DanmakuController)? createdController; const PlDanmaku({ super.key, required this.cid, required this.playerController, + this.type = 'video', + this.createdController, }); @override @@ -43,9 +47,9 @@ class _PlDanmakuState extends State { super.initState(); enableShowDanmaku = setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); - _plDanmakuController = PlDanmakuController(widget.cid); - if (mounted) { - playerController = widget.playerController; + _plDanmakuController = PlDanmakuController(widget.cid, widget.type); + playerController = widget.playerController; + if (mounted && widget.type == 'video') { if (enableShowDanmaku || playerController.isOpenDanmu.value) { _plDanmakuController.initiate( playerController.duration.value.inMilliseconds, @@ -55,13 +59,15 @@ class _PlDanmakuState extends State { ..addStatusLister(playerListener) ..addPositionListener(videoPositionListen); } - playerController.isOpenDanmu.listen((p0) { - if (p0 && !_plDanmakuController.initiated) { - _plDanmakuController.initiate( - playerController.duration.value.inMilliseconds, - playerController.position.value.inMilliseconds); - } - }); + if (widget.type == 'video') { + playerController.isOpenDanmu.listen((p0) { + if (p0 && !_plDanmakuController.initiated) { + _plDanmakuController.initiate( + playerController.duration.value.inMilliseconds, + playerController.position.value.inMilliseconds); + } + }); + } blockTypes = playerController.blockTypes; showArea = playerController.showArea; opacityVal = playerController.opacityVal; @@ -123,11 +129,12 @@ class _PlDanmakuState extends State { // double initDuration = box.maxWidth / 12; return Obx( () => AnimatedOpacity( - opacity: playerController.isOpenDanmu.value ? 1 : 0, + opacity: playerController.isOpenDanmu.value ? 1 : 1, duration: const Duration(milliseconds: 100), child: DanmakuView( createdController: (DanmakuController e) async { playerController.danmakuController = _controller = e; + widget.createdController?.call(e); }, option: DanmakuOption( fontSize: 15 * fontSizeVal, @@ -136,8 +143,7 @@ class _PlDanmakuState extends State { hideTop: blockTypes.contains(5), hideScroll: blockTypes.contains(2), hideBottom: blockTypes.contains(4), - duration: - danmakuDurationVal / playerController.playbackSpeed, + duration: danmakuDurationVal / playerController.playbackSpeed, strokeWidth: strokeWidth, // initDuration / // (danmakuSpeedVal * widget.playerController.playbackSpeed), diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index fa95ce63..6a3525e3 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/live.dart'; @@ -37,6 +39,7 @@ class LiveRoomController extends GetxController { String token = ''; // 弹幕消息列表 RxList messageList = [].obs; + DanmakuController? danmakuController; @override void onInit() { @@ -172,9 +175,28 @@ class LiveRoomController extends GetxController { final List? liveMsg = LiveUtils.decodeMessage(message); if (liveMsg != null) { - messageList.addAll(liveMsg - .where((msg) => msg.type == LiveMessageType.chat) - .toList()); + // 过滤出聊天消息 + var chatMessages = + liveMsg.where((msg) => msg.type == LiveMessageType.chat).toList(); + + // 添加到 messageList + messageList.addAll(chatMessages); + + // 将 chatMessages 转换为 danmakuItems 列表 + List danmakuItems = chatMessages.map((e) { + return DanmakuItem( + e.message ?? '', + color: Color.fromARGB( + 255, + e.color.r, + e.color.g, + e.color.b, + ), + ); + }).toList(); + + // 添加到 danmakuController + danmakuController?.addItems(danmakuItems); } }, onErrorCb: (e) { diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index d9b316e9..d1fc6e07 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/live/message.dart'; +import 'package:pilipala/pages/danmaku/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'controller.dart'; @@ -22,7 +23,7 @@ class LiveRoomPage extends StatefulWidget { class _LiveRoomPageState extends State with TickerProviderStateMixin { final LiveRoomController _liveRoomController = Get.put(LiveRoomController()); - PlPlayerController? plPlayerController; + late PlPlayerController plPlayerController; late Future? _futureBuilder; late Future? _futureBuilderFuture; @@ -32,6 +33,7 @@ class _LiveRoomPageState extends State final ScrollController _scrollController = ScrollController(); late AnimationController fabAnimationCtr; bool _shouldAutoScroll = true; + final int roomId = int.parse(Get.parameters['roomid']!); @override void initState() { @@ -110,8 +112,9 @@ class _LiveRoomPageState extends State future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData && snapshot.data['status']) { + plPlayerController = _liveRoomController.plPlayerController; return PLVideoPlayer( - controller: plPlayerController!, + controller: plPlayerController, bottomControl: BottomControl( controller: plPlayerController, liveRoomCtr: _liveRoomController, @@ -122,6 +125,14 @@ class _LiveRoomPageState extends State }); }, ), + danmuWidget: PlDanmaku( + cid: roomId, + playerController: plPlayerController, + type: 'live', + createdController: (e) { + _liveRoomController.danmakuController = e; + }, + ), ); } else { return const SizedBox(); From a074a360f2f1370a554d9248aa72909b841b268c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 21 Aug 2024 00:14:45 +0800 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/danmaku/controller.dart | 3 ++- lib/pages/danmaku/view.dart | 30 +++++++++++++++++------------ lib/pages/live_room/controller.dart | 28 ++++++++++++++++++++++++--- lib/pages/live_room/view.dart | 15 +++++++++++++-- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index 52c423d7..ed259b96 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -2,8 +2,9 @@ import 'package:pilipala/http/danmaku.dart'; import 'package:pilipala/models/danmaku/dm.pb.dart'; class PlDanmakuController { - PlDanmakuController(this.cid); + PlDanmakuController(this.cid, this.type); final int cid; + final String type; Map> dmSegMap = {}; // 已请求的段落标记 List requestedSeg = []; diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 109f0206..3cf1ed8a 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -12,11 +12,15 @@ import 'package:pilipala/utils/storage.dart'; class PlDanmaku extends StatefulWidget { final int cid; final PlPlayerController playerController; + final String type; + final Function(DanmakuController)? createdController; const PlDanmaku({ super.key, required this.cid, required this.playerController, + this.type = 'video', + this.createdController, }); @override @@ -43,9 +47,9 @@ class _PlDanmakuState extends State { super.initState(); enableShowDanmaku = setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); - _plDanmakuController = PlDanmakuController(widget.cid); - if (mounted) { - playerController = widget.playerController; + _plDanmakuController = PlDanmakuController(widget.cid, widget.type); + playerController = widget.playerController; + if (mounted && widget.type == 'video') { if (enableShowDanmaku || playerController.isOpenDanmu.value) { _plDanmakuController.initiate( playerController.duration.value.inMilliseconds, @@ -55,13 +59,15 @@ class _PlDanmakuState extends State { ..addStatusLister(playerListener) ..addPositionListener(videoPositionListen); } - playerController.isOpenDanmu.listen((p0) { - if (p0 && !_plDanmakuController.initiated) { - _plDanmakuController.initiate( - playerController.duration.value.inMilliseconds, - playerController.position.value.inMilliseconds); - } - }); + if (widget.type == 'video') { + playerController.isOpenDanmu.listen((p0) { + if (p0 && !_plDanmakuController.initiated) { + _plDanmakuController.initiate( + playerController.duration.value.inMilliseconds, + playerController.position.value.inMilliseconds); + } + }); + } blockTypes = playerController.blockTypes; showArea = playerController.showArea; opacityVal = playerController.opacityVal; @@ -128,6 +134,7 @@ class _PlDanmakuState extends State { child: DanmakuView( createdController: (DanmakuController e) async { playerController.danmakuController = _controller = e; + widget.createdController?.call(e); }, option: DanmakuOption( fontSize: 15 * fontSizeVal, @@ -136,8 +143,7 @@ class _PlDanmakuState extends State { hideTop: blockTypes.contains(5), hideScroll: blockTypes.contains(2), hideBottom: blockTypes.contains(4), - duration: - danmakuDurationVal / playerController.playbackSpeed, + duration: danmakuDurationVal / playerController.playbackSpeed, strokeWidth: strokeWidth, // initDuration / // (danmakuSpeedVal * widget.playerController.playbackSpeed), diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index fa95ce63..6a3525e3 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/live.dart'; @@ -37,6 +39,7 @@ class LiveRoomController extends GetxController { String token = ''; // 弹幕消息列表 RxList messageList = [].obs; + DanmakuController? danmakuController; @override void onInit() { @@ -172,9 +175,28 @@ class LiveRoomController extends GetxController { final List? liveMsg = LiveUtils.decodeMessage(message); if (liveMsg != null) { - messageList.addAll(liveMsg - .where((msg) => msg.type == LiveMessageType.chat) - .toList()); + // 过滤出聊天消息 + var chatMessages = + liveMsg.where((msg) => msg.type == LiveMessageType.chat).toList(); + + // 添加到 messageList + messageList.addAll(chatMessages); + + // 将 chatMessages 转换为 danmakuItems 列表 + List danmakuItems = chatMessages.map((e) { + return DanmakuItem( + e.message ?? '', + color: Color.fromARGB( + 255, + e.color.r, + e.color.g, + e.color.b, + ), + ); + }).toList(); + + // 添加到 danmakuController + danmakuController?.addItems(danmakuItems); } }, onErrorCb: (e) { diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index d9b316e9..d1fc6e07 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/live/message.dart'; +import 'package:pilipala/pages/danmaku/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'controller.dart'; @@ -22,7 +23,7 @@ class LiveRoomPage extends StatefulWidget { class _LiveRoomPageState extends State with TickerProviderStateMixin { final LiveRoomController _liveRoomController = Get.put(LiveRoomController()); - PlPlayerController? plPlayerController; + late PlPlayerController plPlayerController; late Future? _futureBuilder; late Future? _futureBuilderFuture; @@ -32,6 +33,7 @@ class _LiveRoomPageState extends State final ScrollController _scrollController = ScrollController(); late AnimationController fabAnimationCtr; bool _shouldAutoScroll = true; + final int roomId = int.parse(Get.parameters['roomid']!); @override void initState() { @@ -110,8 +112,9 @@ class _LiveRoomPageState extends State future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData && snapshot.data['status']) { + plPlayerController = _liveRoomController.plPlayerController; return PLVideoPlayer( - controller: plPlayerController!, + controller: plPlayerController, bottomControl: BottomControl( controller: plPlayerController, liveRoomCtr: _liveRoomController, @@ -122,6 +125,14 @@ class _LiveRoomPageState extends State }); }, ), + danmuWidget: PlDanmaku( + cid: roomId, + playerController: plPlayerController, + type: 'live', + createdController: (e) { + _liveRoomController.danmakuController = e; + }, + ), ); } else { return const SizedBox(); From 44910b35d97fd29c7ad30fe60f497a50872f662c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 22 Aug 2024 20:24:21 +0800 Subject: [PATCH 16/22] =?UTF-8?q?feat:=20=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/live.dart | 33 ++++++ lib/pages/live_room/controller.dart | 21 +++- lib/pages/live_room/view.dart | 163 +++++++++++++++------------- 4 files changed, 145 insertions(+), 75 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index 08a20382..65b74d2d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -558,4 +558,7 @@ class Api { /// 直播间弹幕信息 static const String getDanmuInfo = '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo'; + + /// 直播间发送弹幕 + static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index a405fd58..f6fc4ea4 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -84,4 +84,37 @@ class LiveHttp { }; } } + + // 发送弹幕 + 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'], + }; + } + } } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 6a3525e3..e8db54fb 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; @@ -40,6 +40,8 @@ class LiveRoomController extends GetxController { // 弹幕消息列表 RxList messageList = [].obs; DanmakuController? danmakuController; + // 输入控制器 + TextEditingController inputController = TextEditingController(); @override void onInit() { @@ -222,6 +224,23 @@ class LiveRoomController extends GetxController { 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(); diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index d1fc6e07..92ea8fd8 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -97,7 +97,7 @@ class _LiveRoomPageState extends State @override void dispose() { - plPlayerController!.dispose(); + plPlayerController.dispose(); if (floating != null) { floating!.dispose(); } @@ -238,10 +238,10 @@ class _LiveRoomPageState extends State ), ), PopScope( - canPop: plPlayerController?.isFullScreen.value != true, + canPop: plPlayerController.isFullScreen.value != true, onPopInvoked: (bool didPop) { - if (plPlayerController?.isFullScreen.value == true) { - plPlayerController!.triggerFullScreen(status: false); + if (plPlayerController.isFullScreen.value == true) { + plPlayerController.triggerFullScreen(status: false); } if (MediaQuery.of(context).orientation == Orientation.landscape) { @@ -257,17 +257,53 @@ class _LiveRoomPageState extends State child: videoPlayerPanel, ), ), - const SizedBox(height: 20), // 显示消息的列表 buildMessageListUI( context, _liveRoomController, _scrollController, ), - // 底部安全距离 - SizedBox( - height: MediaQuery.of(context).padding.bottom + 20, - ) + // 弹幕输入框 + Container( + padding: EdgeInsets.only( + left: 14, + right: 14, + top: 4, + bottom: MediaQuery.of(context).padding.bottom + 20), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + border: Border( + top: BorderSide( + color: Colors.white.withOpacity(0.1), + ), + ), + ), + child: Row( + children: [ + 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, + ), + ), + ), + IconButton( + onPressed: () => _liveRoomController.sendMsg(), + icon: const Icon( + Icons.send, + color: Colors.white, + ), + ), + ], + ), + ), ], ), // 定位 快速滑动到底部 @@ -324,15 +360,15 @@ Widget buildMessageListUI( removeBottom: true, child: ShaderMask( shaderCallback: (Rect bounds) { - return const LinearGradient( + return LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, - Colors.black, + Colors.black.withOpacity(0.5), Colors.black, ], - stops: [0.0, 0.1, 1.0], + stops: const [0.01, 0.05, 0.2], ).createShader(bounds); }, blendMode: BlendMode.dstIn, @@ -342,35 +378,46 @@ Widget buildMessageListUI( itemBuilder: (context, index) { final LiveMessageModel liveMsgItem = liveRoomController.messageList[index]; - return Padding( - padding: EdgeInsets.only( - top: index == 0 ? 40.0 : 4.0, - bottom: 4.0, - left: 20.0, - right: 20.0, - ), - child: Text.rich( - TextSpan( - style: const TextStyle(color: Colors.white), - children: [ - TextSpan( - text: '${liveMsgItem.userName}: ', - style: TextStyle( - color: Colors.white.withOpacity(0.6), + 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'); + }, ), - recognizer: TapGestureRecognizer() - ..onTap = () { - // 处理点击事件 - print('Text clicked'); - }, - ), - TextSpan( - children: [ - ...buildMessageTextSpan(context, liveMsgItem) - ], - // text: liveMsgItem.message, - ), - ], + TextSpan( + children: [ + ...buildMessageTextSpan(context, liveMsgItem) + ], + // text: liveMsgItem.message, + ), + ], + ), ), ), ); @@ -392,23 +439,7 @@ List buildMessageTextSpan( if (liveMsgItem.emots == null) { // 没有表情包的消息 inlineSpanList.add( - TextSpan( - text: liveMsgItem.message ?? '', - style: const TextStyle( - shadows: [ - Shadow( - offset: Offset(2.0, 2.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(-1.0, -1.0), - blurRadius: 3.0, - color: Colors.black, - ), - ], - ), - ), + TextSpan(text: liveMsgItem.message ?? ''), ); } else { // 有表情包的消息 使用正则匹配 表情包用图片渲染 @@ -435,23 +466,7 @@ List buildMessageTextSpan( }, onNonMatch: (String nonMatch) { inlineSpanList.add( - TextSpan( - text: nonMatch, - style: const TextStyle( - shadows: [ - Shadow( - offset: Offset(2.0, 2.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(-1.0, -1.0), - blurRadius: 3.0, - color: Colors.black, - ), - ], - ), - ), + TextSpan(text: nonMatch), ); return nonMatch; }, From 844db213a56d480934aecf8b786ab5028d8e2cbc Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 22 Aug 2024 23:57:11 +0800 Subject: [PATCH 17/22] =?UTF-8?q?mod:=20=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/live_room/controller.dart | 27 ++++++++++++++- lib/pages/live_room/view.dart | 52 +++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index e8db54fb..a56a5459 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -42,6 +43,8 @@ class LiveRoomController extends GetxController { DanmakuController? danmakuController; // 输入控制器 TextEditingController inputController = TextEditingController(); + // 加入直播间提示 + RxMap joinRoomTip = {'userName': '', 'message': ''}.obs; @override void onInit() { @@ -176,7 +179,29 @@ class LiveRoomController extends GetxController { onMessageCb: (message) { final List? liveMsg = LiveUtils.decodeMessage(message); - if (liveMsg != null) { + 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(); diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 92ea8fd8..3f563ad6 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -179,6 +179,7 @@ class _LiveRoomPageState extends State ), ), Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ AppBar( centerTitle: false, @@ -263,6 +264,49 @@ class _LiveRoomPageState extends State _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 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( @@ -271,7 +315,8 @@ class _LiveRoomPageState extends State top: 4, bottom: MediaQuery.of(context).padding.bottom + 20), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(20)), border: Border( top: BorderSide( color: Colors.white.withOpacity(0.1), @@ -280,6 +325,7 @@ class _LiveRoomPageState extends State ), child: Row( children: [ + const SizedBox(width: 4), Expanded( child: TextField( controller: _liveRoomController.inputController, @@ -309,10 +355,10 @@ class _LiveRoomPageState extends State // 定位 快速滑动到底部 Positioned( right: 20, - bottom: MediaQuery.of(context).padding.bottom + 20, + bottom: MediaQuery.of(context).padding.bottom + 80, child: SlideTransition( position: Tween( - begin: const Offset(0, 2), + begin: const Offset(0, 4), end: const Offset(0, 0), ).animate(CurvedAnimation( parent: fabAnimationCtr, From 61e019f458542e8f25a14d8153044df9e14f361f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 23 Aug 2024 00:20:20 +0800 Subject: [PATCH 18/22] =?UTF-8?q?fix:=20=E5=90=88=E9=9B=86=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=A1=A5=E5=85=85=EF=BC=88=E7=9B=B4=E6=92=AD=E5=9B=9E?= =?UTF-8?q?=E6=94=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/member/widgets/seasons.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/member/widgets/seasons.dart b/lib/pages/member/widgets/seasons.dart index 1749ff45..615fc44c 100644 --- a/lib/pages/member/widgets/seasons.dart +++ b/lib/pages/member/widgets/seasons.dart @@ -35,7 +35,8 @@ class MemberSeasonsPanel extends StatelessWidget { 'seasonName': item.meta!.name!, }; } - if (category == 1) { + // 2为直播回放 + if (category == 1 || category == 2) { parameters = { 'category': '1', 'mid': item.meta!.mid.toString(), From fc22834e4a66e4d3fd803c3ec0455f13203bfcde Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 23 Aug 2024 23:47:15 +0800 Subject: [PATCH 19/22] =?UTF-8?q?mod:=20=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/danmaku/view.dart | 2 +- lib/pages/live_room/controller.dart | 12 ++- lib/pages/live_room/view.dart | 146 ++++++++++++++++++---------- 3 files changed, 105 insertions(+), 55 deletions(-) diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index e669b881..3cf1ed8a 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -129,7 +129,7 @@ class _PlDanmakuState extends State { // double initDuration = box.maxWidth / 12; return Obx( () => AnimatedOpacity( - opacity: playerController.isOpenDanmu.value ? 1 : 1, + opacity: playerController.isOpenDanmu.value ? 1 : 0, duration: const Duration(milliseconds: 100), child: DanmakuView( createdController: (DanmakuController e) async { diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index a56a5459..99025dce 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -45,6 +45,8 @@ class LiveRoomController extends GetxController { TextEditingController inputController = TextEditingController(); // 加入直播间提示 RxMap joinRoomTip = {'userName': '', 'message': ''}.obs; + // 直播间弹幕开关 默认打开 + RxBool danmakuSwitch = true.obs; @override void onInit() { @@ -69,6 +71,9 @@ class LiveRoomController extends GetxController { userId = userInfo.mid; } liveDanmakuInfo().then((value) => initSocket()); + danmakuSwitch.listen((p0) { + plPlayerController.isOpenDanmu.value = p0; + }); } playerInit(source) async { @@ -87,6 +92,7 @@ class LiveRoomController extends GetxController { enableHA: true, autoplay: true, ); + plPlayerController.isOpenDanmu.value = danmakuSwitch.value; } Future queryLiveInfo() async { @@ -185,6 +191,7 @@ class LiveRoomController extends GetxController { } 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) { @@ -200,6 +207,7 @@ class LiveRoomController extends GetxController { timer.cancel(); } }); + return; } // 过滤出聊天消息 @@ -223,7 +231,9 @@ class LiveRoomController extends GetxController { }).toList(); // 添加到 danmakuController - danmakuController?.addItems(danmakuItems); + if (danmakuSwitch.value) { + danmakuController?.addItems(danmakuItems); + } } }, onErrorCb: (e) { diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 3f563ad6..abbcce13 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -325,7 +325,33 @@ class _LiveRoomPageState extends State ), child: Row( children: [ - const SizedBox(width: 4), + SizedBox( + width: 34, + height: 34, + child: Obx( + () => IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: MaterialStateProperty.resolveWith( + (Set 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, @@ -340,11 +366,19 @@ class _LiveRoomPageState extends State ), ), ), - IconButton( - onPressed: () => _liveRoomController.sendMsg(), - icon: const Icon( - Icons.send, - color: Colors.white, + 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, + ), ), ), ], @@ -418,56 +452,62 @@ Widget buildMessageListUI( ).createShader(bounds); }, blendMode: BlendMode.dstIn, - 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), + 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'); + }, ), - recognizer: TapGestureRecognizer() - ..onTap = () { - // 处理点击事件 - print('Text clicked'); - }, - ), - TextSpan( - children: [ - ...buildMessageTextSpan(context, liveMsgItem) - ], - // text: liveMsgItem.message, - ), - ], + TextSpan( + children: [ + ...buildMessageTextSpan(context, liveMsgItem) + ], + // text: liveMsgItem.message, + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), From 8810a74ebfcfa4cc9e52226089f0e6c79a70a399 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 24 Aug 2024 23:33:19 +0800 Subject: [PATCH 20/22] =?UTF-8?q?fix:=20=E6=9C=AA=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E6=97=B6=E8=BF=94=E5=9B=9E=E5=BD=93=E5=89=8D=E9=A1=B5=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/view.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 0388e962..91c5ae00 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -63,7 +63,7 @@ class _VideoDetailPageState extends State late bool autoPlayEnable; late bool autoPiP; late Floating floating; - bool isShowing = true; + RxBool isShowing = true.obs; // 生命周期监听 late final AppLifecycleListener _lifecycleListener; late double statusHeight; @@ -183,6 +183,7 @@ class _VideoDetailPageState extends State plPlayerController!.addStatusLister(playerListener); vdCtr.autoPlay.value = true; vdCtr.isShowCover.value = false; + isShowing.value = true; autoEnterPip(status: PlayerStatus.playing); } @@ -258,7 +259,7 @@ class _VideoDetailPageState extends State plPlayerController!.pause(); vdCtr.clearSubtitleContent(); } - setState(() => isShowing = false); + isShowing.value = false; super.didPushNext(); } @@ -272,10 +273,8 @@ class _VideoDetailPageState extends State if (plPlayerController != null && plPlayerController!.videoPlayerController != null) { - setState(() { - vdCtr.setSubtitleContent(); - isShowing = true; - }); + vdCtr.setSubtitleContent(); + isShowing.value = true; } vdCtr.isFirstTime = false; final bool autoplay = autoPlayEnable; @@ -652,7 +651,11 @@ class _VideoDetailPageState extends State tag: heroTag, child: Stack( children: [ - if (isShowing) buildVideoPlayerPanel(), + Obx( + () => isShowing.value + ? buildVideoPlayerPanel() + : const SizedBox(), + ), /// 关闭自动播放时 手动播放 Obx( From 8fd3bfae5f9a88a8ce72670b2499f035e51b4f0e Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 25 Aug 2024 22:07:42 +0800 Subject: [PATCH 21/22] =?UTF-8?q?fix:=20=E7=B3=BB=E7=BB=9F=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=A0=87=E8=AE=B0=E5=B7=B2=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 ++++ lib/http/msg.dart | 20 +++++++++++++++++++- lib/pages/message/system/controller.dart | 14 +++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index 31e5a38b..d9286e47 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -554,4 +554,8 @@ class Api { /// 系统通知 static const String messageSystemAPi = '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify'; + + /// 系统通知标记已读 + static const String systemMarkRead = + '${HttpString.messageBaseUrl}/x/sys-msg/update_cursor'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 86789fd1..2de9cd49 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -298,7 +298,6 @@ class MsgHttp { }); if (res.data['code'] == 0) { try { - print(res.data['data']['system_notify_list']); return { 'status': true, 'data': res.data['data']['system_notify_list'] @@ -312,4 +311,23 @@ class MsgHttp { 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'], + }; + } + } } diff --git a/lib/pages/message/system/controller.dart b/lib/pages/message/system/controller.dart index bf31f6bc..f63a659a 100644 --- a/lib/pages/message/system/controller.dart +++ b/lib/pages/message/system/controller.dart @@ -8,8 +8,20 @@ class MessageSystemController extends GetxController { Future queryMessageSystem({String type = 'init'}) async { var res = await MsgHttp.messageSystem(); if (res['status']) { - systemItems.addAll(res['data']); + if (type == 'init') { + systemItems.value = res['data']; + } else { + systemItems.addAll(res['data']); + } + if (systemItems.isNotEmpty) { + systemMarkRead(systemItems.first.cursor!); + } } return res; } + + // 标记已读 + void systemMarkRead(int cursor) async { + await MsgHttp.systemMarkRead(cursor); + } } From f598b6adadf00c0d6f2f7a3d0e088f9faabd952d Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 26 Aug 2024 23:14:37 +0800 Subject: [PATCH 22/22] opt: liveRoom getBuvid --- lib/http/init.dart | 27 +++++++++++++++++++++++++-- lib/pages/live_room/controller.dart | 4 +++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/http/init.dart b/lib/http/init.dart index faa57dd5..6a90a87d 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -29,7 +29,7 @@ class Request { late String systemProxyPort; static final RegExp spmPrefixExp = RegExp(r''); - static late String buvid; + static String? buvid; /// 设置cookie static setCookie() async { @@ -72,7 +72,6 @@ class Request { .map((Cookie cookie) => '${cookie.name}=${cookie.value}') .join('; '); - buvid = cookie.firstWhere((e) => e.name == 'buvid3').value; dio.options.headers['cookie'] = cookieString; } @@ -87,6 +86,30 @@ class Request { return token; } + static Future getBuvid() async { + if (buvid != null) { + return buvid!; + } + + final List 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) { if (status) { dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 99025dce..5d4e2b67 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -47,6 +47,7 @@ class LiveRoomController extends GetxController { RxMap joinRoomTip = {'userName': '', 'message': ''}.obs; // 直播间弹幕开关 默认打开 RxBool danmakuSwitch = true.obs; + late String buvid; @override void onInit() { @@ -63,6 +64,7 @@ class LiveRoomController extends GetxController { if (liveItem != null && liveItem.cover != null && liveItem.cover != '') { cover = liveItem.cover; } + Request.getBuvid().then((value) => buvid = value); } // CDN优化 enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true); @@ -249,7 +251,7 @@ class LiveRoomController extends GetxController { "uid": userId, "roomid": roomId, "protover": 3, - "buvid": Request.buvid, + "buvid": buvid, "platform": "web", "type": 2, "key": token,