Compare commits

...

77 Commits

Author SHA1 Message Date
2ece96df21 fix: 合集顺序播放 2023-11-12 14:07:50 +08:00
c11c5695a2 mod: 合集连播 2023-11-08 23:02:13 +08:00
93581c2932 mod: 播放详情页样式 2023-11-03 23:40:47 +08:00
fd43a8cb31 mod: 历史记录搜索 2023-11-03 23:39:21 +08:00
6f34bacb64 mod: 样式 2023-10-15 16:15:16 +08:00
90314f89ed mod: 还原全屏方式 2023-10-14 17:18:00 +08:00
45c53de2c2 mod: 投稿搜索无内容样式 2023-10-14 17:04:35 +08:00
5c07cb4545 mod: 投稿搜索无内容样式 2023-10-14 16:59:03 +08:00
844053b138 mod: 首次登录时自动加载黑名单 2023-10-14 16:46:41 +08:00
6af8b91f63 fix: 黑名单个数 2023-10-14 16:15:14 +08:00
8aa02f7450 mod: 移除黑名单 2023-10-14 16:07:52 +08:00
38a1f2e1f7 fix: 搜索黑名单问题 2023-10-10 00:01:41 +08:00
b2e1d98f51 fix: 搜索黑名单问题 2023-10-10 00:00:38 +08:00
e19cf92992 mod: 首页布局 2023-10-08 23:16:39 +08:00
80e10aeaad mod: 历史记录删除逻辑 2023-10-08 22:43:44 +08:00
b1a9152a49 mod: 搜索结果拉黑用户逻辑 2023-10-08 22:21:40 +08:00
935b7577b3 mod: 历史记录多选选中样式 2023-10-02 22:12:20 +08:00
fd5c4463d2 mod: 历史记录多选选中样式 2023-10-01 14:27:21 +08:00
7fcbe4dd9d feat: 历史记录多选删除 2023-10-01 10:50:45 +08:00
9a0c9f4021 feat: 按照黑名单对搜索结果进行屏蔽 2023-09-27 23:24:55 +08:00
6b3773a074 mod: 个人主页隐藏背景图 2023-09-27 22:22:23 +08:00
7be8ebaa7e mod: 搜索up主投稿 2023-09-27 22:19:49 +08:00
092b1cee3d mod 2023-09-20 22:53:29 +08:00
252f39e8c7 mod 2023-09-17 20:26:00 +08:00
e631ca04a0 mod: 隐藏签名 2023-09-10 14:52:00 +08:00
3665d6a0f6 mod: 个人中心注释 2023-09-10 14:42:14 +08:00
e3c9e8c13b mod: 移除热搜、搜索提示词 2023-09-09 13:33:30 +08:00
39995bae23 mod: 隐藏番剧、直播搜索 2023-09-09 12:18:38 +08:00
f42d0d01ea mod: custom version 2 2023-09-09 11:46:24 +08:00
0e39453558 fix: 详情页hero取值、请求contentType 2023-09-09 09:41:42 +08:00
8ff4259972 v1.0.7 更新 2023-09-08 22:01:25 +08:00
5082dc6d59 mod: 直播功能注释 2023-09-08 21:51:29 +08:00
627df8e6ad Merge branch 'feature-fullScreen' 2023-09-08 21:04:01 +08:00
2467fd0dea mod: 合并design htmlRender 2023-09-08 18:38:23 +08:00
c6f6af4628 Merge branch 'fix' into alpha 2023-09-08 18:35:36 +08:00
9e907f9151 fix: 转发动态点击、动态详情页加tag 2023-09-08 18:35:05 +08:00
22e17d437b fix: 动态内容图片预览 2023-09-08 18:04:13 +08:00
8a06ce65a5 Merge branch 'fix' into alpha 2023-09-08 17:21:33 +08:00
72ff3fdab0 fix and fix dynamics render 2023-09-08 17:21:11 +08:00
517ca032d2 feat: 动态页自渲染 2023-09-08 16:46:51 +08:00
396f9fbbac Merge branch 'design' into alpha 2023-09-08 13:19:24 +08:00
c0332c74d7 mod: 图片预览优化 issues #61 2023-09-08 13:19:11 +08:00
2669b41ede mod: 修改全屏方式 issues #59 2023-09-07 23:24:05 +08:00
81dace96d7 Merge branch 'design' into alpha 2023-09-07 22:57:12 +08:00
d693d7ad6c Merge branch 'fix' into alpha 2023-09-07 22:57:06 +08:00
8c02a566f6 mod: 还原音轨加载方式(某些资源无声) 2023-09-07 22:56:52 +08:00
a2420d0bef fix: 默认音轨质量 2023-09-07 22:52:46 +08:00
29d3f78da9 Merge branch 'fix' into alpha 2023-09-07 20:31:59 +08:00
a864bea3f4 fix: 专栏动态内容重复 2023-09-07 20:31:45 +08:00
070156da86 Merge branch 'fix' into alpha 2023-09-07 20:04:12 +08:00
69f846760d fix: 尝试解决网络异常导致的白屏 issues #67 2023-09-07 20:03:48 +08:00
2ca79003bf Merge branch 'feature-pip' into alpha 2023-09-07 19:26:00 +08:00
18af065a1e Merge branch 'design' into alpha 2023-09-07 19:25:49 +08:00
0ad54d8c0b mod: 推荐视频增加时长显示 issues #71、推荐栏刷新逻辑 2023-09-07 19:25:35 +08:00
0dfcd4ed40 feat: 视频、直播pip Android端 2023-09-07 18:58:58 +08:00
5b953ae0be feat: 简易后台播放 2023-09-07 08:49:07 +08:00
392980f0e8 fix: 退出后重进,双击不能继续播放 issues #68 2023-09-06 23:08:02 +08:00
4c938ed8aa fix: 动态数据渲染异常 2023-09-06 22:47:54 +08:00
7f961e998c mod: 移除main代理 2023-09-06 21:14:57 +08:00
e6b307ddd7 Merge branch 'fix' into alpha 2023-09-06 21:10:50 +08:00
f5b4ad33c6 fix: iOS端开启代理后请求异常 2023-09-05 21:36:16 +08:00
a2d4613293 merge design 2023-09-04 15:17:19 +08:00
1bebb32a0d mod: 直播页面控制条 2023-09-04 15:01:02 +08:00
217b036ee3 Merge branch 'fix' into alpha 2023-09-04 13:14:52 +08:00
fa95ae0cce mereg feature-media_kit 2023-09-04 13:12:24 +08:00
977bac84c3 mod: 升级播放器依赖、取消buffer遮罩 2023-09-04 13:11:22 +08:00
0f134b8dca mod: 修改取消收藏的逻辑 issues#60 2023-09-04 12:41:28 +08:00
daec283bdf Merge branch 'design' into alpha 2023-09-04 11:17:12 +08:00
6f84eefbe4 feat: 转发内容加上视频标题 issues#63 2023-09-04 11:16:52 +08:00
4a7f2f027f Merge branch 'design' into alpha 2023-09-04 11:11:27 +08:00
a39f81ac2a feat: 弹幕设置 2023-09-04 11:10:54 +08:00
0cb580ba8e fix: 锁定状态、未开启自动播放时返回逻辑 2023-09-03 16:30:03 +08:00
c7187f2456 Merge branch 'design' into alpha 2023-09-03 14:37:03 +08:00
cd38c0799d fix: 竖屏状态下系统状态栏不隐藏 issues#58 2023-09-03 14:36:44 +08:00
aa63007c8a feat: 代理设置 2023-09-03 13:46:51 +08:00
b9b1ac7ec5 feat: 增加设置项 2023-09-03 13:22:20 +08:00
4036262bed fix: 设置项重复、更新内容高度溢出 2023-09-02 21:17:58 +08:00
95 changed files with 4224 additions and 2159 deletions

22
change_log/1.0.7.0908.md Normal file
View File

@ -0,0 +1,22 @@
## 1.0.7
默认倍速、直播弹幕、专栏等功能开发中
### 新功能
+ 弹幕设置、屏蔽功能
+ 不是很完美的后台播放功能
+ 不是很完美的画中画(pip)功能Android端
### 修复
+ 动态页面加载异常
+ 网络异常时页面空白
+ 竖屏全屏状态栏问题
+ iOS端代理请求异常
### 优化
+ 图片预览
+ 全屏播放时自动旋转
+ 转发内容增加视频标题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -37,6 +37,8 @@ PODS:
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
- status_bar_control (3.2.1): - status_bar_control (3.2.1):
- Flutter - Flutter
- system_proxy (0.0.1):
- Flutter
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1): - volume_controller (0.0.1):
@ -65,6 +67,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`) - status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -109,6 +112,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
status_bar_control: status_bar_control:
:path: ".symlinks/plugins/status_bar_control/ios" :path: ".symlinks/plugins/status_bar_control/ios"
system_proxy:
:path: ".symlinks/plugins/system_proxy/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller: volume_controller:
@ -139,6 +144,7 @@ SPEC CHECKSUMS:
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47

View File

@ -0,0 +1,136 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
// ignore: must_be_immutable
class HtmlRender extends StatelessWidget {
String? htmlContent;
final int? imgCount;
final List? imgList;
HtmlRender({
this.htmlContent,
this.imgCount,
this.imgList,
super.key,
});
@override
Widget build(BuildContext context) {
return Html(
data: htmlContent,
// tagsList: Html.tags..addAll(["form", "label", "input"]),
onLinkTap: (url, buildContext, attributes) => {},
extensions: [
TagExtension(
tagsToExtend: {"img"},
builder: (extensionContext) {
String? imgUrl = extensionContext.attributes['src'];
if (imgUrl!.startsWith('//')) {
imgUrl = 'https:$imgUrl';
}
if (imgUrl.startsWith('http://')) {
imgUrl = imgUrl.replaceAll('http://', 'https://');
}
print(imgUrl);
bool isEmote = imgUrl.contains('/emote/');
bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return SizedBox();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!);
return Image.network(
imgUrl,
width: isEmote ? 22 : null,
height: isEmote ? 22 : null,
);
},
),
],
style: {
"html": Style(
fontSize: FontSize.medium,
lineHeight: LineHeight.percent(140),
),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
"a": Style(
color: Theme.of(context).colorScheme.primary,
textDecoration: TextDecoration.none,
),
"p": Style(
margin: Margins.only(bottom: 0),
),
"span": Style(
fontSize: FontSize.medium,
),
"li > p": Style(
display: Display.inline,
),
"li": Style(
padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify,
),
"image": Style(margin: Margins.only(top: 4, bottom: 4)),
"p > img": Style(margin: Margins.only(top: 4, bottom: 4)),
"code": Style(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface),
"code > span": Style(textAlign: TextAlign.start),
"hr": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'table': Style(
border: Border(
right: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
bottom: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'tr': Style(
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
left: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'thead': Style(
backgroundColor: Theme.of(context).colorScheme.background,
),
'th': Style(
padding: HtmlPaddings.only(left: 3, right: 3),
),
'td': Style(
padding: HtmlPaddings.all(4.0),
alignment: Alignment.center,
textAlign: TextAlign.center,
),
},
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/member/index.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';
@ -33,9 +34,10 @@ class VideoCardH extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid); String heroTag = Utils.makeHeroTag(aid);
return GestureDetector( return GestureDetector(
onLongPress: () { onLongPress: () {
if (longPress != null) { // if (longPress != null) {
longPress!(); // longPress!();
} // }
MemberController().blockUser(videoItem.mid);
}, },
// onLongPressEnd: (details) { // onLongPressEnd: (details) {
// if (longPressEnd != null) { // if (longPressEnd != null) {
@ -188,46 +190,7 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
],
),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
// Text(
// Utils.dateFormat(videoItem.pubdate!),
// style: TextStyle(
// fontSize: 11,
// color: Theme.of(context).colorScheme.outline),
// )
const Spacer(), const Spacer(),
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
if (source == 'normal') if (source == 'normal')
SizedBox( SizedBox(
width: 24, width: 24,
@ -261,6 +224,20 @@ class VideoContent extends StatelessWidget {
], ],
), ),
), ),
// PopupMenuItem<String>(
// onTap: () async {
// MemberController().blockUser(videoItem.mid);
// },
// value: 'block',
// height: 35,
// child: const Row(
// children: [
// Icon(Icons.block, size: 16),
// SizedBox(width: 6),
// Text('拉黑up', style: TextStyle(fontSize: 13))
// ],
// ),
// ),
], ],
), ),
), ),

View File

@ -48,7 +48,7 @@ class VideoCardV extends StatelessWidget {
arguments: { arguments: {
'pic': videoItem.pic, 'pic': videoItem.pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
), ),
); );
@ -112,12 +112,22 @@ class VideoCardV extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
if (crossAxisCount == 1 && videoItem.duration != null) if (videoItem.duration != null)
if (crossAxisCount == 1) ...[
PBadge( PBadge(
bottom: 10, bottom: 10,
right: 10, right: 10,
text: videoItem.duration, text: videoItem.duration,
) )
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: videoItem.duration,
)
],
], ],
); );
}), }),
@ -174,7 +184,7 @@ class VideoContent extends StatelessWidget {
], ],
), ),
if (crossAxisCount > 1) ...[ if (crossAxisCount > 1) ...[
const SizedBox(height: 3), const SizedBox(height: 2),
VideoStat( VideoStat(
videoItem: videoItem, videoItem: videoItem,
), ),
@ -247,7 +257,7 @@ class VideoContent extends StatelessWidget {
}, },
), ),
] else ...[ ] else ...[
const SizedBox(height: 26) const SizedBox(height: 24)
] ]
], ],
), ),

View File

@ -164,6 +164,9 @@ class Api {
// 清空历史记录 // 清空历史记录
static const String clearHistory = '/x/v2/history/clear'; static const String clearHistory = '/x/v2/history/clear';
// 删除某条历史记录
static const String delHistory = '/x/v2/history/delete';
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword'; 'https://s.search.bilibili.com/main/hotword';
@ -239,6 +242,9 @@ class Api {
// wts=1689767832 // wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search'; static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// 用户动态 // 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space'; static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -285,6 +291,9 @@ class Api {
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
// 移除黑名单
static const String removeBlack = '/x/relation/modify';
// github 获取最新版 // github 获取最新版
static const String latestApp = static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest'; 'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
@ -294,4 +303,7 @@ class Api {
static const String onlineTotal = '/x/player/online/total'; static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
// 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search';
} }

View File

@ -23,4 +23,31 @@ class BlackHttp {
}; };
} }
} }
// 移除黑名单
static Future removeBlack({required int fid}) async {
var res = await Request().post(
Api.removeBlack,
queryParameters: {
'act': 6,
'csrf': await Request.getCsrf(),
'fid': fid,
'jsonp': 'jsonp',
're_src': 116,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': [],
'msg': '操作成功',
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

40
lib/http/html.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:pilipala/http/index.dart';
class HtmlHttp {
static Future reqHtml(id) async {
var response = await Request().get("https://www.bilibili.com/opus/$id");
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
// 头像
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
avatar = 'https:${avatar.split('@')[0]}';
String uname =
authorHeader.querySelector('.fixed-author-header__author__name')!.text;
// 动态详情
Element opusDetail = appDom.querySelector('.opus-detail')!;
// 发布时间
String updateTime =
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
//
String opusContent =
opusDetail.querySelector('.opus-module-content')!.innerHtml;
String commentId = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': opusContent,
'commentId': commentId
};
}
}

View File

@ -9,7 +9,6 @@ import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/interceptor.dart'; import 'package:pilipala/http/interceptor.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request { class Request {
@ -93,18 +92,14 @@ class Request {
receiveTimeout: const Duration(milliseconds: 12000), receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头. //Http请求头.
headers: { headers: {
// 'cookie': '', 'keep-alive': true,
'user-agent': headerUa('pc'),
'Accept-Encoding': 'gzip'
}, },
persistentConnection: true,
); );
dio = Dio(options) dio = Dio(options);
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
//添加拦截器 //添加拦截器
dio.interceptors.add(ApiInterceptor()); dio.interceptors.add(ApiInterceptor());
@ -216,7 +211,7 @@ class Request {
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'; : 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
} else { } else {
headerUa = headerUa =
'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'; 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15';
} }
return headerUa; return headerUa;
} }

View File

@ -65,7 +65,7 @@ class MemberHttp {
int ps = 30, int ps = 30,
int tid = 0, int tid = 0,
int? pn, int? pn,
String keyword = '', String? keyword,
String order = 'pubdate', String order = 'pubdate',
bool orderAvoided = true, bool orderAvoided = true,
}) async { }) async {
@ -74,7 +74,7 @@ class MemberHttp {
'ps': ps, 'ps': ps,
'tid': tid, 'tid': tid,
'pn': pn, 'pn': pn,
'keyword': keyword, 'keyword': keyword ?? '',
'order': order, 'order': order,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 1550101,
@ -119,4 +119,27 @@ class MemberHttp {
}; };
} }
} }
// 搜索用户动态
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
var res = await Request().get(Api.memberDynamic, data: {
'keyword': '海拔',
'mid': mid,
'pn': pn,
'ps': ps,
'platform': 'web'
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -1,18 +1,56 @@
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data['code'] == 0) { if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(resultMap),
};
}
} else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': HotSearchModel.fromJson(res.data), 'data': HotSearchModel.fromJson(res.data),
}; };
}
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
if (resultMap['result'] is Map) {
resultMap['result']['term'] = term;
}
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else { } else {
return { return {
'status': false, 'status': false,
@ -20,18 +58,6 @@ class SearchHttp {
'msg': '请求错误 🙅', 'msg': '请求错误 🙅',
}; };
} }
}
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) {
res.data['result']['term'] = term;
return {
'status': true,
'data': SearchSuggestModel.fromJson(res.data['result']),
};
} else { } else {
return { return {
'status': false, 'status': false,
@ -61,8 +87,15 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData); var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data; Object data;
try {
switch (searchType) { switch (searchType) {
case SearchType.video: case SearchType.video:
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['result']) {
// 屏蔽推广和拉黑用户
i['available'] = !blackMidsList.contains(i['mid']);
}
data = SearchVideoModel.fromJson(res.data['data']); data = SearchVideoModel.fromJson(res.data['data']);
break; break;
case SearchType.live_room: case SearchType.live_room:
@ -74,16 +107,24 @@ class SearchHttp {
case SearchType.media_bangumi: case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']); data = SearchMBangumiModel.fromJson(res.data['data']);
break; break;
case SearchType.article:
data = SearchArticleModel.fromJson(res.data['data']);
break;
} }
return { return {
'status': true, 'status': true,
'data': data, 'data': data,
}; };
} catch (err) {
print(err);
}
} else { } else {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅', 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
? '没有相关数据'
: res.data['message'],
}; };
} }
} }

View File

@ -231,4 +231,39 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
// 删除历史记录
static Future delHistory(kid) async {
var res = await Request().post(
Api.delHistory,
queryParameters: {
'kid': kid,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '已删除'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 搜索历史记录
static Future searchHistory(
{required int pn, required String keyword}) async {
var res = await Request().get(
Api.searchHistory,
data: {
'pn': pn,
'keyword': keyword,
'business': 'all',
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
} }

View File

@ -20,6 +20,8 @@ import 'package:pilipala/utils/storage.dart';
class VideoHttp { class VideoHttp {
static Box localCache = GStrorage.localCache; static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting; static Box setting = GStrorage.setting;
static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
// 首页推荐视频 // 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async { static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@ -73,6 +75,7 @@ class VideoHttp {
for (var i in res.data['data']['items']) { for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户 // 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' && if (i['card_goto'] != 'ad_av' &&
(!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)); list.add(RecVideoItemAppModel.fromJson(i));

View File

@ -12,20 +12,20 @@ enum SearchType {
live_room, live_room,
// 主播live_user // 主播live_user
// live_user, // live_user,
// 专栏article
// article,
// 话题topic // 话题topic
// topic, // topic,
// 用户bili_user // 用户bili_user
bili_user, bili_user,
// 专栏article
article,
// 相簿photo // 相簿photo
// photo // photo
} }
extension SearchTypeExtension on SearchType { extension SearchTypeExtension on SearchType {
String get type => String get type =>
['video', 'media_bangumi', 'live_room', 'bili_user'][index]; ['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index]; String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
} }
// 搜索类型为视频、专栏及相簿时 // 搜索类型为视频、专栏及相簿时

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list; List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) { SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e)) .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList(); .toList();
} }
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id, this.id,
this.cid, this.cid,
// this.author, // this.author,
// this.mid, this.mid,
// this.typeid, // this.typeid,
// this.typename, // this.typename,
this.arcurl, this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id; int? id;
int? cid; int? cid;
// String? author; // String? author;
// String? mid; int? mid;
// String? typeid; // String? typeid;
// String? typename; // String? typename;
String? arcurl; String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl']; arcurl = json['arcurl'];
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); // title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
description = json['description']; description = json['description'];
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
indexShow = json['index_show']; indexShow = json['index_show'];
} }
} }
class SearchArticleModel {
SearchArticleModel({this.list});
List<SearchArticleItemModel>? list;
SearchArticleModel.fromJson(Map<String, dynamic> json) {
list = json['result'] != null
? json['result']
.map<SearchArticleItemModel>(
(e) => SearchArticleItemModel.fromJson(e))
.toList()
: [];
}
}
class SearchArticleItemModel {
SearchArticleItemModel({
this.pubTime,
this.like,
this.title,
this.subTitle,
this.rankOffset,
this.mid,
this.imageUrls,
this.id,
this.categoryId,
this.view,
this.reply,
this.desc,
this.rankScore,
this.type,
this.templateId,
this.categoryName,
});
int? pubTime;
int? like;
List? title;
String? subTitle;
int? rankOffset;
int? mid;
List? imageUrls;
int? id;
int? categoryId;
int? view;
int? reply;
String? desc;
int? rankScore;
String? type;
int? templateId;
String? categoryName;
SearchArticleItemModel.fromJson(Map<String, dynamic> json) {
pubTime = json['pub_time'];
like = json['like'];
title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
rankOffset = json['rank_offset'];
mid = json['mid'];
imageUrls = json['image_urls'];
id = json['id'];
categoryId = json['category_id'];
view = json['view'];
reply = json['reply'];
desc = json['desc'];
rankScore = json['rank_score'];
type = json['type'];
templateId = json['templateId'];
categoryName = json['category_name'];
}
}

View File

@ -3,17 +3,23 @@ class HistoryData {
this.cursor, this.cursor,
this.tab, this.tab,
this.list, this.list,
this.page,
}); });
Cursor? cursor; Cursor? cursor;
List<HisTabItem>? tab; List<HisTabItem>? tab;
List<HisListItem>? list; List<HisListItem>? list;
Map? page;
HistoryData.fromJson(Map<String, dynamic> json) { HistoryData.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']); cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList(); tab = json['tab'] != null
list = ? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList(); : [];
list = json['list'] != null
? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()
: [];
page = json['page'];
} }
} }
@ -79,6 +85,7 @@ class HisListItem {
this.kid, this.kid,
this.tagName, this.tagName,
this.liveStatus, this.liveStatus,
this.checked,
}); });
String? title; String? title;
@ -105,6 +112,7 @@ class HisListItem {
int? kid; int? kid;
String? tagName; String? tagName;
int? liveStatus; int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) { HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title']; title = json['title'];
@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid']; kid = json['kid'];
tagName = json['tag_name']; tagName = json['tag_name'];
liveStatus = json['live_status']; liveStatus = json['live_status'];
checked = false;
} }
} }

View File

@ -95,6 +95,17 @@ class _AboutPageState extends State<AboutPage> {
style: subTitleStyle, style: subTitleStyle,
), ),
), ),
ListTile(
onTap: () => _aboutController.panDownload(),
title: const Text('网盘下载'),
trailing: Text(
'提取码pili',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
),
ListTile( ListTile(
onTap: () => _aboutController.feedback(), onTap: () => _aboutController.feedback(),
title: const Text('问题反馈'), title: const Text('问题反馈'),
@ -195,6 +206,14 @@ class AboutController extends GetxController {
); );
} }
// 从网盘下载
panDownload() {
launchUrl(
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
mode: LaunchMode.externalApplication,
);
}
// 问题反馈 // 问题反馈
feedback() { feedback() {
launchUrl( launchUrl(

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
} }
return result; return result;
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) {
nextIndex = 0;
}
}
if (nextIndex <= episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
} }

View File

@ -113,6 +113,9 @@ class _BangumiPageState extends State<BangumiPage>
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == if (snapshot.connectionState ==
ConnectionState.done) { ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList; List list = _bangumidController.bangumiFollowList;
if (data['status']) { if (data['status']) {

View File

@ -62,7 +62,7 @@ class BangumiCardV extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -1,4 +1,5 @@
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:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false, centerTitle: false,
title: Obx( title: Obx(
() => Text( () => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000', '黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
// trailing: TextButton( trailing: TextButton(
// onPressed: () {}, onPressed: () => _blackListController
// child: const Text('移除'), .removeBlack(list[index].mid),
// ), child: const Text('移除'),
),
); );
}, },
), ),
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController { class BlackListController extends GetxController {
int currentPage = 1; int currentPage = 1;
int pageSize = 50; int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs; RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async { Future queryBlacklist({type = 'init'}) async {
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) { if (result['status']) {
if (type == 'init') { if (type == 'init') {
blackList.value = result['data'].list; blackList.value = result['data'].list;
total.value = result['data'].total;
} else { } else {
blackList.addAll(result['data'].list); blackList.addAll(result['data'].list);
} }
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
} }
return result; return result;
} }
Future removeBlack(mid) async {
var result = await BlackHttp.removeBlack(fid: mid);
if (result['status']) {
blackList.removeWhere((e) => e.mid == mid);
total.value = total.value - 1;
SmartDialog.showToast(result['msg']);
}
}
} }

View File

@ -29,6 +29,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
bool danmuPlayStatus = true; bool danmuPlayStatus = true;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
late bool enableShowDanmaku; late bool enableShowDanmaku;
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
@override @override
void initState() { void initState() {
@ -58,6 +63,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
} }
} }
}); });
blockTypes = playerController.blockTypes;
showArea = playerController.showArea;
opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal;
danmakuSpeedVal = playerController.danmakuSpeedVal;
} }
// 播放器状态监听 // 播放器状态监听
@ -77,6 +87,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
} }
PlDanmakuController ctr = _plDanmakuController; PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds; int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes;
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
return; return;
@ -99,6 +110,8 @@ class _PlDanmakuState extends State<PlDanmaku> {
var delta = currentPosition - element.progress; var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) { if (delta >= 0 && delta < 200) {
// 屏蔽彩色弹幕
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
_controller!.addItems([ _controller!.addItems([
DanmakuItem( DanmakuItem(
element.content, element.content,
@ -107,6 +120,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
type: DmUtils.getPosition(element.mode), type: DmUtils.getPosition(element.mode),
) )
]); ]);
}
ctr.currentDmIndex++; ctr.currentDmIndex++;
} else { } else {
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
@ -135,9 +149,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
widget.playerController.danmakuController = _controller = e; widget.playerController.danmakuController = _controller = e;
}, },
option: DanmakuOption( option: DanmakuOption(
fontSize: 15, fontSize: 15 * fontSizeVal,
area: 0.5, area: showArea,
duration: 5, opacity: opacityVal,
duration: danmakuSpeedVal * widget.playerController.playbackSpeed,
), ),
statusChanged: (isPlaying) {}, statusChanged: (isPlaying) {},
), ),

View File

@ -214,7 +214,7 @@ class DynamicsController extends GetxController {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -38,15 +38,25 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
// floor 1原创 2转发 // floor 1原创 2转发
if (Get.arguments['floor'] == 1) { if (Get.arguments['floor'] == 1) {
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']); oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
print(oid);
} else {
try {
String type = Get.arguments['item'].modules.moduleDynamic.major.type;
/// TODO
if (type == 'MAJOR_TYPE_OPUS') {
} else { } else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id; oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
} }
} catch (_) {}
}
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11; int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
type = (commentType == 0) ? 11 : commentType; type = (commentType == 0) ? 11 : commentType;
action = action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null; Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = Get.put(DynamicDetailController(oid, type)); _dynamicDetailController =
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
_futureBuilderFuture = _dynamicDetailController!.queryReplyList(); _futureBuilderFuture = _dynamicDetailController!.queryReplyList();
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
scrollController.addListener(_listen); scrollController.addListener(_listen);

View File

@ -212,6 +212,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFutureUp, future: _futureBuilderFutureUp,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data; Map data = snapshot.data;
if (data['status']) { if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value)); return Obx(() => UpPanel(_dynamicsController.upData.value));
@ -232,6 +235,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data; Map data = snapshot.data;
if (data['status']) { if (data['status']) {
List<DynamicItemModel> list = List<DynamicItemModel> list =

View File

@ -28,6 +28,8 @@ Widget content(item, context, source) {
focusNode: FocusNode(), focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(), selectionControls: MaterialTextSelectionControls(),
child: Text.rich( child: Text.rich(
/// fix 默认20px高度
style: const TextStyle(height: 0),
richNode(item, context), richNode(item, context),
maxLines: source == 'detail' ? 999 : 3, maxLines: source == 'detail' ? 999 : 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -41,7 +41,8 @@ class DynamicPanel extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: author(item, context), child: author(item, context),
), ),
if (item!.modules!.moduleDynamic!.desc != null) if (item!.modules!.moduleDynamic!.desc != null ||
item!.modules!.moduleDynamic!.major != null)
content(item, context, source), content(item, context, source),
forWard(item, context, _dynamicsController, source), forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2), const SizedBox(height: 2),

View File

@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
if (item.modules.moduleDynamic.topic != null) ...[
Padding( /// fix #话题跟content重复
padding: floor == 2 // if (item.modules.moduleDynamic.topic != null) ...[
? EdgeInsets.zero // Padding(
: const EdgeInsets.only(left: 12, right: 12), // padding: floor == 2
child: GestureDetector( // ? EdgeInsets.zero
child: Text( // : const EdgeInsets.only(left: 12, right: 12),
'#${item.modules.moduleDynamic.topic.name}', // child: GestureDetector(
style: authorStyle, // child: Text(
), // '#${item.modules.moduleDynamic.topic.name}',
), // style: authorStyle,
), // ),
], // ),
// ),
// ],
Text.rich( Text.rich(
richNode(item, context), richNode(item, context),
// 被转发状态(floor=2) 隐藏 // 被转发状态(floor=2) 隐藏
@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
: const EdgeInsets.only(left: 12, right: 12), : const EdgeInsets.only(left: 12, right: 12),
child: picWidget(item, context), child: picWidget(item, context),
), ),
/// 附加内容 商品信息、直播预约等等
if (item.modules.moduleDynamic.additional != null) if (item.modules.moduleDynamic.additional != null)
addWidget( addWidget(
item, item,
@ -133,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(item.modules.moduleDynamic.desc.text) Text.rich(
richNode(item, context),
// 被转发状态(floor=2) 隐藏
maxLines: source == 'detail' && floor != 2 ? 999 : 4,
overflow: TextOverflow.ellipsis,
),
], ],
) )
: item.modules.moduleDynamic.additional != null : item.modules.moduleDynamic.additional != null

View File

@ -1,20 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/preview/index.dart';
Widget picWidget(item, context) { Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type; String type = item.modules.moduleDynamic.major.type;
List pictures = []; List pictures = [];
if (type == 'MAJOR_TYPE_OPUS') { if (type == 'MAJOR_TYPE_OPUS') {
pictures = item.modules.moduleDynamic.major.opus.pics; /// fix 图片跟rich_node_panel重复
// pictures = item.modules.moduleDynamic.major.opus.pics;
return const SizedBox();
} }
if (type == 'MAJOR_TYPE_DRAW') { if (type == 'MAJOR_TYPE_DRAW') {
pictures = item.modules.moduleDynamic.major.draw.items; pictures = item.modules.moduleDynamic.major.draw.items;
} }
int len = pictures.length; int len = pictures.length;
List picList = []; List<String> picList = [];
List<Widget> list = []; List<Widget> list = [];
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url); picList.add(pictures[i].src ?? pictures[i].url);
@ -23,11 +25,14 @@ Widget picWidget(item, context) {
builder: (context, BoxConstraints box) { builder: (context, BoxConstraints box) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed('/preview', showDialog(
arguments: {'initialPage': i, 'imgList': picList}); useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
}, },
// child: Hero(
// tag: pictures[i].src ?? pictures[i].url,
child: NetworkImgLayer( child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url, src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth, width: box.maxWidth,

View File

@ -1,14 +1,34 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
// 富文本 // 富文本
InlineSpan richNode(item, context) { InlineSpan richNode(item, context) {
final spacer = _VerticalSpaceSpan(0.0);
try {
TextStyle authorStyle = TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary); TextStyle(color: Theme.of(context).colorScheme.primary);
List<InlineSpan> spanChilds = []; List<InlineSpan> spanChilds = [];
for (var i in item.modules.moduleDynamic.desc.richTextNodes) { String contentType = 'desc';
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
dynamic richTextNodes;
if (item.modules.moduleDynamic.desc != null) {
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
} else if (item.modules.moduleDynamic.major != null) {
contentType = 'major';
// 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes =
item.modules.moduleDynamic.major.opus.summary.richTextNodes;
}
if (richTextNodes == null || richTextNodes.isEmpty) {
return spacer;
} else {
for (var i in richTextNodes) {
/// fix 渲染专栏时内容会重复
if (item.modules.moduleDynamic.major.opus.title == null &&
i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
spanChilds.add( spanChilds.add(
TextSpan(text: i.origText, style: const TextStyle(height: 1.65))); TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
} }
@ -67,7 +87,11 @@ InlineSpan richNode(item, context) {
onTap: () { onTap: () {
Get.toNamed( Get.toNamed(
'/webview', '/webview',
parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''}, parameters: {
'url': i.origText,
'type': 'url',
'pageTitle': ''
},
); );
}, },
child: Text( child: Text(
@ -169,7 +193,127 @@ InlineSpan richNode(item, context) {
); );
} }
} }
if (contentType == 'major' &&
item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {
// 图片可能跟其他widget重复渲染
List<OpusPicsModel> pics = item.modules.moduleDynamic.major.opus.pics;
int len = pics.length;
List<String> picList = [];
if (len == 1) {
OpusPicsModel pictureItem = pics.first;
picList.add(pictureItem.url!);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: NetworkImgLayer(
src: pictureItem.url,
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
pictureItem.height! /
pictureItem.width!,
),
),
);
},
),
),
);
}
if (len > 1) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pics[i].url!);
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: pics[i].url,
width: box.maxWidth,
height: box.maxWidth,
),
);
},
),
);
}
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth;
double crossCount = len < 3 ? 2 : 3;
double height = maxWidth /
crossCount *
(len % crossCount == 0
? len ~/ crossCount
: len ~/ crossCount + 1) +
6;
return Container(
padding: const EdgeInsets.only(top: 6),
height: height,
child: GridView.count(
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossCount.toInt(),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
childAspectRatio: 1,
children: list,
),
);
},
),
),
);
}
// spanChilds.add(
// WidgetSpan(
// child: NetworkImgLayer(
// src: pics.first.url,
// type: 'emote',
// width: 100,
// height: 200,
// ),
// ),
// );
}
return TextSpan( return TextSpan(
children: spanChilds, children: spanChilds,
); );
} }
} catch (err) {
print('❌rich_node_panel err: $err');
return spacer;
}
}
class _VerticalSpaceSpan extends WidgetSpan {
_VerticalSpaceSpan(double height)
: super(child: SizedBox(height: height, width: double.infinity));
}

View File

@ -57,20 +57,21 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
const SizedBox(height: 6), const SizedBox(height: 6),
], ],
// const SizedBox(height: 4), // const SizedBox(height: 4),
if (item.modules.moduleDynamic.topic != null) ...[ /// fix #话题跟content重复
Padding( // if (item.modules.moduleDynamic.topic != null) ...[
padding: floor == 2 // Padding(
? EdgeInsets.zero // padding: floor == 2
: const EdgeInsets.only(left: 12, right: 12), // ? EdgeInsets.zero
child: GestureDetector( // : const EdgeInsets.only(left: 12, right: 12),
child: Text( // child: GestureDetector(
'#${item.modules.moduleDynamic.topic.name}', // child: Text(
style: authorStyle, // '#${item.modules.moduleDynamic.topic.name}',
), // style: authorStyle,
), // ),
), // ),
const SizedBox(height: 6), // ),
], // const SizedBox(height: 6),
// ],
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[ if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
Text.rich(richNode(item, context)), Text.rich(richNode(item, context)),
const SizedBox(height: 6), const SizedBox(height: 6),

View File

@ -14,7 +14,7 @@ class FavDetailController extends GetxController {
int currentPage = 1; int currentPage = 1;
bool isLoadingMore = false; bool isLoadingMore = false;
RxMap favInfo = {}.obs; RxMap favInfo = {}.obs;
RxList<FavDetailItemData> favList = [FavDetailItemData()].obs; RxList favList = [].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
int mediaCount = 0; int mediaCount = 0;
@ -61,15 +61,13 @@ class FavDetailController extends GetxController {
aid: id, addIds: '', delIds: mediaId.toString()); aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) { if (result['status']) {
if (result['data']['prompt']) { if (result['data']['prompt']) {
List<FavDetailItemData> dataList = favDetailData.value.medias!; List dataList = favList;
for (var i in dataList) { for (var i in dataList) {
if (i.id == id) { if (i.id == id) {
dataList.remove(i); dataList.remove(i);
break; break;
} }
} }
favDetailData.value.medias = dataList;
favDetailData.refresh();
SmartDialog.showToast('取消收藏'); SmartDialog.showToast('取消收藏');
} }
} }

View File

@ -168,7 +168,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Obx(
() => Text( () => Text(
'${_favDetailController.favInfo['media_count'] ?? '-'}条视频', '${_favDetailController.favList.length}条视频',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
@ -187,13 +187,19 @@ class _FavDetailPageState extends State<FavDetailPage> {
if (_favDetailController.item!.mediaCount == 0) { if (_favDetailController.item!.mediaCount == 0) {
return const NoData(); return const NoData();
} else { } else {
List favList = _favDetailController.favList;
return Obx( return Obx(
() => SliverList( () => favList.isEmpty
delegate: SliverChildBuilderDelegate((context, index) { ? const SliverToBoxAdapter(child: SizedBox())
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return FavVideoCardH( return FavVideoCardH(
videoItem: _favDetailController.favList[index], videoItem: favList[index],
callFn: () => _favDetailController
.onCancelFav(favList[index].id),
); );
}, childCount: _favDetailController.favList.length), }, childCount: favList.length),
), ),
); );
} }

View File

@ -10,42 +10,20 @@ 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 '../controller.dart';
// 收藏视频卡片 - 水平布局 // 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget { class FavVideoCardH extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final FavDetailController _favDetailController = final Function? callFn;
Get.put(FavDetailController());
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key); const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int id = videoItem.id; int id = videoItem.id;
String bvid = videoItem.bvid ?? IdUtils.av2bv(id); String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
String heroTag = Utils.makeHeroTag(id); String heroTag = Utils.makeHeroTag(id);
return Dismissible( return InkWell(
movementDuration: const Duration(milliseconds: 300),
background: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.clear_all_rounded),
SizedBox(width: 6),
Text('取消收藏')
],
)),
direction: DismissDirection.endToStart,
key: ValueKey<int>(videoItem.id),
onDismissed: (DismissDirection direction) {
_favDetailController.onCancelFav(videoItem.id);
// widget.onDeleteNotice();
},
child: InkWell(
onTap: () async { onTap: () async {
// int? seasonId; // int? seasonId;
String? epId; String? epId;
@ -71,8 +49,8 @@ class FavVideoCardH extends StatelessWidget {
Get.toNamed('/video', parameters: parameters, arguments: { Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem, 'videoItem': videoItem,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': // 'videoType':
epId != null ? SearchType.media_bangumi : SearchType.video, // epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
child: Column( child: Column(
@ -113,10 +91,8 @@ class FavVideoCardH extends StatelessWidget {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6), vertical: 1, horizontal: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius: BorderRadius.circular(4),
BorderRadius.circular(4), color: Colors.black54.withOpacity(0.4)),
color:
Colors.black54.withOpacity(0.4)),
child: Text( child: Text(
Utils.timeFormat(videoItem.duration!), Utils.timeFormat(videoItem.duration!),
style: const TextStyle( style: const TextStyle(
@ -129,7 +105,7 @@ class FavVideoCardH extends StatelessWidget {
}, },
), ),
), ),
VideoContent(videoItem: videoItem) VideoContent(videoItem: videoItem, callFn: callFn)
], ],
), ),
); );
@ -138,14 +114,14 @@ class FavVideoCardH extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const VideoContent({super.key, required this.videoItem}); final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -166,6 +142,8 @@ class VideoContent extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const Spacer(), const Spacer(),
Row(
children: [
Text( Text(
videoItem.owner.name, videoItem.owner.name,
style: TextStyle( style: TextStyle(
@ -173,15 +151,50 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
const SizedBox(height: 2), const Spacer(),
Row( SizedBox(
children: [ width: 26,
StatView( height: 26,
theme: 'gray', child: IconButton(
view: videoItem.cntInfo['play'], 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(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
], ],
), ),
], ],

View File

@ -27,12 +27,6 @@ Widget followItem({item}) {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
subtitle: Text(
item.sign,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
dense: true,
trailing: const SizedBox(width: 6), trailing: const SizedBox(width: 6),
); );
} }

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs; RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs; RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override @override
void onInit() { void onInit() {
@ -121,4 +123,80 @@ class HistoryController extends GetxController {
}, },
); );
} }
// 删除某条历史记录
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
}
// 删除已看历史记录
Future onDelHistory() async {
/// TODO 优化
List<HisListItem> result =
historyList.where((e) => e.progress == -1).toList();
for (HisListItem i in result) {
String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
SmartDialog.showToast('操作完成');
}
// 删除选中的记录
Future onDelCheckedHistory() async {
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 {
/// TODO 优化
await SmartDialog.dismiss();
SmartDialog.showLoading(msg: '请求中');
List<HisListItem> result =
historyList.where((e) => e.checked!).toList();
for (HisListItem i in result) {
String str = 'archive';
try {
str = i.history!.business!;
} catch (_) {}
String resKid = '${str}_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
checkedCount.value = 0;
SmartDialog.dismiss();
enableMultiple.value = false;
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -37,6 +37,23 @@ class _HistoryPageState extends State<HistoryPage> {
} }
}, },
); );
_historyController.enableMultiple.listen((p0) {
setState(() {});
});
}
// 选中
onChoose(index) {
_historyController.historyList[index].checked =
!_historyController.historyList[index].checked!;
_historyController.checkedCount.value =
_historyController.historyList.where((item) => item.checked!).length;
_historyController.historyList.refresh();
}
// 更新多选状态
onUpdateMultiple() {
setState(() {});
} }
@override @override
@ -48,14 +65,31 @@ class _HistoryPageState extends State<HistoryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBarWidget(
visible: _historyController.enableMultiple.value,
child1: AppBar(
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back_outlined),
),
title: Text( title: Text(
'观看记录', '观看记录',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [ actions: [
// TextButton(
// onPressed: () {
// _historyController.enableMultiple.value = true;
// setState(() {});
// },
// child: const Text('多选'),
// ),
IconButton(
onPressed: () => Get.toNamed('/historySearch'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton<String>( PopupMenuButton<String>(
onSelected: (String type) { onSelected: (String type) {
// 处理菜单项选择的逻辑 // 处理菜单项选择的逻辑
@ -66,6 +100,13 @@ class _HistoryPageState extends State<HistoryPage> {
case 'clear': case 'clear':
_historyController.onClearHistory(); _historyController.onClearHistory();
break; break;
case 'del':
_historyController.onDelHistory();
break;
case 'multiple':
_historyController.enableMultiple.value = true;
setState(() {});
break;
default: default:
} }
}, },
@ -82,11 +123,62 @@ class _HistoryPageState extends State<HistoryPage> {
value: 'clear', value: 'clear',
child: Text('清空观看记录'), child: Text('清空观看记录'),
), ),
const PopupMenuItem<String>(
value: 'del',
child: Text('删除已看记录'),
),
const PopupMenuItem<String>(
value: 'multiple',
child: Text('多选删除'),
),
], ],
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
), ),
child2: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () {
_historyController.enableMultiple.value = false;
for (var item in _historyController.historyList) {
item.checked = false;
}
_historyController.checkedCount.value = 0;
setState(() {});
},
icon: const Icon(Icons.close_outlined),
),
title: Obx(
() => Text(
'已选择${_historyController.checkedCount.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () {
for (var item in _historyController.historyList) {
item.checked = true;
}
_historyController.checkedCount.value =
_historyController.historyList.length;
_historyController.historyList.refresh();
},
child: const Text('全选'),
),
TextButton(
onPressed: () => _historyController.onDelCheckedHistory(),
child: Text(
'删除',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await _historyController.onRefresh(); await _historyController.onRefresh();
@ -99,6 +191,9 @@ class _HistoryPageState extends State<HistoryPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data; Map data = snapshot.data;
if (data['status']) { if (data['status']) {
return Obx( return Obx(
@ -109,6 +204,9 @@ class _HistoryPageState extends State<HistoryPage> {
return HistoryItem( return HistoryItem(
videoItem: videoItem:
_historyController.historyList[index], _historyController.historyList[index],
ctr: _historyController,
onChoose: () => onChoose(index),
onUpdateMultiple: () => onUpdateMultiple(),
); );
}, },
childCount: childCount:
@ -144,6 +242,36 @@ class _HistoryPageState extends State<HistoryPage> {
], ],
), ),
), ),
// bottomNavigationBar: BottomAppBar(),
);
}
}
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child1,
required this.child2,
required this.visible,
Key? key,
}) : super(key: key);
final PreferredSizeWidget child1;
final PreferredSizeWidget child2;
final bool visible;
@override
Size get preferredSize => child1.preferredSize;
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: !visible ? child1 : child2,
); );
} }
} }

View File

@ -11,12 +11,24 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart'; import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget { class HistoryItem extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const HistoryItem({super.key, required this.videoItem}); final dynamic ctr;
final Function? onChoose;
final Function? onUpdateMultiple;
const HistoryItem({
super.key,
required this.videoItem,
this.ctr,
this.onChoose,
this.onUpdateMultiple,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,6 +37,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid); String heroTag = Utils.makeHeroTag(aid);
return InkWell( return InkWell(
onTap: () async { onTap: () async {
if (ctr!.enableMultiple.value) {
feedBack();
onChoose!();
return;
}
if (videoItem.history.business.contains('article')) { if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ?? int cid = videoItem.history.cid ??
// videoItem.history.oid ?? // videoItem.history.oid ??
@ -72,7 +89,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
); );
} else { } else {
@ -100,7 +117,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );
@ -115,6 +132,17 @@ class HistoryItem extends StatelessWidget {
arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
} }
}, },
onLongPress: () {
if (ctr is HistorySearchController) {
return;
}
if (!ctr!.enableMultiple.value) {
feedBack();
ctr!.enableMultiple.value = true;
onChoose!();
onUpdateMultiple!();
}
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -129,6 +157,8 @@ class HistoryItem extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: StyleString.aspectRatio, aspectRatio: StyleString.aspectRatio,
@ -161,7 +191,8 @@ class HistoryItem extends StatelessWidget {
), ),
// 右上角 // 右上角
if (BusinessType.showBadge.showBadge if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) || .contains(
videoItem.history.business) ||
videoItem.history.business == videoItem.history.business ==
BusinessType.live.type) BusinessType.live.type)
PBadge( PBadge(
@ -176,7 +207,61 @@ class HistoryItem extends StatelessWidget {
}, },
), ),
), ),
VideoContent(videoItem: videoItem) Obx(
() => Positioned.fill(
child: AnimatedOpacity(
opacity: ctr!.enableMultiple.value ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.black.withOpacity(
ctr!.enableMultiple.value &&
videoItem.checked
? 0.6
: 0),
),
child: Center(
child: SizedBox(
width: 34,
height: 34,
child: AnimatedScale(
scale: videoItem.checked ? 1 : 0,
duration:
const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty
.resolveWith(
(states) {
return Colors.white
.withOpacity(0.8);
},
),
),
onPressed: () {
feedBack();
onChoose!();
},
icon: Icon(Icons.done_all_outlined,
color: Theme.of(context)
.colorScheme
.primary),
),
),
),
),
),
),
),
),
],
),
VideoContent(videoItem: videoItem, ctr: ctr)
], ],
), ),
); );
@ -191,7 +276,8 @@ class HistoryItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const VideoContent({super.key, required this.videoItem}); final dynamic? ctr;
const VideoContent({super.key, required this.videoItem, this.ctr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -244,16 +330,12 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline), color: Theme.of(context).colorScheme.outline),
), ),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox( SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
tooltip: '稍后再看', tooltip: '功能菜单',
icon: Icon( icon: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
@ -264,6 +346,10 @@ class VideoContent extends StatelessWidget {
onSelected: (String type) {}, onSelected: (String type) {},
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[ <PopupMenuEntry<String>>[
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
PopupMenuItem<String>( PopupMenuItem<String>(
onTap: () async { onTap: () async {
var res = await UserHttp.toViewLater( var res = await UserHttp.toViewLater(
@ -280,6 +366,19 @@ class VideoContent extends StatelessWidget {
], ],
), ),
), ),
PopupMenuItem<String>(
onTap: () => ctr!.delHistory(
videoItem.kid, videoItem.history.business),
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.close_outlined, size: 16),
SizedBox(width: 6),
Text('删除记录', style: TextStyle(fontSize: 13))
],
),
),
], ],
), ),
), ),

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/history.dart';
class HistorySearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
RxString loadingText = '加载中...'.obs;
bool hasRequest = false;
late int mid;
RxString uname = ''.obs;
int pn = 1;
int count = 0;
RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool enableMultiple = false.obs;
// 清空搜索
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';
if (hasRequest) {
pn = 1;
searchHistories();
}
}
// 搜索视频
Future searchHistories({type = 'init'}) async {
if (type == 'onLoad' && loadingText.value == '没有更多了') {
return;
}
var res = await UserHttp.searchHistory(
pn: pn,
keyword: controller.value.text,
);
if (res['status']) {
if (type == 'init' && pn == 1) {
historyList.value = res['data'].list;
} else {
historyList.addAll(res['data'].list);
}
count = res['data'].page['total'];
if (historyList.length == count) {
loadingText.value = '没有更多了';
}
pn += 1;
hasRequest = true;
}
loadingStatus.value = 'finish';
return res;
}
onLoad() {
searchHistories(type: 'onLoad');
}
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
loadingStatus.value = 'finish';
}
}

View File

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

View File

@ -0,0 +1,174 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/widgets/item.dart';
import 'controller.dart';
class HistorySearchPage extends StatefulWidget {
const HistorySearchPage({super.key});
@override
State<HistorySearchPage> createState() => _HistorySearchPageState();
}
class _HistorySearchPageState extends State<HistorySearchPage> {
final HistorySearchController _historySearchCtr =
Get.put(HistorySearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _historySearchCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_historySearchCtr.onLoad();
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _historySearchCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _historySearchCtr.searchFocusNode,
controller: _historySearchCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _historySearchCtr.onChange(value),
decoration: InputDecoration(
hintText: _historySearchCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _historySearchCtr.onClear(),
),
),
onSubmitted: (String value) => _historySearchCtr.submit(),
),
),
),
body: Obx(
() => Column(
children: _historySearchCtr.loadingStatus.value == 'init'
? [const SizedBox()]
: [
Expanded(
child: FutureBuilder(
future: _historySearchCtr.searchHistories(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => _historySearchCtr.historyList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount:
_historySearchCtr.historyList.length +
1,
itemBuilder: (context, index) {
if (index ==
_historySearchCtr
.historyList.length) {
return Container(
height: MediaQuery.of(context)
.padding
.bottom +
60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
child: Center(
child: Obx(
() => Text(
_historySearchCtr
.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return HistoryItem(
videoItem: _historySearchCtr
.historyList[index],
ctr: _historySearchCtr,
onChoose: null,
onUpdateMultiple: () => null,
);
;
}
},
)
: _historySearchCtr.loadingStatus.value ==
'loading'
? const SizedBox(child: Text('加载中...'))
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
);
}
},
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,9 @@
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/http/black.dart';
import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/models/user/black.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin { class HomeController extends GetxController with GetTickerProviderStateMixin {
@ -15,6 +17,13 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
RxString userFace = ''.obs; RxString userFace = ''.obs;
var userInfo; var userInfo;
static Box setting = GStrorage.setting;
late List<int> blackMidsList;
int currentPage = 1;
int pageSize = 50;
int total = 0;
List<BlackListItem> blackList = [BlackListItem()];
@override @override
void onInit() { void onInit() {
@ -51,7 +60,33 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
void updateLoginStatus(val) async { void updateLoginStatus(val) async {
userInfo = await userInfoCache.get('userInfoCache'); userInfo = await userInfoCache.get('userInfoCache');
userLogin.value = val ?? false; userLogin.value = val ?? false;
if (val) return; if (val) {
// 获取黑名单
await queryBlacklist();
blackMidsList = blackList.map<int>((e) => e.mid!).toList();
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
return;
}
userFace.value = userInfo != null ? userInfo.face : ''; userFace.value = userInfo != null ? userInfo.face : '';
} }
Future queryBlacklist({type = 'init'}) async {
if (type == 'init') {
currentPage = 1;
}
var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);
if (result['status']) {
if (type == 'init') {
blackList = result['data'].list;
total = result['data'].total;
} else {
blackList.addAll(result['data'].list);
}
currentPage += 1;
if (blackList.length < total) {
await queryBlacklist(type: 'onLoad');
}
}
return result;
}
} }

View File

@ -8,7 +8,8 @@ import 'package:pilipala/utils/feed_back.dart';
import './controller.dart'; import './controller.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key); Function? callFn;
HomePage({Key? key, this.callFn}) : super(key: key);
@override @override
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
@ -25,15 +26,16 @@ class _HomePageState extends State<HomePage>
showUserBottonSheet() { showUserBottonSheet() {
feedBack(); feedBack();
showModalBottomSheet( widget.callFn!();
context: context, // showModalBottomSheet(
builder: (_) => const SizedBox( // context: context,
height: 450, // builder: (_) => const SizedBox(
child: MinePage(), // height: 450,
), // child: MinePage(),
clipBehavior: Clip.hardEdge, // ),
isScrollControlled: true, // clipBehavior: Clip.hardEdge,
); // isScrollControlled: true,
// );
} }
@override @override
@ -50,37 +52,6 @@ class _HomePageState extends State<HomePage>
ctr: _homeController, ctr: _homeController,
callback: showUserBottonSheet, callback: showUserBottonSheet,
), ),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 42,
child: Align(
alignment: Alignment.center,
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs) Tab(text: i['label'])
],
isScrollable: true,
dividerColor: Colors.transparent,
enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10),
onTap: (value) {
feedBack();
if (_homeController.initialIndex == value) {
_homeController.tabsCtrList[value]().animateToTop();
}
_homeController.initialIndex = value;
},
),
),
),
Expanded(
child: TabBarView(
controller: _homeController.tabController,
children: _homeController.tabsPageList,
),
),
], ],
), ),
); );
@ -128,8 +99,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
child: Row( child: Row(
children: [ children: [
const Expanded(child: SearchPage()),
const SizedBox(width: 10),
Obx( Obx(
() => ctr!.userLogin.value () => ctr!.userLogin.value
? Stack( ? Stack(
@ -182,6 +151,8 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
), ),
const SizedBox(width: 10),
const Expanded(child: SearchPage()),
], ],
), ),
), ),

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart';
class LeftDrawer extends StatefulWidget {
const LeftDrawer({super.key});
@override
State<LeftDrawer> createState() => _LeftDrawerState();
}
class _LeftDrawerState extends State<LeftDrawer> {
@override
Widget build(BuildContext context) {
return Drawer(
width: MediaQuery.of(context).size.width * 0.84,
child: const Column(
children: [
Expanded(child: MinePage()),
Expanded(child: MediaPage()),
],
),
);
}
}

View File

@ -0,0 +1,19 @@
import 'package:get/get.dart';
import 'package:pilipala/http/html.dart';
class HtmlRenderController extends GetxController {
late String id;
late Map response;
@override
void onInit() {
super.onInit();
id = Get.parameters['id']!;
}
Future reqHtml() async {
var res = await HtmlHttp.reqHtml(id);
response = res;
return res;
}
}

View File

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

117
lib/pages/html/view.dart Normal file
View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'controller.dart';
class HtmlRenderPage extends StatefulWidget {
const HtmlRenderPage({super.key});
@override
State<HtmlRenderPage> createState() => _HtmlRenderPageState();
}
class _HtmlRenderPageState extends State<HtmlRenderPage> {
HtmlRenderController htmlRenderCtr = Get.put(HtmlRenderController());
late String title;
late String id;
@override
void initState() {
super.initState();
title = Get.parameters['title']!;
id = Get.parameters['id']!;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text(title),
),
body: SingleChildScrollView(
child: Column(
children: [
FutureBuilder(
future: htmlRenderCtr.reqHtml(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: Row(
children: [
NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: htmlRenderCtr.response['avatar']!,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(htmlRenderCtr.response['uname'],
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleSmall!
.fontSize,
)),
Text(
htmlRenderCtr.response['updateTime'],
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
),
),
],
),
const Spacer(),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: HtmlRender(
htmlContent: htmlRenderCtr.response['content'],
),
),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 8,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
),
),
],
);
} else {
return Text('error');
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
],
),
),
);
}
}

View File

@ -84,6 +84,9 @@ class _LivePageState extends State<LivePage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
if (data['status']) { if (data['status']) {
return SliverLayoutBuilder( return SliverLayoutBuilder(

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/bottom_control.dart';
class LiveRoomPage extends StatefulWidget { class LiveRoomPage extends StatefulWidget {
const LiveRoomPage({super.key}); const LiveRoomPage({super.key});
@ -18,6 +22,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bool isShowCover = true; bool isShowCover = true;
bool isPlay = true; bool isPlay = true;
Floating? floating;
@override @override
void initState() { void initState() {
@ -31,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
} }
}, },
); );
if (Platform.isAndroid) {
floating = Floating();
}
} }
@override @override
void dispose() { void dispose() {
plPlayerController!.dispose(); plPlayerController!.dispose();
if (floating != null) {
floating!.dispose();
}
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
Widget childWhenDisabled = Scaffold(
return Scaffold(
primary: true, primary: true,
appBar: AppBar( appBar: AppBar(
centerTitle: false, centerTitle: false,
@ -87,14 +97,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
), ),
body: Column( body: Column(
children: [ children: [
Hero( Stack(
tag: _liveRoomController.heroTag,
child: Stack(
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(controller: plPlayerController!) ? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(), : const SizedBox(),
), ),
// if (_liveRoomController.liveItem != null && // if (_liveRoomController.liveItem != null &&
@ -115,70 +130,28 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
// ), // ),
], ],
), ),
),
// Container(
// height: 45,
// padding: const EdgeInsets.only(left: 12, right: 12),
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.background,
// border: Border(
// bottom: BorderSide(
// color: Theme.of(context).dividerColor.withOpacity(0.1)),
// ),
// ),
// child: Row(children: <Widget>[
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.subtitles_outlined,
// size: 21,
// ),
// ),
// ),
// const Spacer(),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.hd_outlined,
// size: 20,
// ),
// ),
// ),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () => _liveRoomController
// .setVolumn(plPlayerController!.volume.value),
// icon: Obx(() => Icon(
// _liveRoomController.volumeOff.value
// ? Icons.volume_off_outlined
// : Icons.volume_up_outlined,
// size: 21,
// )),
// ),
// ),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () => {},
// // plPlayerController!.goToFullscreen(context),
// icon: const Icon(
// Icons.fullscreen,
// ),
// ),
// ),
// ]),
// ),
], ],
), ),
); );
Widget childWhenEnabled = AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
),
)
: const SizedBox(),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
);
} else {
return childWhenDisabled;
}
} }
} }

View File

@ -0,0 +1,151 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/liveRoom/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
class BottomControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final LiveRoomController? liveRoomCtr;
final Floating? floating;
const BottomControl({
this.controller,
this.liveRoomCtr,
this.floating,
Key? key,
}) : super(key: key);
@override
State<BottomControl> createState() => _BottomControlState();
@override
Size get preferredSize => throw UnimplementedError();
}
class _BottomControlState extends State<BottomControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
Box localCache = GStrorage.localCache;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
centerTitle: false,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Row(
children: [
// ComBtn(
// icon: const Icon(
// Icons.subtitles_outlined,
// size: 18,
// color: Colors.white,
// ),
// fuc: () => Get.back(),
// ),
const Spacer(),
// ComBtn(
// icon: const Icon(
// Icons.hd_outlined,
// size: 18,
// color: Colors.white,
// ),
// fuc: () => {},
// ),
// const SizedBox(width: 4),
// Obx(
// () => ComBtn(
// icon: Icon(
// widget.liveRoomCtr!.volumeOff.value
// ? Icons.volume_off_outlined
// : Icons.volume_up_outlined,
// size: 18,
// color: Colors.white,
// ),
// fuc: () => {},
// ),
// ),
// const SizedBox(width: 4),
if (Platform.isAndroid) ...[
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
bool canUsePiP = false;
widget.controller!.hiddenControls(false);
try {
canUsePiP = await widget.floating!.isPipAvailable;
} on PlatformException catch (_) {
canUsePiP = false;
}
if (canUsePiP) {
await widget.floating!.enable();
} else {}
},
icon: const Icon(
Icons.picture_in_picture_outlined,
size: 18,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
],
ComBtn(
icon: const Icon(
Icons.fullscreen,
size: 20,
color: Colors.white,
),
fuc: () => widget.controller!.triggerFullScreen(),
),
],
),
);
}
}
class MSliderTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData? sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
const double trackHeight = 3;
final double trackLeft = offset.dx;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
}

View File

@ -4,53 +4,11 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart'; // import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[
const HomePage(),
const DynamicsPage(),
const MediaPage(),
];
RxList navigationBars = [
{
'icon': const Icon(
Icons.favorite_outline,
size: 21,
),
'selectIcon': const Icon(
Icons.favorite,
size: 21,
),
'label': "首页",
},
{
'icon': const Icon(
Icons.motion_photos_on_outlined,
size: 21,
),
'selectIcon': const Icon(
Icons.motion_photos_on,
size: 21,
),
'label': "动态",
},
{
'icon': const Icon(
Icons.folder_outlined,
size: 20,
),
'selectIcon': const Icon(
Icons.folder,
size: 21,
),
'label': "媒体库",
}
].obs;
final StreamController<bool> bottomBarStream = final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/home/widgets/left_drawer.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
@ -18,80 +19,16 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin { class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController()); final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController()); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final DynamicsController _dynamicController = Get.put(DynamicsController());
final MediaController _mediaController = Get.put(MediaController());
PageController? _pageController;
late AnimationController? _animationController;
late Animation<double>? _fadeAnimation;
late Animation<double>? _slideAnimation;
int selectedIndex = 0; int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
reverseDuration: const Duration(milliseconds: 0),
value: 1,
vsync: this,
);
_fadeAnimation =
Tween<double>(begin: 0.8, end: 1.0).animate(_animationController!);
_slideAnimation =
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
} }
void setIndex(int value) async { void openDrawer() {
feedBack(); _scaffoldKey.currentState?.openDrawer();
if (selectedIndex != value) {
selectedIndex = value;
_animationController!.reverse().then((_) {
selectedIndex = value;
_animationController!.forward();
});
setState(() {});
}
_pageController!.jumpToPage(value);
var currentPage = _mainController.pages[value];
if (currentPage is HomePage) {
if (_homeController.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_homeController.onRefresh();
} else {
_homeController.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_homeController.flag = true;
} else {
_homeController.flag = false;
}
if (currentPage is DynamicsPage) {
if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_dynamicController.onRefresh();
} else {
_dynamicController.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_dynamicController.flag = true;
} else {
_dynamicController.flag = false;
}
if (currentPage is MediaPage) {
_mediaController.queryFavFolder();
}
} }
@override @override
@ -113,55 +50,10 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
return WillPopScope( return WillPopScope(
onWillPop: () => _mainController.onBackPressed(context), onWillPop: () => _mainController.onBackPressed(context),
child: Scaffold( child: Scaffold(
key: _scaffoldKey,
extendBody: true, extendBody: true,
body: FadeTransition( drawer: const LeftDrawer(),
opacity: _fadeAnimation!, body: HomePage(callFn: openDrawer),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _slideAnimation!,
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.linear,
),
),
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
onPageChanged: (index) {
selectedIndex = index;
setState(() {});
},
children: _mainController.pages,
),
),
),
bottomNavigationBar: StreamBuilder(
stream: _mainController.bottomBarStream.stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 1000),
offset: Offset(0, snapshot.data ? 0 : 1),
child: NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
);
},
),
), ),
); );
} }

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget { class MediaPage extends StatefulWidget {
const MediaPage({super.key}); const MediaPage({super.key});
@ -15,7 +12,6 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage> class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
late MediaController mediaController; late MediaController mediaController;
late Future _futureBuilderFuture;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -24,13 +20,6 @@ class _MediaPageState extends State<MediaPage>
void initState() { void initState() {
super.initState(); super.initState();
mediaController = Get.put(MediaController()); mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
mediaController.userLogin.listen((status) {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
} }
@override @override
@ -38,22 +27,8 @@ class _MediaPageState extends State<MediaPage>
super.build(context); super.build(context);
Color primary = Theme.of(context).colorScheme.primary; Color primary = Theme.of(context).colorScheme.primary;
return Scaffold( return Scaffold(
appBar: AppBar(toolbarHeight: 30),
body: Column( body: Column(
children: [ children: [
ListTile(
leading: null,
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
'媒体库',
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
fontWeight: FontWeight.bold,
),
),
),
),
for (var i in mediaController.list) ...[ for (var i in mediaController.list) ...[
ListTile( ListTile(
onTap: () => i['onTap'](), onTap: () => i['onTap'](),
@ -74,203 +49,8 @@ class _MediaPageState extends State<MediaPage>
), ),
), ),
], ],
Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
], ],
), ),
); );
} }
Widget favFolder(mediaController, context) {
return Column(
children: [
Divider(
height: 35,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
ListTile(
onTap: () {},
leading: null,
dense: true,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Obx(
() => Text.rich(
TextSpan(
children: [
TextSpan(
text: '收藏夹 ',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleMedium!.fontSize,
fontWeight: FontWeight.bold),
),
if (mediaController.favFolderData.value.count != null)
TextSpan(
text: mediaController.favFolderData.value.count
.toString(),
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
),
trailing: IconButton(
onPressed: () {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
),
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 170 * MediaQuery.of(context).textScaleFactor,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
List favFolderList =
mediaController.favFolderData.value.list!;
int favFolderCount =
mediaController.favFolderData.value.count!;
bool flag = favFolderCount > favFolderList.length;
return Obx(() => ListView.builder(
itemCount:
mediaController.favFolderData.value.list!.length +
(flag ? 1 : 0),
itemBuilder: (context, index) {
if (flag && index == favFolderList.length) {
return Padding(
padding: const EdgeInsets.only(
right: 14, bottom: 35),
child: Center(
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.5);
}),
),
onPressed: () => Get.toNamed('/fav'),
icon: Icon(
Icons.arrow_forward_ios,
size: 18,
color: Theme.of(context)
.colorScheme
.primary,
),
),
));
} else {
return FavFolderItem(
item: mediaController
.favFolderData.value.list![index],
index: index);
}
},
scrollDirection: Axis.horizontal,
));
} else {
return SizedBox(
height: 160,
child: Center(child: Text(data['msg'])),
);
}
} else {
// 骨架屏
return const SizedBox();
}
}),
),
],
);
}
}
class FavFolderItem extends StatelessWidget {
const FavFolderItem({super.key, this.item, this.index});
final FavFolderItemData? item;
final int? index;
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item!.fid);
return Container(
margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14),
child: GestureDetector(
onTap: () => Get.toNamed('/favDetail',
arguments: item,
parameters: {'mediaId': item!.id.toString(), 'heroTag': heroTag}),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Container(
width: 180,
height: 110,
margin: const EdgeInsets.only(bottom: 8),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.onInverseSurface,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.onInverseSurface,
offset: const Offset(4, -12), // 阴影与容器的距离
blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。
spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。
),
],
),
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: item!.cover,
width: box.maxWidth,
height: box.maxHeight,
),
);
},
),
),
Text(
' ${item!.title}',
overflow: TextOverflow.fade,
maxLines: 1,
),
Text(
'${item!.mediaCount}条视频',
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline),
)
],
),
),
);
}
} }

View File

@ -19,6 +19,7 @@ class MemberController extends GetxController {
// 投稿列表 // 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs; RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo; var userInfo;
Box setting = GStrorage.setting;
@override @override
void onInit() { void onInit() {
@ -70,11 +71,11 @@ class MemberController extends GetxController {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'), content: Text(memberInfo.value.isFollowed! ? '取消关注该用户?' : '关注该用户?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了')), child: const Text('取消')),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await VideoHttp.relationMod( await VideoHttp.relationMod(
@ -95,4 +96,48 @@ class MemberController extends GetxController {
}, },
); );
} }
// 拉黑用户
Future blockUser(int mid) async {
if (userInfoCache.get('userInfoCache') == null) {
SmartDialog.showToast('账号未登录');
return;
}
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 {
var res = await VideoHttp.relationMod(
mid: mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
List<int> blackMidsList = setting
.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
blackMidsList.add(mid);
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
}
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -102,40 +102,79 @@ class _MemberPageState extends State<MemberPage>
}, },
), ),
actions: [ actions: [
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
// PopupMenuButton(
// icon: const Icon(Icons.more_vert),
// itemBuilder: (BuildContext context) => <PopupMenuEntry>[
// if (_memberController.ownerMid !=
// _memberController.mid) ...[
// PopupMenuItem(
// onTap: () => _memberController.blockUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.block, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.attribute.value != 128
// ? '加入黑名单'
// : '移除黑名单'),
// ],
// ),
// )
// ],
// PopupMenuItem(
// onTap: () => _memberController.shareUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.share_outlined, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.ownerMid !=
// _memberController.mid
// ? '分享UP主'
// : '分享我的主页'),
// ],
// ),
// ),
// ],
// ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Stack( background: Stack(
children: [ children: [
if (_memberController.face != null) // if (_memberController.face != null)
Positioned.fill( // Positioned.fill(
bottom: 10, // bottom: 10,
child: Container( // child: Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
image: DecorationImage( // image: DecorationImage(
fit: BoxFit.fitWidth, // fit: BoxFit.fitWidth,
image: NetworkImage(_memberController.face!), // image: NetworkImage(_memberController.face!),
alignment: Alignment.topCenter, // alignment: Alignment.topCenter,
isAntiAlias: true, // isAntiAlias: true,
), // ),
), // ),
foregroundDecoration: BoxDecoration( // foregroundDecoration: BoxDecoration(
gradient: LinearGradient( // gradient: LinearGradient(
colors: [ // colors: [
Theme.of(context) // Theme.of(context)
.colorScheme // .colorScheme
.background // .background
.withOpacity(0.44), // .withOpacity(0.44),
Theme.of(context).colorScheme.background, // Theme.of(context).colorScheme.background,
], // ],
begin: Alignment.topCenter, // begin: Alignment.topCenter,
end: Alignment.bottomCenter, // end: Alignment.bottomCenter,
stops: const [0.0, 0.46], // stops: const [0.0, 0.46],
), // ),
), // ),
), // ),
), // ),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -179,109 +218,109 @@ class _MemberPageState extends State<MemberPage>
fontWeight: fontWeight:
FontWeight.bold), FontWeight.bold),
)), )),
const SizedBox(width: 2), // const SizedBox(width: 2),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '女')
const Icon( // const Icon(
FontAwesomeIcons.venus, // FontAwesomeIcons.venus,
size: 14, // size: 14,
color: Colors.pink, // color: Colors.pink,
), // ),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '男')
const Icon( // const Icon(
FontAwesomeIcons.mars, // FontAwesomeIcons.mars,
size: 14, // size: 14,
color: Colors.blue, // color: Colors.blue,
), // ),
const SizedBox(width: 4), // const SizedBox(width: 4),
Image.asset( // Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png', // 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11, // height: 11,
), // ),
const SizedBox(width: 6), // const SizedBox(width: 6),
if (_memberController.memberInfo // if (_memberController.memberInfo
.value.vip!.status == // .value.vip!.status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'] != // 'img_label_uri_hans'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'], // 'img_label_uri_hans'],
height: 20, // height: 20,
), // ),
] else if (_memberController // ] else if (_memberController
.memberInfo // .memberInfo
.value // .value
.vip! // .vip!
.status == // .status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'] != // 'img_label_uri_hans_static'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'], // 'img_label_uri_hans_static'],
height: 20, // height: 20,
), // ),
] // ]
], ],
), ),
if (_memberController.memberInfo.value // if (_memberController.memberInfo.value
.official!['title'] != // .official!['title'] !=
'') ...[ // '') ...[
const SizedBox(height: 6), // const SizedBox(height: 6),
Text.rich( // Text.rich(
maxLines: 2, // maxLines: 2,
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['role'] == // .official!['role'] ==
1 // 1
? '个人认证:' // ? '个人认证:'
: '企业认证:', // : '企业认证:',
style: TextStyle( // style: TextStyle(
color: Theme.of(context) // color: Theme.of(context)
.primaryColor, // .primaryColor,
), // ),
children: [ // children: [
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['title'], // .official!['title'],
), // ),
], // ],
), // ),
softWrap: true, // softWrap: true,
), // ),
], // ],
const SizedBox(height: 4), // const SizedBox(height: 4),
if (_memberController // if (_memberController
.memberInfo.value.sign != // .memberInfo.value.sign !=
'') // '')
SelectableRegion( // SelectableRegion(
magnifierConfiguration: // magnifierConfiguration:
const TextMagnifierConfiguration(), // const TextMagnifierConfiguration(),
focusNode: FocusNode(), // focusNode: FocusNode(),
selectionControls: // selectionControls:
MaterialTextSelectionControls(), // MaterialTextSelectionControls(),
child: Text( // child: Text(
_memberController // _memberController
.memberInfo.value.sign!, // .memberInfo.value.sign!,
textAlign: TextAlign.left, // textAlign: TextAlign.left,
maxLines: 2, // maxLines: 2,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
), // ),
], ],
), ),
], ],
@ -308,28 +347,29 @@ class _MemberPageState extends State<MemberPage>
return MediaQuery.of(context).padding.top + kToolbarHeight; return MediaQuery.of(context).padding.top + kToolbarHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Column( // body: Column(
children: [ // children: [
SizedBox( // SizedBox(
width: double.infinity, // width: double.infinity,
height: 50, // height: 50,
child: TabBar(controller: _tabController, tabs: const [ // child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'), // Tab(text: '主页'),
Tab(text: '动态'), // Tab(text: '动态'),
Tab(text: '投稿'), // Tab(text: '投稿'),
]), // ]),
), // ),
Expanded( // Expanded(
child: TabBarView( // child: TabBarView(
controller: _tabController, // controller: _tabController,
children: const [ // children: const [
Text('主页'), // Text('主页'),
MemberDynamicPanel(), // MemberDynamicPanel(),
ArchivePanel(), // ArchivePanel(),
], // ],
)) // ))
], // ],
), // ),
body: ArchivePanel(),
), ),
); );
} }

View File

@ -76,79 +76,79 @@ Widget profile(ctr, {loadingStatus = false}) {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( // Padding(
padding: const EdgeInsets.only(left: 10, right: 10), // padding: const EdgeInsets.only(left: 10, right: 10),
child: Row( // child: Row(
mainAxisSize: MainAxisSize.max, // mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, // mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ // children: [
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? ctr.userStat!['following'].toString() // ? ctr.userStat!['following'].toString()
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold), // fontWeight: FontWeight.bold),
), // ),
Text( // Text(
'关注', // '关注',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize), // .fontSize),
) // )
], // ],
), // ),
), // ),
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? Utils.numFormat( // ? Utils.numFormat(
ctr.userStat!['follower'], // ctr.userStat!['follower'],
) // )
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold)), // fontWeight: FontWeight.bold)),
Text('粉丝', // Text('粉丝',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize)) // .fontSize))
], // ],
), // ),
), // ),
Column( // Column(
children: [ // children: [
const Text('-', // const Text('-',
style: TextStyle(fontWeight: FontWeight.bold)), // style: TextStyle(fontWeight: FontWeight.bold)),
Text( // Text(
'获赞', // '获赞',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize), // .fontSize),
) // )
], // ],
), // ),
], // ],
), // ),
), // ),
const SizedBox(height: 10), // const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid) ...[ if (ctr.ownerMid != ctr.mid) ...[
Row( Row(
children: [ children: [

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/archive.dart';
class MemberSearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
RxString loadingText = '加载中...'.obs;
bool hasRequest = false;
late int mid;
RxString uname = ''.obs;
int archivePn = 1;
int archiveCount = 0;
RxList<VListItemModel> archiveList = <VListItemModel>[].obs;
int dynamic_pn = 1;
RxList<VListItemModel> dynamicList = <VListItemModel>[].obs;
int ps = 30;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
uname.value = Get.parameters['uname']!;
}
// 清空搜索
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';
if (hasRequest) {
archivePn = 1;
searchArchives();
}
}
// 搜索视频
Future searchArchives({type = 'init'}) async {
if (type == 'onLoad' && loadingText.value == '没有更多了') {
return;
}
var res = await MemberHttp.memberArchive(
mid: mid,
pn: archivePn,
keyword: controller.value.text,
order: 'pubdate',
);
if (res['status']) {
if (type == 'init' || archivePn == 1) {
archiveList.value = res['data'].list.vlist;
} else {
archiveList.addAll(res['data'].list.vlist);
}
archiveCount = res['data'].page['count'];
if (archiveList.length == archiveCount) {
loadingText.value = '没有更多了';
}
archivePn += 1;
hasRequest = true;
if (res['data'].list.vlist.isEmpty) {
loadingStatus.value = 'finish';
}
}
// loadingStatus.value = 'finish';
return res;
}
// 搜索动态
Future searchDynamic() async {}
//
onLoad() {
searchArchives(type: 'onLoad');
}
}

View File

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

View File

@ -0,0 +1,208 @@
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/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'controller.dart';
class MemberSearchPage extends StatefulWidget {
const MemberSearchPage({super.key});
@override
State<MemberSearchPage> createState() => _MemberSearchPageState();
}
class _MemberSearchPageState extends State<MemberSearchPage>
with SingleTickerProviderStateMixin {
final MemberSearchController _memberSearchCtr =
Get.put(MemberSearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _memberSearchCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_memberSearchCtr.onLoad();
});
}
},
);
// _tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
// _tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _memberSearchCtr.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _memberSearchCtr.searchFocusNode,
controller: _memberSearchCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _memberSearchCtr.onChange(value),
decoration: InputDecoration(
hintText: _memberSearchCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _memberSearchCtr.onClear(),
),
),
onSubmitted: (String value) => _memberSearchCtr.submit(),
),
),
),
body: Obx(
() => Column(
children: _memberSearchCtr.loadingStatus.value == 'init'
? [
Expanded(
child: Center(
child: Text('搜索「${_memberSearchCtr.uname.value}」的视频'),
),
),
]
: [
// TabBar(
// controller: _tabController,
// tabs: const [
// Tab(text: "视频"),
// Tab(text: "动态"),
// ],
// ),
Expanded(
child:
// TabBarView(
// controller: _tabController,
// children: [
FutureBuilder(
future: _memberSearchCtr.searchArchives(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => _memberSearchCtr.archiveList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount:
_memberSearchCtr.archiveList.length +
1,
itemBuilder: (context, index) {
if (index ==
_memberSearchCtr
.archiveList.length) {
return Container(
height: MediaQuery.of(context)
.padding
.bottom +
60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
child: Center(
child: Obx(
() => Text(
_memberSearchCtr
.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return VideoCardH(
videoItem: _memberSearchCtr
.archiveList[index]);
}
},
)
: _memberSearchCtr.loadingStatus.value ==
'loading'
? ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
)
: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: Center(
child: Text(
'没有搜索到相关内容\n请尝试别的搜索词',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.titleSmall,
),
),
),
),
],
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
);
}
},
),
// ],
// ),
),
],
),
),
);
}
}

View File

@ -72,12 +72,8 @@ class _MinePageState extends State<MinePage> {
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
), ),
body: LayoutBuilder( body: SingleChildScrollView(
builder: (context, constraint) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: SizedBox(
height: constraint.maxHeight,
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
@ -85,9 +81,11 @@ class _MinePageState extends State<MinePage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) { if (snapshot.data['status']) {
return Obx( return Obx(() => userInfoBuild(mineController, context));
() => userInfoBuild(mineController, context));
} else { } else {
return userInfoBuild(mineController, context); return userInfoBuild(mineController, context);
} }
@ -100,9 +98,6 @@ class _MinePageState extends State<MinePage> {
), ),
), ),
); );
},
),
);
} }
Widget userInfoBuild(_mineController, context) { Widget userInfoBuild(_mineController, context) {
@ -135,85 +130,9 @@ class _MinePageState extends State<MinePage> {
_mineController.userInfo.value.uname ?? '点击头像登录', _mineController.userInfo.value.uname ?? '点击头像登录',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(width: 4),
Image.asset(
'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png',
height: 10,
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(TextSpan(children: [
TextSpan(
text: '硬币: ',
style:
TextStyle(color: Theme.of(context).colorScheme.outline)),
TextSpan(
text: (_mineController.userInfo.value.money ?? 'pilipala')
.toString(),
style:
TextStyle(color: Theme.of(context).colorScheme.primary)),
]))
], ],
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
if (_mineController.userInfo.value.levelInfo != null) ...[
LayoutBuilder(
builder: (context, BoxConstraints box) {
LevelInfo levelInfo = _mineController.userInfo.value.levelInfo;
return SizedBox(
width: box.maxWidth,
height: 24,
child: Stack(
children: [
Positioned(
top: 0,
right: 0,
bottom: 0,
child: Container(
color: Theme.of(context).colorScheme.primary,
height: 24,
constraints:
const BoxConstraints(minWidth: 100), // 设置最小宽度为100
width: box.maxWidth *
(1 - (levelInfo.currentExp! / levelInfo.nextExp!)),
child: Center(
child: Text(
'${levelInfo.currentExp!}/${levelInfo.nextExp!}',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
),
),
),
),
),
Positioned(
top: 23,
left: 0,
bottom: 0,
child: Container(
width: box.maxWidth *
(_mineController
.userInfo.value.levelInfo!.currentExp! /
_mineController
.userInfo.value.levelInfo!.nextExp!),
height: 1,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
);
},
),
],
const SizedBox(height: 30),
Padding( Padding(
padding: const EdgeInsets.only(left: 12, right: 12), padding: const EdgeInsets.only(left: 12, right: 12),
child: LayoutBuilder( child: LayoutBuilder(

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -17,17 +18,6 @@ class PreviewController extends GetxController {
bool photos = true; bool photos = true;
String currentImgUrl = ''; String currentImgUrl = '';
@override
void onInit() {
super.onInit();
if (Get.arguments != null) {
initialPage.value = Get.arguments['initialPage']!;
currentPage.value = Get.arguments['initialPage']! + 1;
imgList.value = Get.arguments['imgList'];
currentImgUrl = imgList[initialPage.value];
}
}
requestPermission() async { requestPermission() async {
Map<Permission, PermissionStatus> statuses = await [ Map<Permission, PermissionStatus> statuses = await [
Permission.storage, Permission.storage,
@ -40,10 +30,11 @@ class PreviewController extends GetxController {
// 图片分享 // 图片分享
void onShareImg() async { void onShareImg() async {
requestPermission(); SmartDialog.showLoading();
var response = await Dio().get(imgList[initialPage.value], var response = await Dio().get(imgList[initialPage.value],
options: Options(responseType: ResponseType.bytes)); options: Options(responseType: ResponseType.bytes));
final temp = await getTemporaryDirectory(); final temp = await getTemporaryDirectory();
SmartDialog.dismiss();
String imgName = String imgName =
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg"; "plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
var path = '${temp.path}/$imgName'; var path = '${temp.path}/$imgName';

View File

@ -15,7 +15,13 @@ import 'package:status_bar_control/status_bar_control.dart';
typedef DoubleClickAnimationListener = void Function(); typedef DoubleClickAnimationListener = void Function();
class ImagePreview extends StatefulWidget { class ImagePreview extends StatefulWidget {
const ImagePreview({Key? key}) : super(key: key); final int? initialPage;
final List<String>? imgList;
const ImagePreview({
Key? key,
this.initialPage,
this.imgList,
}) : super(key: key);
@override @override
_ImagePreviewState createState() => _ImagePreviewState(); _ImagePreviewState createState() => _ImagePreviewState();
@ -34,6 +40,11 @@ class _ImagePreviewState extends State<ImagePreview>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_previewController.initialPage.value = widget.initialPage!;
_previewController.currentPage.value = widget.initialPage! + 1;
_previewController.imgList.value = widget.imgList!;
_previewController.currentImgUrl = widget.imgList![widget.initialPage!];
// animationController = AnimationController( // animationController = AnimationController(
// vsync: this, duration: const Duration(milliseconds: 400)); // vsync: this, duration: const Duration(milliseconds: 400));
setStatusBar(); setStatusBar();
@ -42,9 +53,8 @@ class _ImagePreviewState extends State<ImagePreview>
} }
onOpenMenu() { onOpenMenu() {
SmartDialog.show( showDialog(
useSystem: true, context: context,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
@ -55,7 +65,7 @@ class _ImagePreviewState extends State<ImagePreview>
ListTile( ListTile(
onTap: () { onTap: () {
_previewController.onShareImg(); _previewController.onShareImg();
SmartDialog.dismiss(); Get.back();
}, },
dense: true, dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)), title: const Text('分享', style: TextStyle(fontSize: 14)),
@ -65,8 +75,8 @@ class _ImagePreviewState extends State<ImagePreview>
Clipboard.setData( Clipboard.setData(
ClipboardData(text: _previewController.currentImgUrl)) ClipboardData(text: _previewController.currentImgUrl))
.then((value) { .then((value) {
Get.back();
SmartDialog.showToast('已复制到粘贴板'); SmartDialog.showToast('已复制到粘贴板');
SmartDialog.dismiss();
}).catchError((err) { }).catchError((err) {
SmartDialog.showNotify( SmartDialog.showNotify(
msg: err.toString(), msg: err.toString(),
@ -79,6 +89,7 @@ class _ImagePreviewState extends State<ImagePreview>
), ),
ListTile( ListTile(
onTap: () { onTap: () {
Get.back();
DownloadUtils.downloadImg(_previewController.currentImgUrl); DownloadUtils.downloadImg(_previewController.currentImgUrl);
}, },
dense: true, dense: true,
@ -93,13 +104,21 @@ class _ImagePreviewState extends State<ImagePreview>
// 设置状态栏图标透明 // 设置状态栏图标透明
setStatusBar() async { setStatusBar() async {
await StatusBarControl.setHidden(true, animation: StatusBarAnimation.SLIDE); if (Platform.isIOS) {
await StatusBarControl.setHidden(true,
animation: StatusBarAnimation.SLIDE);
}
if (Platform.isAndroid) {
await StatusBarControl.setColor(Colors.transparent);
}
} }
@override @override
void dispose() { void dispose() {
// animationController.dispose(); // animationController.dispose();
try {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE); StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
} catch (_) {}
_doubleClickAnimationController.dispose(); _doubleClickAnimationController.dispose();
clearGestureDetailsCache(); clearGestureDetailsCache();
super.dispose(); super.dispose();
@ -129,9 +148,6 @@ class _ImagePreviewState extends State<ImagePreview>
direction: DismissiblePageDismissDirection.down, direction: DismissiblePageDismissDirection.down,
disabled: _dismissDisabled, disabled: _dismissDisabled,
isFullScreen: true, isFullScreen: true,
child: Hero(
tag: _previewController
.imgList[_previewController.initialPage.value],
child: GestureDetector( child: GestureDetector(
onLongPress: () => onOpenMenu(), onLongPress: () => onOpenMenu(),
child: ExtendedImageGesturePageView.builder( child: ExtendedImageGesturePageView.builder(
@ -144,10 +160,10 @@ class _ImagePreviewState extends State<ImagePreview>
canScrollPage: (GestureDetails? gestureDetails) => canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0, gestureDetails!.totalScale! <= 1.0,
preloadPagesCount: 2, preloadPagesCount: 2,
itemCount: _previewController.imgList.length, itemCount: widget.imgList!.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network( return ExtendedImage.network(
_previewController.imgList[index], widget.imgList![index],
fit: BoxFit.contain, fit: BoxFit.contain,
mode: ExtendedImageMode.gesture, mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) { onDoubleTap: (ExtendedImageGestureState state) {
@ -213,8 +229,8 @@ class _ImagePreviewState extends State<ImagePreview>
color: Colors.white, color: Colors.white,
), ),
), ),
const SizedBox(height: 10.0), // const SizedBox(height: 10.0),
Text('${((progress ?? 0.0) * 100).toInt()}%'), // Text('${((progress ?? 0.0) * 100).toInt()}%',),
], ],
), ),
); );
@ -234,14 +250,13 @@ class _ImagePreviewState extends State<ImagePreview>
), ),
), ),
), ),
),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
child: Container( child: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom, top: 20), bottom: MediaQuery.of(context).padding.bottom + 30),
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@ -262,8 +277,7 @@ class _ImagePreviewState extends State<ImagePreview>
TextSpan( TextSpan(
text: _previewController.currentPage.toString()), text: _previewController.currentPage.toString()),
const TextSpan(text: ' / '), const TextSpan(text: ' / '),
TextSpan( TextSpan(text: widget.imgList!.length.toString()),
text: _previewController.imgList.length.toString()),
]), ]),
), ),
), ),

View File

@ -49,7 +49,7 @@ class RcmdController extends GetxController {
videoList.value = res['data']; videoList.value = res['data'];
} }
} else if (type == 'onRefresh') { } else if (type == 'onRefresh') {
videoList.insertAll(0, res['data']); videoList.value = res['data'];
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
videoList.addAll(res['data']); videoList.addAll(res['data']);
} }

View File

@ -77,7 +77,8 @@ class _RcmdPageState extends State<RcmdPage>
), ),
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
return await _rcmdController.onRefresh(); await _rcmdController.onRefresh();
await Future.delayed(const Duration(milliseconds: 300));
}, },
child: CustomScrollView( child: CustomScrollView(
controller: _rcmdController.scrollController, controller: _rcmdController.scrollController,

View File

@ -27,7 +27,9 @@ class SSearchController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
searchDefault(); // if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
// searchDefault();
// }
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) { if (Get.parameters['keyword'] != null) {
@ -49,7 +51,7 @@ class SSearchController extends GetxController {
searchSuggestList.value = []; searchSuggestList.value = [];
return; return;
} }
_debouncer.call(() => querySearchSuggest(value)); // _debouncer.call(() => querySearchSuggest(value));
} }
void onClear() { void onClear() {

View File

@ -140,138 +140,18 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: _history(),
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
Visibility(
visible: _searchController.enableHotKey,
child: hotSearch(_searchController)),
// 搜索历史
_history()
],
),
), ),
); );
}, },
); );
} }
Widget _searchSuggest() {
SSearchController _ssCtr = _searchController;
return Obx(
() => _ssCtr.searchSuggestList.isNotEmpty &&
_ssCtr.searchSuggestList.first.term != null &&
_ssCtr.controller.value.text != ''
? ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _ssCtr.searchSuggestList.length,
itemBuilder: (context, index) {
return InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
onTap: () => _ssCtr
.onClickKeyword(_ssCtr.searchSuggestList[index].term!),
child: Padding(
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
child: _ssCtr.searchSuggestList[index].textRich,
),
);
},
)
: const SizedBox(),
);
}
Widget hotSearch(ctr) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'大家都在搜',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(
height: 34,
child: TextButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: () => ctr.queryHotSearchList(),
icon: const Icon(Icons.refresh_outlined, size: 18),
label: const Text('刷新'),
),
),
],
),
),
LayoutBuilder(
builder: (context, boxConstraints) {
final double width = boxConstraints.maxWidth;
return FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList.value,
onClick: (keyword) async {
_searchController.searchFocusNode.unfocus();
await Future.delayed(
const Duration(milliseconds: 150));
_searchController.onClickKeyword(keyword);
},
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 缓存数据
if (_searchController.hotSearchList.isNotEmpty) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
);
} else {
return const SizedBox();
}
}
},
);
},
),
],
),
);
}
Widget _history() { Widget _history() {
return Obx( return Obx(
() => Container( () => Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0), padding: const EdgeInsets.fromLTRB(10, 4, 6, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -84,12 +84,12 @@ class _SearchPanelState extends State<SearchPanel>
ctr: _searchPanelController, ctr: _searchPanelController,
list: list.value, list: list.value,
); );
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return searchMbangumiPanel(context, ctr, list); // return searchMbangumiPanel(context, ctr, list);
case SearchType.bili_user: case SearchType.bili_user:
return searchUserPanel(context, ctr, list); return searchUserPanel(context, ctr, list);
case SearchType.live_room: // case SearchType.live_room:
return searchLivePanel(context, ctr, list); // return searchLivePanel(context, ctr, list);
default: default:
return const SizedBox(); return const SizedBox();
} }
@ -115,12 +115,12 @@ class _SearchPanelState extends State<SearchPanel>
switch (widget.searchType) { switch (widget.searchType) {
case SearchType.video: case SearchType.video:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return const MediaBangumiSkeleton(); // return const MediaBangumiSkeleton();
case SearchType.bili_user: case SearchType.bili_user:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.live_room: // case SearchType.live_room:
return const VideoCardHSkeleton(); // return const VideoCardHSkeleton();
default: default:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
} }

View File

@ -124,7 +124,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -37,8 +37,6 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [ children: [
Text( Text(
i!.uname, i!.uname,
@ -46,19 +44,6 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
fontSize: 14, fontSize: 14,
), ),
), ),
const SizedBox(width: 6),
Image.asset(
'assets/images/lv/lv${i!.level}.png',
height: 11,
),
],
),
Row(
children: [
Text('粉丝:${i.fans} ', style: style),
Text(' 视频:${i.videos}', style: style)
],
),
if (i.officialVerify['desc'] != '') if (i.officialVerify['desc'] != '')
Text( Text(
i.officialVerify['desc'], i.officialVerify['desc'],

View File

@ -55,6 +55,18 @@ class _ExtraSettingState extends State<ExtraSetting> {
defaultVal: true, defaultVal: true,
callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
), ),
const SetSwitchItem(
title: '搜索默认词',
subTitle: '是否展示搜索框默认词',
setKey: SettingBoxKey.enableSearchWord,
defaultVal: true,
),
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem( const SetSwitchItem(
title: '快速收藏', title: '快速收藏',
subTitle: '点按收藏至默认,长按选择文件夹', subTitle: '点按收藏至默认,长按选择文件夹',

View File

@ -60,6 +60,12 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.autoPlayEnable, setKey: SettingBoxKey.autoPlayEnable,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: '后台播放',
subTitle: '进入后台时继续播放',
setKey: SettingBoxKey.enableBackgroundPlay,
defaultVal: false,
),
const SetSwitchItem( const SetSwitchItem(
title: '自动全屏', title: '自动全屏',
subTitle: '视频开始播放时进入全屏', subTitle: '视频开始播放时进入全屏',
@ -90,18 +96,6 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.enableAutoBrightness, setKey: SettingBoxKey.enableAutoBrightness,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: '自动全屏',
subTitle: '视频开始播放时进入全屏',
setKey: SettingBoxKey.enableAutoEnter,
defaultVal: false,
),
const SetSwitchItem(
title: '自动退出',
subTitle: '视频结束播放时退出全屏',
setKey: SettingBoxKey.enableAutoExit,
defaultVal: false,
),
const SetSwitchItem( const SetSwitchItem(
title: '双击快退/快进', title: '双击快退/快进',
subTitle: '左侧双击快退,右侧双击快进', subTitle: '左侧双击快退,右侧双击快进',

View File

@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:floating/floating.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.dart'; import 'package:get/get.dart';
@ -11,6 +13,7 @@ import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/pages/video/detail/widgets/header_control.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -20,7 +23,7 @@ class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
/// 路由传参 /// 路由传参
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!); RxInt cid = int.parse(Get.parameters['cid']!).obs;
RxInt danmakuCid = 0.obs; RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
// 视频详情 // 视频详情
@ -76,6 +79,8 @@ class VideoDetailController extends GetxController
bool enableHeart = true; bool enableHeart = true;
var userInfo; var userInfo;
late bool isFirstTime = true; late bool isFirstTime = true;
Floating? floating;
late PreferredSizeWidget headerControl;
@override @override
void onInit() { void onInit() {
@ -103,7 +108,15 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) { localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false; enableHeart = false;
} }
danmakuCid.value = cid; danmakuCid.value = cid.value;
if (Platform.isAndroid) {
floating = Floating();
}
headerControl = HeaderControl(
controller: plPlayerController,
videoDetailCtr: this,
floating: floating,
);
} }
showReplyReplyPanel() { showReplyReplyPanel() {
@ -167,7 +180,13 @@ class VideoDetailController extends GetxController
playerInit(); playerInit();
} }
Future playerInit({video, audio, seekToTime, duration}) async { Future playerInit({
video,
audio,
seekToTime,
duration,
bool autoplay = true,
}) async {
/// 设置/恢复 屏幕亮度 /// 设置/恢复 屏幕亮度
if (brightness != null) { if (brightness != null) {
ScreenBrightness().setScreenBrightness(brightness!); ScreenBrightness().setScreenBrightness(brightness!);
@ -196,15 +215,16 @@ class VideoDetailController extends GetxController
// 默认1倍速 // 默认1倍速
speed: 1.0, speed: 1.0,
bvid: bvid, bvid: bvid,
cid: cid, cid: cid.value,
enableHeart: enableHeart, enableHeart: enableHeart,
isFirstTime: isFirstTime, isFirstTime: isFirstTime,
autoplay: autoplay,
); );
} }
// 视频链接 // 视频链接
Future queryVideoUrl() async { Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];
@ -287,6 +307,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
List<int> numbers = audiosList.map((map) => map.id!).toList(); List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers); int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
if (!numbers.contains(resultAudioQa)) {
closestNumber = 30280;
}
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
} }
} catch (e) { } catch (e) {

View File

@ -11,6 +11,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -58,10 +59,14 @@ class VideoIntroController extends GetxController {
RxString total = '1'.obs; RxString total = '1'.obs;
Timer? timer; Timer? timer;
bool isPaused = false; bool isPaused = false;
String heroTag = '';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
try {
heroTag = Get.arguments['heroTag'];
} catch (_) {}
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (Get.arguments.isNotEmpty) { if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) { if (Get.arguments.containsKey('videoItem')) {
@ -330,7 +335,8 @@ class VideoIntroController extends GetxController {
// 分享视频 // 分享视频
Future actionShareVideo() async { Future actionShareVideo() async {
var result = await Share.share('${HttpString.baseUrl}/video/$bvid') var result = await Share.share(
'${videoDetail.value.title} - ${HttpString.baseUrl}/video/$bvid')
.whenComplete(() {}); .whenComplete(() {});
return result; return result;
} }
@ -441,7 +447,7 @@ class VideoIntroController extends GetxController {
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
@ -485,4 +491,45 @@ class VideoIntroController extends GetxController {
} }
super.onClose(); super.onClose();
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
bool isPages = false;
if (videoDetail.value.ugcSeason != null) {
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
List<SectionItem> sections = ugcSeason.sections!;
episodes = [];
for (int i = 0; i < sections.length; i++) {
List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
} else if (videoDetail.value.pages != null) {
isPages = true;
List<Part> pages = videoDetail.value.pages!;
episodes = [];
episodes.addAll(pages);
}
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1;
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) {
nextIndex = 0;
}
if (platRepeat == PlayRepeat.listOrder) {
return;
}
}
int cid = episodes[nextIndex].cid!;
String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid);
}
} }

View File

@ -249,83 +249,83 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
maxLines: 1, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 20), // const SizedBox(width: 20),
SizedBox( // SizedBox(
width: 34, // width: 34,
height: 34, // height: 34,
child: IconButton( // child: IconButton(
style: ButtonStyle( // style: ButtonStyle(
padding: // padding:
MaterialStateProperty.all(EdgeInsets.zero), // MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: // backgroundColor:
MaterialStateProperty.resolveWith((states) { // MaterialStateProperty.resolveWith((states) {
return t.highlightColor.withOpacity(0.2); // return t.highlightColor.withOpacity(0.2);
}), // }),
), // ),
onPressed: showIntroDetail, // onPressed: showIntroDetail,
icon: Icon( // icon: Icon(
Icons.more_horiz, // Icons.more_horiz,
color: Theme.of(context).colorScheme.primary, // color: Theme.of(context).colorScheme.primary,
), // ),
), // ),
), // ),
], ],
), ),
), ),
GestureDetector( // GestureDetector(
behavior: HitTestBehavior.translucent, // behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), // onTap: () => showIntroDetail(),
child: Row( // child: Row(
children: [ // children: [
StatView( // StatView(
theme: 'gray', // theme: 'gray',
view: !widget.loadingStatus // view: !widget.loadingStatus
? widget.videoDetail!.stat!.view // ? widget.videoDetail!.stat!.view
: videoItem['stat'].view, // : videoItem['stat'].view,
size: 'medium', // size: 'medium',
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
StatDanMu( // StatDanMu(
theme: 'gray', // theme: 'gray',
danmu: !widget.loadingStatus // danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku // ? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku, // : videoItem['stat'].danmaku,
size: 'medium', // size: 'medium',
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
Text( // Text(
Utils.dateFormat( // Utils.dateFormat(
!widget.loadingStatus // !widget.loadingStatus
? widget.videoDetail!.pubdate // ? widget.videoDetail!.pubdate
: videoItem['pubdate'], // : videoItem['pubdate'],
formatType: 'detail'), // formatType: 'detail'),
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
color: t.colorScheme.outline, // color: t.colorScheme.outline,
), // ),
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal) // if (videoIntroController.isShowOnlineTotal)
Obx( // Obx(
() => Text( // () => Text(
'${videoIntroController.total.value}人在看', // '${videoIntroController.total.value}人在看',
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
color: t.colorScheme.outline, // color: t.colorScheme.outline,
), // ),
), // ),
), // ),
], // ],
), // ),
), // ),
const SizedBox(height: 7), // const SizedBox(height: 7),
// 点赞收藏转发 布局样式1 // 点赞收藏转发 布局样式1
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.only(top: 7, bottom: 7), padding: const EdgeInsets.only(top: 10, bottom: 7),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: actionRow( child: actionRow(
context, context,
@ -382,14 +382,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10), const SizedBox(width: 10),
Text(owner.name, Text(owner.name,
style: const TextStyle(fontSize: 13)), style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(), const Spacer(),
AnimatedOpacity( AnimatedOpacity(
opacity: loadingStatus ? 0 : 1, opacity: loadingStatus ? 0 : 1,
@ -516,52 +508,52 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [ return Row(children: [
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), // icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(), // onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value, // selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b), // icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(), // onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value, // selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart), // icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(), // onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'), // onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, // selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: !loadingStatus // text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString() // ? widget.videoDetail!.stat!.favorite!.toString()
: '-', // : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
ActionRowItem( // ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment), // icon: const Icon(FontAwesomeIcons.comment),
onTap: () { // onTap: () {
videoDetailCtr.tabCtr.animateTo(1); // videoDetailCtr.tabCtr.animateTo(1);
}, // },
selectStatus: false, // selectStatus: false,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
ActionRowItem( ActionRowItem(
icon: const Icon(FontAwesomeIcons.share), icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),

View File

@ -57,20 +57,6 @@ class IntroDetail extends StatelessWidget {
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Row(
children: [
StatView(
theme: 'gray',
view: videoDetail!.stat!.view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: videoDetail!.stat!.danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text( Text(
Utils.dateFormat(videoDetail!.pubdate, Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'), formatType: 'detail'),
@ -79,8 +65,6 @@ class IntroDetail extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
],
),
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,

View File

@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row(children: [ child: Row(children: [
actionRowLineItem( ActionRowLineItem(
context, onTap: () => {},
() => {}, loadingStatus: loadingStatus,
loadingStatus, text: '推荐',
'推荐',
selectStatus: true,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'弹幕',
selectStatus: false, selectStatus: false,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
actionRowLineItem( ActionRowLineItem(
context, onTap: () => {},
() => {}, loadingStatus: loadingStatus,
loadingStatus, text: '弹幕',
'评论列表',
selectStatus: false, selectStatus: false,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
actionRowLineItem( ActionRowLineItem(
context, onTap: () => {},
() => {}, loadingStatus: loadingStatus,
loadingStatus, text: '评论列表',
'播放列表', selectStatus: false,
),
const SizedBox(width: 8),
ActionRowLineItem(
onTap: () => {},
loadingStatus: loadingStatus,
text: '播放列表',
selectStatus: false, selectStatus: false,
), ),
]), ]),
@ -99,3 +95,62 @@ class MenuRow extends StatelessWidget {
); );
} }
} }
class ActionRowLineItem extends StatelessWidget {
final bool? selectStatus;
final Function? onTap;
final bool? loadingStatus;
final String? text;
const ActionRowLineItem(
{super.key,
this.selectStatus,
this.onTap,
this.text,
this.loadingStatus = false});
@override
Widget build(BuildContext context) {
return Material(
color: selectStatus!
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(30)),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => {
feedBack(),
onTap!(),
},
child: Container(
padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(30)),
border: Border.all(
color: selectStatus!
? Colors.transparent
: Theme.of(context).colorScheme.secondaryContainer,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: Text(
text!,
style: TextStyle(
fontSize: 13,
color: selectStatus!
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.outline),
),
),
],
),
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
class SeasonPanel extends StatefulWidget { class SeasonPanel extends StatefulWidget {
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes; late List<EpisodeItem> episodes;
late int cid;
late int currentIndex; late int currentIndex;
String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes /// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个 /// 有多个episodes时只显示其中一个
@ -48,6 +54,11 @@ class _SeasonPanelState extends State<SeasonPanel> {
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id) // .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!; // .episodes!;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
_videoDetailController.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((e) => e.cid == cid);
});
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.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/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart'; import './controller.dart';
@ -22,6 +23,9 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
future: _releatedController.queryRelatedVideo(), future: _releatedController.queryRelatedVideo(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
if (snapshot.data!['status']) { if (snapshot.data!['status']) {
// 请求成功 // 请求成功
return SliverList( return SliverList(
@ -51,9 +55,7 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
}, childCount: snapshot.data['data'].length + 1)); }, childCount: snapshot.data['data'].length + 1));
} else { } else {
// 请求错误 // 请求错误
return const Center( return HttpError(errMsg: '出错了', fn: () {});
child: Text('出错了'),
);
} }
} else { } else {
// 骨架屏 // 骨架屏

View File

@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
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/replyNew/index.dart'; import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
@ -773,7 +774,7 @@ InlineSpan buildContent(
// 图片渲染 // 图片渲染
if (content.pictures.isNotEmpty) { if (content.pictures.isNotEmpty) {
List picList = []; List<String> picList = [];
int len = content.pictures.length; int len = content.pictures.length;
if (len == 1) { if (len == 1) {
Map pictureItem = content.pictures.first; Map pictureItem = content.pictures.first;
@ -785,8 +786,13 @@ InlineSpan buildContent(
builder: (context, BoxConstraints box) { builder: (context, BoxConstraints box) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed('/preview', showDialog(
arguments: {'initialPage': 0, 'imgList': picList}); useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
@ -814,8 +820,13 @@ InlineSpan buildContent(
builder: (context, BoxConstraints box) { builder: (context, BoxConstraints box) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed('/preview', showDialog(
arguments: {'initialPage': i, 'imgList': picList}); useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
src: content.pictures[i]['img_src'], src: content.pictures[i]['img_src'],

View File

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.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:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -17,6 +20,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -33,13 +37,12 @@ class VideoDetailPage extends StatefulWidget {
class _VideoDetailPageState extends State<VideoDetailPage> class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware { with TickerProviderStateMixin, RouteAware {
final VideoDetailController videoDetailController = late VideoDetailController videoDetailController;
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream; late StreamController<double> appbarStream;
final VideoIntroController videoIntroController = late VideoIntroController videoIntroController;
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']); late String heroTag;
PlayerStatus playerStatus = PlayerStatus.playing; PlayerStatus playerStatus = PlayerStatus.playing;
double doubleOffset = 0; double doubleOffset = 0;
@ -51,15 +54,24 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
// 自动退出全屏 // 自动退出全屏
late bool autoExitFullcreen; late bool autoExitFullcreen;
Floating? floating;
late BangumiIntroController bangumiIntroController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
statusBarHeight = localCache.get('statusBarHeight'); statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen = autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
videoSourceInit(); videoSourceInit();
appbarStreamListen(); appbarStreamListen();
if (Platform.isAndroid) {
floating = Floating();
}
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
@ -83,16 +95,39 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
// 播放器状态监听 // 播放器状态监听
void playerListener(PlayerStatus? status) { void playerListener(PlayerStatus? status) async {
playerStatus = status!; playerStatus = status!;
if (status == PlayerStatus.completed) { if (status == PlayerStatus.completed) {
// 结束播放退出全屏 // 结束播放退出全屏
if (autoExitFullcreen) { if (autoExitFullcreen) {
plPlayerController!.triggerFullScreen(status: false); plPlayerController!.triggerFullScreen(status: false);
} }
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
}
/// 单个循环
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.play();
}
// 播放完展示控制栏 // 播放完展示控制栏
try {
PiPStatus currentStatus =
await videoDetailController.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false); plPlayerController!.onLockControl(false);
} }
} catch (_) {}
}
} }
// 继续播放或重新播放 // 继续播放或重新播放
@ -102,6 +137,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.play(); plPlayerController!.play();
} }
/// 未开启自动播放时触发播放
Future<void> handlePlay() async { Future<void> handlePlay() async {
await videoDetailController.playerInit(); await videoDetailController.playerInit();
plPlayerController = videoDetailController.plPlayerController; plPlayerController = videoDetailController.plPlayerController;
@ -111,8 +147,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override @override
void dispose() { void dispose() {
if (plPlayerController != null) {
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose(); plPlayerController!.dispose();
}
if (floating != null) {
floating!.dispose();
}
super.dispose(); super.dispose();
} }
@ -134,9 +175,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 返回当前页面时 // 返回当前页面时
void didPopNext() async { void didPopNext() async {
videoDetailController.isFirstTime = false; videoDetailController.isFirstTime = false;
videoDetailController.playerInit(); bool autoplay =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
videoDetailController.playerInit(autoplay: autoplay);
videoDetailController.autoPlay.value = true;
videoIntroController.isPaused = false; videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0) { if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.play(); plPlayerController!.play();
} }
@ -156,7 +200,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
final double pinnedHeaderHeight = final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight; statusBarHeight + kToolbarHeight + videoHeight;
return SafeArea( Widget childWhenDisabled = SafeArea(
top: false, top: false,
bottom: false, bottom: false,
child: Stack( child: Stack(
@ -203,6 +247,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController, plPlayerController,
videoDetailCtr: videoDetailCtr:
videoDetailController, videoDetailController,
floating: floating,
), ),
danmuWidget: Obx( danmuWidget: Obx(
() => PlDanmaku( () => PlDanmaku(
@ -314,51 +359,30 @@ class _VideoDetailPageState extends State<VideoDetailPage>
), ),
]; ];
}, },
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回
pinnedHeaderSliverHeightBuilder: () { pinnedHeaderSliverHeightBuilder: () {
return playerStatus != PlayerStatus.playing return pinnedHeaderHeight;
? statusBarHeight + kToolbarHeight
: pinnedHeaderHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Container( body: Container(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: CustomScrollView(
children: [
Opacity(
opacity: 0,
child: SizedBox(
width: double.infinity,
height: 0,
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
),
Expanded(
child: TabBarView(
controller: videoDetailController.tabCtr,
children: [
Builder(
builder: (context) {
return CustomScrollView(
key: const PageStorageKey<String>('简介'), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[ slivers: <Widget>[
if (videoDetailController.videoType == if (videoDetailController.videoType ==
SearchType.video) ...[ SearchType.video) ...[
const VideoIntroPanel(), const VideoIntroPanel(),
] else if (videoDetailController.videoType == ] else
SearchType.media_bangumi) ...[ // if (videoDetailController.videoType ==
BangumiIntroPanel( // SearchType.media_bangumi) ...[
cid: videoDetailController.cid) // BangumiIntroPanel(
], // cid: videoDetailController.cid)
// ],
// if (videoDetailController.videoType == // if (videoDetailController.videoType ==
// SearchType.video) ...[ // SearchType.video) ...[
// SliverPersistentHeader( // SliverPersistentHeader(
@ -375,42 +399,70 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: Divider( child: Divider(
indent: 12, indent: 12,
endIndent: 12, endIndent: 12,
color: Theme.of(context) color:
.dividerColor Theme.of(context).dividerColor.withOpacity(0.06),
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
],
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
)
],
), ),
), ),
// const RelatedVideoPanel(),
], ],
), ),
), ),
), ),
), ),
/// 重新进入会刷新
// 播放完成/暂停播放 // 播放完成/暂停播放
StreamBuilder( // StreamBuilder(
stream: appbarStream.stream, // stream: appbarStream.stream,
initialData: 0, // initialData: 0,
builder: ((context, snapshot) { // builder: ((context, snapshot) {
return ScrollAppBar( // return ScrollAppBar(
snapshot.data!.toDouble(), // snapshot.data!.toDouble(),
() => continuePlay(), // () => continuePlay(),
playerStatus, // playerStatus,
null, // null,
); // );
}), // }),
) // )
], ],
), ),
); );
Widget childWhenEnabled = FutureBuilder(
key: Key(heroTag),
future: _futureBuilderFuture,
builder: ((context, snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() => !videoDetailController.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
),
danmuWidget: Obx(
() => PlDanmaku(
key: Key(
videoDetailController.danmakuCid.value.toString()),
cid: videoDetailController.danmakuCid.value,
playerController: plPlayerController!,
),
),
),
);
} else {
return const SizedBox();
}
}),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
);
} else {
return childWhenDisabled;
}
} }
} }

View File

@ -48,15 +48,15 @@ class ScrollAppBar extends StatelessWidget {
], ],
), ),
), ),
actions: [ // actions: [
IconButton( // IconButton(
onPressed: () {}, // onPressed: () {},
icon: const Icon( // icon: const Icon(
Icons.share, // Icons.share,
size: 20, // size: 20,
)), // )),
const SizedBox(width: 12) // const SizedBox(width: 12)
], // ],
), ),
), ),
), ),

View File

@ -1,18 +1,29 @@
import 'dart:io';
import 'package:floating/floating.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:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller; final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr; final VideoDetailController? videoDetailCtr;
final Floating? floating;
const HeaderControl({ const HeaderControl({
this.controller, this.controller,
this.videoDetailCtr, this.videoDetailCtr,
this.floating,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -29,6 +40,7 @@ class _HeaderControlState extends State<HeaderControl> {
TextStyle subTitleStyle = const TextStyle(fontSize: 12); TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14); TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight); Size get preferredSize => const Size(double.infinity, kToolbarHeight);
Box localCache = GStrorage.localCache;
@override @override
void initState() { void initState() {
@ -138,17 +150,17 @@ class _HeaderControlState extends State<HeaderControl> {
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle), style: subTitleStyle),
), ),
// ListTile(
// onTap: () {},
// dense: true,
// enabled: false,
// leading: const Icon(Icons.play_circle_outline, size: 20),
// title: Text('播放设置', style: titleStyle),
// ),
ListTile( ListTile(
onTap: () {}, onTap: () => {Get.back(), showSetRepeat()},
dense: true,
leading: const Icon(Icons.repeat, size: 20),
title: Text('播放顺序', style: titleStyle),
subtitle: Text(widget.controller!.playRepeat.description,
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetDanmaku()},
dense: true, dense: true,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20), leading: const Icon(Icons.subtitles_outlined, size: 20),
title: Text('弹幕设置', style: titleStyle), title: Text('弹幕设置', style: titleStyle),
), ),
@ -454,6 +466,300 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
/// 弹幕功能
void showSetDanmaku() async {
// 屏蔽类型
List<Map<String, dynamic>> blockTypesList = [
{'value': 5, 'label': '顶部'},
{'value': 2, 'label': '滚动'},
{'value': 4, 'label': '底部'},
{'value': 6, 'label': '彩色'},
];
List blockTypes = widget.controller!.blockTypes;
// 显示区域
List<Map<String, dynamic>> showAreas = [
{'value': 0.25, 'label': '1/4屏'},
{'value': 0.5, 'label': '半屏'},
{'value': 0.75, 'label': '3/4屏'},
{'value': 1.0, 'label': '满屏'},
];
double showArea = widget.controller!.showArea;
// 不透明度
double opacityVal = widget.controller!.opacityVal;
// 字体大小
double fontSizeVal = widget.controller!.fontSizeVal;
// 弹幕速度
double danmakuSpeedVal = widget.controller!.danmakuSpeedVal;
DanmakuController danmakuController = widget.controller!.danmakuController!;
await showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return StatefulBuilder(builder: (context, StateSetter setState) {
return Container(
width: double.infinity,
height: 580,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.only(left: 14, right: 14),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45,
child: Center(child: Text('弹幕设置', style: titleStyle)),
),
const SizedBox(height: 10),
const Text('按类型屏蔽'),
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 18),
child: Row(
children: [
for (var i in blockTypesList) ...[
ActionRowLineItem(
onTap: () async {
bool isChoose = blockTypes.contains(i['value']);
if (isChoose) {
blockTypes.remove(i['value']);
} else {
blockTypes.add(i['value']);
}
widget.controller!.blockTypes = blockTypes;
setState(() {});
try {
DanmakuOption currentOption =
danmakuController.option;
DanmakuOption updatedOption =
currentOption.copyWith(
hideTop: blockTypes.contains(5),
hideBottom: blockTypes.contains(4),
hideScroll: blockTypes.contains(2),
// 添加或修改其他需要修改的选项属性
);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
text: i['label'],
selectStatus: blockTypes.contains(i['value']),
),
const SizedBox(width: 10),
]
],
),
),
const Text('显示区域'),
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 18),
child: Row(
children: [
for (var i in showAreas) ...[
ActionRowLineItem(
onTap: () {
showArea = i['value'];
widget.controller!.showArea = showArea;
setState(() {});
try {
DanmakuOption currentOption =
danmakuController.option;
DanmakuOption updatedOption =
currentOption.copyWith(area: i['value']);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
text: i['label'],
selectStatus: showArea == i['value'],
),
const SizedBox(width: 10),
]
],
),
),
Text('不透明度 ${opacityVal * 100}%'),
Padding(
padding: const EdgeInsets.only(
top: 0,
bottom: 6,
left: 10,
right: 10,
),
child: SliderTheme(
data: SliderThemeData(
trackShape: MSliderTrackShape(),
thumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
trackHeight: 10,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6.0),
),
child: Slider(
min: 0,
max: 1,
value: opacityVal,
divisions: 10,
label: '${opacityVal * 100}%',
onChanged: (double val) {
opacityVal = val;
widget.controller!.opacityVal = opacityVal;
setState(() {});
try {
DanmakuOption currentOption =
danmakuController.option;
DanmakuOption updatedOption =
currentOption.copyWith(opacity: val);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
),
),
),
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
Padding(
padding: const EdgeInsets.only(
top: 0,
bottom: 6,
left: 10,
right: 10,
),
child: SliderTheme(
data: SliderThemeData(
trackShape: MSliderTrackShape(),
thumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
trackHeight: 10,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6.0),
),
child: Slider(
min: 0.5,
max: 2.5,
value: fontSizeVal,
divisions: 20,
label: '${(fontSizeVal * 100).toStringAsFixed(1)}%',
onChanged: (double val) {
fontSizeVal = val;
widget.controller!.fontSizeVal = fontSizeVal;
setState(() {});
try {
DanmakuOption currentOption =
danmakuController.option;
DanmakuOption updatedOption =
currentOption.copyWith(
fontSize: (15 * fontSizeVal).toDouble(),
);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
),
),
),
Text('弹幕时长 ${danmakuSpeedVal.toString()}'),
Padding(
padding: const EdgeInsets.only(
top: 0,
bottom: 6,
left: 10,
right: 10,
),
child: SliderTheme(
data: SliderThemeData(
trackShape: MSliderTrackShape(),
thumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
trackHeight: 10,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6.0),
),
child: Slider(
min: 1,
max: 6,
value: danmakuSpeedVal,
divisions: 10,
label: danmakuSpeedVal.toString(),
onChanged: (double val) {
danmakuSpeedVal = val;
widget.controller!.danmakuSpeedVal = danmakuSpeedVal;
setState(() {});
try {
DanmakuOption currentOption =
danmakuController.option;
DanmakuOption updatedOption =
currentOption.copyWith(duration: val);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
),
),
),
],
),
),
);
});
},
);
}
/// 播放顺序
void showSetRepeat() async {
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择播放顺序', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in PlayRepeat.values) ...[
ListTile(
onTap: () {
widget.controller!.setPlayRepeat(i);
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: widget.controller!.playRepeat == i
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
)
],
],
),
),
),
],
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _ = widget.controller!; final _ = widget.controller!;
@ -526,6 +832,39 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
if (Platform.isAndroid) ...[
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
bool canUsePiP = false;
widget.controller!.hiddenControls(false);
try {
canUsePiP = await widget.floating!.isPipAvailable;
} on PlatformException catch (_) {
canUsePiP = false;
}
if (canUsePiP) {
final aspectRatio = Rational(
widget.videoDetailCtr!.data.dash!.video!.first.width!,
widget.videoDetailCtr!.data.dash!.video!.first.height!,
);
await widget.floating!.enable(aspectRatio: aspectRatio);
} else {}
},
icon: const Icon(
Icons.picture_in_picture_outlined,
size: 19,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
],
Obx( Obx(
() => SizedBox( () => SizedBox(
width: 45, width: 45,
@ -556,3 +895,21 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
} }
class MSliderTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData? sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
const double trackHeight = 3;
final double trackLeft = offset.dx;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
}

View File

@ -99,8 +99,6 @@ class WebviewController extends GetxController {
HomeController homeCtr = Get.find<HomeController>(); HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true); homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face; homeCtr.userFace.value = result['data'].face;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true); await LoginUtils.refreshLoginStatus(true);
} catch (err) { } catch (err) {
SmartDialog.show(builder: (context) { SmartDialog.show(builder: (context) {

View File

@ -13,14 +13,17 @@ import 'package:media_kit_video/media_kit_video.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import 'package:status_bar_control/status_bar_control.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
// import 'package:wakelock_plus/wakelock_plus.dart'; // import 'package:wakelock_plus/wakelock_plus.dart';
Box videoStorage = GStrorage.video; Box videoStorage = GStrorage.video;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
Box localCache = GStrorage.localCache;
class PlPlayerController { class PlPlayerController {
Player? _videoPlayerController; Player? _videoPlayerController;
@ -104,6 +107,7 @@ class PlPlayerController {
]; ];
PreferredSizeWidget? headerControl; PreferredSizeWidget? headerControl;
PreferredSizeWidget? bottomControl;
Widget? danmuWidget; Widget? danmuWidget;
/// 数据加载监听 /// 数据加载监听
@ -199,12 +203,39 @@ class PlPlayerController {
Rx<bool> isOpenDanmu = false.obs; Rx<bool> isOpenDanmu = false.obs;
// 关联弹幕控制器 // 关联弹幕控制器
DanmakuController? danmakuController; DanmakuController? danmakuController;
// 弹幕相关配置
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
// 添加一个私有构造函数 // 添加一个私有构造函数
PlPlayerController._() { PlPlayerController._() {
_videoType = videoType; _videoType = videoType;
isOpenDanmu.value = isOpenDanmu.value =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
blockTypes =
localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []);
showArea = localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5);
// 不透明度
opacityVal =
localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0);
// 字体大小
fontSizeVal =
localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0);
// 弹幕速度
danmakuSpeedVal =
localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0);
playRepeat = PlayRepeat.values.toList().firstWhere(
(e) =>
e.value ==
videoStorage.get(VideoBoxKey.playRepeat,
defaultValue: PlayRepeat.pause.value),
);
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) { // if (status == PlayerStatus.playing) {
// WakelockPlus.enable(); // WakelockPlus.enable();
@ -372,7 +403,7 @@ class PlPlayerController {
Media(assetUrl, httpHeaders: dataSource.httpHeaders), Media(assetUrl, httpHeaders: dataSource.httpHeaders),
play: false, play: false,
); );
} else if (dataSource.type == DataSourceType.network) { }
player.open( player.open(
Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders), Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders),
play: false, play: false,
@ -381,12 +412,6 @@ class PlPlayerController {
// player.setAudioTrack( // player.setAudioTrack(
// AudioTrack.uri(dataSource.audioSource!), // AudioTrack.uri(dataSource.audioSource!),
// ); // );
} else {
player.open(
Media(dataSource.file!.path, httpHeaders: dataSource.httpHeaders),
play: false,
);
}
return player; return player;
} }
@ -524,6 +549,12 @@ class PlPlayerController {
/// 设置倍速 /// 设置倍速
Future<void> setPlaybackSpeed(double speed) async { Future<void> setPlaybackSpeed(double speed) async {
await _videoPlayerController?.setRate(speed); await _videoPlayerController?.setRate(speed);
try {
DanmakuOption currentOption = danmakuController!.option;
DanmakuOption updatedOption = currentOption.copyWith(
duration: (currentOption.duration / speed) * playbackSpeed);
danmakuController!.updateOption(updatedOption);
} catch (_) {}
_playbackSpeed.value = speed; _playbackSpeed.value = speed;
} }
@ -725,11 +756,18 @@ class PlPlayerController {
} }
} }
void hiddenControls(bool val) {
showControls.value = val;
}
/// 设置长按倍速状态 live模式下禁用 /// 设置长按倍速状态 live模式下禁用
void setDoubleSpeedStatus(bool val) { void setDoubleSpeedStatus(bool val) {
if (videoType.value == 'live') { if (videoType.value == 'live') {
return; return;
} }
if (controlsLock.value) {
return;
}
_doubleSpeedStatus.value = val; _doubleSpeedStatus.value = val;
double currentSpeed = playbackSpeed; double currentSpeed = playbackSpeed;
if (val) { if (val) {
@ -754,7 +792,7 @@ class PlPlayerController {
Future<void> triggerFullScreen({bool status = true}) async { Future<void> triggerFullScreen({bool status = true}) async {
FullScreenMode mode = FullScreenModeCode.fromCode( FullScreenMode mode = FullScreenModeCode.fromCode(
setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!; setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
await StatusBarControl.setHidden(true, animation: StatusBarAnimation.FADE);
if (!isFullScreen.value && status) { if (!isFullScreen.value && status) {
/// 按照视频宽高比决定全屏方向 /// 按照视频宽高比决定全屏方向
switch (mode) { switch (mode) {
@ -773,7 +811,7 @@ class PlPlayerController {
/// 进入全屏 /// 进入全屏
await enterFullScreen(); await enterFullScreen();
// //
await verticalScreen(); await verticalScreen();
break; break;
case FullScreenMode.horizontal: case FullScreenMode.horizontal:
@ -791,20 +829,29 @@ class PlPlayerController {
useSafeArea: false, useSafeArea: false,
builder: (context) => Dialog.fullscreen( builder: (context) => Dialog.fullscreen(
backgroundColor: Colors.black, backgroundColor: Colors.black,
child: SafeArea(
bottom:
direction.value == 'vertical' || mode == FullScreenMode.vertical
? true
: false,
child: PLVideoPlayer( child: PLVideoPlayer(
controller: this, controller: this,
headerControl: headerControl, headerControl: headerControl,
bottomControl: bottomControl,
danmuWidget: danmuWidget, danmuWidget: danmuWidget,
), ),
), ),
),
); );
if (result == null) { if (result == null) {
// 退出全屏 // 退出全屏
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
exitFullScreen(); exitFullScreen();
await verticalScreen(); await verticalScreen();
toggleFullScreen(false); toggleFullScreen(false);
} }
} else if (isFullScreen.value) { } else if (isFullScreen.value) {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
Get.back(); Get.back();
exitFullScreen(); exitFullScreen();
await verticalScreen(); await verticalScreen();
@ -842,6 +889,9 @@ class PlPlayerController {
if (!_enableHeart) { if (!_enableHeart) {
return false; return false;
} }
if (videoType.value == 'live') {
return;
}
// 播放状态变化时,更新 // 播放状态变化时,更新
if (type == 'status') { if (type == 'status') {
await VideoHttp.heartBeat( await VideoHttp.heartBeat(
@ -862,6 +912,11 @@ class PlPlayerController {
} }
} }
setPlayRepeat(PlayRepeat type) {
playRepeat = type;
videoStorage.put(VideoBoxKey.playRepeat, type.value);
}
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) { if (type == 'single' && playerCount.value > 1) {
@ -891,6 +946,13 @@ class PlPlayerController {
// playerStatus.status.close(); // playerStatus.status.close();
// dataStatus.status.close(); // dataStatus.status.close();
/// 缓存本次弹幕选项
localCache.put(LocalCacheKey.danmakuBlockType, blockTypes);
localCache.put(LocalCacheKey.danmakuShowArea, showArea);
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal);
removeListeners(); removeListeners();
await _videoPlayerController?.dispose(); await _videoPlayerController?.dispose();
_videoPlayerController = null; _videoPlayerController = null;

View File

@ -0,0 +1,25 @@
enum PlayRepeat {
pause,
listOrder,
singleCycle,
listCycle,
}
extension PlayRepeatExtension on PlayRepeat {
static final List<String> _descList = [
'播完暂停',
'顺序播放',
'单个循环',
'列表循环',
];
get description => _descList[index];
static final List<double> _valueList = [
1,
2,
3,
4,
];
get value => _valueList[index];
get defaultValue => _valueList[1];
}

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -11,16 +12,17 @@ Future<void> landScape() async {
if (kIsWeb) { if (kIsWeb) {
await document.documentElement?.requestFullscreen(); await document.documentElement?.requestFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) { } else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode( // await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky, // SystemUiMode.immersiveSticky,
overlays: [], // overlays: [],
); // );
await SystemChrome.setPreferredOrientations( // await SystemChrome.setPreferredOrientations(
[ // [
DeviceOrientation.landscapeLeft, // DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight, // DeviceOrientation.landscapeRight,
], // ],
); // );
await AutoOrientation.landscapeAutoMode(forceSensor: true);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video') await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod( .invokeMethod(

View File

@ -29,11 +29,13 @@ import 'widgets/forward_seek.dart';
class PLVideoPlayer extends StatefulWidget { class PLVideoPlayer extends StatefulWidget {
final PlPlayerController controller; final PlPlayerController controller;
final PreferredSizeWidget? headerControl; final PreferredSizeWidget? headerControl;
final PreferredSizeWidget? bottomControl;
final Widget? danmuWidget; final Widget? danmuWidget;
const PLVideoPlayer({ const PLVideoPlayer({
required this.controller, required this.controller,
this.headerControl, this.headerControl,
this.bottomControl,
this.danmuWidget, this.danmuWidget,
super.key, super.key,
}); });
@ -70,6 +72,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late FullScreenMode mode; late FullScreenMode mode;
late int defaultBtmProgressBehavior; late int defaultBtmProgressBehavior;
late bool enableQuickDouble; late bool enableQuickDouble;
late bool enableBackgroundPlay;
void onDoubleTapSeekBackward() { void onDoubleTapSeekBackward() {
setState(() { setState(() {
@ -86,8 +89,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 双击播放、暂停 // 双击播放、暂停
void onDoubleTapCenter() { void onDoubleTapCenter() {
final _ = widget.controller; final _ = widget.controller;
if (_.playerStatus.status.value == PlayerStatus.playing) { if (_.videoPlayerController!.state.playing) {
_.togglePlay(); _.pause();
} else { } else {
_.play(); _.play();
} }
@ -120,11 +123,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
vsync: this, duration: const Duration(milliseconds: 300)); vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!; videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl; widget.controller.headerControl = widget.headerControl;
widget.controller.bottomControl = widget.bottomControl;
widget.controller.danmuWidget = widget.danmuWidget; widget.controller.danmuWidget = widget.danmuWidget;
defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior, defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,
defaultValue: BtmProgresBehavior.values.first.code); defaultValue: BtmProgresBehavior.values.first.code);
enableQuickDouble = enableQuickDouble =
setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true); setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true);
enableBackgroundPlay =
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
Future.microtask(() async { Future.microtask(() async {
try { try {
@ -225,6 +231,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Video( () => Video(
controller: videoController, controller: videoController,
controls: NoVideoControls, controls: NoVideoControls,
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
subtitleViewConfiguration: SubtitleViewConfiguration( subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle, style: subTitleStyle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -239,7 +246,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Align( () => Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: FractionalTranslation( child: FractionalTranslation(
translation: const Offset(0.0, 1), // 上下偏移量(负数向上偏移) translation: const Offset(0.0, 0.3), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity( child: AnimatedOpacity(
curve: Curves.easeInOut, curve: Curves.easeInOut,
opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0, opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,
@ -248,26 +255,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0x88000000), color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0), borderRadius: BorderRadius.circular(16.0),
), ),
height: 34.0, height: 32.0,
width: 86.0, width: 70.0,
child: Row( child: const Center(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Text(
children: [
const SizedBox(width: 3),
Image.asset(
'assets/images/run-pokemon.gif',
height: 20,
),
const Text(
'倍速中', '倍速中',
style: TextStyle(color: Colors.white, fontSize: 12), style: TextStyle(color: Colors.white, fontSize: 13),
),
const SizedBox(width: 4),
],
),
), ),
)),
), ),
), ),
), ),
@ -427,23 +424,23 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
Obx(() { // Obx(() {
if (_.buffered.value == Duration.zero) { // if (_.buffered.value == Duration.zero) {
return Positioned.fill( // return Positioned.fill(
child: Container( // child: Container(
color: Colors.black, // color: Colors.black,
child: Center( // child: Center(
child: Image.asset( // child: Image.asset(
'assets/images/loading.gif', // 'assets/images/loading.gif',
height: 25, // height: 25,
), // ),
), // ),
), // ),
); // );
} else { // } else {
return Container(); // return Container();
} // }
}), // }),
/// 弹幕面板 /// 弹幕面板
if (widget.danmuWidget != null) if (widget.danmuWidget != null)
@ -562,9 +559,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 头部、底部控制条 // 头部、底部控制条
Obx( Obx(
() => Visibility( () => Column(
visible: _.videoType.value != 'live',
child: Column(
children: [ children: [
if (widget.headerControl != null) if (widget.headerControl != null)
ClipRect( ClipRect(
@ -583,15 +578,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
controller: animationController, controller: animationController,
visible: !_.controlsLock.value && _.showControls.value, visible: !_.controlsLock.value && _.showControls.value,
position: 'bottom', position: 'bottom',
child: BottomControl( child: widget.bottomControl ??
BottomControl(
controller: widget.controller, controller: widget.controller,
triggerFullScreen: widget.controller.triggerFullScreen), triggerFullScreen:
widget.controller.triggerFullScreen),
), ),
), ),
], ],
), ),
), ),
),
/// 进度条 live模式下禁用 /// 进度条 live模式下禁用
Obx( Obx(
@ -608,6 +604,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
!_.isFullScreen.value) { !_.isFullScreen.value) {
return Container(); return Container();
} }
if (_.videoType.value == 'live') {
return Container();
}
if (value > max || max <= 0) { if (value > max || max <= 0) {
return Container(); return Container();
} }

View File

@ -14,9 +14,11 @@ import 'package:pilipala/pages/follow/index.dart';
import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/html/index.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart'; import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/member_search/index.dart';
import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart'; import 'package:pilipala/pages/searchResult/index.dart';
@ -34,6 +36,8 @@ import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../pages/history_search/index.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
bool iosTransition = bool iosTransition =
setting.get(SettingBoxKey.iosTransition, defaultValue: false); setting.get(SettingBoxKey.iosTransition, defaultValue: false);
@ -41,19 +45,19 @@ bool iosTransition =
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
// 首页(推荐) // 首页(推荐)
CustomGetPage(name: '/', page: () => const HomePage()), CustomGetPage(name: '/', page: () => HomePage()),
// 热门 // 热门
CustomGetPage(name: '/hot', page: () => const HotPage()), CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情 // 视频详情
CustomGetPage(name: '/video', page: () => const VideoDetailPage()), CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览 // 图片预览
GetPage( // GetPage(
name: '/preview', // name: '/preview',
page: () => const ImagePreview(), // page: () => const ImagePreview(),
transition: Transition.fade, // transition: Transition.fade,
transitionDuration: const Duration(milliseconds: 300), // transitionDuration: const Duration(milliseconds: 300),
showCupertinoParallax: false, // showCupertinoParallax: false,
), // ),
// //
CustomGetPage(name: '/webview', page: () => const WebviewPage()), CustomGetPage(name: '/webview', page: () => const WebviewPage()),
// 设置 // 设置
@ -85,6 +89,7 @@ class Routes {
CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()), CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
// 用户中心 // 用户中心
CustomGetPage(name: '/member', page: () => const MemberPage()), CustomGetPage(name: '/member', page: () => const MemberPage()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复 // 二级回复
CustomGetPage( CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), name: '/replyReply', page: () => const VideoReplyReplyPanel()),
@ -107,6 +112,11 @@ class Routes {
name: '/displayModeSetting', page: () => const SetDiaplayMode()), name: '/displayModeSetting', page: () => const SetDiaplayMode()),
// 关于 // 关于
CustomGetPage(name: '/about', page: () => const AboutPage()), CustomGetPage(name: '/about', page: () => const AboutPage()),
//
CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),
// 历史记录搜索
CustomGetPage(
name: '/historySearch', page: () => const HistorySearchPage()),
]; ];
} }

View File

@ -119,7 +119,7 @@ class PiliSchame {
arguments: { arguments: {
'pic': bangumiDetail.cover, 'pic': bangumiDetail.cover,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
), ),
); );

View File

@ -3,6 +3,7 @@ import 'package:ns_danmaku/ns_danmaku.dart';
class DmUtils { class DmUtils {
static Color decimalToColor(int decimalColor) { static Color decimalToColor(int decimalColor) {
// 16777215 表示白色
int red = (decimalColor >> 16) & 0xFF; int red = (decimalColor >> 16) & 0xFF;
int green = (decimalColor >> 8) & 0xFF; int green = (decimalColor >> 8) & 0xFF;
int blue = decimalColor & 0xFF; int blue = decimalColor & 0xFF;

View File

@ -1,8 +1,6 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
class LoginUtils { class LoginUtils {
@ -17,12 +15,6 @@ class LoginUtils {
MineController mineCtr = Get.find<MineController>(); MineController mineCtr = Get.find<MineController>();
mineCtr.userLogin.value = status; mineCtr.userLogin.value = status;
DynamicsController dynamicsCtr = Get.find<DynamicsController>();
dynamicsCtr.userLogin.value = status;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.userLogin.value = status;
} catch (err) { } catch (err) {
SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}'); SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}');
} }

View File

@ -34,7 +34,12 @@ class GStrorage {
}, },
); );
// 本地缓存 // 本地缓存
localCache = await Hive.openBox('localCache'); localCache = await Hive.openBox(
'localCache',
compactionStrategy: (entries, deletedEntries) {
return deletedEntries > 4;
},
);
// 设置 // 设置
setting = await Hive.openBox('setting'); setting = await Hive.openBox('setting');
// 搜索历史 // 搜索历史
@ -102,6 +107,7 @@ class SettingBoxKey {
// youtube 双击快进快退 // youtube 双击快进快退
static const String enableQuickDouble = 'enableQuickDouble'; static const String enableQuickDouble = 'enableQuickDouble';
static const String enableShowDanmaku = 'enableShowDanmaku'; static const String enableShowDanmaku = 'enableShowDanmaku';
static const String enableBackgroundPlay = 'enableBackgroundPlay';
/// 隐私 /// 隐私
static const String blackMidsList = 'blackMidsList'; static const String blackMidsList = 'blackMidsList';
@ -113,6 +119,8 @@ class SettingBoxKey {
static const String enableHotKey = 'enableHotKey'; static const String enableHotKey = 'enableHotKey';
static const String enableQuickFav = 'enableQuickFav'; static const String enableQuickFav = 'enableQuickFav';
static const String enableWordRe = 'enableWordRe'; static const String enableWordRe = 'enableWordRe';
static const String enableSearchWord = 'enableSearchWord';
static const String enableRcmdDynamic = 'enableRcmdDynamic';
/// 外观 /// 外观
static const String themeMode = 'themeMode'; static const String themeMode = 'themeMode';
@ -132,6 +140,13 @@ class LocalCacheKey {
// //
static const String wbiKeys = 'wbiKeys'; static const String wbiKeys = 'wbiKeys';
static const String timeStamp = 'timeStamp'; static const String timeStamp = 'timeStamp';
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕速度
static const String danmakuBlockType = 'danmakuBlockType';
static const String danmakuShowArea = 'danmakuShowArea';
static const String danmakuOpacity = 'danmakuOpacity';
static const String danmakuFontScale = 'danmakuFontScale';
static const String danmakuSpeed = 'danmakuSpeed';
} }
class VideoBoxKey { class VideoBoxKey {
@ -141,4 +156,6 @@ class VideoBoxKey {
static const String videoBrightness = 'videoBrightness'; static const String videoBrightness = 'videoBrightness';
// 倍速 // 倍速
static const String videoSpeed = 'videoSpeed'; static const String videoSpeed = 'videoSpeed';
// 播放顺序
static const String playRepeat = 'playRepeat';
} }

View File

@ -215,7 +215,10 @@ class Utils {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('🎉 发现新版本 '), title: const Text('🎉 发现新版本 '),
content: Column( content: SizedBox(
height: 280,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -228,6 +231,8 @@ class Utils {
Text(data.body!), Text(data.body!),
], ],
), ),
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
@ -245,11 +250,11 @@ class Utils {
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
}, },
child: const Text('网盘下载'), child: const Text('网盘'),
), ),
TextButton( TextButton(
onPressed: () => matchVersion(data), onPressed: () => matchVersion(data),
child: const Text('Github下载'), child: const Text('Github'),
), ),
], ],
); );

View File

@ -65,6 +65,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
auto_orientation:
dependency: "direct main"
description:
name: auto_orientation
sha256: cd56bb59b36fa54cc28ee254bc600524f022a4862f31d5ab20abd7bb1c54e678
url: "https://pub.dev"
source: hosted
version: "2.3.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -257,6 +265,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
csslib:
dependency: transitive
description:
name: csslib
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
url: "https://pub.dev"
source: hosted
version: "0.17.3"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -321,14 +337,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
dio_http2_adapter:
dependency: "direct main"
description:
name: dio_http2_adapter
sha256: ada89ff1ea108c191188e5112b1ae87f12f5995f8cbf50afe87a736e36f1a5af
url: "https://pub.dev"
source: hosted
version: "2.3.1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -425,6 +433,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
floating:
dependency: "direct main"
description:
name: floating
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -454,6 +470,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee"
url: "https://pub.dev"
source: hosted
version: "3.0.0-beta.2"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -581,6 +605,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
html:
dependency: "direct main"
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -589,14 +621,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
http2:
dependency: transitive
description:
name: http2
sha256: "38db0c4aa9f1cd238a5d2e86aa0cc7cc91c77e0c6c94ba64bbe85e4ff732a952"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
http_client_helper: http_client_helper:
dependency: transitive dependency: transitive
description: description:
@ -677,6 +701,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
list_counter:
dependency: transitive
description:
name: list_counter
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
url: "https://pub.dev"
source: hosted
version: "1.0.2"
loading_more_list: loading_more_list:
dependency: "direct main" dependency: "direct main"
description: description:
@ -721,12 +753,12 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit name: media_kit
sha256: "66f04934bcadf592f24d829127471e4dc304de8e9bba5795ade2f3e95552ebfc" sha256: "92c7f59e075d74471b31e703f81ccc1d7102739ebcce945b30a6417fa2f751d5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.6" version: "1.1.7"
media_kit_libs_android_video: media_kit_libs_android_video:
dependency: "direct main" dependency: transitive
description: description:
name: media_kit_libs_android_video name: media_kit_libs_android_video
sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b" sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b"
@ -734,7 +766,7 @@ packages:
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
media_kit_libs_ios_video: media_kit_libs_ios_video:
dependency: "direct main" dependency: transitive
description: description:
name: media_kit_libs_ios_video name: media_kit_libs_ios_video
sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159 sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159
@ -742,7 +774,7 @@ packages:
source: hosted source: hosted
version: "1.1.3" version: "1.1.3"
media_kit_libs_linux: media_kit_libs_linux:
dependency: "direct main" dependency: transitive
description: description:
name: media_kit_libs_linux name: media_kit_libs_linux
sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8" sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8"
@ -750,15 +782,23 @@ packages:
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
media_kit_libs_macos_video: media_kit_libs_macos_video:
dependency: "direct main" dependency: transitive
description: description:
name: media_kit_libs_macos_video name: media_kit_libs_macos_video
sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2 sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.3" version: "1.1.3"
media_kit_libs_windows_video: media_kit_libs_video:
dependency: "direct main" dependency: "direct main"
description:
name: media_kit_libs_video
sha256: d961c49bc0d454524014b76fd66db1aa06e673f03b616f5fdbc59c405178a878
url: "https://pub.dev"
source: hosted
version: "1.0.1"
media_kit_libs_windows_video:
dependency: transitive
description: description:
name: media_kit_libs_windows_video name: media_kit_libs_windows_video
sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350" sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350"
@ -766,7 +806,7 @@ packages:
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
media_kit_native_event_loop: media_kit_native_event_loop:
dependency: "direct main" dependency: transitive
description: description:
name: media_kit_native_event_loop name: media_kit_native_event_loop
sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4 sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4
@ -777,10 +817,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_video name: media_kit_video
sha256: "809a3797da7d49fad85f139555b352dd615f9d2da6ae9f1745c6978963491bae" sha256: cd3ab78e7626146f115134b82c4029ac5987ba6351719c9067d86789723e0c12
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.7" version: "1.1.8"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -1211,6 +1251,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
system_proxy:
dependency: "direct main"
description:
name: system_proxy
sha256: bbdfc9736a963409941fb0e7c494606c1f13c2be34de15833ee385da83cf7ab0
url: "https://pub.dev"
source: hosted
version: "0.1.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.6 version: 1.0.7
environment: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@ -43,7 +43,6 @@ dependencies:
# 网络 # 网络
dio: ^5.3.0 dio: ^5.3.0
cookie_jar: ^4.0.8 cookie_jar: ^4.0.8
dio_http2_adapter: ^2.3.1
dio_cookie_manager: ^3.1.0 dio_cookie_manager: ^3.1.0
connectivity_plus: ^4.0.1 connectivity_plus: ^4.0.1
@ -84,14 +83,9 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
# 视频播放器 # 视频播放器
media_kit: ^1.1.4 # Primary package. media_kit: ^1.1.7
media_kit_video: ^1.1.5 # For video rendering. media_kit_video: ^1.1.8
media_kit_native_event_loop: ^1.0.7 # Support for higher number of concurrent instances & better performance. media_kit_libs_video: ^1.0.1
media_kit_libs_android_video: ^1.3.2 # Android package for video native libraries.
media_kit_libs_ios_video: ^1.1.3 # iOS package for video native libraries.
media_kit_libs_macos_video: ^1.1.3 # macOS package for video native libraries.
media_kit_libs_windows_video: ^1.0.7 # Windows package for video native libraries.
media_kit_libs_linux: ^1.1.1
# 音量、亮度、屏幕控制 # 音量、亮度、屏幕控制
flutter_volume_controller: ^1.2.7 flutter_volume_controller: ^1.2.7
@ -100,7 +94,7 @@ dependencies:
universal_platform: ^1.0.0+1 universal_platform: ^1.0.0+1
# 进度条 # 进度条
audio_video_progress_bar: ^1.0.1 audio_video_progress_bar: ^1.0.1
# auto_orientation: ^2.3.1 auto_orientation: ^2.3.1
protobuf: ^3.0.0 protobuf: ^3.0.0
animations: ^2.0.7 animations: ^2.0.7
@ -121,6 +115,15 @@ dependencies:
ref: master ref: master
# 状态栏图标控制 # 状态栏图标控制
status_bar_control: ^3.2.1 status_bar_control: ^3.2.1
# 代理
system_proxy: ^0.1.0
# pip
floating: ^2.0.1
# html解析
html: ^0.15.4
# html渲染
flutter_html: ^3.0.0-beta.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: