Compare commits

...

45 Commits

Author SHA1 Message Date
5bf7b69d79 feat: 收藏搜索结果删除 2024-02-16 09:33:59 +08:00
d57f84a1d7 fix: 路由跳转传参丢失 2024-02-15 21:59:28 +08:00
32b2f0ceff Merge pull request #539 from orz12/fix-speed-dialog-cannot-dismiss
fix: 播放速度dialog无法关闭
2024-02-15 21:10:52 +08:00
bae871cfa1 Merge branch 'feature-replyItem' 2024-02-15 21:07:55 +08:00
d95fe9fe14 mod: MorePanel样式 2024-02-15 21:07:23 +08:00
eb006e4c55 Merge branch 'feature-replyItem' 2024-02-14 20:09:48 +08:00
cb88d0c9ae Merge branch 'feature-liveRoomRender' 2024-02-14 20:09:40 +08:00
3efad736ae fix: 直播闪退 issues #540 2024-02-14 19:38:55 +08:00
42ad959155 fix: 速度设置无法取消 2024-02-14 08:44:00 +08:00
cdf800c49f mod: 评论复制逻辑 issues #420 #331 #297 #152 2024-02-13 23:33:51 +08:00
569277572a Merge pull request #536 from KoolShow/fix_seekto_regexp
fix: 含有小时的时间无法跳转
2024-02-12 18:11:26 +08:00
19b84571c1 Merge branch 'main' into fix_seekto_regexp 2024-02-12 18:11:15 +08:00
0812b8339e Merge branch 'feature-replyItem' 2024-02-12 17:55:08 +08:00
b817a0c807 修正正则表达式以匹配含小时的时间 2024-02-12 17:20:18 +08:00
3da70d7e27 Merge branch 'fix-replyRepeat' 2024-02-12 16:55:56 +08:00
5e59db85be fix: 评论笔记跳转 issues #472 2024-02-12 16:51:05 +08:00
89026e671c mod: 收藏视频相关 issues #51 #534 2024-02-12 10:07:29 +08:00
1c8e7e53a5 Merge branch 'fix-replyRepeat' 2024-02-11 23:20:11 +08:00
b264427be6 fix: 切换合集评论不刷新 issues #326 #525 2024-02-11 23:07:44 +08:00
d5134f972d Merge branch 'feature-liveRoomRender' 2024-02-11 18:48:24 +08:00
e2fd01a6d5 fix: video Storage初始化 2024-02-11 18:47:51 +08:00
289cc99bc2 mod 2024-02-11 09:10:45 +08:00
3d5ebe7e99 fix: 视频详情页评论重复请求 2024-02-10 19:57:10 +08:00
d9964d37a4 Merge branch 'fix-favBangumiPushError' 2024-02-10 19:24:54 +08:00
5da39a9c52 Merge branch 'feature-cacheManage' 2024-02-10 19:22:49 +08:00
44a162762c fix: 评论页面路由跳转 issues #405 2024-02-09 23:24:26 +08:00
d0f036ec35 fix: 评论回复多张图片拉伸 2024-02-09 09:32:28 +08:00
10b928474b mod 2024-02-08 22:46:39 +08:00
94f3b7c1e4 fix: minePage 路由跳转 2024-02-08 21:33:02 +08:00
fb8b2de115 feat: up搜索 2024-02-08 21:27:22 +08:00
0d5d33a365 feat: up投稿排序 2024-02-08 10:29:26 +08:00
c39e91073b feat: 应用内缓存清理 2024-02-07 22:57:30 +08:00
d258474a5a mod: 直播页面内容更新 2024-02-07 22:23:29 +08:00
083739e562 mod: 收藏卡片内容修改 2024-02-06 11:13:33 +08:00
71ccb9c0e5 fix: 收藏国创跳转异常 2024-02-06 11:01:36 +08:00
4a5f4ca2ca fix: 限时免费无法播放 issues #457 2024-02-06 00:14:46 +08:00
78ade4a193 mod: 移除评论按【最多回复】排序 issues #298 2024-02-05 23:41:40 +08:00
ae14653e72 Merge pull request #434 from orz12/mod-not-login-recommend2
mod: 推荐功能增强,新增模拟未登录和过滤器
2024-02-05 00:35:46 +08:00
01ac2c13e1 Merge branch 'main' into mod-not-login-recommend2 2024-02-05 00:35:11 +08:00
9e471b83d9 mod: cancel Get.snackbar 2024-02-05 00:19:03 +08:00
a560d66567 mod: rcmd FutureBuilder 2024-02-04 23:03:24 +08:00
80b39daaff mod: jumpUrl增加icon显示 issues #471 2024-02-04 22:06:45 +08:00
b0d8f5d0b6 Merge branch 'main' into pr/434 2024-01-27 10:28:55 +08:00
9122dd7f3a mod: 新增推荐过滤器,回退model转换修改,移除不必要的futureBuilder 2024-01-20 17:07:10 +08:00
41ddeab41a 新增模拟未登录推荐,独立推荐设置,新增accesskey风控警告,统一推荐逻辑 2024-01-20 15:14:52 +08:00
54 changed files with 1832 additions and 667 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -324,8 +324,9 @@ class VideoContent extends StatelessWidget {
reSrc: 11, reSrc: 11,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast( SmartDialog.showToast(res['code'] == 0
res['msg'] ?? '成功'); ? '成功'
: res['msg']);
}, },
child: const Text('确认'), child: const Text('确认'),
) )

View File

@ -158,12 +158,12 @@ class VideoCardV extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
if (videoItem.duration != null) if (videoItem.duration > 0)
if (crossAxisCount == 1) ...[ if (crossAxisCount == 1) ...[
PBadge( PBadge(
bottom: 10, bottom: 10,
right: 10, right: 10,
text: videoItem.duration, text: Utils.timeFormat(videoItem.duration),
) )
] else ...[ ] else ...[
PBadge( PBadge(
@ -171,7 +171,7 @@ class VideoCardV extends StatelessWidget {
right: 7, right: 7,
size: 'small', size: 'small',
type: 'gray', type: 'gray',
text: videoItem.duration, text: Utils.timeFormat(videoItem.duration),
) )
], ],
], ],
@ -331,10 +331,8 @@ class VideoStat extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
children: [ children: [
if (videoItem.stat.view != '-') TextSpan(text: '${Utils.numFormat(videoItem.stat.view)}观看'),
TextSpan(text: '${videoItem.stat.view}观看'), TextSpan(text: '${Utils.numFormat(videoItem.stat.danmu)}弹幕'),
if (videoItem.stat.danmu != '-')
TextSpan(text: '${videoItem.stat.danmu}弹幕'),
], ],
), ),
); );

View File

@ -214,6 +214,9 @@ class Api {
// https://api.bilibili.com/x/relation/tags // https://api.bilibili.com/x/relation/tags
static const String followingsClass = '/x/relation/tags'; static const String followingsClass = '/x/relation/tags';
// 搜索follow
static const followSearch = '/x/relation/followings/search';
// 粉丝 // 粉丝
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc // vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention // order_type 排序规则 最近访问传空,最常访问传 attention
@ -230,6 +233,10 @@ class Api {
static const String liveRoomInfo = static const String liveRoomInfo =
'${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo'; '${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo';
// 直播间详情 H5
static const String liveRoomInfoH5 =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getH5InfoByRoom';
// 用户信息 需要Wbi签名 // 用户信息 需要Wbi签名
// https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482 // https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482
static const String memberInfo = '/x/space/wbi/acc/info'; static const String memberInfo = '/x/space/wbi/acc/info';

View File

@ -1,5 +1,6 @@
import '../models/live/item.dart'; import '../models/live/item.dart';
import '../models/live/room_info.dart'; import '../models/live/room_info.dart';
import '../models/live/room_info_h5.dart';
import 'api.dart'; import 'api.dart';
import 'init.dart'; import 'init.dart';
@ -46,4 +47,22 @@ class LiveHttp {
}; };
} }
} }
static Future liveRoomInfoH5({roomId, qn}) async {
var res = await Request().get(Api.liveRoomInfoH5, data: {
'room_id': roomId,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': RoomInfoH5Model.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -461,4 +461,41 @@ class MemberHttp {
}; };
} }
} }
// 搜索follow
static Future getfollowSearch({
required int mid,
required int ps,
required int pn,
required String name,
}) async {
Map<String, dynamic> data = {
'vmid': mid,
'pn': pn,
'ps': ps,
'order': 'desc',
'order_type': 'attention',
'gaia_source': 'main_web',
'name': name,
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
var res = await Request().get(Api.followSearch, data: {
...data,
'w_rid': params['w_rid'],
'wts': params['wts'],
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': FollowDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -9,6 +9,7 @@ import '../models/user/fav_folder.dart';
import '../models/video/ai.dart'; import '../models/video/ai.dart';
import '../models/video/play/url.dart'; import '../models/video/play/url.dart';
import '../models/video_detail_res.dart'; import '../models/video_detail_res.dart';
import '../utils/recommend_filter.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
import 'api.dart'; import 'api.dart';
@ -46,8 +47,13 @@ class VideoHttp {
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['item']) { for (var i in res.data['data']['item']) {
//过滤掉live与ad以及拉黑用户 //过滤掉live与ad以及拉黑用户
if (i['goto'] == 'av' && !blackMidsList.contains(i['owner']['mid'])) { if (i['goto'] == 'av' &&
list.add(RecVideoItemModel.fromJson(i)); (i['owner'] != null &&
!blackMidsList.contains(i['owner']['mid']))) {
RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
} }
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
@ -59,7 +65,9 @@ class VideoHttp {
} }
} }
static Future rcmdVideoListApp({int? ps, required int freshIdx}) async { // 添加额外的loginState变量模拟未登录状态
static Future rcmdVideoListApp(
{bool loginStatus = true, required int freshIdx}) async {
try { try {
var res = await Request().get( var res = await Request().get(
Api.recommendListApp, Api.recommendListApp,
@ -72,9 +80,11 @@ class VideoHttp {
'device_name': 'vivo', 'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false', 'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey, 'appkey': Constants.appKey,
'access_key': localCache 'access_key': loginStatus
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ?? ? (localCache.get(LocalCacheKey.accessKey,
'' defaultValue: {})['value'] ??
'')
: ''
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@ -87,12 +97,15 @@ class VideoHttp {
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null && (i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) { !blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i)); RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
} }
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
return {'status': false, 'data': [], 'msg': ''}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} catch (err) { } catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()}; return {'status': false, 'data': [], 'msg': err.toString()};
@ -203,7 +216,10 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<HotVideoItemModel> list = []; List<HotVideoItemModel> list = [];
for (var i in res.data['data']) { for (var i in res.data['data']) {
list.add(HotVideoItemModel.fromJson(i)); HotVideoItemModel videoItem = HotVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem, relatedVideos: true)) {
list.add(videoItem);
}
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
@ -306,7 +322,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'data': []}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} }

View File

@ -21,6 +21,7 @@ import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc. import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart'; import './services/loggeer.dart';
@ -34,6 +35,7 @@ void main() async {
await setupServiceLocator(); await setupServiceLocator();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
RecommendFilter();
// 异常捕获 logo记录 // 异常捕获 logo记录
final Catcher2Options debugConfig = Catcher2Options( final Catcher2Options debugConfig = Catcher2Options(
@ -68,7 +70,6 @@ void main() async {
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
)); ));
Data.init(); Data.init();
GStrorage.lazyInit();
PiliSchame.init(); PiliSchame.init();
}); });
} }

View File

@ -1,7 +1,7 @@
// 首页推荐类型 // 首页推荐类型
enum RcmdType { web, app } enum RcmdType { web, app, notLogin }
extension RcmdTypeExtension on RcmdType { extension RcmdTypeExtension on RcmdType {
String get values => ['web', 'app'][index]; String get values => ['web', 'app', 'notLogin'][index];
String get labels => ['web端', 'app端'][index]; String get labels => ['web端', 'app端', '游客模式'][index];
} }

View File

@ -1,6 +1,6 @@
enum ReplySortType { time, like, reply } enum ReplySortType { time, like }
extension ReplySortTypeExtension on ReplySortType { extension ReplySortTypeExtension on ReplySortType {
String get titles => ['最新评论', '最热评论', '回复最多'][index]; String get titles => ['最新评论', '最热评论'][index];
String get labels => ['最新', '最热', '最多回复'][index]; String get labels => ['最新', '最热'][index];
} }

View File

@ -28,7 +28,7 @@ class RecVideoItemAppModel {
int? cid; int? cid;
String? pic; String? pic;
RcmdStat? stat; RcmdStat? stat;
String? duration; int? duration;
String? title; String? title;
int? isFollowed; int? isFollowed;
RcmdOwner? owner; RcmdOwner? owner;
@ -54,13 +54,27 @@ class RecVideoItemAppModel {
cid = json['player_args'] != null ? json['player_args']['cid'] : -1; cid = json['player_args'] != null ? json['player_args']['cid'] : -1;
pic = json['cover']; pic = json['cover'];
stat = RcmdStat.fromJson(json); stat = RcmdStat.fromJson(json);
duration = json['cover_right_text']; // 改用player_args中的duration作为原始数据秒数
duration = json['player_args'] != null
? json['player_args']['duration']
: -1;
//duration = json['cover_right_text'];
title = json['title']; title = json['title'];
isFollowed = 0;
owner = RcmdOwner.fromJson(json); owner = RcmdOwner.fromJson(json);
rcmdReason = json['rcmd_reason_style'] != null rcmdReason = json['rcmd_reason_style'] != null
? RcmdReason.fromJson(json['rcmd_reason_style']) ? RcmdReason.fromJson(json['rcmd_reason_style'])
: null; : null;
// 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
isFollowed = rcmdReason != null &&
rcmdReason!.content != null &&
rcmdReason!.content!.contains('关注')
? 1
: 0;
// 如果是就无需再显示推荐原因交由view统一处理即可
if (isFollowed == 1) {
rcmdReason = null;
}
goto = json['goto']; goto = json['goto'];
param = int.parse(json['param']); param = int.parse(json['param']);
uri = json['uri']; uri = json['uri'];

View File

@ -0,0 +1,130 @@
class RoomInfoH5Model {
RoomInfoH5Model({
this.roomInfo,
this.anchorInfo,
this.isRoomFeed,
this.watchedShow,
this.likeInfoV3,
this.blockInfo,
});
RoomInfo? roomInfo;
AnchorInfo? anchorInfo;
int? isRoomFeed;
Map? watchedShow;
LikeInfoV3? likeInfoV3;
Map? blockInfo;
RoomInfoH5Model.fromJson(Map<String, dynamic> json) {
roomInfo = RoomInfo.fromJson(json['room_info']);
anchorInfo = AnchorInfo.fromJson(json['anchor_info']);
isRoomFeed = json['is_room_feed'];
watchedShow = json['watched_show'];
likeInfoV3 = LikeInfoV3.fromJson(json['like_info_v3']);
blockInfo = json['block_info'];
}
}
class RoomInfo {
RoomInfo({
this.uid,
this.roomId,
this.title,
this.cover,
this.description,
this.liveStatus,
this.liveStartTime,
this.areaId,
this.areaName,
this.parentAreaId,
this.parentAreaName,
this.online,
this.background,
this.appBackground,
this.liveId,
});
int? uid;
int? roomId;
String? title;
String? cover;
String? description;
int? liveStatus;
int? liveStartTime;
int? areaId;
String? areaName;
int? parentAreaId;
String? parentAreaName;
int? online;
String? background;
String? appBackground;
String? liveId;
RoomInfo.fromJson(Map<String, dynamic> json) {
uid = json['uid'];
roomId = json['room_id'];
title = json['title'];
cover = json['cover'];
description = json['description'];
liveStatus = json['liveS_satus'];
liveStartTime = json['live_start_time'];
areaId = json['area_id'];
areaName = json['area_name'];
parentAreaId = json['parent_area_id'];
parentAreaName = json['parent_area_name'];
online = json['online'];
background = json['background'];
appBackground = json['app_background'];
liveId = json['live_id'];
}
}
class AnchorInfo {
AnchorInfo({
this.baseInfo,
this.relationInfo,
});
BaseInfo? baseInfo;
RelationInfo? relationInfo;
AnchorInfo.fromJson(Map<String, dynamic> json) {
baseInfo = BaseInfo.fromJson(json['base_info']);
relationInfo = RelationInfo.fromJson(json['relation_info']);
}
}
class BaseInfo {
BaseInfo({
this.uname,
this.face,
});
String? uname;
String? face;
BaseInfo.fromJson(Map<String, dynamic> json) {
uname = json['uname'];
face = json['face'];
}
}
class RelationInfo {
RelationInfo({this.attention});
int? attention;
RelationInfo.fromJson(Map<String, dynamic> json) {
attention = json['attention'];
}
}
class LikeInfoV3 {
LikeInfoV3({this.totalLikes});
int? totalLikes;
LikeInfoV3.fromJson(Map<String, dynamic> json) {
totalLikes = json['total_likes'];
}
}

View File

@ -1,5 +1,3 @@
import 'package:pilipala/utils/utils.dart';
import './model_owner.dart'; import './model_owner.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -38,7 +36,7 @@ class RecVideoItemModel {
@HiveField(6) @HiveField(6)
String? title = ''; String? title = '';
@HiveField(7) @HiveField(7)
String? duration = ''; int? duration = -1;
@HiveField(8) @HiveField(8)
int? pubdate = -1; int? pubdate = -1;
@HiveField(9) @HiveField(9)
@ -58,7 +56,7 @@ class RecVideoItemModel {
uri = json["uri"]; uri = json["uri"];
pic = json["pic"]; pic = json["pic"];
title = json["title"]; title = json["title"];
duration = Utils.tampToSeektime(json["duration"]); duration = json["duration"];
pubdate = json["pubdate"]; pubdate = json["pubdate"];
owner = Owner.fromJson(json["owner"]); owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]); stat = Stat.fromJson(json["stat"]);
@ -77,14 +75,15 @@ class Stat {
this.danmu, this.danmu,
}); });
@HiveField(0) @HiveField(0)
String? view; int? view;
@HiveField(1) @HiveField(1)
int? like; int? like;
@HiveField(2) @HiveField(2)
int? danmu; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = Utils.numFormat(json["view"]); // 无需在model中转换以保留原始数据在view层处理即可
view = json["view"];
like = json["like"]; like = json["like"];
danmu = json['danmaku']; danmu = json['danmaku'];
} }

View File

@ -24,7 +24,7 @@ class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
uri: fields[4] as String?, uri: fields[4] as String?,
pic: fields[5] as String?, pic: fields[5] as String?,
title: fields[6] as String?, title: fields[6] as String?,
duration: fields[7] as String?, duration: fields[7] as int?,
pubdate: fields[8] as int?, pubdate: fields[8] as int?,
owner: fields[9] as Owner?, owner: fields[9] as Owner?,
stat: fields[10] as Stat?, stat: fields[10] as Stat?,
@ -87,7 +87,7 @@ class StatAdapter extends TypeAdapter<Stat> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return Stat( return Stat(
view: fields[0] as String?, view: fields[0] as int?,
like: fields[1] as int?, like: fields[1] as int?,
danmu: fields[2] as int?, danmu: fields[2] as int?,
); );

View File

@ -7,6 +7,7 @@ import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart'; import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart';
class AboutPage extends StatefulWidget { class AboutPage extends StatefulWidget {
const AboutPage({super.key}); const AboutPage({super.key});
@ -17,6 +18,19 @@ class AboutPage extends StatefulWidget {
class _AboutPageState extends State<AboutPage> { class _AboutPageState extends State<AboutPage> {
final AboutController _aboutController = Get.put(AboutController()); final AboutController _aboutController = Get.put(AboutController());
String cacheSize = '';
@override
void initState() {
super.initState();
// 读取缓存占用
getCacheSize();
}
Future<void> getCacheSize() async {
final res = await CacheManage().loadApplicationCache();
setState(() => cacheSize = res);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -138,6 +152,17 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('错误日志'), title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
), ),
ListTile(
onTap: () async {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
}
},
title: const Text('清除缓存'),
subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
], ],
), ),
), ),

View File

@ -151,7 +151,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
if (item.badge != null && vipStatus != 1) { if (item.badge != null && item.badge == '会员' && vipStatus != 1) {
SmartDialog.showToast('需要大会员'); SmartDialog.showToast('需要大会员');
return; return;
} }
@ -255,11 +255,24 @@ class _BangumiPanelState extends State<BangumiPanel> {
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
if (widget.pages[i].badge != null) ...[ if (widget.pages[i].badge != null) ...[
Image.asset( if (widget.pages[i].badge == '会员') ...[
'assets/images/big-vip.png', Image.asset(
height: 16, 'assets/images/big-vip.png',
), height: 16,
], ),
],
if (widget.pages[i].badge != '会员') ...[
const Spacer(),
Text(
widget.pages[i].badge!,
style: TextStyle(
fontSize: 11,
color:
Theme.of(context).colorScheme.primary,
),
),
],
]
], ],
), ),
const SizedBox(height: 3), const SizedBox(height: 3),

View File

@ -37,6 +37,10 @@ class DynamicDetailController extends GetxController {
} }
int deaultReplySortIndex = int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0); setting.get(SettingBoxKey.replySortType, defaultValue: 0);
if (deaultReplySortIndex == 2) {
setting.put(SettingBoxKey.replySortType, 0);
deaultReplySortIndex = 0;
}
_sortType = ReplySortType.values[deaultReplySortIndex]; _sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels; sortTypeLabel.value = _sortType.labels;
@ -92,9 +96,6 @@ class DynamicDetailController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@ -24,11 +24,13 @@ class _FavDetailPageState extends State<FavDetailPage> {
Get.put(FavDetailController()); Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC; // a
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late String mediaId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
@ -94,8 +96,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () =>
'/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'), Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
// IconButton( // IconButton(

View File

@ -9,14 +9,20 @@ import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
// 收藏视频卡片 - 水平布局 // 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget { class FavVideoCardH extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
final int? searchType;
const FavVideoCardH({Key? key, required this.videoItem, this.callFn}) const FavVideoCardH({
: super(key: key); Key? key,
required this.videoItem,
this.callFn,
this.searchType,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,7 +33,9 @@ class FavVideoCardH extends StatelessWidget {
onTap: () async { onTap: () async {
// int? seasonId; // int? seasonId;
String? epId; String? epId;
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') { if (videoItem.ogv != null &&
(videoItem.ogv['type_name'] == '番剧' ||
videoItem.ogv['type_name'] == '国创')) {
videoItem.cid = await SearchHttp.ab2c(bvid: bvid); videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
// seasonId = videoItem.ogv['season_id']; // seasonId = videoItem.ogv['season_id'];
epId = videoItem.epId; epId = videoItem.epId;
@ -84,28 +92,31 @@ class FavVideoCardH extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
Positioned( PBadge(
right: 4, text: Utils.timeFormat(videoItem.duration!),
bottom: 4, right: 6.0,
child: Container( bottom: 6.0,
padding: const EdgeInsets.symmetric( type: 'gray',
vertical: 1, horizontal: 6), ),
decoration: BoxDecoration( if (videoItem.ogv != null) ...[
borderRadius: BorderRadius.circular(4), PBadge(
color: Colors.black54.withOpacity(0.4)), text: videoItem.ogv['type_name'],
child: Text( top: 6.0,
Utils.timeFormat(videoItem.duration!), right: 6.0,
style: const TextStyle( bottom: null,
fontSize: 11, color: Colors.white), left: null,
),
), ),
) ],
], ],
); );
}, },
), ),
), ),
VideoContent(videoItem: videoItem, callFn: callFn) VideoContent(
videoItem: videoItem,
callFn: callFn,
searchType: searchType,
)
], ],
), ),
); );
@ -121,93 +132,123 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn}); final int? searchType;
const VideoContent({
super.key,
required this.videoItem,
this.callFn,
this.searchType,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Column(
videoItem.title, crossAxisAlignment: CrossAxisAlignment.start,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(videoItem.ctime!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row(
children: [ children: [
StatView( Text(
theme: 'gray', videoItem.title,
view: videoItem.cntInfo['play'], textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(width: 8), if (videoItem.ogv != null) ...[
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']), Text(
const Spacer(), videoItem.intro,
SizedBox( style: TextStyle(
width: 26, fontSize:
height: 26, Theme.of(context).textTheme.labelMedium!.fontSize,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 18,
), ),
), ),
],
const Spacer(),
Text(
Utils.dateFormat(videoItem.favTime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.owner.name != '') ...[
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
],
),
), ),
], ],
), ),
searchType != 1
? Positioned(
right: 0,
bottom: -4,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
)
: const SizedBox(),
], ],
), ),
), ),

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_detail.dart';
import '../../http/video.dart';
class FavSearchController extends GetxController { class FavSearchController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs; Rx<TextEditingController> controller = TextEditingController().obs;
@ -72,4 +75,21 @@ class FavSearchController extends GetxController {
if (!hasMore) return; if (!hasMore) return;
searchFav(type: 'onLoad'); searchFav(type: 'onLoad');
} }
onCancelFav(int id) async {
var result = await VideoHttp.favVideo(
aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) {
if (result['data']['prompt']) {
List dataList = favList;
for (var i in dataList) {
if (i.id == id) {
dataList.remove(i);
break;
}
}
SmartDialog.showToast('取消收藏');
}
}
}
} }

View File

@ -8,9 +8,7 @@ import 'package:pilipala/pages/fav_detail/widget/fav_video_card.dart';
import 'controller.dart'; import 'controller.dart';
class FavSearchPage extends StatefulWidget { class FavSearchPage extends StatefulWidget {
final int? sourceType; const FavSearchPage({super.key});
final int? mediaId;
const FavSearchPage({super.key, this.sourceType, this.mediaId});
@override @override
State<FavSearchPage> createState() => _FavSearchPageState(); State<FavSearchPage> createState() => _FavSearchPageState();
@ -19,11 +17,12 @@ class FavSearchPage extends StatefulWidget {
class _FavSearchPageState extends State<FavSearchPage> { class _FavSearchPageState extends State<FavSearchPage> {
final FavSearchController _favSearchCtr = Get.put(FavSearchController()); final FavSearchController _favSearchCtr = Get.put(FavSearchController());
late ScrollController scrollController; late ScrollController scrollController;
late int searchType;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
searchType = int.parse(Get.parameters['searchType']!);
scrollController = _favSearchCtr.scrollController; scrollController = _favSearchCtr.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@ -100,7 +99,11 @@ class _FavSearchPageState extends State<FavSearchPage> {
} else { } else {
return FavVideoCardH( return FavVideoCardH(
videoItem: _favSearchCtr.favList[index], videoItem: _favSearchCtr.favList[index],
callFn: () => null, searchType: searchType,
callFn: () => searchType != 1
? _favSearchCtr
.onCancelFav(_favSearchCtr.favList[index].id!)
: {},
); );
} }
}, },

View File

@ -37,6 +37,29 @@ class _FollowPageState extends State<FollowPage> {
: '${_followController.name}的关注', : '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [
IconButton(
onPressed: () => Get.toNamed('/followSearch?mid=$mid'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => Get.toNamed('/blackListPage'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.block, size: 19),
SizedBox(width: 10),
Text('黑名单管理'),
],
),
)
],
),
const SizedBox(width: 6),
],
), ),
body: Obx( body: Obx(
() => !_followController.isOwner.value () => !_followController.isOwner.value
@ -87,3 +110,22 @@ class _FollowPageState extends State<FollowPage> {
); );
} }
} }
class _FakeAPI {
static const List<String> _kOptions = <String>[
'aardvark',
'bobcat',
'chameleon',
];
// Searches the options, but injects a fake "network" delay.
static Future<Iterable<String>> search(String query) async {
await Future<void>.delayed(
const Duration(seconds: 1)); // Fake 1 second delay.
if (query == '') {
return const Iterable<String>.empty();
}
return _kOptions.where((String option) {
return option.contains(query.toLowerCase());
});
}
}

View File

@ -42,7 +42,7 @@ class FollowItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
trailing: ctr!.isOwner.value trailing: ctr != null && ctr!.isOwner.value
? SizedBox( ? SizedBox(
height: 34, height: 34,
child: TextButton( child: TextButton(

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import '../../models/follow/result.dart';
class FollowSearchController extends GetxController {
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
late int mid = 1;
RxString uname = ''.obs;
int ps = 20;
int pn = 1;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
RxInt total = 0.obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
// 清空搜索
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
} else {
Get.back();
}
}
void onChange(value) {
searchKeyWord.value = value;
}
// 提交搜索内容
void submit() {
loadingStatus.value = 'loading';
searchFollow();
}
Future searchFollow({type = 'init'}) async {
if (controller.value.text == '') {
return {'status': true, 'data': <FollowItemModel>[].obs};
}
if (type == 'init') {
ps = 1;
}
var res = await MemberHttp.getfollowSearch(
mid: mid,
ps: ps,
pn: pn,
name: controller.value.text,
);
if (res['status']) {
if (type == 'init') {
followList.value = res['data'].list;
} else {
followList.addAll(res['data'].list);
}
total.value = res['data'].total;
}
return res;
}
void onLoad() {
searchFollow(type: 'onLoad');
}
}

View File

@ -0,0 +1,4 @@
library follow_search;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,121 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/follow_search/index.dart';
import '../follow/widgets/follow_item.dart';
class FollowSearchPage extends StatefulWidget {
const FollowSearchPage({super.key});
@override
State<FollowSearchPage> createState() => _FollowSearchPageState();
}
class _FollowSearchPageState extends State<FollowSearchPage> {
final FollowSearchController _followSearchController =
Get.put(FollowSearchController());
late Future? _futureBuilder;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilder = _followSearchController.searchFollow();
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () {
_followSearchController.onLoad();
});
}
},
);
}
void reRequest() {
setState(() {
_futureBuilder = _followSearchController.searchFollow();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: reRequest,
icon: const Icon(CupertinoIcons.search, size: 22),
),
const SizedBox(width: 6),
],
title: TextField(
autofocus: true,
focusNode: _followSearchController.searchFocusNode,
controller: _followSearchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _followSearchController.onChange(value),
decoration: InputDecoration(
hintText: _followSearchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _followSearchController.onClear(),
),
),
onSubmitted: (String value) => reRequest(),
),
),
body: FutureBuilder(
future: _futureBuilder,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data == null) {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
if (data['status']) {
RxList followList = _followSearchController.followList;
return Obx(
() => followList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: followList.length,
itemBuilder: ((context, index) {
return FollowItem(
item: followList[index],
);
}),
)
: CustomScrollView(
slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
} else {
return const SizedBox();
}
}),
);
}
}

View File

@ -96,9 +96,6 @@ class HtmlRenderController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@ -184,18 +184,32 @@ class VideoStat extends StatelessWidget {
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
), ),
), ),
child: RichText( child: Row(
maxLines: 1, mainAxisAlignment: MainAxisAlignment.spaceBetween,
textAlign: TextAlign.justify, children: [
softWrap: false, Text(
text: TextSpan( liveItem!.areaName!,
style: const TextStyle(fontSize: 11, color: Colors.white), style: const TextStyle(fontSize: 11, color: Colors.white),
children: [ ),
TextSpan(text: liveItem!.areaName!), Text(
TextSpan(text: liveItem!.watchedShow!['text_small']), liveItem!.watchedShow!['text_small'],
], style: const TextStyle(fontSize: 11, color: Colors.white),
), ),
],
), ),
// child: RichText(
// maxLines: 1,
// textAlign: TextAlign.justify,
// softWrap: false,
// text: TextSpan(
// style: const TextStyle(fontSize: 11, color: Colors.white),
// children: [
// TextSpan(text: liveItem!.areaName!),
// TextSpan(text: liveItem!.watchedShow!['text_small']),
// ],
// ),
// ),
); );
} }
} }

View File

@ -3,6 +3,7 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart'; import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart'; import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart';
class LiveRoomController extends GetxController { class LiveRoomController extends GetxController {
String cover = ''; String cover = '';
@ -14,13 +15,7 @@ class LiveRoomController extends GetxController {
RxBool volumeOff = false.obs; RxBool volumeOff = false.obs;
PlPlayerController plPlayerController = PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'live'); PlPlayerController.getInstance(videoType: 'live');
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
// MeeduPlayerController meeduPlayerController = MeeduPlayerController(
// colorTheme: Theme.of(Get.context!).colorScheme.primary,
// pipEnabled: true,
// controlsStyle: ControlsStyle.live,
// enabledButtons: const EnabledButtons(pip: true),
// );
@override @override
void onInit() { void onInit() {
@ -36,11 +31,10 @@ class LiveRoomController extends GetxController {
cover = liveItem.cover; cover = liveItem.cover;
} }
} }
queryLiveInfo();
} }
playerInit(source) { playerInit(source) async {
plPlayerController.setDataSource( await plPlayerController.setDataSource(
DataSource( DataSource(
videoSource: source, videoSource: source,
audioSource: null, audioSource: null,
@ -66,7 +60,8 @@ class LiveRoomController extends GetxController {
String videoUrl = (item.urlInfo?.first.host)! + String videoUrl = (item.urlInfo?.first.host)! +
item.baseUrl! + item.baseUrl! +
item.urlInfo!.first.extra!; item.urlInfo!.first.extra!;
playerInit(videoUrl); await playerInit(videoUrl);
return res;
} }
} }
@ -80,4 +75,12 @@ class LiveRoomController extends GetxController {
volumeOff.value = true; volumeOff.value = true;
} }
} }
Future queryLiveInfoH5() async {
var res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
if (res['status']) {
roomInfoH5.value = res['data'];
}
return res;
}
} }

View File

@ -19,6 +19,8 @@ class LiveRoomPage extends StatefulWidget {
class _LiveRoomPageState extends State<LiveRoomPage> { class _LiveRoomPageState extends State<LiveRoomPage> {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController()); final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
late Future? _futureBuilder;
late Future? _futureBuilderFuture;
bool isShowCover = true; bool isShowCover = true;
bool isPlay = true; bool isPlay = true;
@ -27,18 +29,16 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
plPlayerController = _liveRoomController.plPlayerController;
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) {
if (status == PlayerStatus.playing) {
isShowCover = false;
setState(() {});
}
},
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = Floating(); floating = Floating();
} }
videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
}
Future<void> videoSourceInit() async {
_futureBuilder = _liveRoomController.queryLiveInfoH5();
plPlayerController = _liveRoomController.plPlayerController;
} }
@override @override
@ -52,57 +52,123 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
);
} else {
return const SizedBox();
}
},
);
Widget childWhenDisabled = Scaffold( Widget childWhenDisabled = Scaffold(
primary: true, primary: true,
appBar: PreferredSize( backgroundColor: Colors.black,
preferredSize: Size.fromHeight( body: Stack(
MediaQuery.of(context).orientation == Orientation.portrait ? 56 : 0,
),
child: AppBar(
centerTitle: false,
titleSpacing: 0,
title: _liveRoomController.liveItem != null
? Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController
.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
)
: const SizedBox(),
// actions: [
// SizedBox(
// height: 34,
// child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
// ),
// const SizedBox(width: 12),
// ],
),
),
body: Column(
children: [ children: [
Stack( // Obx(
// () => Positioned.fill(
// child: Opacity(
// opacity: 0.8,
// child: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// '' &&
// _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// null
// ? NetworkImgLayer(
// width: Get.width,
// height: Get.height,
// src: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground ??
// '',
// )
// : Image.asset(
// 'assets/images/live/default_bg.webp',
// width: Get.width,
// height: Get.height,
// ),
// ),
// ),
// ),
Positioned.fill(
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
width: Get.width,
height: Get.height,
),
),
),
Column(
children: [ children: [
AppBar(
centerTitle: false,
titleSpacing: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
toolbarHeight:
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
title: FutureBuilder(
future: _futureBuilder,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController
.roomInfoH5.value.anchorInfo!.baseInfo!.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.roomInfoH5.value
.anchorInfo!.baseInfo!.uname!,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController
.roomInfoH5.value.watchedShow !=
null)
Text(
_liveRoomController.roomInfoH5.value
.watchedShow!['text_large'] ??
'',
style: const TextStyle(fontSize: 12),
),
],
),
],
),
);
} else {
return const SizedBox();
}
},
),
),
PopScope( PopScope(
canPop: plPlayerController?.isFullScreen.value != true, canPop: plPlayerController?.isFullScreen.value != true,
onPopInvoked: (bool didPop) { onPopInvoked: (bool didPop) {
@ -120,55 +186,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
Orientation.landscape Orientation.landscape
? Get.size.height ? Get.size.height
: Get.size.width * 9 / 16, : Get.size.width * 9 / 16,
child: plPlayerController!.videoPlayerController != null child: videoPlayerPanel,
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
), ),
), ),
// if (_liveRoomController.liveItem != null &&
// _liveRoomController.liveItem.cover != null)
// Visibility(
// visible: isShowCover,
// child: Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: NetworkImgLayer(
// type: 'emote',
// src: _liveRoomController.liveItem.cover,
// width: Get.size.width,
// height: videoHeight,
// ),
// ),
// ),
], ],
), ),
], ],
), ),
); );
Widget childWhenEnabled = AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
),
)
: const SizedBox(),
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
return PiPSwitcher( return PiPSwitcher(
childWhenDisabled: childWhenDisabled, childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled, childWhenEnabled: videoPlayerPanel,
floating: floating,
); );
} else { } else {
return childWhenDisabled; return childWhenDisabled;

View File

@ -105,7 +105,7 @@ class _MemberPageState extends State<MemberPage>
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'), '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
PopupMenuButton( PopupMenuButton(

View File

@ -25,7 +25,7 @@ class MemberArchiveController extends GetxController {
// 获取用户投稿 // 获取用户投稿
Future getMemberArchive(type) async { Future getMemberArchive(type) async {
if (type == 'onRefresh') { if (type == 'init') {
pn = 1; pn = 1;
} }
var res = await MemberHttp.memberArchive( var res = await MemberHttp.memberArchive(
@ -34,7 +34,12 @@ class MemberArchiveController extends GetxController {
order: currentOrder['type']!, order: currentOrder['type']!,
); );
if (res['status']) { if (res['status']) {
archivesList.addAll(res['data'].list.vlist); if (type == 'init') {
archivesList.value = res['data'].list.vlist;
}
if (type == 'onLoad') {
archivesList.addAll(res['data'].list.vlist);
}
count = res['data'].page['count']; count = res['data'].page['count'];
pn += 1; pn += 1;
} }
@ -42,13 +47,14 @@ class MemberArchiveController extends GetxController {
} }
toggleSort() async { toggleSort() async {
pn = 1; List<String> typeList = orderList.map((e) => e['type']!).toList();
int index = orderList.indexOf(currentOrder); int index = typeList.indexOf(currentOrder['type']!);
if (index == orderList.length - 1) { if (index == orderList.length - 1) {
currentOrder.value = orderList.first; currentOrder.value = orderList.first;
} else { } else {
currentOrder.value = orderList[index + 1]; currentOrder.value = orderList[index + 1];
} }
getMemberArchive('init');
} }
// 上拉加载 // 上拉加载

View File

@ -25,8 +25,7 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
final String heroTag = Utils.makeHeroTag(mid); final String heroTag = Utils.makeHeroTag(mid);
_memberArchivesController = _memberArchivesController =
Get.put(MemberArchiveController(), tag: heroTag); Get.put(MemberArchiveController(), tag: heroTag);
_futureBuilderFuture = _futureBuilderFuture = _memberArchivesController.getMemberArchive('init');
_memberArchivesController.getMemberArchive('onRefresh');
scrollController = _memberArchivesController.scrollController; scrollController = _memberArchivesController.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@ -48,39 +47,16 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium), title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium),
// actions: [ actions: [
// Obx( Obx(
// () => PopupMenuButton<String>( () => TextButton.icon(
// padding: EdgeInsets.zero, icon: const Icon(Icons.sort, size: 20),
// tooltip: '投稿排序', onPressed: _memberArchivesController.toggleSort,
// icon: Icon( label: Text(_memberArchivesController.currentOrder['label']!),
// Icons.more_vert_outlined, ),
// color: Theme.of(context).colorScheme.outline, ),
// ), const SizedBox(width: 6),
// position: PopupMenuPosition.under, ],
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
// for (var i in _memberArchivesController.orderList) ...[
// PopupMenuItem<String>(
// onTap: () {},
// value: _memberArchivesController.currentOrder['label'],
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(i['label']!),
// if (_memberArchivesController.currentOrder['label'] ==
// i['label']) ...[
// const SizedBox(width: 10),
// const Icon(Icons.done, size: 20),
// ],
// ],
// ),
// ),
// ]
// ],
// ),
// ),
// ],
), ),
body: CustomScrollView( body: CustomScrollView(
controller: _memberArchivesController.scrollController, controller: _memberArchivesController.scrollController,

View File

@ -119,7 +119,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/follow?mid=${userInfo.value.mid}'); Get.toNamed('/follow?mid=${userInfo.value.mid}', preventDuplicates: false);
} }
pushFans() { pushFans() {
@ -127,7 +127,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/fan?mid=${userInfo.value.mid}'); Get.toNamed('/fan?mid=${userInfo.value.mid}', preventDuplicates: false);
} }
pushDynamic() { pushDynamic() {
@ -135,6 +135,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}'); Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}',
preventDuplicates: false);
} }
} }

View File

@ -9,14 +9,15 @@ import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController { class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int _currentPage = 0; int _currentPage = 0;
RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs; // RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs;
RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs; // RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;
bool isLoadingMore = true; bool isLoadingMore = true;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
RxInt crossAxisCount = 2.obs; RxInt crossAxisCount = 2.obs;
late bool enableSaveLastData; late bool enableSaveLastData;
late String defaultRcmdType = 'web'; late String defaultRcmdType = 'web';
late RxList<dynamic> videoList;
@override @override
void onInit() { void onInit() {
@ -27,81 +28,58 @@ class RcmdController extends GetxController {
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false); setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
defaultRcmdType = defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web'); setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
if (defaultRcmdType == 'web') {
videoList = <RecVideoItemModel>[].obs;
} else {
videoList = <RecVideoItemAppModel>[].obs;
}
} }
// 获取推荐 // 获取推荐
Future queryRcmdFeed(type) async { Future queryRcmdFeed(type) async {
print(defaultRcmdType);
if (defaultRcmdType == 'app') {
return await queryRcmdFeedApp(type);
}
if (defaultRcmdType == 'web') {
return await queryRcmdFeedWeb(type);
}
}
// 获取app端推荐
Future queryRcmdFeedApp(type) async {
if (isLoadingMore == false) { if (isLoadingMore == false) {
return; return;
} }
if (type == 'onRefresh') { if (type == 'onRefresh') {
_currentPage = 0; _currentPage = 0;
} }
var res = await VideoHttp.rcmdVideoListApp( late final Map<String, dynamic> res;
freshIdx: _currentPage, switch (defaultRcmdType) {
); case 'app':
case 'notLogin':
res = await VideoHttp.rcmdVideoListApp(
loginStatus: defaultRcmdType != 'notLogin',
freshIdx: _currentPage,
);
break;
default: //'web'
res = await VideoHttp.rcmdVideoList(
freshIdx: _currentPage,
ps: 20,
);
}
if (res['status']) { if (res['status']) {
if (type == 'init') { if (type == 'init') {
if (appVideoList.isNotEmpty) { if (videoList.isNotEmpty) {
appVideoList.addAll(res['data']); videoList.addAll(res['data']);
} else { } else {
appVideoList.value = res['data']; videoList.value = res['data'];
} }
} else if (type == 'onRefresh') { } else if (type == 'onRefresh') {
if (enableSaveLastData) { if (enableSaveLastData) {
appVideoList.insertAll(0, res['data']); videoList.insertAll(0, res['data']);
} else { } else {
appVideoList.value = res['data']; videoList.value = res['data'];
} }
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
appVideoList.addAll(res['data']); videoList.addAll(res['data']);
} }
_currentPage += 1; _currentPage += 1;
} // 若videoList数量太小可能会影响翻页此时再次请求
isLoadingMore = false; // 为避免请求到的数据太少时还在反复请求要求本次返回数据大于1条才触发
return res; if (res['data'].length > 1 && videoList.length < 10) {
} queryRcmdFeed('onLoad');
// 获取web端推荐
Future queryRcmdFeedWeb(type) async {
if (isLoadingMore == false) {
return;
}
if (type == 'onRefresh') {
_currentPage = 0;
}
var res = await VideoHttp.rcmdVideoList(
ps: 20,
freshIdx: _currentPage,
);
if (res['status']) {
if (type == 'init') {
if (webVideoList.isNotEmpty) {
webVideoList.addAll(res['data']);
} else {
webVideoList.value = res['data'];
}
} else if (type == 'onRefresh') {
if (enableSaveLastData) {
webVideoList.insertAll(0, res['data']);
} else {
webVideoList.value = res['data'];
}
} else if (type == 'onLoad') {
webVideoList.addAll(res['data']);
} }
_currentPage += 1;
} }
isLoadingMore = false; isLoadingMore = false;
return res; return res;
@ -118,7 +96,7 @@ class RcmdController extends GetxController {
queryRcmdFeed('onLoad'); queryRcmdFeed('onLoad');
} }
// 返回顶部并刷新 // 返回顶部
void animateToTop() async { void animateToTop() async {
if (scrollController.offset >= if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) { MediaQuery.of(Get.context!).size.height * 5) {

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -97,24 +96,18 @@ class _RcmdPageState extends State<RcmdPage>
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
if (data['status']) { if (data['status']) {
return Platform.isAndroid || Platform.isIOS return Obx(
? Obx( () {
() => contentGrid( if (_rcmdController.isLoadingMore &&
_rcmdController, _rcmdController.videoList.isEmpty) {
_rcmdController.defaultRcmdType == 'web' return contentGrid(_rcmdController, []);
? _rcmdController.webVideoList } else {
: _rcmdController.appVideoList), // 显示视频列表
) return contentGrid(
: SliverLayoutBuilder( _rcmdController, _rcmdController.videoList);
builder: (context, boxConstraints) { }
return Obx( },
() => contentGrid( );
_rcmdController,
_rcmdController.defaultRcmdType == 'web'
? _rcmdController.webVideoList
: _rcmdController.appVideoList),
);
});
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
@ -127,20 +120,12 @@ class _RcmdPageState extends State<RcmdPage>
); );
} }
} else { } else {
// 缓存数据
// if (_rcmdController.videoList.isNotEmpty) {
// return contentGrid(
// _rcmdController, _rcmdController.videoList);
// }
// // 骨架屏
// else {
return contentGrid(_rcmdController, []); return contentGrid(_rcmdController, []);
// }
} }
}, },
), ),
), ),
LoadingMore(ctr: _rcmdController) LoadingMore(ctr: _rcmdController),
], ],
), ),
), ),

View File

@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/common/dynamics_type.dart'; import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/rcmd_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -20,26 +18,23 @@ class ExtraSetting extends StatefulWidget {
class _ExtraSettingState extends State<ExtraSetting> { class _ExtraSettingState extends State<ExtraSetting> {
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache; static Box localCache = GStrorage.localCache;
late dynamic defaultRcmdType;
late dynamic defaultReplySort; late dynamic defaultReplySort;
late dynamic defaultDynamicType; late dynamic defaultDynamicType;
late dynamic enableSystemProxy; late dynamic enableSystemProxy;
late String defaultSystemProxyHost; late String defaultSystemProxyHost;
late String defaultSystemProxyPort; late String defaultSystemProxyPort;
Box userInfoCache = GStrorage.userInfo;
var userInfo;
bool userLogin = false; bool userLogin = false;
var accessKeyInfo;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 首页默认推荐类型
defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
// 默认优先显示最新评论 // 默认优先显示最新评论
defaultReplySort = defaultReplySort =
setting.get(SettingBoxKey.replySortType, defaultValue: 0); setting.get(SettingBoxKey.replySortType, defaultValue: 0);
if (defaultReplySort == 2) {
setting.put(SettingBoxKey.replySortType, 0);
defaultReplySort = 0;
}
// 优先展示全部动态 all // 优先展示全部动态 all
defaultDynamicType = defaultDynamicType =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0); setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
@ -49,9 +44,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: ''); localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
defaultSystemProxyPort = defaultSystemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: ''); localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: null);
} }
// 设置代理 // 设置代理
@ -159,12 +151,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableSearchWord, setKey: SettingBoxKey.enableSearchWord,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem( const SetSwitchItem(
title: '快速收藏', title: '快速收藏',
subTitle: '点按收藏至默认,长按选择文件夹', subTitle: '点按收藏至默认,长按选择文件夹',
@ -177,50 +163,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableWordRe, setKey: SettingBoxKey.enableWordRe,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: '首页推荐刷新',
subTitle: '下拉刷新时保留上次内容',
setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false,
),
ListTile(
dense: false,
title: Text('首页推荐类型', style: titleStyle),
subtitle: Text(
'当前使用「$defaultRcmdType端」推荐',
style: subTitleStyle,
),
onTap: () async {
String? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<String>(
title: '推荐类型',
value: defaultRcmdType,
values: RcmdType.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList(),
);
},
);
if (result != null) {
if (result == 'app') {
// app端推荐需要access_key
if (accessKeyInfo == null) {
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
await MemberHttp.cookieToKey();
}
}
defaultRcmdType = result;
setting.put(SettingBoxKey.defaultRcmdType, result);
SmartDialog.showToast('下次启动时生效');
setState(() {});
}
},
),
const SetSwitchItem( const SetSwitchItem(
title: '启用ai总结', title: '启用ai总结',
subTitle: '视频详情页开启ai总结', subTitle: '视频详情页开启ai总结',

View File

@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/common/rcmd_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
class RecommendSetting extends StatefulWidget {
const RecommendSetting({super.key});
@override
State<RecommendSetting> createState() => _RecommendSettingState();
}
class _RecommendSettingState extends State<RecommendSetting> {
Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic defaultRcmdType;
Box userInfoCache = GStrorage.userInfo;
late dynamic userInfo;
bool userLogin = false;
late dynamic accessKeyInfo;
// late int filterUnfollowedRatio;
late int minDurationForRcmd;
late int minLikeRatioForRecommend;
@override
void initState() {
super.initState();
// 首页默认推荐类型
defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: null);
// filterUnfollowedRatio = setting
// .get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);
minDurationForRcmd =
setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);
minLikeRatioForRecommend =
setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'推荐设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
ListTile(
dense: false,
title: Text('首页推荐类型', style: titleStyle),
subtitle: Text(
'当前使用「$defaultRcmdType端」推荐¹',
style: subTitleStyle,
),
onTap: () async {
String? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<String>(
title: '推荐类型',
value: defaultRcmdType,
values: RcmdType.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList(),
);
},
);
if (result != null) {
if (result == 'app') {
// app端推荐需要access_key
if (accessKeyInfo == null) {
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
// 显示一个确认框,告知用户可能会导致账号被风控
SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text(
'使用app端推荐需获取access_key有小概率触发风控导致账号退出在官方版本app重新登录即可解除是否继续'),
actions: [
TextButton(
onPressed: () {
result = null;
SmartDialog.dismiss();
},
child: const Text('取消'),
),
TextButton(
onPressed: () async {
SmartDialog.dismiss();
await MemberHttp.cookieToKey();
},
child: const Text('确定'),
),
],
);
});
}
}
if (result != null) {
defaultRcmdType = result;
setting.put(SettingBoxKey.defaultRcmdType, result);
SmartDialog.showToast('下次启动时生效');
setState(() {});
}
}
},
),
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态(仅app端)',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem(
title: '首页推荐刷新',
subTitle: '下拉刷新时保留上次内容',
setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false,
),
// 分割线
const Divider(height: 1),
ListTile(
dense: false,
title: Text('点赞率过滤', style: titleStyle),
subtitle: Text(
'过滤掉点赞数/播放量「小于$minLikeRatioForRecommend%」的推荐视频(仅web端)',
style: subTitleStyle,
),
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '选择点赞率0即不过滤',
value: minLikeRatioForRecommend,
values: [0, 1, 2, 3, 4].map((e) {
return {'title': '$e %', 'value': e};
}).toList());
},
);
if (result != null) {
minLikeRatioForRecommend = result;
setting.put(SettingBoxKey.minLikeRatioForRecommend, result);
RecommendFilter.update();
setState(() {});
}
},
),
ListTile(
dense: false,
title: Text('视频时长过滤', style: titleStyle),
subtitle: Text(
'过滤掉时长「小于$minDurationForRcmd秒」的推荐视频',
style: subTitleStyle,
),
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '选择时长0即不过滤',
value: minDurationForRcmd,
values: [0, 30, 60, 90, 120].map((e) {
return {'title': '$e', 'value': e};
}).toList());
},
);
if (result != null) {
minDurationForRcmd = result;
setting.put(SettingBoxKey.minDurationForRcmd, result);
RecommendFilter.update();
setState(() {});
}
},
),
SetSwitchItem(
title: '已关注Up豁免推荐过滤',
subTitle: '推荐中已关注用户发布的内容不会被过滤',
setKey: SettingBoxKey.exemptFilterForFollowed,
defaultVal: true,
callFn: (_) => {RecommendFilter.update},
),
// ListTile(
// dense: false,
// title: Text('按比例过滤未关注Up', style: titleStyle),
// subtitle: Text(
// '滤除推荐中占比「$filterUnfollowedRatio%」的未关注用户发布的内容',
// style: subTitleStyle,
// ),
// onTap: () async {
// int? result = await showDialog(
// context: context,
// builder: (context) {
// return SelectDialog<int>(
// title: '选择滤除比例0即不过滤',
// value: filterUnfollowedRatio,
// values: [0, 16, 32, 48, 64].map((e) {
// return {'title': '$e %', 'value': e};
// }).toList());
// },
// );
// if (result != null) {
// filterUnfollowedRatio = result;
// setting.put(
// SettingBoxKey.filterUnfollowedRatio, result);
// RecommendFilter.update();
// setState(() {});
// }
// },
// ),
SetSwitchItem(
title: '过滤器也应用于相关视频',
subTitle: '视频详情页的相关视频也进行过滤²',
setKey: SettingBoxKey.applyFilterToRelatedVideos,
defaultVal: true,
callFn: (_) => {RecommendFilter.update},
),
ListTile(
dense: true,
subtitle: Text(
'¹ 若默认web端推荐不太符合预期可尝试切换至app端。\n'
'¹ 选择“模拟未登录(notLogin)”将以空的key请求推荐接口但播放页仍会携带用户信息保证账号能正常记录进度、点赞投币等。\n\n'
'² 由于接口未提供关注信息无法豁免相关视频中的已关注Up。\n\n'
'* 其它(如热门视频、手动搜索、链接跳转等)均不受过滤器影响。\n'
'* 设定较严苛的条件可导致推荐项数锐减或多次请求,请酌情选择。\n'
'* 后续可能会增加更多过滤条件,敬请期待。',
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline.withOpacity(0.7)),
),
)
],
),
);
}
}

View File

@ -24,6 +24,11 @@ class SettingPage extends StatelessWidget {
dense: false, dense: false,
title: const Text('隐私设置'), title: const Text('隐私设置'),
), ),
ListTile(
onTap: () => Get.toNamed('/recommendSetting'),
dense: false,
title: const Text('推荐设置'),
),
ListTile( ListTile(
onTap: () => Get.toNamed('/playSetting'), onTap: () => Get.toNamed('/playSetting'),
dense: false, dense: false,

View File

@ -19,6 +19,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart'; import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
class VideoDetailController extends GetxController class VideoDetailController extends GetxController
@ -61,7 +62,7 @@ class VideoDetailController extends GetxController
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
int oid = 0; RxInt oid = 0.obs;
// 评论id 请求楼中楼评论使用 // 评论id 请求楼中楼评论使用
int fRpid = 0; int fRpid = 0;
@ -135,13 +136,14 @@ class VideoDetailController extends GetxController
defaultValue: VideoDecodeFormats.values.last.code); defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
} }
showReplyReplyPanel() { showReplyReplyPanel() {
PersistentBottomSheetController? ctr = PersistentBottomSheetController? ctr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) { scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel( return VideoReplyReplyPanel(
oid: oid, oid: oid.value,
rpid: fRpid, rpid: fRpid,
closePanel: () => { closePanel: () => {
fRpid = 0, fRpid = 0,

View File

@ -298,7 +298,6 @@ class VideoIntroController extends GetxController {
await queryVideoInFolder(); await queryVideoInFolder();
int defaultFolderId = favFolderData.value.list!.first.id!; int defaultFolderId = favFolderData.value.list!.first.id!;
int favStatus = favFolderData.value.list!.first.favState!; int favStatus = favFolderData.value.list!.first.favState!;
print('favStatus: $favStatus');
var result = await VideoHttp.favVideo( var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid), aid: IdUtils.bv2av(bvid),
addIds: favStatus == 0 ? '$defaultFolderId' : '', addIds: favStatus == 0 ? '$defaultFolderId' : '',
@ -310,6 +309,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功'); SmartDialog.showToast('✅ 操作成功');
} }
} else {
SmartDialog.showToast(result['msg']);
} }
return; return;
} }
@ -340,6 +341,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功'); SmartDialog.showToast('✅ 操作成功');
} }
} else {
SmartDialog.showToast(result['msg']);
} }
} }
@ -476,6 +479,7 @@ class VideoIntroController extends GetxController {
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cid.value = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();

View File

@ -41,17 +41,25 @@ class VideoReplyController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
final int deaultReplySortIndex = int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0) as int; setting.get(SettingBoxKey.replySortType, defaultValue: 0) as int;
if (deaultReplySortIndex == 2) {
setting.put(SettingBoxKey.replySortType, 0);
deaultReplySortIndex = 0;
}
_sortType = ReplySortType.values[deaultReplySortIndex]; _sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels; sortTypeLabel.value = _sortType.labels;
} }
Future queryReplyList({type = 'init'}) async { Future queryReplyList({type = 'init'}) async {
if (isLoadingMore) {
return;
}
isLoadingMore = true; isLoadingMore = true;
if (type == 'init') { if (type == 'init') {
currentPage = 0; currentPage = 0;
noMore.value = '';
} }
if (noMore.value == '没有更多了') { if (noMore.value == '没有更多了') {
return; return;
@ -115,9 +123,6 @@ class VideoReplyController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@ -16,11 +16,13 @@ import 'widgets/reply_item.dart';
class VideoReplyPanel extends StatefulWidget { class VideoReplyPanel extends StatefulWidget {
final String? bvid; final String? bvid;
final int? oid;
final int rpid; final int rpid;
final String? replyLevel; final String? replyLevel;
const VideoReplyPanel({ const VideoReplyPanel({
this.bvid, this.bvid,
this.oid,
this.rpid = 0, this.rpid = 0,
this.replyLevel, this.replyLevel,
super.key, super.key,
@ -48,16 +50,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0; // int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
replyLevel = widget.replyLevel ?? '1'; replyLevel = widget.replyLevel ?? '1';
if (replyLevel == '2') { if (replyLevel == '2') {
_videoReplyController = Get.put( _videoReplyController = Get.put(
VideoReplyController(oid, widget.rpid.toString(), replyLevel), VideoReplyController(widget.oid, widget.rpid.toString(), replyLevel),
tag: widget.rpid.toString()); tag: widget.rpid.toString());
} else { } else {
_videoReplyController = _videoReplyController = Get.put(
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag); VideoReplyController(widget.oid, '', replyLevel),
tag: heroTag);
} }
fabAnimationCtr = AnimationController( fabAnimationCtr = AnimationController(
@ -75,7 +78,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) { scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
() {
_videoReplyController.onLoad(); _videoReplyController.onLoad();
}); });
} }
@ -110,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) { if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid; videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!; videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem; videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(); videoDetailCtr.showReplyReplyPanel();

View File

@ -1,7 +1,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
@ -12,10 +12,9 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/utils/feed_back.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/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'zan.dart'; import 'zan.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -48,6 +47,17 @@ class ReplyItem extends StatelessWidget {
replyReply!(replyItem); replyReply!(replyItem);
} }
}, },
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replyItem);
},
);
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -123,98 +133,6 @@ class ReplyItem extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// 头像、昵称
// SizedBox(
// width: double.infinity,
// child: Stack(
// children: [
// GestureDetector(
// behavior: HitTestBehavior.opaque,
// onTap: () {
// feedBack();
// Get.toNamed('/member?mid=${replyItem!.mid}', arguments: {
// 'face': replyItem!.member!.avatar!,
// 'heroTag': heroTag
// });
// },
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// lfAvtar(context, heroTag),
// const SizedBox(width: 12),
// Text(
// replyItem!.member!.uname!,
// style: TextStyle(
// color: replyItem!.member!.vip!['vipStatus'] > 0
// ? const Color.fromARGB(255, 251, 100, 163)
// : Theme.of(context).colorScheme.outline,
// fontSize: 13,
// ),
// ),
// const SizedBox(width: 6),
// Image.asset(
// 'assets/images/lv/lv${replyItem!.member!.level}.png',
// height: 11,
// ),
// const SizedBox(width: 6),
// if (replyItem!.isUp!)
// const PBadge(
// text: 'UP',
// size: 'small',
// stack: 'normal',
// fs: 9,
// ),
// ],
// ),
// ),
// Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: Container(
// width: double.infinity,
// height: 45,
// decoration: BoxDecoration(
// image: replyItem!.member!.userSailing!.cardbg != null
// ? DecorationImage(
// alignment: Alignment.centerRight,
// fit: BoxFit.fitHeight,
// image: NetworkImage(
// replyItem!.member!.userSailing!.cardbg!['image'],
// ),
// )
// : null,
// ),
// ),
// ),
// if (replyItem!.member!.userSailing!.cardbg != null &&
// replyItem!.member!.userSailing!.cardbg!['fan']['number'] > 0)
// Positioned(
// top: 10,
// left: Get.size.width / 7 * 5.8,
// child: DefaultTextStyle(
// style: TextStyle(
// fontFamily: 'fansCard',
// fontSize: 9,
// color: Theme.of(context).colorScheme.primary,
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Text('NO.'),
// Text(
// replyItem!.member!.userSailing!.cardbg!['fan']
// ['num_desc'],
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// ),
/// fix Stack内GestureDetector onTap无效 /// fix Stack内GestureDetector onTap无效
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@ -291,30 +209,26 @@ class ReplyItem extends StatelessWidget {
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: SelectableRegion( child: Text.rich(
focusNode: FocusNode(), style: const TextStyle(height: 1.75),
selectionControls: MaterialTextSelectionControls(), maxLines:
child: Text.rich( replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
style: const TextStyle(height: 1.75), overflow: TextOverflow.ellipsis,
maxLines: TextSpan(
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999, children: [
overflow: TextOverflow.ellipsis, if (replyItem!.isTop!)
TextSpan( const WidgetSpan(
children: [ alignment: PlaceholderAlignment.top,
if (replyItem!.isTop!) child: PBadge(
const WidgetSpan( text: 'TOP',
alignment: PlaceholderAlignment.top, size: 'small',
child: PBadge( stack: 'normal',
text: 'TOP', type: 'line',
size: 'small', fs: 9,
stack: 'normal',
type: 'line',
fs: 9,
),
), ),
buildContent(context, replyItem!, replyReply, null), ),
], buildContent(context, replyItem!, replyReply, null),
), ],
), ),
), ),
), ),
@ -447,6 +361,17 @@ class ReplyItemRow extends StatelessWidget {
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
onTap: () => replyReply!(replyItem), onTap: () => replyReply!(replyItem),
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replies![i]);
},
);
},
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@ -541,7 +466,6 @@ InlineSpan buildContent(
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content; final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[]; final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false;
// 投票 // 投票
if (content.vote.isNotEmpty) { if (content.vote.isNotEmpty) {
@ -571,7 +495,8 @@ InlineSpan buildContent(
}); });
} }
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); // content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message.replaceAll('&amp;', '&') content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<') .replaceAll('&lt;', '<')
.replaceAll('&gt;', '>') .replaceAll('&gt;', '>')
.replaceAll('&quot;', '"') .replaceAll('&quot;', '"')
@ -586,21 +511,21 @@ InlineSpan buildContent(
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')), e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
]; ];
String patternStr = String patternStr = specialTokens.map(RegExp.escape).join('|');
specialTokens.map(RegExp.escape).join('|');
if (patternStr.isNotEmpty) { if (patternStr.isNotEmpty) {
patternStr += "|"; patternStr += "|";
} }
patternStr += r'(\b\d{1,2}[:]\d{2}\b)'; patternStr += r'(\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b)';
final RegExp pattern = RegExp(patternStr); final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = []; List<String> matchedStrs = [];
void addPlainTextSpan(str){ void addPlainTextSpan(str) {
spanChilds.add(TextSpan( spanChilds.add(TextSpan(
text: str, text: str,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => ..onTap =
replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); () => replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
} }
// 分割文本并处理每个部分 // 分割文本并处理每个部分
content.message.splitMapJoin( content.message.splitMapJoin(
pattern, pattern,
@ -638,7 +563,9 @@ InlineSpan buildContent(
}, },
), ),
); );
} else if (RegExp(r'^\b[0-9]{1,2}[:][0-9]{2}\b$').hasMatch(matchStr)) { } else if (RegExp(r'^\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b$')
.hasMatch(matchStr)) {
matchStr = matchStr.replaceAll('', ':');
spanChilds.add( spanChilds.add(
TextSpan( TextSpan(
text: ' $matchStr ', text: ' $matchStr ',
@ -649,7 +576,6 @@ InlineSpan buildContent(
..onTap = () { ..onTap = () {
// 跳转到指定位置 // 跳转到指定位置
try { try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr'); SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']) Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController .plPlayerController
@ -674,57 +600,88 @@ InlineSpan buildContent(
addPlainTextSpan(matchStr); addPlainTextSpan(matchStr);
return ""; return "";
} }
spanChilds.add( spanChilds.addAll(
TextSpan( [
text: content.jumpUrl[matchStr]['title'], if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
style: TextStyle( WidgetSpan(
color: Theme.of(context).colorScheme.primary, child: Image.network(
), content.jumpUrl[matchStr]['prefix_icon'],
recognizer: TapGestureRecognizer() height: 19,
..onTap = () { color: Theme.of(context).colorScheme.primary,
if (appUrlSchema == '') { ),
final String str = Uri.parse(matchStr).pathSegments[0]; )
final Map matchRes = IdUtils.matchAvorBv(input: str); ],
final List matchKeys = matchRes.keys.toList(); TextSpan(
if (matchKeys.isNotEmpty) { text: content.jumpUrl[matchStr]['title'],
if (matchKeys.first == 'BV') { style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final String title = content.jumpUrl[matchStr]['title'];
if (appUrlSchema == '') {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed( Get.toNamed(
'/searchResult', '/webview',
parameters: {'keyword': matchRes['BV']}, parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
); );
} }
} else { } else {
Get.toNamed( if (appUrlSchema.startsWith('bilibili://search')) {
'/webview', Get.toNamed('/searchResult',
parameters: { parameters: {'keyword': title});
'url': matchStr, } else if (matchStr.startsWith('https://b23.tv')) {
'type': 'url', final String redirectUrl =
'pageTitle': '' await UrlUtils.parseRedirectUrl(matchStr);
}, final String pathSegment = Uri.parse(redirectUrl).path;
); final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': title
},
);
}
} }
} else { },
if (appUrlSchema.startsWith('bilibili://search')) { )
Get.toNamed('/searchResult', parameters: { ],
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
),
); );
if (appUrlSchema.startsWith('bilibili://search')) {
spanChilds.add(
WidgetSpan(
child: Icon(
FontAwesomeIcons.magnifyingGlass,
size: 9,
color: Theme.of(context).colorScheme.primary,
),
alignment: PlaceholderAlignment.top,
),
);
}
// 只显示一次 // 只显示一次
matchedStrs.add(matchStr); matchedStrs.add(matchStr);
} else { } else {
@ -739,6 +696,47 @@ InlineSpan buildContent(
}, },
); );
if (content.jumpUrl.keys.isNotEmpty) {
List<String> unmatchedItems = content.jumpUrl.keys
.toList()
.where((item) => !content.message.contains(item))
.toList();
if (unmatchedItems.isNotEmpty) {
for (int i = 0; i < unmatchedItems.length; i++) {
String patternStr = unmatchedItems[i];
spanChilds.addAll(
[
if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[patternStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan(
text: content.jumpUrl[patternStr]['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/webview',
parameters: {
'url': patternStr,
'type': 'url',
'pageTitle': content.jumpUrl[patternStr]['title']
},
);
},
)
],
);
}
}
}
// 图片渲染 // 图片渲染
if (content.pictures.isNotEmpty) { if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[]; final List<String> picList = <String>[];
@ -753,11 +751,15 @@ InlineSpan buildContent(
builder: (BuildContext context, BoxConstraints box) { builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度 double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble(); // double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth / double height = 100;
2 * try {
pictureItem['img_height'] / height = ((box.maxWidth /
pictureItem['img_width'])) 2 *
.truncateToDouble(); pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
@ -797,8 +799,7 @@ InlineSpan buildContent(
), ),
), ),
); );
} } else if (len > 1) {
if (len > 1) {
List<Widget> list = []; List<Widget> list = [];
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
picList.add(content.pictures[i]['img_src']); picList.add(content.pictures[i]['img_src']);
@ -816,10 +817,11 @@ InlineSpan buildContent(
); );
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
src: content.pictures[i]['img_src'], src: content.pictures[i]['img_src'],
width: box.maxWidth, width: box.maxWidth,
height: box.maxWidth, height: box.maxWidth,
), origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
); );
}, },
), ),
@ -880,3 +882,100 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember)); // spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds); return TextSpan(children: spanChilds);
} }
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
Future<dynamic> menuActionHandler(String type) async {
String message = item.content.message ?? item.content;
switch (type) {
case 'copyAll':
await Clipboard.setData(ClipboardData(text: message));
SmartDialog.showToast('已复制');
Get.back();
break;
case 'copyFreedom':
Get.back();
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('自由复制'),
content: SelectableText(message),
);
},
);
break;
// case 'block':
// SmartDialog.showToast('加入黑名单');
// break;
// case 'report':
// SmartDialog.showToast('举报');
// break;
// case 'delete':
// SmartDialog.showToast('删除');
// break;
default:
}
}
@override
Widget build(BuildContext context) {
Color errorColor = Theme.of(context).colorScheme.error;
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
ListTile(
onTap: () async => await menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () async => await menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
// ListTile(
// onTap: () async => await menuActionHandler('block'),
// minLeadingWidth: 0,
// leading: Icon(Icons.block_outlined, color: errorColor),
// title: Text('加入黑名单', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('report'),
// minLeadingWidth: 0,
// leading: Icon(Icons.report_outlined, color: errorColor),
// title: Text('举报', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('del'),
// minLeadingWidth: 0,
// leading: Icon(Icons.delete_outline, color: errorColor),
// title: Text('删除', style: TextStyle(color: errorColor)),
// ),
],
),
);
}
}

