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)
- status_bar_control (3.2.1):
- Flutter
- system_proxy (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1):
@ -65,6 +67,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/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`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -109,6 +112,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/ios"
status_bar_control:
:path: ".symlinks/plugins/status_bar_control/ios"
system_proxy:
:path: ".symlinks/plugins/system_proxy/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
@ -139,6 +144,7 @@ SPEC CHECKSUMS:
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
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/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -33,9 +34,10 @@ class VideoCardH extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
}
// if (longPress != null) {
// longPress!();
// }
MemberController().blockUser(videoItem.mid);
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
@ -188,46 +190,7 @@ class VideoContent extends StatelessWidget {
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(),
// 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')
SizedBox(
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: {
'pic': videoItem.pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
// 'videoType': SearchType.media_bangumi,
},
),
);
@ -112,12 +112,22 @@ class VideoCardV extends StatelessWidget {
height: maxHeight,
),
),
if (crossAxisCount == 1 && videoItem.duration != null)
if (videoItem.duration != null)
if (crossAxisCount == 1) ...[
PBadge(
bottom: 10,
right: 10,
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) ...[
const SizedBox(height: 3),
const SizedBox(height: 2),
VideoStat(
videoItem: videoItem,
),
@ -247,7 +257,7 @@ class VideoContent extends StatelessWidget {
},
),
] 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 delHistory = '/x/v2/history/delete';
// 热搜
static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword';
@ -239,6 +242,9 @@ class Api {
// wts=1689767832
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';
@ -285,6 +291,9 @@ class Api {
// 黑名单
static const String blackLst = '/x/relation/blacks';
// 移除黑名单
static const String removeBlack = '/x/relation/modify';
// github 获取最新版
static const String latestApp =
'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 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/http/constants.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';
class Request {
@ -93,18 +92,14 @@ class Request {
receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头.
headers: {
// 'cookie': '',
'keep-alive': true,
'user-agent': headerUa('pc'),
'Accept-Encoding': 'gzip'
},
persistentConnection: true,
);
dio = Dio(options)
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
dio = Dio(options);
//添加拦截器
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';
} else {
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;
}

View File

@ -65,7 +65,7 @@ class MemberHttp {
int ps = 30,
int tid = 0,
int? pn,
String keyword = '',
String? keyword,
String order = 'pubdate',
bool orderAvoided = true,
}) async {
@ -74,7 +74,7 @@ class MemberHttp {
'ps': ps,
'tid': tid,
'pn': pn,
'keyword': keyword,
'keyword': keyword ?? '',
'order': order,
'platform': 'web',
'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/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async {
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 {
'status': true,
'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 {
return {
'status': false,
@ -20,18 +58,6 @@ class SearchHttp {
'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 {
return {
'status': false,
@ -61,8 +87,15 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data;
try {
switch (searchType) {
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']);
break;
case SearchType.live_room:
@ -74,16 +107,24 @@ class SearchHttp {
case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']);
break;
case SearchType.article:
data = SearchArticleModel.fromJson(res.data['data']);
break;
}
return {
'status': true,
'data': data,
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'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']};
}
}
// 删除历史记录
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 {
static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting;
static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@ -73,6 +75,7 @@ class VideoHttp {
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i));

View File

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

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList();
}
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id,
this.cid,
// this.author,
// this.mid,
this.mid,
// this.typeid,
// this.typename,
this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id;
int? cid;
// String? author;
// String? mid;
int? mid;
// String? typeid;
// String? typename;
String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl'];
aid = json['aid'];
bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
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.tab,
this.list,
this.page,
});
Cursor? cursor;
List<HisTabItem>? tab;
List<HisListItem>? list;
Map? page;
HistoryData.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']);
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList();
list =
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList();
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
tab = json['tab'] != null
? json['tab'].map<HisTabItem>((e) => HisTabItem.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.tagName,
this.liveStatus,
this.checked,
});
String? title;
@ -105,6 +112,7 @@ class HisListItem {
int? kid;
String? tagName;
int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title'];
@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid'];
tagName = json['tag_name'];
liveStatus = json['live_status'];
checked = false;
}
}

View File

@ -95,6 +95,17 @@ class _AboutPageState extends State<AboutPage> {
style: subTitleStyle,
),
),
ListTile(
onTap: () => _aboutController.panDownload(),
title: const Text('网盘下载'),
trailing: Text(
'提取码pili',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
),
ListTile(
onTap: () => _aboutController.feedback(),
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() {
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/pages/video/detail/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/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
}
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) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList;
if (data['status']) {

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false,
title: Obx(
() => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000',
'黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
overflow: TextOverflow.ellipsis,
),
dense: true,
// trailing: TextButton(
// onPressed: () {},
// child: const Text('移除'),
// ),
trailing: TextButton(
onPressed: () => _blackListController
.removeBlack(list[index].mid),
child: const Text('移除'),
),
);
},
),
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController {
int currentPage = 1;
int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async {
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) {
if (type == 'init') {
blackList.value = result['data'].list;
total.value = result['data'].total;
} else {
blackList.addAll(result['data'].list);
}
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
}
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;
Box setting = GStrorage.setting;
late bool enableShowDanmaku;
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
@override
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;
int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes;
if (!playerController.isOpenDanmu.value) {
return;
@ -99,6 +110,8 @@ class _PlDanmakuState extends State<PlDanmaku> {
var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) {
// 屏蔽彩色弹幕
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
_controller!.addItems([
DanmakuItem(
element.content,
@ -107,6 +120,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
type: DmUtils.getPosition(element.mode),
)
]);
}
ctr.currentDmIndex++;
} else {
if (!playerController.isOpenDanmu.value) {
@ -135,9 +149,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
widget.playerController.danmakuController = _controller = e;
},
option: DanmakuOption(
fontSize: 15,
area: 0.5,
duration: 5,
fontSize: 15 * fontSizeVal,
area: showArea,
opacity: opacityVal,
duration: danmakuSpeedVal * widget.playerController.playbackSpeed,
),
statusChanged: (isPlaying) {},
),

View File

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

View File

@ -38,15 +38,25 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
// floor 1原创 2转发
if (Get.arguments['floor'] == 1) {
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 {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
}
} catch (_) {}
}
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
type = (commentType == 0) ? 11 : commentType;
action =
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();
titleStreamC = StreamController<bool>();
scrollController.addListener(_listen);

View File

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

View File

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

View File

@ -41,7 +41,8 @@ class DynamicPanel extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
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),
forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2),

View File

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

View File

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

View File

@ -1,14 +1,34 @@
import 'package:flutter/material.dart';
import 'package:get/get.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) {
final spacer = _VerticalSpaceSpan(0.0);
try {
TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary);
List<InlineSpan> spanChilds = [];
for (var i in item.modules.moduleDynamic.desc.richTextNodes) {
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
String contentType = 'desc';
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(
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
}
@ -67,7 +87,11 @@ InlineSpan richNode(item, context) {
onTap: () {
Get.toNamed(
'/webview',
parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''},
parameters: {
'url': i.origText,
'type': 'url',
'pageTitle': ''
},
);
},
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(
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: 4),
if (item.modules.moduleDynamic.topic != null) ...[
Padding(
padding: floor == 2
? EdgeInsets.zero
: const EdgeInsets.only(left: 12, right: 12),
child: GestureDetector(
child: Text(
'#${item.modules.moduleDynamic.topic.name}',
style: authorStyle,
),
),
),
const SizedBox(height: 6),
],
/// fix #话题跟content重复
// if (item.modules.moduleDynamic.topic != null) ...[
// Padding(
// padding: floor == 2
// ? EdgeInsets.zero
// : const EdgeInsets.only(left: 12, right: 12),
// child: GestureDetector(
// child: Text(
// '#${item.modules.moduleDynamic.topic.name}',
// style: authorStyle,
// ),
// ),
// ),
// const SizedBox(height: 6),
// ],
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
Text.rich(richNode(item, context)),
const SizedBox(height: 6),

View File

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

View File

@ -168,7 +168,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'${_favDetailController.favInfo['media_count'] ?? '-'}条视频',
'${_favDetailController.favList.length}条视频',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
@ -187,13 +187,19 @@ class _FavDetailPageState extends State<FavDetailPage> {
if (_favDetailController.item!.mediaCount == 0) {
return const NoData();
} else {
List favList = _favDetailController.favList;
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
() => favList.isEmpty
? const SliverToBoxAdapter(child: SizedBox())
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
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/common/widgets/network_img_layer.dart';
import '../controller.dart';
// 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget {
final dynamic videoItem;
final FavDetailController _favDetailController =
Get.put(FavDetailController());
final Function? callFn;
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
: super(key: key);
@override
Widget build(BuildContext context) {
int id = videoItem.id;
String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
String heroTag = Utils.makeHeroTag(id);
return Dismissible(
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(
return InkWell(
onTap: () async {
// int? seasonId;
String? epId;
@ -71,8 +49,8 @@ class FavVideoCardH extends StatelessWidget {
Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem,
'heroTag': heroTag,
'videoType':
epId != null ? SearchType.media_bangumi : SearchType.video,
// 'videoType':
// epId != null ? SearchType.media_bangumi : SearchType.video,
});
},
child: Column(
@ -113,10 +91,8 @@ class FavVideoCardH extends StatelessWidget {
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(4),
color:
Colors.black54.withOpacity(0.4)),
borderRadius: BorderRadius.circular(4),
color: Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
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 {
final dynamic videoItem;
const VideoContent({super.key, required this.videoItem});
final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn});
@override
Widget build(BuildContext context) {
@ -166,6 +142,8 @@ class VideoContent extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
@ -173,15 +151,50 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 2),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
const Spacer(),
SizedBox(
width: 26,
height: 26,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
),
const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
],
),
],

View File

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

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs;
RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override
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
@ -48,14 +65,31 @@ class _HistoryPageState extends State<HistoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
appBar: AppBarWidget(
visible: _historyController.enableMultiple.value,
child1: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back_outlined),
),
title: Text(
'观看记录',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
// TextButton(
// onPressed: () {
// _historyController.enableMultiple.value = true;
// setState(() {});
// },
// child: const Text('多选'),
// ),
IconButton(
onPressed: () => Get.toNamed('/historySearch'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
@ -66,6 +100,13 @@ class _HistoryPageState extends State<HistoryPage> {
case 'clear':
_historyController.onClearHistory();
break;
case 'del':
_historyController.onDelHistory();
break;
case 'multiple':
_historyController.enableMultiple.value = true;
setState(() {});
break;
default:
}
},
@ -82,11 +123,62 @@ class _HistoryPageState extends State<HistoryPage> {
value: 'clear',
child: Text('清空观看记录'),
),
const PopupMenuItem<String>(
value: 'del',
child: Text('删除已看记录'),
),
const PopupMenuItem<String>(
value: 'multiple',
child: Text('多选删除'),
),
],
),
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(
onRefresh: () async {
await _historyController.onRefresh();
@ -99,6 +191,9 @@ class _HistoryPageState extends State<HistoryPage> {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
return Obx(
@ -109,6 +204,9 @@ class _HistoryPageState extends State<HistoryPage> {
return HistoryItem(
videoItem:
_historyController.historyList[index],
ctr: _historyController,
onChoose: () => onChoose(index),
onUpdateMultiple: () => onUpdateMultiple(),
);
},
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/search_type.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/utils.dart';
class HistoryItem extends StatelessWidget {
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
Widget build(BuildContext context) {
@ -25,6 +37,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid);
return InkWell(
onTap: () async {
if (ctr!.enableMultiple.value) {
feedBack();
onChoose!();
return;
}
if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ??
// videoItem.history.oid ??
@ -72,7 +89,7 @@ class HistoryItem extends StatelessWidget {
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
// 'videoType': SearchType.media_bangumi,
},
);
} else {
@ -100,7 +117,7 @@ class HistoryItem extends StatelessWidget {
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
// 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
@ -115,6 +132,17 @@ class HistoryItem extends StatelessWidget {
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(
children: [
Padding(
@ -129,6 +157,8 @@ class HistoryItem extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
@ -161,7 +191,8 @@ class HistoryItem extends StatelessWidget {
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) ||
.contains(
videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
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 {
final dynamic videoItem;
const VideoContent({super.key, required this.videoItem});
final dynamic? ctr;
const VideoContent({super.key, required this.videoItem, this.ctr});
@override
Widget build(BuildContext context) {
@ -244,16 +330,12 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
tooltip: '功能菜单',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
@ -264,6 +346,10 @@ class VideoContent extends StatelessWidget {
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
PopupMenuItem<String>(
onTap: () async {
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:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/black.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/models/user/black.dart';
import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
@ -15,6 +17,13 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
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
void onInit() {
@ -51,7 +60,33 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
void updateLoginStatus(val) async {
userInfo = await userInfoCache.get('userInfoCache');
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 : '';
}
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';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
Function? callFn;
HomePage({Key? key, this.callFn}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
@ -25,15 +26,16 @@ class _HomePageState extends State<HomePage>
showUserBottonSheet() {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
widget.callFn!();
// showModalBottomSheet(
// context: context,
// builder: (_) => const SizedBox(
// height: 450,
// child: MinePage(),
// ),
// clipBehavior: Clip.hardEdge,
// isScrollControlled: true,
// );
}
@override
@ -50,37 +52,6 @@ class _HomePageState extends State<HomePage>
ctr: _homeController,
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(
children: [
const Expanded(child: SearchPage()),
const SizedBox(width: 10),
Obx(
() => ctr!.userLogin.value
? 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,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data as Map;
if (data['status']) {
return SliverLayoutBuilder(

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'controller.dart';
import 'widgets/bottom_control.dart';
class LiveRoomPage extends StatefulWidget {
const LiveRoomPage({super.key});
@ -18,6 +22,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bool isShowCover = true;
bool isPlay = true;
Floating? floating;
@override
void initState() {
@ -31,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
},
);
if (Platform.isAndroid) {
floating = Floating();
}
}
@override
void dispose() {
plPlayerController!.dispose();
if (floating != null) {
floating!.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
return Scaffold(
Widget childWhenDisabled = Scaffold(
primary: true,
appBar: AppBar(
centerTitle: false,
@ -87,14 +97,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
),
body: Column(
children: [
Hero(
tag: _liveRoomController.heroTag,
child: Stack(
Stack(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(controller: plPlayerController!)
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
),
// 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:flutter/material.dart';
import 'package:hive/hive.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/pages/dynamics/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
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 =
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/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/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
@ -18,80 +19,16 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController());
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;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
@override
void 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 {
feedBack();
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();
}
void openDrawer() {
_scaffoldKey.currentState?.openDrawer();
}
@override
@ -113,55 +50,10 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
return WillPopScope(
onWillPop: () => _mainController.onBackPressed(context),
child: Scaffold(
key: _scaffoldKey,
extendBody: true,
body: FadeTransition(
opacity: _fadeAnimation!,
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(),
],
),
);
},
),
drawer: const LeftDrawer(),
body: HomePage(callFn: openDrawer),
),
);
}

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.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/utils/utils.dart';
class MediaPage extends StatefulWidget {
const MediaPage({super.key});
@ -15,7 +12,6 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
late MediaController mediaController;
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@ -24,13 +20,6 @@ class _MediaPageState extends State<MediaPage>
void initState() {
super.initState();
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
mediaController.userLogin.listen((status) {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
}
@override
@ -38,22 +27,8 @@ class _MediaPageState extends State<MediaPage>
super.build(context);
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
body: Column(
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) ...[
ListTile(
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;
var userInfo;
Box setting = GStrorage.setting;
@override
void onInit() {
@ -70,11 +71,11 @@ class MemberController extends GetxController {
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'),
content: Text(memberInfo.value.isFollowed! ? '取消关注该用户?' : '关注该用户?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了')),
child: const Text('取消')),
TextButton(
onPressed: () async {
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: [
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),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
if (_memberController.face != null)
Positioned.fill(
bottom: 10,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
image: NetworkImage(_memberController.face!),
alignment: Alignment.topCenter,
isAntiAlias: true,
),
),
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context)
.colorScheme
.background
.withOpacity(0.44),
Theme.of(context).colorScheme.background,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.46],
),
),
),
),
// if (_memberController.face != null)
// Positioned.fill(
// bottom: 10,
// child: Container(
// decoration: BoxDecoration(
// image: DecorationImage(
// fit: BoxFit.fitWidth,
// image: NetworkImage(_memberController.face!),
// alignment: Alignment.topCenter,
// isAntiAlias: true,
// ),
// ),
// foregroundDecoration: BoxDecoration(
// gradient: LinearGradient(
// colors: [
// Theme.of(context)
// .colorScheme
// .background
// .withOpacity(0.44),
// Theme.of(context).colorScheme.background,
// ],
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// stops: const [0.0, 0.46],
// ),
// ),
// ),
// ),
Positioned(
left: 0,
right: 0,
@ -179,109 +218,109 @@ class _MemberPageState extends State<MemberPage>
fontWeight:
FontWeight.bold),
)),
const SizedBox(width: 2),
if (_memberController
.memberInfo.value.sex ==
'')
const Icon(
FontAwesomeIcons.venus,
size: 14,
color: Colors.pink,
),
if (_memberController
.memberInfo.value.sex ==
'')
const Icon(
FontAwesomeIcons.mars,
size: 14,
color: Colors.blue,
),
const SizedBox(width: 4),
Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11,
),
const SizedBox(width: 6),
if (_memberController.memberInfo
.value.vip!.status ==
1 &&
_memberController.memberInfo
.value.vip!.label![
'img_label_uri_hans'] !=
'') ...[
Image.network(
_memberController.memberInfo
.value.vip!.label![
'img_label_uri_hans'],
height: 20,
),
] else if (_memberController
.memberInfo
.value
.vip!
.status ==
1 &&
_memberController.memberInfo
.value.vip!.label![
'img_label_uri_hans_static'] !=
'') ...[
Image.network(
_memberController.memberInfo
.value.vip!.label![
'img_label_uri_hans_static'],
height: 20,
),
]
// const SizedBox(width: 2),
// if (_memberController
// .memberInfo.value.sex ==
// '女')
// const Icon(
// FontAwesomeIcons.venus,
// size: 14,
// color: Colors.pink,
// ),
// if (_memberController
// .memberInfo.value.sex ==
// '男')
// const Icon(
// FontAwesomeIcons.mars,
// size: 14,
// color: Colors.blue,
// ),
// const SizedBox(width: 4),
// Image.asset(
// 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
// height: 11,
// ),
// const SizedBox(width: 6),
// if (_memberController.memberInfo
// .value.vip!.status ==
// 1 &&
// _memberController.memberInfo
// .value.vip!.label![
// 'img_label_uri_hans'] !=
// '') ...[
// Image.network(
// _memberController.memberInfo
// .value.vip!.label![
// 'img_label_uri_hans'],
// height: 20,
// ),
// ] else if (_memberController
// .memberInfo
// .value
// .vip!
// .status ==
// 1 &&
// _memberController.memberInfo
// .value.vip!.label![
// 'img_label_uri_hans_static'] !=
// '') ...[
// Image.network(
// _memberController.memberInfo
// .value.vip!.label![
// 'img_label_uri_hans_static'],
// height: 20,
// ),
// ]
],
),
if (_memberController.memberInfo.value
.official!['title'] !=
'') ...[
const SizedBox(height: 6),
Text.rich(
maxLines: 2,
TextSpan(
text: _memberController
.memberInfo
.value
.official!['role'] ==
1
? '个人认证:'
: '企业认证:',
style: TextStyle(
color: Theme.of(context)
.primaryColor,
),
children: [
TextSpan(
text: _memberController
.memberInfo
.value
.official!['title'],
),
],
),
softWrap: true,
),
],
const SizedBox(height: 4),
if (_memberController
.memberInfo.value.sign !=
'')
SelectableRegion(
magnifierConfiguration:
const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls:
MaterialTextSelectionControls(),
child: Text(
_memberController
.memberInfo.value.sign!,
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// if (_memberController.memberInfo.value
// .official!['title'] !=
// '') ...[
// const SizedBox(height: 6),
// Text.rich(
// maxLines: 2,
// TextSpan(
// text: _memberController
// .memberInfo
// .value
// .official!['role'] ==
// 1
// ? '个人认证:'
// : '企业认证:',
// style: TextStyle(
// color: Theme.of(context)
// .primaryColor,
// ),
// children: [
// TextSpan(
// text: _memberController
// .memberInfo
// .value
// .official!['title'],
// ),
// ],
// ),
// softWrap: true,
// ),
// ],
// const SizedBox(height: 4),
// if (_memberController
// .memberInfo.value.sign !=
// '')
// SelectableRegion(
// magnifierConfiguration:
// const TextMagnifierConfiguration(),
// focusNode: FocusNode(),
// selectionControls:
// MaterialTextSelectionControls(),
// child: Text(
// _memberController
// .memberInfo.value.sign!,
// textAlign: TextAlign.left,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// ),
// ),
],
),
],
@ -308,28 +347,29 @@ class _MemberPageState extends State<MemberPage>
return MediaQuery.of(context).padding.top + kToolbarHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [
SizedBox(
width: double.infinity,
height: 50,
child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'),
Tab(text: '动态'),
Tab(text: '投稿'),
]),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: const [
Text('主页'),
MemberDynamicPanel(),
ArchivePanel(),
],
))
],
),
// body: Column(
// children: [
// SizedBox(
// width: double.infinity,
// height: 50,
// child: TabBar(controller: _tabController, tabs: const [
// Tab(text: '主页'),
// Tab(text: '动态'),
// Tab(text: '投稿'),
// ]),
// ),
// Expanded(
// child: TabBarView(
// controller: _tabController,
// children: const [
// Text('主页'),
// MemberDynamicPanel(),
// ArchivePanel(),
// ],
// ))
// ],
// ),
body: ArchivePanel(),
),
);
}

View File

@ -76,79 +76,79 @@ Widget profile(ctr, {loadingStatus = false}) {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
InkWell(
onTap: () {
Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['following'].toString()
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold),
),
Text(
'关注',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
),
InkWell(
onTap: () {
Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? Utils.numFormat(
ctr.userStat!['follower'],
)
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold)),
Text('粉丝',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize))
],
),
),
Column(
children: [
const Text('-',
style: TextStyle(fontWeight: FontWeight.bold)),
Text(
'获赞',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
],
),
),
const SizedBox(height: 10),
// Padding(
// padding: const EdgeInsets.only(left: 10, right: 10),
// child: Row(
// mainAxisSize: MainAxisSize.max,
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// InkWell(
// onTap: () {
// Get.toNamed(
// '/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
// },
// child: Column(
// children: [
// Text(
// !loadingStatus
// ? ctr.userStat!['following'].toString()
// : '-',
// style: const TextStyle(
// fontWeight: FontWeight.bold),
// ),
// Text(
// '关注',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// )
// ],
// ),
// ),
// InkWell(
// onTap: () {
// Get.toNamed(
// '/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
// },
// child: Column(
// children: [
// Text(
// !loadingStatus
// ? Utils.numFormat(
// ctr.userStat!['follower'],
// )
// : '-',
// style: const TextStyle(
// fontWeight: FontWeight.bold)),
// Text('粉丝',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize))
// ],
// ),
// ),
// Column(
// children: [
// const Text('-',
// style: TextStyle(fontWeight: FontWeight.bold)),
// Text(
// '获赞',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// )
// ],
// ),
// ],
// ),
// ),
// const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid) ...[
Row(
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),
],
),
body: LayoutBuilder(
builder: (context, constraint) {
return SingleChildScrollView(
body: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: SizedBox(
height: constraint.maxHeight,
child: Column(
children: [
const SizedBox(height: 10),
@ -85,9 +81,11 @@ class _MinePageState extends State<MinePage> {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
return Obx(
() => userInfoBuild(mineController, context));
return Obx(() => userInfoBuild(mineController, context));
} else {
return userInfoBuild(mineController, context);
}
@ -100,9 +98,6 @@ class _MinePageState extends State<MinePage> {
),
),
);
},
),
);
}
Widget userInfoBuild(_mineController, context) {
@ -135,85 +130,9 @@ class _MinePageState extends State<MinePage> {
_mineController.userInfo.value.uname ?? '点击头像登录',
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),
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: const EdgeInsets.only(left: 12, right: 12),
child: LayoutBuilder(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -140,138 +140,18 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
Visibility(
visible: _searchController.enableHotKey,
child: hotSearch(_searchController)),
// 搜索历史
_history()
],
),
child: _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() {
return Obx(
() => Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
padding: const EdgeInsets.fromLTRB(10, 4, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

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

View File

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

View File

@ -37,8 +37,6 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
i!.uname,
@ -46,19 +44,6 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
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'] != '')
Text(
i.officialVerify['desc'],

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/reply/item.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/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
@ -20,7 +23,7 @@ class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
/// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
RxInt cid = int.parse(Get.parameters['cid']!).obs;
RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag'];
// 视频详情
@ -76,6 +79,8 @@ class VideoDetailController extends GetxController
bool enableHeart = true;
var userInfo;
late bool isFirstTime = true;
Floating? floating;
late PreferredSizeWidget headerControl;
@override
void onInit() {
@ -103,7 +108,15 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
}
danmakuCid.value = cid;
danmakuCid.value = cid.value;
if (Platform.isAndroid) {
floating = Floating();
}
headerControl = HeaderControl(
controller: plPlayerController,
videoDetailCtr: this,
floating: floating,
);
}
showReplyReplyPanel() {
@ -167,7 +180,13 @@ class VideoDetailController extends GetxController
playerInit();
}
Future playerInit({video, audio, seekToTime, duration}) async {
Future playerInit({
video,
audio,
seekToTime,
duration,
bool autoplay = true,
}) async {
/// 设置/恢复 屏幕亮度
if (brightness != null) {
ScreenBrightness().setScreenBrightness(brightness!);
@ -196,15 +215,16 @@ class VideoDetailController extends GetxController
// 默认1倍速
speed: 1.0,
bvid: bvid,
cid: cid,
cid: cid.value,
enableHeart: enableHeart,
isFirstTime: isFirstTime,
autoplay: autoplay,
);
}
// 视频链接
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']) {
data = result['data'];
@ -287,6 +307,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) {
List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
if (!numbers.contains(resultAudioQa)) {
closestNumber = 30280;
}
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
}
} 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/pages/video/detail/controller.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/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
@ -58,10 +59,14 @@ class VideoIntroController extends GetxController {
RxString total = '1'.obs;
Timer? timer;
bool isPaused = false;
String heroTag = '';
@override
void onInit() {
super.onInit();
try {
heroTag = Get.arguments['heroTag'];
} catch (_) {}
userInfo = userInfoCache.get('userInfoCache');
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
@ -330,7 +335,8 @@ class VideoIntroController extends GetxController {
// 分享视频
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(() {});
return result;
}
@ -441,7 +447,7 @@ class VideoIntroController extends GetxController {
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
@ -485,4 +491,45 @@ class VideoIntroController extends GetxController {
}
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,
fontWeight: FontWeight.w500,
),
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 20),
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding:
MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return t.highlightColor.withOpacity(0.2);
}),
),
onPressed: showIntroDetail,
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.primary,
),
),
),
// const SizedBox(width: 20),
// SizedBox(
// width: 34,
// height: 34,
// child: IconButton(
// style: ButtonStyle(
// padding:
// MaterialStateProperty.all(EdgeInsets.zero),
// backgroundColor:
// MaterialStateProperty.resolveWith((states) {
// return t.highlightColor.withOpacity(0.2);
// }),
// ),
// onPressed: showIntroDetail,
// icon: Icon(
// Icons.more_horiz,
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
// ),
],
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
StatView(
theme: 'gray',
view: !widget.loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal)
Obx(
() => Text(
'${videoIntroController.total.value}人在看',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
],
),
),
const SizedBox(height: 7),
// GestureDetector(
// behavior: HitTestBehavior.translucent,
// onTap: () => showIntroDetail(),
// child: Row(
// children: [
// StatView(
// theme: 'gray',
// view: !widget.loadingStatus
// ? widget.videoDetail!.stat!.view
// : videoItem['stat'].view,
// size: 'medium',
// ),
// const SizedBox(width: 10),
// StatDanMu(
// theme: 'gray',
// danmu: !widget.loadingStatus
// ? widget.videoDetail!.stat!.danmaku
// : videoItem['stat'].danmaku,
// size: 'medium',
// ),
// const SizedBox(width: 10),
// Text(
// Utils.dateFormat(
// !widget.loadingStatus
// ? widget.videoDetail!.pubdate
// : videoItem['pubdate'],
// formatType: 'detail'),
// style: TextStyle(
// fontSize: 12,
// color: t.colorScheme.outline,
// ),
// ),
// const SizedBox(width: 10),
// if (videoIntroController.isShowOnlineTotal)
// Obx(
// () => Text(
// '${videoIntroController.total.value}人在看',
// style: TextStyle(
// fontSize: 12,
// color: t.colorScheme.outline,
// ),
// ),
// ),
// ],
// ),
// ),
// const SizedBox(height: 7),
// 点赞收藏转发 布局样式1
SingleChildScrollView(
padding: const EdgeInsets.only(top: 7, bottom: 7),
padding: const EdgeInsets.only(top: 10, bottom: 7),
scrollDirection: Axis.horizontal,
child: actionRow(
context,
@ -382,14 +382,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10),
Text(owner.name,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(),
AnimatedOpacity(
opacity: loadingStatus ? 0 : 1,
@ -516,52 +508,52 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment),
onTap: () {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
),
const SizedBox(width: 8),
// Obx(
// () => ActionRowItem(
// icon: const Icon(FontAwesomeIcons.thumbsUp),
// onTap: () => videoIntroController.actionLikeVideo(),
// selectStatus: videoIntroController.hasLike.value,
// loadingStatus: loadingStatus,
// text:
// !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
// ),
// ),
// const SizedBox(width: 8),
// Obx(
// () => ActionRowItem(
// icon: const Icon(FontAwesomeIcons.b),
// onTap: () => videoIntroController.actionCoinVideo(),
// selectStatus: videoIntroController.hasCoin.value,
// loadingStatus: loadingStatus,
// text:
// !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
// ),
// ),
// const SizedBox(width: 8),
// Obx(
// () => ActionRowItem(
// icon: const Icon(FontAwesomeIcons.heart),
// onTap: () => showFavBottomSheet(),
// onLongPress: () => showFavBottomSheet(type: 'longPress'),
// selectStatus: videoIntroController.hasFav.value,
// loadingStatus: loadingStatus,
// text: !loadingStatus
// ? widget.videoDetail!.stat!.favorite!.toString()
// : '-',
// ),
// ),
// const SizedBox(width: 8),
// ActionRowItem(
// icon: const Icon(FontAwesomeIcons.comment),
// onTap: () {
// videoDetailCtr.tabCtr.animateTo(1);
// },
// selectStatus: false,
// loadingStatus: loadingStatus,
// text:
// !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
// ),
// const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),

View File

@ -57,20 +57,6 @@ class IntroDetail extends StatelessWidget {
),
),
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(
Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'),
@ -79,8 +65,6 @@ class IntroDetail extends StatelessWidget {
color: Theme.of(context).colorScheme.outline,
),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,

View File

@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget {
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: [
actionRowLineItem(
context,
() => {},
loadingStatus,
'推荐',
selectStatus: true,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'弹幕',
ActionRowLineItem(
onTap: () => {},
loadingStatus: loadingStatus,
text: '推荐',
selectStatus: false,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'评论列表',
ActionRowLineItem(
onTap: () => {},
loadingStatus: loadingStatus,
text: '弹幕',
selectStatus: false,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'播放列表',
ActionRowLineItem(
onTap: () => {},
loadingStatus: loadingStatus,
text: '评论列表',
selectStatus: false,
),
const SizedBox(width: 8),
ActionRowLineItem(
onTap: () => {},
loadingStatus: loadingStatus,
text: '播放列表',
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:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart';
class SeasonPanel extends StatefulWidget {
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes;
late int cid;
late int currentIndex;
String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
@override
void initState() {
super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个
@ -48,6 +54,11 @@ class _SeasonPanelState extends State<SeasonPanel> {
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!;
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 {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.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/video_card_h.dart';
import './controller.dart';
@ -22,6 +23,9 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
future: _releatedController.queryRelatedVideo(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
if (snapshot.data!['status']) {
// 请求成功
return SliverList(
@ -51,9 +55,7 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
}, childCount: snapshot.data['data'].length + 1));
} else {
// 请求错误
return const Center(
child: Text('出错了'),
);
return HttpError(errMsg: '出错了', fn: () {});
}
} 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/models/common/reply_type.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/replyNew/index.dart';
import 'package:pilipala/utils/feed_back.dart';
@ -773,7 +774,7 @@ InlineSpan buildContent(
// 图片渲染
if (content.pictures.isNotEmpty) {
List picList = [];
List<String> picList = [];
int len = content.pictures.length;
if (len == 1) {
Map pictureItem = content.pictures.first;
@ -785,8 +786,13 @@ InlineSpan buildContent(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
Get.toNamed('/preview',
arguments: {'initialPage': 0, 'imgList': picList});
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
@ -814,8 +820,13 @@ InlineSpan buildContent(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
Get.toNamed('/preview',
arguments: {'initialPage': i, 'imgList': picList});
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: content.pictures[i]['img_src'],

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
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:get/get.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/related/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 'widgets/app_bar.dart';
@ -33,13 +37,12 @@ class VideoDetailPage extends StatefulWidget {
class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware {
final VideoDetailController videoDetailController =
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
late VideoDetailController videoDetailController;
PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream;
final VideoIntroController videoIntroController =
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
late VideoIntroController videoIntroController;
late String heroTag;
PlayerStatus playerStatus = PlayerStatus.playing;
double doubleOffset = 0;
@ -51,15 +54,24 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late Future _futureBuilderFuture;
// 自动退出全屏
late bool autoExitFullcreen;
Floating? floating;
late BangumiIntroController bangumiIntroController;
@override
void 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');
autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
videoSourceInit();
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!;
if (status == PlayerStatus.completed) {
// 结束播放退出全屏
if (autoExitFullcreen) {
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);
}
} catch (_) {}
}
}
// 继续播放或重新播放
@ -102,6 +137,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.play();
}
/// 未开启自动播放时触发播放
Future<void> handlePlay() async {
await videoDetailController.playerInit();
plPlayerController = videoDetailController.plPlayerController;
@ -111,8 +147,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
void dispose() {
if (plPlayerController != null) {
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose();
}
if (floating != null) {
floating!.dispose();
}
super.dispose();
}
@ -134,9 +175,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 返回当前页面时
void didPopNext() async {
videoDetailController.isFirstTime = false;
videoDetailController.playerInit();
bool autoplay =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
videoDetailController.playerInit(autoplay: autoplay);
videoDetailController.autoPlay.value = true;
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0) {
if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.play();
}
@ -156,7 +200,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight;
return SafeArea(
Widget childWhenDisabled = SafeArea(
top: false,
bottom: false,
child: Stack(
@ -203,6 +247,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController,
videoDetailCtr:
videoDetailController,
floating: floating,
),
danmuWidget: Obx(
() => PlDanmaku(
@ -314,51 +359,30 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
];
},
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回
pinnedHeaderSliverHeightBuilder: () {
return playerStatus != PlayerStatus.playing
? statusBarHeight + kToolbarHeight
: pinnedHeaderHeight;
return pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
body: Container(
color: Theme.of(context).colorScheme.background,
child: Column(
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(
child: CustomScrollView(
key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
if (videoDetailController.videoType ==
SearchType.video) ...[
const VideoIntroPanel(),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
BangumiIntroPanel(
cid: videoDetailController.cid)
],
] else
// if (videoDetailController.videoType ==
// SearchType.media_bangumi) ...[
// BangumiIntroPanel(
// cid: videoDetailController.cid)
// ],
// if (videoDetailController.videoType ==
// SearchType.video) ...[
// SliverPersistentHeader(
@ -375,42 +399,70 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: Divider(
indent: 12,
endIndent: 12,
color: Theme.of(context)
.dividerColor
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
],
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
)
],
color:
Theme.of(context).dividerColor.withOpacity(0.06),
),
),
// const RelatedVideoPanel(),
],
),
),
),
),
/// 重新进入会刷新
// 播放完成/暂停播放
StreamBuilder(
stream: appbarStream.stream,
initialData: 0,
builder: ((context, snapshot) {
return ScrollAppBar(
snapshot.data!.toDouble(),
() => continuePlay(),
playerStatus,
null,
);
}),
)
// StreamBuilder(
// stream: appbarStream.stream,
// initialData: 0,
// builder: ((context, snapshot) {
// return ScrollAppBar(
// snapshot.data!.toDouble(),
// () => continuePlay(),
// playerStatus,
// 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: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.share,
size: 20,
)),
const SizedBox(width: 12)
],
// actions: [
// IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.share,
// size: 20,
// )),
// 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/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.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/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr;
final Floating? floating;
const HeaderControl({
this.controller,
this.videoDetailCtr,
this.floating,
Key? key,
}) : super(key: key);
@ -29,6 +40,7 @@ class _HeaderControlState extends State<HeaderControl> {
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() {
@ -138,17 +150,17 @@ class _HeaderControlState extends State<HeaderControl> {
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle),
),
// ListTile(
// onTap: () {},
// dense: true,
// enabled: false,
// leading: const Icon(Icons.play_circle_outline, size: 20),
// title: Text('播放设置', style: titleStyle),
// ),
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,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20),
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
Widget build(BuildContext context) {
final _ = widget.controller!;
@ -526,6 +832,39 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
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(
() => SizedBox(
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>();
homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true);
} catch (err) {
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:pilipala/http/video.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/storage.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:wakelock_plus/wakelock_plus.dart';
Box videoStorage = GStrorage.video;
Box setting = GStrorage.setting;
Box localCache = GStrorage.localCache;
class PlPlayerController {
Player? _videoPlayerController;
@ -104,6 +107,7 @@ class PlPlayerController {
];
PreferredSizeWidget? headerControl;
PreferredSizeWidget? bottomControl;
Widget? danmuWidget;
/// 数据加载监听
@ -199,12 +203,39 @@ class PlPlayerController {
Rx<bool> isOpenDanmu = false.obs;
// 关联弹幕控制器
DanmakuController? danmakuController;
// 弹幕相关配置
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
// 添加一个私有构造函数
PlPlayerController._() {
_videoType = videoType;
isOpenDanmu.value =
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) {
// if (status == PlayerStatus.playing) {
// WakelockPlus.enable();
@ -372,7 +403,7 @@ class PlPlayerController {
Media(assetUrl, httpHeaders: dataSource.httpHeaders),
play: false,
);
} else if (dataSource.type == DataSourceType.network) {
}
player.open(
Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders),
play: false,
@ -381,12 +412,6 @@ class PlPlayerController {
// player.setAudioTrack(
// AudioTrack.uri(dataSource.audioSource!),
// );
} else {
player.open(
Media(dataSource.file!.path, httpHeaders: dataSource.httpHeaders),
play: false,
);
}
return player;
}
@ -524,6 +549,12 @@ class PlPlayerController {
/// 设置倍速
Future<void> setPlaybackSpeed(double speed) async {
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;
}
@ -725,11 +756,18 @@ class PlPlayerController {
}
}
void hiddenControls(bool val) {
showControls.value = val;
}
/// 设置长按倍速状态 live模式下禁用
void setDoubleSpeedStatus(bool val) {
if (videoType.value == 'live') {
return;
}
if (controlsLock.value) {
return;
}
_doubleSpeedStatus.value = val;
double currentSpeed = playbackSpeed;
if (val) {
@ -754,7 +792,7 @@ class PlPlayerController {
Future<void> triggerFullScreen({bool status = true}) async {
FullScreenMode mode = FullScreenModeCode.fromCode(
setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
await StatusBarControl.setHidden(true, animation: StatusBarAnimation.FADE);
if (!isFullScreen.value && status) {
/// 按照视频宽高比决定全屏方向
switch (mode) {
@ -773,7 +811,7 @@ class PlPlayerController {
/// 进入全屏
await enterFullScreen();
//
//
await verticalScreen();
break;
case FullScreenMode.horizontal:
@ -791,20 +829,29 @@ class PlPlayerController {
useSafeArea: false,
builder: (context) => Dialog.fullscreen(
backgroundColor: Colors.black,
child: SafeArea(
bottom:
direction.value == 'vertical' || mode == FullScreenMode.vertical
? true
: false,
child: PLVideoPlayer(
controller: this,
headerControl: headerControl,
bottomControl: bottomControl,
danmuWidget: danmuWidget,
),
),
),
);
if (result == null) {
// 退出全屏
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
exitFullScreen();
await verticalScreen();
toggleFullScreen(false);
}
} else if (isFullScreen.value) {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
Get.back();
exitFullScreen();
await verticalScreen();
@ -842,6 +889,9 @@ class PlPlayerController {
if (!_enableHeart) {
return false;
}
if (videoType.value == 'live') {
return;
}
// 播放状态变化时,更新
if (type == 'status') {
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 {
// 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) {
@ -891,6 +946,13 @@ class PlPlayerController {
// playerStatus.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();
await _videoPlayerController?.dispose();
_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 'package:device_info_plus/device_info_plus.dart';
import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -11,16 +12,17 @@ Future<void> landScape() async {
if (kIsWeb) {
await document.documentElement?.requestFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
);
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
);
// await SystemChrome.setEnabledSystemUIMode(
// SystemUiMode.immersiveSticky,
// overlays: [],
// );
// await SystemChrome.setPreferredOrientations(
// [
// DeviceOrientation.landscapeLeft,
// DeviceOrientation.landscapeRight,
// ],
// );
await AutoOrientation.landscapeAutoMode(forceSensor: true);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(

View File

@ -29,11 +29,13 @@ import 'widgets/forward_seek.dart';
class PLVideoPlayer extends StatefulWidget {
final PlPlayerController controller;
final PreferredSizeWidget? headerControl;
final PreferredSizeWidget? bottomControl;
final Widget? danmuWidget;
const PLVideoPlayer({
required this.controller,
this.headerControl,
this.bottomControl,
this.danmuWidget,
super.key,
});
@ -70,6 +72,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late FullScreenMode mode;
late int defaultBtmProgressBehavior;
late bool enableQuickDouble;
late bool enableBackgroundPlay;
void onDoubleTapSeekBackward() {
setState(() {
@ -86,8 +89,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 双击播放、暂停
void onDoubleTapCenter() {
final _ = widget.controller;
if (_.playerStatus.status.value == PlayerStatus.playing) {
_.togglePlay();
if (_.videoPlayerController!.state.playing) {
_.pause();
} else {
_.play();
}
@ -120,11 +123,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
widget.controller.bottomControl = widget.bottomControl;
widget.controller.danmuWidget = widget.danmuWidget;
defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,
defaultValue: BtmProgresBehavior.values.first.code);
enableQuickDouble =
setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true);
enableBackgroundPlay =
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
Future.microtask(() async {
try {
@ -225,6 +231,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Video(
controller: videoController,
controls: NoVideoControls,
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
textAlign: TextAlign.center,
@ -239,7 +246,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => Align(
alignment: Alignment.topCenter,
child: FractionalTranslation(
translation: const Offset(0.0, 1), // 上下偏移量(负数向上偏移)
translation: const Offset(0.0, 0.3), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity(
curve: Curves.easeInOut,
opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,
@ -248,26 +255,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
borderRadius: BorderRadius.circular(16.0),
),
height: 34.0,
width: 86.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(width: 3),
Image.asset(
'assets/images/run-pokemon.gif',
height: 20,
),
const Text(
height: 32.0,
width: 70.0,
child: const Center(
child: Text(
'倍速中',
style: TextStyle(color: Colors.white, fontSize: 12),
),
const SizedBox(width: 4),
],
),
style: TextStyle(color: Colors.white, fontSize: 13),
),
)),
),
),
),
@ -427,23 +424,23 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
Obx(() {
if (_.buffered.value == Duration.zero) {
return Positioned.fill(
child: Container(
color: Colors.black,
child: Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
),
),
);
} else {
return Container();
}
}),
// Obx(() {
// if (_.buffered.value == Duration.zero) {
// return Positioned.fill(
// child: Container(
// color: Colors.black,
// child: Center(
// child: Image.asset(
// 'assets/images/loading.gif',
// height: 25,
// ),
// ),
// ),
// );
// } else {
// return Container();
// }
// }),
/// 弹幕面板
if (widget.danmuWidget != null)
@ -562,9 +559,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 头部、底部控制条
Obx(
() => Visibility(
visible: _.videoType.value != 'live',
child: Column(
() => Column(
children: [
if (widget.headerControl != null)
ClipRect(
@ -583,15 +578,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
controller: animationController,
visible: !_.controlsLock.value && _.showControls.value,
position: 'bottom',
child: BottomControl(
child: widget.bottomControl ??
BottomControl(
controller: widget.controller,
triggerFullScreen: widget.controller.triggerFullScreen),
triggerFullScreen:
widget.controller.triggerFullScreen),
),
),
],
),
),
),
/// 进度条 live模式下禁用
Obx(
@ -608,6 +604,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
!_.isFullScreen.value) {
return Container();
}
if (_.videoType.value == 'live') {
return Container();
}
if (value > max || max <= 0) {
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/home/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/liveRoom/view.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/search/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/utils/storage.dart';
import '../pages/history_search/index.dart';
Box setting = GStrorage.setting;
bool iosTransition =
setting.get(SettingBoxKey.iosTransition, defaultValue: false);
@ -41,19 +45,19 @@ bool iosTransition =
class Routes {
static final List<GetPage> getPages = [
// 首页(推荐)
CustomGetPage(name: '/', page: () => const HomePage()),
CustomGetPage(name: '/', page: () => HomePage()),
// 热门
CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情
CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览
GetPage(
name: '/preview',
page: () => const ImagePreview(),
transition: Transition.fade,
transitionDuration: const Duration(milliseconds: 300),
showCupertinoParallax: false,
),
// GetPage(
// name: '/preview',
// page: () => const ImagePreview(),
// transition: Transition.fade,
// transitionDuration: const Duration(milliseconds: 300),
// showCupertinoParallax: false,
// ),
//
CustomGetPage(name: '/webview', page: () => const WebviewPage()),
// 设置
@ -85,6 +89,7 @@ class Routes {
CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
// 用户中心
CustomGetPage(name: '/member', page: () => const MemberPage()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复
CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()),
@ -107,6 +112,11 @@ class Routes {
name: '/displayModeSetting', page: () => const SetDiaplayMode()),
// 关于
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: {
'pic': bangumiDetail.cover,
'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 {
static Color decimalToColor(int decimalColor) {
// 16777215 表示白色
int red = (decimalColor >> 16) & 0xFF;
int green = (decimalColor >> 8) & 0xFF;
int blue = decimalColor & 0xFF;

View File

@ -1,8 +1,6 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart';
class LoginUtils {
@ -17,12 +15,6 @@ class LoginUtils {
MineController mineCtr = Get.find<MineController>();
mineCtr.userLogin.value = status;
DynamicsController dynamicsCtr = Get.find<DynamicsController>();
dynamicsCtr.userLogin.value = status;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.userLogin.value = status;
} catch (err) {
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');
// 搜索历史
@ -102,6 +107,7 @@ class SettingBoxKey {
// youtube 双击快进快退
static const String enableQuickDouble = 'enableQuickDouble';
static const String enableShowDanmaku = 'enableShowDanmaku';
static const String enableBackgroundPlay = 'enableBackgroundPlay';
/// 隐私
static const String blackMidsList = 'blackMidsList';
@ -113,6 +119,8 @@ class SettingBoxKey {
static const String enableHotKey = 'enableHotKey';
static const String enableQuickFav = 'enableQuickFav';
static const String enableWordRe = 'enableWordRe';
static const String enableSearchWord = 'enableSearchWord';
static const String enableRcmdDynamic = 'enableRcmdDynamic';
/// 外观
static const String themeMode = 'themeMode';
@ -132,6 +140,13 @@ class LocalCacheKey {
//
static const String wbiKeys = 'wbiKeys';
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 {
@ -141,4 +156,6 @@ class VideoBoxKey {
static const String videoBrightness = 'videoBrightness';
// 倍速
static const String videoSpeed = 'videoSpeed';
// 播放顺序
static const String playRepeat = 'playRepeat';
}

View File

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

View File

@ -65,6 +65,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -257,6 +265,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csslib:
dependency: transitive
description:
name: csslib
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
url: "https://pub.dev"
source: hosted
version: "0.17.3"
cupertino_icons:
dependency: "direct main"
description:
@ -321,14 +337,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -425,6 +433,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
floating:
dependency: "direct main"
description:
name: floating
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter:
dependency: "direct main"
description: flutter
@ -454,6 +470,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct dev"
description:
@ -581,6 +605,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
html:
dependency: "direct main"
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http:
dependency: transitive
description:
@ -589,14 +621,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -677,6 +701,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -721,12 +753,12 @@ packages:
dependency: "direct main"
description:
name: media_kit
sha256: "66f04934bcadf592f24d829127471e4dc304de8e9bba5795ade2f3e95552ebfc"
sha256: "92c7f59e075d74471b31e703f81ccc1d7102739ebcce945b30a6417fa2f751d5"
url: "https://pub.dev"
source: hosted
version: "1.1.6"
version: "1.1.7"
media_kit_libs_android_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b"
@ -734,7 +766,7 @@ packages:
source: hosted
version: "1.3.3"
media_kit_libs_ios_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_ios_video
sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159
@ -742,7 +774,7 @@ packages:
source: hosted
version: "1.1.3"
media_kit_libs_linux:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_linux
sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8"
@ -750,15 +782,23 @@ packages:
source: hosted
version: "1.1.2"
media_kit_libs_macos_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_macos_video
sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2
url: "https://pub.dev"
source: hosted
version: "1.1.3"
media_kit_libs_windows_video:
media_kit_libs_video:
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:
name: media_kit_libs_windows_video
sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350"
@ -766,7 +806,7 @@ packages:
source: hosted
version: "1.0.8"
media_kit_native_event_loop:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_native_event_loop
sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4
@ -777,10 +817,10 @@ packages:
dependency: "direct main"
description:
name: media_kit_video
sha256: "809a3797da7d49fad85f139555b352dd615f9d2da6ae9f1745c6978963491bae"
sha256: cd3ab78e7626146f115134b82c4029ac5987ba6351719c9067d86789723e0c12
url: "https://pub.dev"
source: hosted
version: "1.1.7"
version: "1.1.8"
meta:
dependency: transitive
description:
@ -1211,6 +1251,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
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
# 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.
version: 1.0.6
version: 1.0.7
environment:
sdk: ">=2.19.6 <3.0.0"
@ -43,7 +43,6 @@ dependencies:
# 网络
dio: ^5.3.0
cookie_jar: ^4.0.8
dio_http2_adapter: ^2.3.1
dio_cookie_manager: ^3.1.0
connectivity_plus: ^4.0.1
@ -84,14 +83,9 @@ dependencies:
crypto: ^3.0.3
# 视频播放器
media_kit: ^1.1.4 # Primary package.
media_kit_video: ^1.1.5 # For video rendering.
media_kit_native_event_loop: ^1.0.7 # Support for higher number of concurrent instances & better performance.
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
media_kit: ^1.1.7
media_kit_video: ^1.1.8
media_kit_libs_video: ^1.0.1
# 音量、亮度、屏幕控制
flutter_volume_controller: ^1.2.7
@ -100,7 +94,7 @@ dependencies:
universal_platform: ^1.0.0+1
# 进度条
audio_video_progress_bar: ^1.0.1
# auto_orientation: ^2.3.1
auto_orientation: ^2.3.1
protobuf: ^3.0.0
animations: ^2.0.7
@ -121,6 +115,15 @@ dependencies:
ref: master
# 状态栏图标控制
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:
flutter_test: