mod: 新增推荐过滤器,回退model转换修改,移除不必要的futureBuilder

This commit is contained in:
orz12
2024-01-20 17:07:10 +08:00
parent 41ddeab41a
commit 9122dd7f3a
14 changed files with 274 additions and 60 deletions

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),
) )
], ],
], ],
@ -330,10 +330,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

@ -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';
@ -49,7 +50,10 @@ class VideoHttp {
if (i['goto'] == 'av' && if (i['goto'] == 'av' &&
(i['owner'] != null && (i['owner'] != null &&
!blackMidsList.contains(i['owner']['mid']))) { !blackMidsList.contains(i['owner']['mid']))) {
list.add(RecVideoItemModel.fromJson(i)); RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)){
list.add(videoItem);
}
} }
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
@ -93,7 +97,10 @@ 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};
@ -209,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 {

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';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -32,6 +33,7 @@ void main() async {
await setupServiceLocator(); await setupServiceLocator();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
RecommendFilter();
runApp(const MyApp()); runApp(const MyApp());
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

View File

@ -40,7 +40,7 @@ class RecVideoItemAppModel {
@HiveField(5) @HiveField(5)
RcmdStat? stat; RcmdStat? stat;
@HiveField(6) @HiveField(6)
String? duration; int? duration;
@HiveField(7) @HiveField(7)
String? title; String? title;
@HiveField(8) @HiveField(8)
@ -79,13 +79,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

@ -23,7 +23,7 @@ class RecVideoItemAppModelAdapter extends TypeAdapter<RecVideoItemAppModel> {
cid: fields[3] as int?, cid: fields[3] as int?,
pic: fields[4] as String?, pic: fields[4] as String?,
stat: fields[5] as RcmdStat?, stat: fields[5] as RcmdStat?,
duration: fields[6] as String?, duration: fields[6] as int?,
title: fields[7] as String?, title: fields[7] as String?,
isFollowed: fields[8] as int?, isFollowed: fields[8] as int?,
owner: fields[9] as RcmdOwner?, owner: fields[9] as RcmdOwner?,

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

@ -55,12 +55,13 @@ class RcmdController extends GetxController {
} }
late final Map<String,dynamic> res; late final Map<String,dynamic> res;
switch (defaultRcmdType) { switch (defaultRcmdType) {
case 'app': case 'notLogin': case 'app':
res = await VideoHttp.rcmdVideoListApp( case 'notLogin':
loginStatus: defaultRcmdType != 'notLogin', res = await VideoHttp.rcmdVideoListApp(
freshIdx: _currentPage, loginStatus: defaultRcmdType != 'notLogin',
); freshIdx: _currentPage,
break; );
break;
default: //'web' default: //'web'
res = await VideoHttp.rcmdVideoList( res = await VideoHttp.rcmdVideoList(
freshIdx: _currentPage, freshIdx: _currentPage,
@ -83,10 +84,16 @@ class RcmdController extends GetxController {
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
videoList.addAll(res['data']); videoList.addAll(res['data']);
} }
// 目前仅支持app端系列保存缓存
if (defaultRcmdType != 'web') { if (defaultRcmdType != 'web') {
recVideo.put('cacheList', res['data']); recVideo.put('cacheList', res['data']);
} }
_currentPage += 1; _currentPage += 1;
// 若videoList数量太小可能会影响翻页此时再次请求
// 为避免请求到的数据太少时还在反复请求要求本次返回数据大于1条才触发
if (res['data'].length > 1 && videoList.length < 10){
queryRcmdFeed('onLoad');
}
} else { } else {
Get.snackbar('提示', res['msg']); Get.snackbar('提示', res['msg']);
} }

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';
@ -8,7 +7,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; // import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart'; import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
@ -26,7 +25,6 @@ class RcmdPage extends StatefulWidget {
class _RcmdPageState extends State<RcmdPage> class _RcmdPageState extends State<RcmdPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final RcmdController _rcmdController = Get.put(RcmdController()); final RcmdController _rcmdController = Get.put(RcmdController());
late Future _futureBuilderFuture;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -34,7 +32,7 @@ class _RcmdPageState extends State<RcmdPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init'); _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController; ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream = StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream; Get.find<MainController>().bottomBarStream;
@ -90,21 +88,21 @@ class _RcmdPageState extends State<RcmdPage>
slivers: [ slivers: [
SliverPadding( SliverPadding(
padding: padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0), const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
sliver: Obx(() { sliver: Obx(() { // 使用Obx来监听数据的变化
// 使用Obx来监听数据的变化 if (_rcmdController.isLoadingMore && _rcmdController.videoList.isEmpty) {
if (_rcmdController.isLoadingMore) {
// 如果正在加载,则显示骨架屏
return contentGrid(_rcmdController, []); return contentGrid(_rcmdController, []);
// 如果正在加载并且列表为空,则显示加载指示器
// return const SliverToBoxAdapter(
// child: Center(child: CircularProgressIndicator()),
// );
} else { } else {
// 显示视频列表 // 显示视频列表
return contentGrid( return contentGrid(_rcmdController, _rcmdController.videoList);
_rcmdController,
_rcmdController.videoList);
} }
}), }),
), ),
LoadingMore(ctr: _rcmdController) LoadingMore(ctr: _rcmdController),
], ],
), ),
), ),

View File

@ -4,6 +4,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/common/rcmd_type.dart'; import 'package:pilipala/models/common/rcmd_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@ -23,6 +24,9 @@ class _RecommendSettingState extends State<RecommendSetting> {
late dynamic userInfo; late dynamic userInfo;
bool userLogin = false; bool userLogin = false;
late dynamic accessKeyInfo; late dynamic accessKeyInfo;
// late int filterUnfollowedRatio;
late int minDurationForRcmd;
late int minLikeRatioForRecommend;
@override @override
void initState() { void initState() {
@ -33,6 +37,12 @@ class _RecommendSettingState extends State<RecommendSetting> {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null; userLogin = userInfo != null;
accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: 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 @override
@ -53,23 +63,11 @@ class _RecommendSettingState extends State<RecommendSetting> {
), ),
body: ListView( body: ListView(
children: [ children: [
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem(
title: '首页推荐刷新',
subTitle: '下拉刷新时保留上次内容',
setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false,
),
ListTile( ListTile(
dense: false, dense: false,
title: Text('首页推荐类型', style: titleStyle), title: Text('首页推荐类型', style: titleStyle),
subtitle: Text( subtitle: Text(
'当前使用「$defaultRcmdType端」推荐', '当前使用「$defaultRcmdType端」推荐¹',
style: subTitleStyle, style: subTitleStyle,
), ),
onTap: () async { onTap: () async {
@ -100,7 +98,7 @@ class _RecommendSettingState extends State<RecommendSetting> {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: const Text( content: const Text(
'使用app端推荐需获取access_key有小概率触发风控导致账号退出在官方app重新登录即可解除是否继续'), '使用app端推荐需获取access_key有小概率触发风控导致账号退出在官方版本app重新登录即可解除是否继续'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -130,6 +128,131 @@ class _RecommendSettingState extends State<RecommendSetting> {
} }
}, },
), ),
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

@ -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

@ -124,6 +124,11 @@ class SettingBoxKey {
enableRcmdDynamic = 'enableRcmdDynamic', enableRcmdDynamic = 'enableRcmdDynamic',
defaultRcmdType = 'defaultRcmdType', defaultRcmdType = 'defaultRcmdType',
enableSaveLastData = 'enableSaveLastData', enableSaveLastData = 'enableSaveLastData',
minDurationForRcmd = 'minDurationForRcmd',
minLikeRatioForRecommend = 'minLikeRatioForRecommend',
exemptFilterForFollowed = 'exemptFilterForFollowed',
//filterUnfollowedRatio = 'filterUnfollowedRatio',
applyFilterToRelatedVideos = 'applyFilterToRelatedVideos',
/// 其他 /// 其他
autoUpdate = 'autoUpdate', autoUpdate = 'autoUpdate',

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();
} }