View File

@ -570,8 +570,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
); );
}, },
), ),
VideoReplyPanel( Obx(
bvid: videoDetailController.bvid, () => VideoReplyPanel(
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
),
) )
], ],
), ),

View File

@ -438,7 +438,7 @@ class _HeaderControlState extends State<HeaderControl> {
}), }),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => Get.back(),
child: Text( child: Text(
'取消', '取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),

View File

@ -586,6 +586,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 进度条 live模式下禁用 /// 进度条 live模式下禁用
Obx( Obx(
() { () {
final int value = _.sliderPositionSeconds.value; final int value = _.sliderPositionSeconds.value;
@ -609,7 +610,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
if (_.videoType.value == 'live') { if (_.videoType.value == 'live') {
return nil; return const SizedBox();
} }
if (value > max || max <= 0) { if (value > max || max <= 0) {
return nil; return nil;

View File

@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/setting/pages/logs.dart'; import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart'; import '../pages/about/index.dart';
@ -39,6 +40,7 @@ import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart'; import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart';
import '../pages/setting/play_setting.dart'; import '../pages/setting/play_setting.dart';
import '../pages/setting/privacy_setting.dart'; import '../pages/setting/privacy_setting.dart';
import '../pages/setting/style_setting.dart'; import '../pages/setting/style_setting.dart';
@ -102,7 +104,9 @@ class Routes {
// 二级回复 // 二级回复
CustomGetPage( CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// 推荐设置
CustomGetPage(
name: '/recommendSetting', page: () => const RecommendSetting()),
// 播放设置 // 播放设置
CustomGetPage(name: '/playSetting', page: () => const PlaySetting()), CustomGetPage(name: '/playSetting', page: () => const PlaySetting()),
// 外观设置 // 外观设置
@ -154,6 +158,8 @@ class Routes {
name: '/memberSeasons', page: () => const MemberSeasonsPage()), name: '/memberSeasons', page: () => const MemberSeasonsPage()),
// 日志 // 日志
CustomGetPage(name: '/logs', page: () => const LogsPage()), CustomGetPage(name: '/logs', page: () => const LogsPage()),
// 搜索关注
CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()),
]; ];
} }

154
lib/utils/cache_manage.dart Normal file
View File

@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class CacheManage {
CacheManage._internal();
static final CacheManage cacheManage = CacheManage._internal();
factory CacheManage() => cacheManage;
// 获取缓存目录
Future<String> loadApplicationCache() async {
/// clear all of image in memory
// clearMemoryImageCache();
/// get ImageCache
// var res = getMemoryImageCache();
// 缓存大小
double cacheSize = 0;
// cached_network_image directory
Directory tempDirectory = await getTemporaryDirectory();
// get_storage directory
Directory docDirectory = await getApplicationDocumentsDirectory();
// 获取缓存大小
if (tempDirectory.existsSync()) {
double value = await getTotalSizeOfFilesInDir(tempDirectory);
cacheSize += value;
}
/// 获取缓存大小 dioCache
if (docDirectory.existsSync()) {
double value = 0;
String dioCacheFileName =
'${docDirectory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
value = await getTotalSizeOfFilesInDir(dioCacheFile);
}
cacheSize += value;
}
return formatSize(cacheSize);
}
// 循环计算文件的大小(递归)
Future<double> getTotalSizeOfFilesInDir(final FileSystemEntity file) async {
if (file is File) {
int length = await file.length();
return double.parse(length.toString());
}
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
double total = 0;
for (final FileSystemEntity child in children) {
total += await getTotalSizeOfFilesInDir(child);
}
return total;
}
return 0;
}
// 缓存大小格式转换
String formatSize(double value) {
List<String> unitArr = ['B', 'K', 'M', 'G'];
int index = 0;
while (value > 1024) {
index++;
value = value / 1024;
}
String size = value.toStringAsFixed(2);
return size + unitArr[index];
}
// 清除缓存
Future<bool> clearCacheAll() async {
bool cleanStatus = await SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('该操作将清除图片及网络请求缓存数据,确认清除?'),
actions: [
TextButton(
onPressed: (() => {SmartDialog.dismiss()}),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
SmartDialog.dismiss();
SmartDialog.showLoading(msg: '正在清除...');
try {
// 清除缓存 图片缓存
await clearLibraryCache();
Timer(const Duration(milliseconds: 500), () {
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
});
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());
}
},
child: const Text('确认'),
)
],
);
},
).then((res) {
return true;
});
return cleanStatus;
}
/// 清除 Documents 目录下的 DioCache.db
Future clearApplicationCache() async {
Directory directory = await getApplicationDocumentsDirectory();
if (directory.existsSync()) {
String dioCacheFileName =
'${directory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
dioCacheFile.delete();
}
}
}
// 清除 Library/Caches 目录及文件缓存
Future clearLibraryCache() async {
var appDocDir = await getTemporaryDirectory();
if (appDocDir.existsSync()) {
await appDocDir.delete(recursive: true);
}
}
/// 递归方式删除目录及文件
Future deleteDirectory(FileSystemEntity file) async {
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
for (final FileSystemEntity child in children) {
await deleteDirectory(child);
}
}
await file.delete();
}
}

View File

@ -0,0 +1,52 @@
import 'dart:math';
import 'storage.dart';
class RecommendFilter {
// static late int filterUnfollowedRatio;
static late int minDurationForRcmd;
static late int minLikeRatioForRecommend;
static late bool exemptFilterForFollowed;
static late bool applyFilterToRelatedVideos;
RecommendFilter() {
update();
}
static void update() {
var setting = GStrorage.setting;
// filterUnfollowedRatio =
// setting.get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);
minDurationForRcmd =
setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);
minLikeRatioForRecommend =
setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);
exemptFilterForFollowed =
setting.get(SettingBoxKey.exemptFilterForFollowed, defaultValue: true);
applyFilterToRelatedVideos = setting
.get(SettingBoxKey.applyFilterToRelatedVideos, defaultValue: true);
}
static bool filter(dynamic videoItem, {bool relatedVideos = false}) {
if (relatedVideos && !applyFilterToRelatedVideos) {
return false;
}
//由于相关视频中没有已关注标签,只能视为非关注视频
if (!relatedVideos &&
videoItem.isFollowed == 1 &&
exemptFilterForFollowed) {
return false;
}
if (videoItem.duration > 0 && videoItem.duration < minDurationForRcmd) {
return true;
}
if (videoItem.stat.view is int &&
videoItem.stat.view > -1 &&
videoItem.stat.like is int &&
videoItem.stat.like > -1 &&
videoItem.stat.like * 100 <
minLikeRatioForRecommend * videoItem.stat.view) {
return true;
}
return false;
}
}

View File

@ -42,6 +42,8 @@ class GStrorage {
return deletedEntries > 10; return deletedEntries > 10;
}, },
); );
// 视频设置
video = await Hive.openBox('video');
} }
static void regAdapter() { static void regAdapter() {
@ -52,11 +54,6 @@ class GStrorage {
Hive.registerAdapter(HotSearchItemAdapter()); Hive.registerAdapter(HotSearchItemAdapter());
} }
static Future<void> lazyInit() async {
// 视频设置
video = await Hive.openBox('video');
}
static Future<void> close() async { static Future<void> close() async {
// user.compact(); // user.compact();
// user.close(); // user.close();
@ -105,17 +102,24 @@ class SettingBoxKey {
/// 隐私 /// 隐私
blackMidsList = 'blackMidsList', blackMidsList = 'blackMidsList',
/// 推荐
enableRcmdDynamic = 'enableRcmdDynamic',
defaultRcmdType = 'defaultRcmdType',
enableSaveLastData = 'enableSaveLastData',
minDurationForRcmd = 'minDurationForRcmd',
minLikeRatioForRecommend = 'minLikeRatioForRecommend',
exemptFilterForFollowed = 'exemptFilterForFollowed',
//filterUnfollowedRatio = 'filterUnfollowedRatio',
applyFilterToRelatedVideos = 'applyFilterToRelatedVideos',
/// 其他 /// 其他
autoUpdate = 'autoUpdate', autoUpdate = 'autoUpdate',
defaultRcmdType = 'defaultRcmdType',
replySortType = 'replySortType', replySortType = 'replySortType',
defaultDynamicType = 'defaultDynamicType', defaultDynamicType = 'defaultDynamicType',
enableHotKey = 'enableHotKey', enableHotKey = 'enableHotKey',
enableQuickFav = 'enableQuickFav', enableQuickFav = 'enableQuickFav',
enableWordRe = 'enableWordRe', enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord', enableSearchWord = 'enableSearchWord',
enableRcmdDynamic = 'enableRcmdDynamic',
enableSaveLastData = 'enableSaveLastData',
enableSystemProxy = 'enableSystemProxy', enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi'; enableAi = 'enableAi';

61
lib/utils/url_utils.dart Normal file
View File

@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import '../http/search.dart';
import 'id_utils.dart';
import 'utils.dart';
class UrlUtils {
// 302重定向路由截取
static Future<String> parseRedirectUrl(String url) async {
late String redirectUrl;
final dio = Dio();
dio.options.followRedirects = false;
dio.options.validateStatus = (status) {
return status == 200 || status == 301 || status == 302;
};
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}
return redirectUrl;
}
// 匹配url路由跳转
static matchUrlPush(
String pathSegment,
String title,
String redirectUrl,
) async {
final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (matchRes.containsKey('BV')) {
final String bv = matchRes['BV'];
final int cid = await SearchHttp.ab2c(bvid: bv);
final String heroTag = Utils.makeHeroTag(bv);
await Get.toNamed(
'/video?bvid=$bv&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} else {
await Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title,
},
);
}
}
}

View File

@ -9,7 +9,6 @@ import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get_utils/get_utils.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -28,10 +27,16 @@ class Utils {
return tempPath; return tempPath;
} }
static String numFormat(int number) { static String numFormat(dynamic number) {
if (number == null){
return '0';
}
if (number is String) {
return number;
}
final String res = (number / 10000).toString(); final String res = (number / 10000).toString();
if (int.parse(res.split('.')[0]) >= 1) { if (int.parse(res.split('.')[0]) >= 1) {
return '${(number / 10000).toPrecision(1)}'; return '${(number / 10000).toStringAsFixed(1)}';
} else { } else {
return number.toString(); return number.toString();
} }

View File

@ -188,6 +188,7 @@ flutter:
- assets/images/ - assets/images/
- assets/images/lv/ - assets/images/lv/
- assets/images/logo/ - assets/images/logo/
- assets/images/live/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware