Compare commits

..

29 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
83 changed files with 2673 additions and 3137 deletions

View File

@ -1,24 +0,0 @@
## 1.0.8
直播弹幕、循环播放等功能开发中
### 新功能
+ 用户拉黑功能
+ gif图片保存
+ 删除已看历史记录
### 修复
+ 弹幕数量较少
+ 弹幕屏蔽设置自动记忆
+ 动态页面渲染
+ 用户主页数据错乱
+ 大家都在搜空白
+ 默认自动全屏,顶部操作栏丢失
### 优化
+ 全屏状态栏区域显示优化
+ 图片保存至PiliPala文件夹
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,7 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class HtmlRender extends StatelessWidget { class HtmlRender extends StatelessWidget {
@ -20,47 +20,35 @@ class HtmlRender extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Html( return Html(
data: htmlContent, data: htmlContent,
// tagsList: Html.tags..addAll(["form", "label", "input"]),
onLinkTap: (url, buildContext, attributes) => {}, onLinkTap: (url, buildContext, attributes) => {},
extensions: [ extensions: [
TagExtension( TagExtension(
tagsToExtend: {"img"}, tagsToExtend: {"img"},
builder: (extensionContext) { builder: (extensionContext) {
try { String? imgUrl = extensionContext.attributes['src'];
Map attributes = extensionContext.attributes;
List key = attributes.keys.toList();
String? imgUrl = key.contains('src')
? attributes['src']
: attributes['data-src'];
if (imgUrl!.startsWith('//')) { if (imgUrl!.startsWith('//')) {
imgUrl = 'https:$imgUrl'; imgUrl = 'https:$imgUrl';
} }
if (imgUrl.startsWith('http://')) { if (imgUrl.startsWith('http://')) {
imgUrl = imgUrl.replaceAll('http://', 'https://'); imgUrl = imgUrl.replaceAll('http://', 'https://');
} }
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
print(imgUrl);
bool isEmote = imgUrl.contains('/emote/'); bool isEmote = imgUrl.contains('/emote/');
bool isMall = imgUrl.contains('/mall/'); bool isMall = imgUrl.contains('/mall/');
if (isMall) { if (isMall) {
return const SizedBox(); return SizedBox();
} }
// bool inTable = // bool inTable =
// extensionContext.element!.previousElementSibling == null || // extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null; // extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!); // imgUrl = Utils().imageUrl(imgUrl!);
// return Image.network( return Image.network(
// imgUrl, imgUrl,
// width: isEmote ? 22 : null, width: isEmote ? 22 : null,
// height: isEmote ? 22 : null, height: isEmote ? 22 : null,
// );
return NetworkImgLayer(
width: isEmote ? 22 : Get.size.width - 24,
height: isEmote ? 22 : 200,
src: imgUrl,
); );
} catch (err) {
print(err);
return const SizedBox();
}
}, },
), ),
], ],
@ -75,13 +63,11 @@ class HtmlRender extends StatelessWidget {
textDecoration: TextDecoration.none, textDecoration: TextDecoration.none,
), ),
"p": Style( "p": Style(
margin: Margins.only(bottom: 10), margin: Margins.only(bottom: 0),
), ),
"span": Style( "span": Style(
fontSize: FontSize.medium, fontSize: FontSize.medium,
height: Height(1.65),
), ),
"div": Style(height: Height.auto()),
"li > p": Style( "li > p": Style(
display: Display.inline, display: Display.inline,
), ),
@ -89,7 +75,61 @@ class HtmlRender extends StatelessWidget {
padding: HtmlPaddings.only(bottom: 4), padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify, textAlign: TextAlign.justify,
), ),
"img": Style(margin: Margins.only(top: 4, bottom: 4)), "image": Style(margin: Margins.only(top: 4, bottom: 4)),
"p > img": Style(margin: Margins.only(top: 4, bottom: 4)),
"code": Style(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface),
"code > span": Style(textAlign: TextAlign.start),
"hr": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'table': Style(
border: Border(
right: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
bottom: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'tr': Style(
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
left: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'thead': Style(
backgroundColor: Theme.of(context).colorScheme.background,
),
'th': Style(
padding: HtmlPaddings.only(left: 3, right: 3),
),
'td': Style(
padding: HtmlPaddings.all(4.0),
alignment: Alignment.center,
textAlign: TextAlign.center,
),
}, },
); );
} }

View File

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

View File

@ -48,7 +48,7 @@ class VideoCardV extends StatelessWidget {
arguments: { arguments: {
'pic': videoItem.pic, 'pic': videoItem.pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
), ),
); );
@ -62,15 +62,6 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag, 'heroTag': heroTag,
}); });
break; break;
// 动态
case 'picture':
Get.toNamed('/htmlRender', parameters: {
'url': videoItem.uri,
'title': videoItem.title,
'id': videoItem.param.toString(),
'dynamicType': 'picture'
});
break;
default: default:
SmartDialog.showToast(videoItem.goto); SmartDialog.showToast(videoItem.goto);
Get.toNamed( Get.toNamed(

View File

@ -97,9 +97,6 @@ class Api {
// 操作用户关系 // 操作用户关系
static const String relationMod = '/x/relation/modify'; static const String relationMod = '/x/relation/modify';
// 相互关系查询
static const String relationSearch = '/x/space/wbi/acc/relation';
// 评论列表 // 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11 // https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
static const String replyList = '/x/v2/reply'; static const String replyList = '/x/v2/reply';
@ -245,6 +242,9 @@ class Api {
// wts=1689767832 // wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search'; static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// 用户动态 // 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space'; static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -303,4 +303,7 @@ class Api {
static const String onlineTotal = '/x/player/online/total'; static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
// 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search';
} }

View File

@ -2,37 +2,4 @@ class HttpString {
static const String baseUrl = 'https://www.bilibili.com'; static const String baseUrl = 'https://www.bilibili.com';
static const String baseApiUrl = 'https://api.bilibili.com'; static const String baseApiUrl = 'https://api.bilibili.com';
static const String tUrl = 'https://api.vc.bilibili.com'; static const String tUrl = 'https://api.vc.bilibili.com';
static const List<int> validateStatusCodes = [
302,
304,
307,
400,
401,
403,
404,
405,
409,
412,
500,
503,
504,
509,
616,
617,
625,
626,
628,
629,
632,
643,
650,
652,
658,
662,
688,
689,
701,
799,
8888
];
} }

View File

@ -17,6 +17,9 @@ class DanmakaHttp {
'oid': cid, 'oid': cid,
'segment_index': segmentIndex, 'segment_index': segmentIndex,
}; };
// 计算函数
Future<DmSegMobileReply> computeTask(Map<String, int> params) async {
var response = await Request().get( var response = await Request().get(
Api.webDanmaku, Api.webDanmaku,
data: params, data: params,
@ -24,4 +27,7 @@ class DanmakaHttp {
); );
return DmSegMobileReply.fromBuffer(response.data); return DmSegMobileReply.fromBuffer(response.data);
} }
return await compute(computeTask, params);
}
} }

View File

@ -3,12 +3,8 @@ import 'package:html/parser.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
class HtmlHttp { class HtmlHttp {
// article static Future reqHtml(id) async {
static Future reqHtml(id, dynamicType) async { var response = await Request().get("https://www.bilibili.com/opus/$id");
var response = await Request().get(
"https://www.bilibili.com/opus/$id",
extra: {'ua': 'pc'},
);
Document rootTree = parse(response.data); Document rootTree = parse(response.data);
Element body = rootTree.body!; Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!; Element appDom = body.querySelector('#app')!;
@ -38,46 +34,7 @@ class HtmlHttp {
'uname': uname, 'uname': uname,
'updateTime': updateTime, 'updateTime': updateTime,
'content': opusContent, 'content': opusContent,
'commentId': int.parse(commentId) 'commentId': commentId
};
}
// read
static Future reqReadHtml(id, dynamicType) async {
var response = await Request().get(
"https://www.bilibili.com/$dynamicType/$id/",
extra: {'ua': 'pc'},
);
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!;
// 头像
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// print(avatar);
// avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
return {
'status': true,
'avatar': '',
'uname': uname,
'updateTime': '',
'content': opusContent,
'commentId': int.parse(number)
}; };
} }
} }

View File

@ -4,7 +4,6 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -60,6 +59,9 @@ class Request {
static Future<String> getCsrf() async { static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar var cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseApiUrl)); .loadForRequest(Uri.parse(HttpString.baseApiUrl));
// for (var i in cookies) {
// print(i);
// }
String token = ''; String token = '';
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) { if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
token = cookies.firstWhere((e) => e.name == 'bili_jct').value; token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
@ -89,18 +91,15 @@ class Request {
//响应流上前后两次接受到数据的间隔,单位为毫秒。 //响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 12000), receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头. //Http请求头.
headers: {}, headers: {
'keep-alive': true,
'user-agent': headerUa('pc'),
'Accept-Encoding': 'gzip'
},
persistentConnection: true,
); );
dio = Dio(options) dio = Dio(options);
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
//添加拦截器 //添加拦截器
dio.interceptors.add(ApiInterceptor()); dio.interceptors.add(ApiInterceptor());
@ -114,26 +113,30 @@ class Request {
dio.transformer = BackgroundTransformer(); dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) { dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || return status! >= 200 && status < 300 || status == 304 || status == 302;
HttpString.validateStatusCodes.contains(status);
}; };
} }
/* /*
* get请求 * get请求
*/ */
get(url, {data, options, cancelToken, extra}) async { get(url, {data, cacheOptions, options, cancelToken, extra}) async {
Response response; Response response;
Options options = Options(); Options options;
String ua = 'pc';
ResponseType resType = ResponseType.json; ResponseType resType = ResponseType.json;
if (extra != null) { if (extra != null) {
ua = extra!['ua'] ?? 'pc';
resType = extra!['resType'] ?? ResponseType.json; resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) {
options.headers = {'user-agent': headerUa(type: extra['ua'])};
}
} }
if (cacheOptions != null) {
cacheOptions.headers = {'user-agent': headerUa(ua)};
options = cacheOptions;
} else {
options = Options();
options.headers = {'user-agent': headerUa(ua)};
options.responseType = resType; options.responseType = resType;
}
try { try {
response = await dio.get( response = await dio.get(
url, url,
@ -200,19 +203,15 @@ class Request {
token.cancel("cancelled"); token.cancel("cancelled");
} }
String headerUa({type = 'mob'}) { String headerUa(ua) {
String headerUa = ''; String headerUa = '';
if (type == 'mob') { if (ua == 'mob') {
if (Platform.isIOS) { headerUa = Platform.isIOS
headerUa = ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1'; : 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
} else { } else {
headerUa = headerUa =
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile 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';
}
} else {
headerUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
} }
return headerUa; return headerUa;
} }

View File

@ -46,10 +46,7 @@ class ApiInterceptor extends Interceptor {
void onError(DioException err, ErrorInterceptorHandler handler) async { void onError(DioException err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误 // 处理网络请求错误
// handler.next(err); // handler.next(err);
SmartDialog.showToast( SmartDialog.showToast(await dioError(err));
await dioError(err),
displayType: SmartToastType.onlyRefresh,
);
super.onError(err, handler); super.onError(err, handler);
} }

View File

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

View File

@ -1,13 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data is String) { if (res.data is String) {
@ -36,14 +39,16 @@ class SearchHttp {
static Future searchSuggest({required term}) async { static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest, var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term}); data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) { if (res.data is String) {
if (res.data['result'] is Map) { Map<String, dynamic> resultMap = json.decode(res.data);
res.data['result']['term'] = term; if (resultMap['code'] == 0) {
if (resultMap['result'] is Map) {
resultMap['result']['term'] = term;
} }
return { return {
'status': true, 'status': true,
'data': res.data['result'] is Map 'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(res.data['result']) ? SearchSuggestModel.fromJson(resultMap['result'])
: [], : [],
}; };
} else { } else {
@ -53,6 +58,13 @@ class SearchHttp {
'msg': '请求错误 🙅', 'msg': '请求错误 🙅',
}; };
} }
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
} }
// 分类搜索 // 分类搜索
@ -78,6 +90,12 @@ class SearchHttp {
try { try {
switch (searchType) { switch (searchType) {
case SearchType.video: case SearchType.video:
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['result']) {
// 屏蔽推广和拉黑用户
i['available'] = !blackMidsList.contains(i['mid']);
}
data = SearchVideoModel.fromJson(res.data['data']); data = SearchVideoModel.fromJson(res.data['data']);
break; break;
case SearchType.live_room: case SearchType.live_room:
@ -89,9 +107,9 @@ class SearchHttp {
case SearchType.media_bangumi: case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']); data = SearchMBangumiModel.fromJson(res.data['data']);
break; break;
// case SearchType.article: case SearchType.article:
// data = SearchArticleModel.fromJson(res.data['data']); data = SearchArticleModel.fromJson(res.data['data']);
// break; break;
} }
return { return {
'status': true, 'status': true,

View File

@ -8,7 +8,6 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/history.dart'; import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class UserHttp { class UserHttp {
static Future<dynamic> userStat({required int mid}) async { static Future<dynamic> userStat({required int mid}) async {
@ -238,7 +237,7 @@ class UserHttp {
var res = await Request().post( var res = await Request().post(
Api.delHistory, Api.delHistory,
queryParameters: { queryParameters: {
'kid': 'archive_$kid', 'kid': kid,
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -250,26 +249,19 @@ class UserHttp {
} }
} }
// 相互关系查询 // 搜索历史记录
static Future relationSearch(int mid) async { static Future searchHistory(
Map params = await WbiSign().makSign({ {required int pn, required String keyword}) async {
'mid': mid,
'token': '',
'platform': 'web',
'web_location': 1550101,
});
var res = await Request().get( var res = await Request().get(
Api.relationSearch, Api.searchHistory,
data: { data: {
'mid': mid, 'pn': pn,
'w_rid': params['w_rid'], 'keyword': keyword,
'wts': params['wts'], 'business': 'all',
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
// relation 主动状态 return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
// 被动状态
return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }

View File

@ -17,7 +17,7 @@ enum SearchType {
// 用户bili_user // 用户bili_user
bili_user, bili_user,
// 专栏article // 专栏article
// article, article,
// 相簿photo // 相簿photo
// photo // photo
} }

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list; List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) { SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e)) .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList(); .toList();
} }
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id, this.id,
this.cid, this.cid,
// this.author, // this.author,
// this.mid, this.mid,
// this.typeid, // this.typeid,
// this.typename, // this.typename,
this.arcurl, this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id; int? id;
int? cid; int? cid;
// String? author; // String? author;
// String? mid; int? mid;
// String? typeid; // String? typeid;
// String? typename; // String? typename;
String? arcurl; String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl']; arcurl = json['arcurl'];
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); // title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
description = json['description']; description = json['description'];
@ -397,6 +399,7 @@ class SearchArticleItemModel {
this.pubTime, this.pubTime,
this.like, this.like,
this.title, this.title,
this.subTitle,
this.rankOffset, this.rankOffset,
this.mid, this.mid,
this.imageUrls, this.imageUrls,
@ -414,6 +417,7 @@ class SearchArticleItemModel {
int? pubTime; int? pubTime;
int? like; int? like;
List? title; List? title;
String? subTitle;
int? rankOffset; int? rankOffset;
int? mid; int? mid;
List? imageUrls; List? imageUrls;
@ -431,6 +435,7 @@ class SearchArticleItemModel {
pubTime = json['pub_time']; pubTime = json['pub_time'];
like = json['like']; like = json['like'];
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
rankOffset = json['rank_offset']; rankOffset = json['rank_offset'];
mid = json['mid']; mid = json['mid'];
imageUrls = json['image_urls']; imageUrls = json['image_urls'];

View File

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

View File

@ -184,7 +184,7 @@ class AboutController extends GetxController {
// 获取远程版本 // 获取远程版本
Future getRemoteApp() async { Future getRemoteApp() async {
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'}); var result = await Request().get(Api.latestApp);
data = LatestDataModel.fromJson(result.data); data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data; remoteAppInfo = data;
remoteVersion.value = data.tagName!; remoteVersion.value = data.tagName!;

View File

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

View File

@ -67,11 +67,10 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
); );
} else { } else {
// 请求错误 // 请求错误
// return HttpError( return HttpError(
// errMsg: snapshot.data['msg'], errMsg: snapshot.data['msg'],
// fn: () => Get.back(), fn: () => Get.back(),
// ); );
return SizedBox();
} }
} else { } else {
return BangumiInfo( return BangumiInfo(
@ -261,15 +260,9 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [ children: [
Text( Text(
!widget.loadingStatus !widget.loadingStatus
? (widget.bangumiDetail!.areas!
.isNotEmpty
? widget.bangumiDetail!.areas! ? widget.bangumiDetail!.areas!
.first['name'] .first['name']
: '') : bangumiItem!.areas!.first['name'],
: (bangumiItem!.areas!.isNotEmpty
? bangumiItem!
.areas!.first['name']
: ''),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: t.colorScheme.outline, color: t.colorScheme.outline,

View File

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

View File

@ -61,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false, centerTitle: false,
title: Obx( title: Obx(
() => Text( () => Text(
'黑名单管理 - ${_blackListController.blackList.length}', '黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -138,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController { class BlackListController extends GetxController {
int currentPage = 1; int currentPage = 1;
int pageSize = 50; int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs; RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async { Future queryBlacklist({type = 'init'}) async {
@ -148,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) { if (result['status']) {
if (type == 'init') { if (type == 'init') {
blackList.value = result['data'].list; blackList.value = result['data'].list;
total.value = result['data'].total;
} else { } else {
blackList.addAll(result['data'].list); blackList.addAll(result['data'].list);
} }
@ -161,6 +163,7 @@ class BlackListController extends GetxController {
var result = await BlackHttp.removeBlack(fid: mid); var result = await BlackHttp.removeBlack(fid: mid);
if (result['status']) { if (result['status']) {
blackList.removeWhere((e) => e.mid == mid); blackList.removeWhere((e) => e.mid == mid);
total.value = total.value - 1;
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
} }

View File

@ -10,32 +10,22 @@ class PlDanmakuController {
// 按 6min 分段 // 按 6min 分段
int segCount = 0; int segCount = 0;
List<DmSegMobileReply> dmSegList = []; List<DmSegMobileReply> dmSegList = [];
int currentSegIndex = 1; int currentSegIndex = 0;
int currentDmIndex = 0; int currentDmIndex = 0;
void calcSegment() { void calcSegment() {
dmSegList.clear();
// 视频分段数
segCount = (videoDuration.inSeconds / (60 * 6)).ceil(); segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
dmSegList = List<DmSegMobileReply>.generate(
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
// 当前分段
try {
currentSegIndex =
(playerController.position.value.inSeconds / (60 * 6)).ceil();
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
} catch (_) {}
} }
Future<List<DmSegMobileReply>> queryDanmaku() async { Future<List<DmSegMobileReply>> queryDanmaku() async {
// dmSegList.clear(); dmSegList.clear();
for (int segIndex = 1; segIndex <= segCount; segIndex++) {
DmSegMobileReply result = DmSegMobileReply result =
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex); await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex);
if (result.elems.isNotEmpty) { if (result.elems.isNotEmpty) {
result.elems.sort((a, b) => (a.progress).compareTo(b.progress)); result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
// dmSegList.add(result); dmSegList.add(result);
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex; }
dmSegList[currentSegIndex - 1] = result;
} }
if (dmSegList.isNotEmpty) { if (dmSegList.isNotEmpty) {
findClosestPositionIndex(playerController.position.value.inMilliseconds); findClosestPositionIndex(playerController.position.value.inMilliseconds);

View File

@ -1,4 +1,3 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -89,15 +88,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
PlDanmakuController ctr = _plDanmakuController; PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds; int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes; blockTypes = playerController.blockTypes;
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty) {
ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
ctr.queryDanmaku();
});
}
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
return; return;
} }
@ -149,8 +140,6 @@ class _PlDanmakuState extends State<PlDanmaku> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, box) {
double initDuration = box.maxWidth / 12;
return Obx( return Obx(
() => AnimatedOpacity( () => AnimatedOpacity(
opacity: playerController.isOpenDanmu.value ? 1 : 0, opacity: playerController.isOpenDanmu.value ? 1 : 0,
@ -163,16 +152,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
fontSize: 15 * fontSizeVal, fontSize: 15 * fontSizeVal,
area: showArea, area: showArea,
opacity: opacityVal, opacity: opacityVal,
hideTop: blockTypes.contains(5), duration: danmakuSpeedVal * widget.playerController.playbackSpeed,
hideScroll: blockTypes.contains(2),
hideBottom: blockTypes.contains(4),
duration: initDuration /
(danmakuSpeedVal * widget.playerController.playbackSpeed),
), ),
statusChanged: (isPlaying) {}, statusChanged: (isPlaying) {},
), ),
), ),
); );
});
} }
} }

View File

@ -149,30 +149,10 @@ class DynamicsController extends GetxController {
case 'DYNAMIC_TYPE_ARTICLE': case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title; String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl; String url = item.modules.moduleDynamic.major.opus.jumpUrl;
if (url.contains('opus') || url.contains('read')) {
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(url);
String number = matches.first.group(0)!;
if (url.contains('read')) {
number = 'cv$number';
}
Get.toNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
'title': title,
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
});
} else {
Get.toNamed( Get.toNamed(
'/webview', '/webview',
parameters: { parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
'url': 'https:$url',
'type': 'note',
'pageTitle': title
},
); );
}
break; break;
case 'DYNAMIC_TYPE_PGC': case 'DYNAMIC_TYPE_PGC':
print('番剧'); print('番剧');
@ -234,7 +214,7 @@ class DynamicsController extends GetxController {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/reply.dart';
@ -18,7 +17,6 @@ class DynamicDetailController extends GetxController {
RxString noMore = ''.obs; RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs; RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs; RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time; ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -10,10 +9,7 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart'; import 'package:pilipala/pages/dynamics/deatil/index.dart';
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart'; import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../widgets/dynamic_panel.dart'; import '../widgets/dynamic_panel.dart';
@ -25,18 +21,15 @@ class DynamicDetailPage extends StatefulWidget {
State<DynamicDetailPage> createState() => _DynamicDetailPageState(); State<DynamicDetailPage> createState() => _DynamicDetailPageState();
} }
class _DynamicDetailPageState extends State<DynamicDetailPage> class _DynamicDetailPageState extends State<DynamicDetailPage> {
with TickerProviderStateMixin { late DynamicDetailController? _dynamicDetailController;
late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title late StreamController<bool> titleStreamC; // appBar title
late ScrollController scrollController; final ScrollController scrollController = ScrollController();
bool _visibleTitle = false; bool _visibleTitle = false;
String? action; String? action;
// 回复类型 // 回复类型
late int type; late int type;
bool _isFabVisible = true;
@override @override
void initState() { void initState() {
@ -57,30 +50,37 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
} }
} catch (_) {} } catch (_) {}
} }
int commentType = 11; int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
try {
commentType = Get.arguments['item'].basic!['comment_type'];
} catch (_) {}
type = (commentType == 0) ? 11 : commentType; type = (commentType == 0) ? 11 : commentType;
action = action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null; Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = _dynamicDetailController =
Get.put(DynamicDetailController(oid, type), tag: oid.toString()); Get.put(DynamicDetailController(oid, type), tag: oid.toString());
_futureBuilderFuture = _dynamicDetailController.queryReplyList(); _futureBuilderFuture = _dynamicDetailController!.queryReplyList();
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
scrollController.addListener(_listen);
if (action == 'comment') { if (action == 'comment') {
_visibleTitle = true; _visibleTitle = true;
titleStreamC.add(true); titleStreamC.add(true);
} }
}
fabAnimationCtr = AnimationController( void _listen() async {
vsync: this, if (scrollController.position.pixels >=
duration: const Duration(milliseconds: 300), scrollController.position.maxScrollExtent - 300) {
); EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
fabAnimationCtr.forward(); _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
// 滚动事件监听 });
scrollListener(); }
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
} }
void replyReply(replyItem) { void replyReply(replyItem) {
@ -107,58 +107,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
); );
} }
void scrollListener() {
scrollController = _dynamicDetailController.scrollController;
scrollController.addListener(
() {
// 分页加载
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
});
}
// 标题
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
// fab按钮
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
_hideFab();
}
},
);
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
fabAnimationCtr.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
fabAnimationCtr.reverse();
}
}
@override @override
void dispose() { void dispose() {
scrollController.removeListener(() {}); scrollController.removeListener(() {});
fabAnimationCtr.dispose();
scrollController.dispose();
super.dispose(); super.dispose();
} }
@ -185,17 +136,15 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await _dynamicDetailController.queryReplyList(); await _dynamicDetailController!.queryReplyList();
}, },
child: Stack( child: CustomScrollView(
children: [
CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
if (action != 'comment') if (action != 'comment')
SliverToBoxAdapter( SliverToBoxAdapter(
child: DynamicPanel( child: DynamicPanel(
item: _dynamicDetailController.item, item: _dynamicDetailController!.item,
source: 'detail', source: 'detail',
), ),
), ),
@ -207,9 +156,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
border: Border( border: Border(
top: BorderSide( top: BorderSide(
width: 0.6, width: 0.6,
color: Theme.of(context) color: Theme.of(context).dividerColor.withOpacity(0.05),
.dividerColor
.withOpacity(0.05),
), ),
), ),
), ),
@ -226,9 +173,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
scale: animation, child: child); scale: animation, child: child);
}, },
child: Text( child: Text(
'${_dynamicDetailController.acount.value}', '${_dynamicDetailController!.acount.value}',
key: ValueKey<int>( key: ValueKey<int>(
_dynamicDetailController.acount.value), _dynamicDetailController!.acount.value),
), ),
), ),
), ),
@ -238,11 +185,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
height: 35, height: 35,
child: TextButton.icon( child: TextButton.icon(
onPressed: () => onPressed: () =>
_dynamicDetailController.queryBySort(), _dynamicDetailController!.queryBySort(),
icon: const Icon(Icons.sort, size: 16), icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text( label: Obx(() => Text(
_dynamicDetailController _dynamicDetailController!.sortTypeLabel.value,
.sortTypeLabel.value,
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
)), )),
), ),
@ -261,11 +207,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
if (snapshot.data['status']) { if (snapshot.data['status']) {
// 请求成功 // 请求成功
return Obx( return Obx(
() => _dynamicDetailController.replyList.isEmpty && () => _dynamicDetailController!.replyList.isEmpty &&
_dynamicDetailController.isLoadingMore _dynamicDetailController!.isLoadingMore
? SliverList( ? SliverList(
delegate: SliverChildBuilderDelegate( delegate:
(context, index) { SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton(); return const VideoReplySkeleton();
}, childCount: 8), }, childCount: 8),
) )
@ -273,7 +219,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) {
if (index == if (index ==
_dynamicDetailController _dynamicDetailController!
.replyList.length) { .replyList.length) {
return Container( return Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -287,7 +233,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
child: Center( child: Center(
child: Obx( child: Obx(
() => Text( () => Text(
_dynamicDetailController _dynamicDetailController!
.noMore.value, .noMore.value,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -301,7 +247,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
); );
} else { } else {
return ReplyItem( return ReplyItem(
replyItem: _dynamicDetailController replyItem: _dynamicDetailController!
.replyList[index], .replyList[index],
showReplyRow: true, showReplyRow: true,
replyLevel: '1', replyLevel: '1',
@ -309,15 +255,15 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
replyReply(replyItem), replyReply(replyItem),
replyType: ReplyType.values[type], replyType: ReplyType.values[type],
addReply: (replyItem) { addReply: (replyItem) {
_dynamicDetailController _dynamicDetailController!
.replyList[index].replies! .replyList[index].replies!
.add(replyItem); .add(replyItem);
}, },
); );
} }
}, },
childCount: _dynamicDetailController childCount:
.replyList.length + _dynamicDetailController!.replyList.length +
1, 1,
), ),
), ),
@ -341,52 +287,6 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
) )
], ],
), ),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[type],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList
.add(value['data']),
_dynamicDetailController.acount.value++
}
},
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
),
],
),
), ),
); );
} }

View File

@ -225,10 +225,8 @@ InlineSpan richNode(item, context) {
width: box.maxWidth / 2, width: box.maxWidth / 2,
height: box.maxWidth * height: box.maxWidth *
0.5 * 0.5 *
(pictureItem.height != null && pictureItem.height! /
pictureItem.width != null pictureItem.width!,
? pictureItem.height! / pictureItem.width!
: 1),
), ),
), ),
); );

View File

@ -16,16 +16,13 @@ class FansPage extends StatefulWidget {
} }
class _FansPageState extends State<FansPage> { class _FansPageState extends State<FansPage> {
late String mid; final FansController _fansController = Get.put(FansController());
late FansController _fansController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mid = Get.parameters['mid']!;
_fansController = Get.put(FansController(), tag: mid);
_futureBuilderFuture = _fansController.queryFans('init'); _futureBuilderFuture = _fansController.queryFans('init');
scrollController.addListener( scrollController.addListener(
() async { () async {

View File

@ -49,8 +49,8 @@ class FavVideoCardH extends StatelessWidget {
Get.toNamed('/video', parameters: parameters, arguments: { Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem, 'videoItem': videoItem,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': // 'videoType':
epId != null ? SearchType.media_bangumi : SearchType.video, // epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
child: Column( child: Column(
@ -142,6 +142,8 @@ class VideoContent extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const Spacer(), const Spacer(),
Row(
children: [
Text( Text(
videoItem.owner.name, videoItem.owner.name,
style: TextStyle( style: TextStyle(
@ -149,14 +151,6 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(), const Spacer(),
SizedBox( SizedBox(
width: 26, width: 26,

View File

@ -16,16 +16,13 @@ class FollowPage extends StatefulWidget {
} }
class _FollowPageState extends State<FollowPage> { class _FollowPageState extends State<FollowPage> {
late String mid; final FollowController _followController = Get.put(FollowController());
late FollowController _followController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mid = Get.parameters['mid']!;
_followController = Get.put(FollowController(), tag: mid);
_futureBuilderFuture = _followController.queryFollowings('init'); _futureBuilderFuture = _followController.queryFollowings('init');
scrollController.addListener( scrollController.addListener(
() async { () async {

View File

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

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs; RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs; RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override @override
void onInit() { void onInit() {
@ -123,8 +125,15 @@ class HistoryController extends GetxController {
} }
// 删除某条历史记录 // 删除某条历史记录
Future delHistory(kid) async { Future delHistory(kid, business) async {
var res = await UserHttp.delHistory(kid); 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']) { if (res['status']) {
historyList.removeWhere((e) => e.kid == kid); historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']); SmartDialog.showToast(res['msg']);
@ -133,12 +142,61 @@ class HistoryController extends GetxController {
// 删除已看历史记录 // 删除已看历史记录
Future onDelHistory() async { Future onDelHistory() async {
/// TODO 优化
List<HisListItem> result = List<HisListItem> result =
historyList.where((e) => e.progress == -1).toList(); historyList.where((e) => e.progress == -1).toList();
for (HisListItem i in result) { for (HisListItem i in result) {
await UserHttp.delHistory(i.kid); String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid); historyList.removeWhere((e) => e.kid == i.kid);
} }
SmartDialog.showToast('操作完成'); SmartDialog.showToast('操作完成');
} }
// 删除选中的记录
Future onDelCheckedHistory() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选历史记录吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
/// TODO 优化
await SmartDialog.dismiss();
SmartDialog.showLoading(msg: '请求中');
List<HisListItem> result =
historyList.where((e) => e.checked!).toList();
for (HisListItem i in result) {
String str = 'archive';
try {
str = i.history!.business!;
} catch (_) {}
String resKid = '${str}_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
checkedCount.value = 0;
SmartDialog.dismiss();
enableMultiple.value = false;
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -37,6 +37,23 @@ class _HistoryPageState extends State<HistoryPage> {
} }
}, },
); );
_historyController.enableMultiple.listen((p0) {
setState(() {});
});
}
// 选中
onChoose(index) {
_historyController.historyList[index].checked =
!_historyController.historyList[index].checked!;
_historyController.checkedCount.value =
_historyController.historyList.where((item) => item.checked!).length;
_historyController.historyList.refresh();
}
// 更新多选状态
onUpdateMultiple() {
setState(() {});
} }
@override @override
@ -48,14 +65,31 @@ class _HistoryPageState extends State<HistoryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBarWidget(
visible: _historyController.enableMultiple.value,
child1: AppBar(
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back_outlined),
),
title: Text( title: Text(
'观看记录', '观看记录',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [ actions: [
// TextButton(
// onPressed: () {
// _historyController.enableMultiple.value = true;
// setState(() {});
// },
// child: const Text('多选'),
// ),
IconButton(
onPressed: () => Get.toNamed('/historySearch'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton<String>( PopupMenuButton<String>(
onSelected: (String type) { onSelected: (String type) {
// 处理菜单项选择的逻辑 // 处理菜单项选择的逻辑
@ -69,6 +103,10 @@ class _HistoryPageState extends State<HistoryPage> {
case 'del': case 'del':
_historyController.onDelHistory(); _historyController.onDelHistory();
break; break;
case 'multiple':
_historyController.enableMultiple.value = true;
setState(() {});
break;
default: default:
} }
}, },
@ -89,11 +127,58 @@ class _HistoryPageState extends State<HistoryPage> {
value: 'del', value: 'del',
child: Text('删除已看记录'), child: Text('删除已看记录'),
), ),
const PopupMenuItem<String>(
value: 'multiple',
child: Text('多选删除'),
),
], ],
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
), ),
child2: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () {
_historyController.enableMultiple.value = false;
for (var item in _historyController.historyList) {
item.checked = false;
}
_historyController.checkedCount.value = 0;
setState(() {});
},
icon: const Icon(Icons.close_outlined),
),
title: Obx(
() => Text(
'已选择${_historyController.checkedCount.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () {
for (var item in _historyController.historyList) {
item.checked = true;
}
_historyController.checkedCount.value =
_historyController.historyList.length;
_historyController.historyList.refresh();
},
child: const Text('全选'),
),
TextButton(
onPressed: () => _historyController.onDelCheckedHistory(),
child: Text(
'删除',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await _historyController.onRefresh(); await _historyController.onRefresh();
@ -120,6 +205,8 @@ class _HistoryPageState extends State<HistoryPage> {
videoItem: videoItem:
_historyController.historyList[index], _historyController.historyList[index],
ctr: _historyController, ctr: _historyController,
onChoose: () => onChoose(index),
onUpdateMultiple: () => onUpdateMultiple(),
); );
}, },
childCount: childCount:
@ -155,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

@ -12,13 +12,23 @@ import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget { class HistoryItem extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final HistoryController? ctr; final dynamic ctr;
const HistoryItem({super.key, required this.videoItem, this.ctr}); final Function? onChoose;
final Function? onUpdateMultiple;
const HistoryItem({
super.key,
required this.videoItem,
this.ctr,
this.onChoose,
this.onUpdateMultiple,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,6 +37,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid); String heroTag = Utils.makeHeroTag(aid);
return InkWell( return InkWell(
onTap: () async { onTap: () async {
if (ctr!.enableMultiple.value) {
feedBack();
onChoose!();
return;
}
if (videoItem.history.business.contains('article')) { if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ?? int cid = videoItem.history.cid ??
// videoItem.history.oid ?? // videoItem.history.oid ??
@ -74,7 +89,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
); );
} else { } else {
@ -102,7 +117,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );
@ -117,6 +132,17 @@ class HistoryItem extends StatelessWidget {
arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
} }
}, },
onLongPress: () {
if (ctr is HistorySearchController) {
return;
}
if (!ctr!.enableMultiple.value) {
feedBack();
ctr!.enableMultiple.value = true;
onChoose!();
onUpdateMultiple!();
}
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -131,6 +157,8 @@ class HistoryItem extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: StyleString.aspectRatio, aspectRatio: StyleString.aspectRatio,
@ -163,7 +191,8 @@ class HistoryItem extends StatelessWidget {
), ),
// 右上角 // 右上角
if (BusinessType.showBadge.showBadge if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) || .contains(
videoItem.history.business) ||
videoItem.history.business == videoItem.history.business ==
BusinessType.live.type) BusinessType.live.type)
PBadge( PBadge(
@ -178,6 +207,60 @@ class HistoryItem extends StatelessWidget {
}, },
), ),
), ),
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) VideoContent(videoItem: videoItem, ctr: ctr)
], ],
), ),
@ -193,7 +276,7 @@ class HistoryItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final HistoryController? ctr; final dynamic? ctr;
const VideoContent({super.key, required this.videoItem, this.ctr}); const VideoContent({super.key, required this.videoItem, this.ctr});
@override @override
@ -247,10 +330,6 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline), color: Theme.of(context).colorScheme.outline),
), ),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox( SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@ -267,6 +346,10 @@ class VideoContent extends StatelessWidget {
onSelected: (String type) {}, onSelected: (String type) {},
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[ <PopupMenuEntry<String>>[
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
PopupMenuItem<String>( PopupMenuItem<String>(
onTap: () async { onTap: () async {
var res = await UserHttp.toViewLater( var res = await UserHttp.toViewLater(
@ -284,7 +367,8 @@ class VideoContent extends StatelessWidget {
), ),
), ),
PopupMenuItem<String>( PopupMenuItem<String>(
onTap: () => ctr!.delHistory(videoItem.kid), onTap: () => ctr!.delHistory(
videoItem.kid, videoItem.history.business),
value: 'pause', value: 'pause',
height: 35, height: 35,
child: const Row( child: const Row(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,112 +1,19 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/html.dart'; import 'package:pilipala/http/html.dart';
import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class HtmlRenderController extends GetxController { class HtmlRenderController extends GetxController {
late String id; late String id;
late String dynamicType;
late int type;
RxInt oid = (-1).obs;
late Map response; late Map response;
int? floor;
int currentPage = 0;
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
id = Get.parameters['id']!; id = Get.parameters['id']!;
dynamicType = Get.parameters['dynamicType']!;
type = dynamicType == 'picture' ? 11 : 12;
} }
// 请求动态内容 Future reqHtml() async {
Future reqHtml(id) async { var res = await HtmlHttp.reqHtml(id);
late dynamic res;
if (dynamicType == 'opus' || dynamicType == 'picture') {
res = await HtmlHttp.reqHtml(id, dynamicType);
} else {
res = await HtmlHttp.reqReadHtml(id, dynamicType);
}
response = res; response = res;
oid.value = res['commentId'];
return res; return res;
} }
// 请求评论
Future queryReplyList({reqType = 'init'}) async {
var res = await ReplyHttp.replyList(
oid: oid.value,
pageNum: currentPage + 1,
type: type,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount;
if (replies.isNotEmpty) {
currentPage++;
noMore.value = '加载中...';
if (replies.length < 20) {
noMore.value = '没有更多了';
}
} else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
}
if (reqType == 'init') {
// 添加置顶回复
if (res['data'].upper.top != null) {
bool flag = res['data']
.topReplies
.any((reply) => reply.rpid == res['data'].upper.top.rpid);
if (!flag) {
replies.insert(0, res['data'].upper.top);
}
}
replies.insertAll(0, res['data'].topReplies);
replyList.value = replies;
} else {
replyList.addAll(replies);
}
}
isLoadingMore = false;
return res;
}
// 排序搜索评论
queryBySort() {
feedBack();
switch (_sortType) {
case ReplySortType.time:
_sortType = ReplySortType.like;
break;
case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time;
break;
default:
}
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
currentPage = 0;
replyList.clear();
queryReplyList(reqType: 'init');
}
} }

View File

@ -1,19 +1,7 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/html_render.dart'; import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'controller.dart'; import 'controller.dart';
@ -24,104 +12,16 @@ class HtmlRenderPage extends StatefulWidget {
State<HtmlRenderPage> createState() => _HtmlRenderPageState(); State<HtmlRenderPage> createState() => _HtmlRenderPageState();
} }
class _HtmlRenderPageState extends State<HtmlRenderPage> class _HtmlRenderPageState extends State<HtmlRenderPage> {
with TickerProviderStateMixin { HtmlRenderController htmlRenderCtr = Get.put(HtmlRenderController());
final HtmlRenderController _htmlRenderCtr = Get.put(HtmlRenderController());
late String title; late String title;
late String id; late String id;
late String url;
late String dynamicType;
late int type;
bool _isFabVisible = true;
late Future _futureBuilderFuture;
late ScrollController scrollController;
late AnimationController fabAnimationCtr;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
title = Get.parameters['title']!; title = Get.parameters['title']!;
id = Get.parameters['id']!; id = Get.parameters['id']!;
url = Get.parameters['url']!;
dynamicType = Get.parameters['dynamicType']!;
type = dynamicType == 'picture' ? 11 : 12;
_futureBuilderFuture = _htmlRenderCtr.reqHtml(id);
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
scrollListener();
}
void scrollListener() {
scrollController = _htmlRenderCtr.scrollController;
scrollController.addListener(
() {
// 分页加载
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_htmlRenderCtr.queryReplyList(reqType: 'onLoad');
});
}
// 标题
// if (scrollController.offset > 55 && !_visibleTitle) {
// _visibleTitle = true;
// titleStreamC.add(true);
// } else if (scrollController.offset <= 55 && _visibleTitle) {
// _visibleTitle = false;
// titleStreamC.add(false);
// }
// fab按钮
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
_hideFab();
}
},
);
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
fabAnimationCtr.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
fabAnimationCtr.reverse();
}
}
void replyReply(replyItem) {
int oid = replyItem.oid;
int rpid = replyItem.rpid!;
Get.to(
() => Scaffold(
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: Text(
'评论详情',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: VideoReplyReplyPanel(
oid: oid,
rpid: rpid,
source: 'dynamic',
replyType: ReplyType.values[type],
firstFloor: replyItem,
),
),
);
} }
@override @override
@ -129,68 +29,16 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: false, centerTitle: false,
titleSpacing: 0, title: Text(title),
title: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [ body: SingleChildScrollView(
const SizedBox(width: 4),
IconButton(
onPressed: () {
Get.toNamed('/webview', parameters: {
'url': 'https:$url',
'type': 'url',
'pageTitle': title,
});
},
icon: const Icon(Icons.open_in_browser_outlined, size: 19),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => {
Clipboard.setData(ClipboardData(text: url)),
SmartDialog.showToast('已复制'),
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy_rounded, size: 19),
SizedBox(width: 10),
Text('复制链接'),
],
),
),
PopupMenuItem(
onTap: () => {},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.share_outlined, size: 19),
SizedBox(width: 10),
Text('分享'),
],
),
),
],
),
const SizedBox(width: 6)
],
),
body: Stack(
children: [
SingleChildScrollView(
controller: scrollController,
child: Column( child: Column(
children: [ children: [
FutureBuilder( FutureBuilder(
future: _futureBuilderFuture, future: htmlRenderCtr.reqHtml(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data; var data = snapshot.data;
fabAnimationCtr.forward();
if (data['status']) { if (data['status']) {
return Column( return Column(
children: [ children: [
@ -202,14 +50,13 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
width: 40, width: 40,
height: 40, height: 40,
type: 'avatar', type: 'avatar',
src: _htmlRenderCtr.response['avatar']!, src: htmlRenderCtr.response['avatar']!,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Column( Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [ children: [
Text(_htmlRenderCtr.response['uname'], Text(htmlRenderCtr.response['uname'],
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
@ -217,11 +64,10 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
.fontSize, .fontSize,
)), )),
Text( Text(
_htmlRenderCtr.response['updateTime'], htmlRenderCtr.response['updateTime'],
style: TextStyle( style: TextStyle(
color: Theme.of(context) color:
.colorScheme Theme.of(context).colorScheme.outline,
.outline,
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
.labelSmall! .labelSmall!
@ -235,9 +81,9 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: HtmlRender( child: HtmlRender(
htmlContent: _htmlRenderCtr.response['content'], htmlContent: htmlRenderCtr.response['content'],
), ),
), ),
Container( Container(
@ -255,7 +101,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
], ],
); );
} else { } else {
return const Text('error'); return Text('error');
} }
} else { } else {
// 骨架屏 // 骨架屏
@ -263,195 +109,9 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
} }
}, },
), ),
Obx(
() => _htmlRenderCtr.oid.value != -1
? Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
),
height: 45,
padding: const EdgeInsets.only(left: 12, right: 6),
child: Row(
children: [
const Text('回复'),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () => _htmlRenderCtr.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(
() => Text(
_htmlRenderCtr.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
),
),
),
)
],
),
)
: const SizedBox(),
),
Obx(
() => _htmlRenderCtr.oid.value != -1
? FutureBuilder(
future: _htmlRenderCtr.queryReplyList(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map data = snapshot.data as Map;
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _htmlRenderCtr.replyList.isEmpty &&
_htmlRenderCtr.isLoadingMore
? ListView.builder(
itemCount: 5,
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return const VideoReplySkeleton();
},
)
: ListView.builder(
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemCount:
_htmlRenderCtr.replyList.length +
1,
itemBuilder: (context, index) {
if (index ==
_htmlRenderCtr
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_htmlRenderCtr
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _htmlRenderCtr
.replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyType:
ReplyType.values[type],
addReply: (replyItem) {
_htmlRenderCtr
.replyList[index].replies!
.add(replyItem);
},
);
}
},
),
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 5,
itemBuilder: (context, index) {
return const VideoReplySkeleton();
},
);
}
},
)
: const SizedBox(),
)
], ],
), ),
), ),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _htmlRenderCtr.oid.value,
root: 0,
parent: 0,
replyType: ReplyType.values[type],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_htmlRenderCtr.replyList.add(value['data']),
_htmlRenderCtr.acount.value++
}
},
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
),
],
),
); );
} }
} }

View File

@ -25,9 +25,9 @@ class LiveController extends GetxController {
// 获取推荐 // 获取推荐
Future queryLiveList(type) async { Future queryLiveList(type) async {
// if (type == 'init') { if (type == 'init') {
// _currentPage = 1; _currentPage = 1;
// } }
var res = await LiveHttp.liveList( var res = await LiveHttp.liveList(
pn: _currentPage, pn: _currentPage,
); );

View File

@ -21,15 +21,11 @@ class LivePage extends StatefulWidget {
State<LivePage> createState() => _LivePageState(); State<LivePage> createState() => _LivePageState();
} }
class _LivePageState extends State<LivePage> class _LivePageState extends State<LivePage> {
with AutomaticKeepAliveClientMixin {
final LiveController _liveController = Get.put(LiveController()); final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late ScrollController scrollController; late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -41,7 +37,7 @@ class _LivePageState extends State<LivePage>
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('liveList', const Duration(seconds: 1), () { EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
_liveController.isLoadingMore = true; _liveController.isLoadingMore = true;
_liveController.onLoad(); _liveController.onLoad();
}); });
@ -148,9 +144,9 @@ class _LivePageState extends State<LivePage>
return SliverGrid( return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距 // 行间距
mainAxisSpacing: StyleString.safeSpace, mainAxisSpacing: StyleString.cardSpace + 4,
// 列间距 // 列间距
crossAxisSpacing: StyleString.safeSpace, crossAxisSpacing: StyleString.cardSpace + 4,
// 列数 // 列数
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
mainAxisExtent: mainAxisExtent:

View File

@ -24,7 +24,7 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId); String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card( return Card(
elevation: 0, elevation: crossAxisCount == 1 ? 0 : 1,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: GestureDetector( child: GestureDetector(
@ -102,7 +102,7 @@ class LiveContent extends StatelessWidget {
child: Padding( child: Padding(
padding: crossAxisCount == 1 padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4) ? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(5, 8, 5, 4), : const EdgeInsets.fromLTRB(9, 8, 9, 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,

View File

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

View File

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

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget { class MediaPage extends StatefulWidget {
const MediaPage({super.key}); const MediaPage({super.key});
@ -15,7 +12,6 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage> class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
late MediaController mediaController; late MediaController mediaController;
late Future _futureBuilderFuture;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -24,13 +20,6 @@ class _MediaPageState extends State<MediaPage>
void initState() { void initState() {
super.initState(); super.initState();
mediaController = Get.put(MediaController()); mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
mediaController.userLogin.listen((status) {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
} }
@override @override
@ -38,22 +27,8 @@ class _MediaPageState extends State<MediaPage>
super.build(context); super.build(context);
Color primary = Theme.of(context).colorScheme.primary; Color primary = Theme.of(context).colorScheme.primary;
return Scaffold( return Scaffold(
appBar: AppBar(toolbarHeight: 30),
body: Column( body: Column(
children: [ children: [
ListTile(
leading: null,
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
'媒体库',
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
fontWeight: FontWeight.bold,
),
),
),
),
for (var i in mediaController.list) ...[ for (var i in mediaController.list) ...[
ListTile( ListTile(
onTap: () => i['onTap'](), onTap: () => i['onTap'](),
@ -74,206 +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) {
if (snapshot.data == null) {
return const SizedBox();
}
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

@ -3,26 +3,23 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart';
class MemberController extends GetxController { class MemberController extends GetxController {
late int mid; late int mid;
Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs; Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs;
Map? userStat; Map? userStat;
RxString face = ''.obs; String? face;
String? heroTag; String? heroTag;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
late int ownerMid; late int ownerMid;
// 投稿列表 // 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs; RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo; var userInfo;
RxInt attribute = (-1).obs; Box setting = GStrorage.setting;
RxString attributeText = '关注'.obs;
@override @override
void onInit() { void onInit() {
@ -30,9 +27,8 @@ class MemberController extends GetxController {
mid = int.parse(Get.parameters['mid']!); mid = int.parse(Get.parameters['mid']!);
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
ownerMid = userInfo != null ? userInfo.mid : -1; ownerMid = userInfo != null ? userInfo.mid : -1;
face.value = Get.arguments['face'] ?? ''; face = Get.arguments['face'] ?? '';
heroTag = Get.arguments['heroTag'] ?? ''; heroTag = Get.arguments['heroTag'] ?? '';
relationSearch();
} }
// 获取用户信息 // 获取用户信息
@ -41,7 +37,6 @@ class MemberController extends GetxController {
var res = await MemberHttp.memberInfo(mid: mid); var res = await MemberHttp.memberInfo(mid: mid);
if (res['status']) { if (res['status']) {
memberInfo.value = res['data']; memberInfo.value = res['data'];
face.value = res['data'].face;
} }
return res; return res;
} }
@ -69,25 +64,18 @@ class MemberController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
if (attribute.value == 128) {
blockUser();
return;
}
SmartDialog.show( SmartDialog.show(
useSystem: true, useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide, animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'), content: Text(memberInfo.value.isFollowed! ? '取消关注该用户?' : '关注该用户?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: Text( child: const Text('取消')),
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await VideoHttp.relationMod( await VideoHttp.relationMod(
@ -96,7 +84,8 @@ class MemberController extends GetxController {
reSrc: 11, reSrc: 11,
); );
memberInfo.value.isFollowed = !memberInfo.value.isFollowed!; memberInfo.value.isFollowed = !memberInfo.value.isFollowed!;
relationSearch(); SmartDialog.dismiss();
SmartDialog.showLoading();
SmartDialog.dismiss(); SmartDialog.dismiss();
memberInfo.update((val) {}); memberInfo.update((val) {});
}, },
@ -108,26 +97,9 @@ class MemberController extends GetxController {
); );
} }
// 关系查询
Future relationSearch() async {
if (userInfo == null) return;
if (mid == ownerMid) return;
var res = await UserHttp.relationSearch(mid);
if (res['status']) {
attribute.value = res['data']['relation']['attribute'];
attributeText.value = attribute.value == 0
? '关注'
: attribute.value == 2
? '已关注'
: attribute.value == 6
? '已互粉'
: '已拉黑';
}
}
// 拉黑用户 // 拉黑用户
Future blockUser() async { Future blockUser(int mid) async {
if (userInfo == null) { if (userInfoCache.get('userInfoCache') == null) {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
@ -137,12 +109,12 @@ class MemberController extends GetxController {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text(attribute.value != 128 ? '拉黑UP主?' : '从黑名单移除UP主'), content: const Text('拉黑该用户?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: Text( child: Text(
'点错了', '取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),
), ),
), ),
@ -150,16 +122,15 @@ class MemberController extends GetxController {
onPressed: () async { onPressed: () async {
var res = await VideoHttp.relationMod( var res = await VideoHttp.relationMod(
mid: mid, mid: mid,
act: attribute.value != 128 ? 5 : 6, act: 5,
reSrc: 11, reSrc: 11,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (res['status']) { if (res['status']) {
attribute.value = attribute.value != 128 ? 128 : 0; List<int> blackMidsList = setting
attributeText.value = attribute.value == 128 ? '已拉黑' : '关注'; .get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
memberInfo.value.isFollowed = false; blackMidsList.add(mid);
relationSearch(); setting.put(SettingBoxKey.blackMidsList, blackMidsList);
memberInfo.update((val) {});
} }
}, },
child: const Text('确认'), child: const Text('确认'),
@ -169,8 +140,4 @@ class MemberController extends GetxController {
}, },
); );
} }
void shareUser() {
Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');
}
} }

View File

@ -8,7 +8,6 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/member/archive/view.dart'; import 'package:pilipala/pages/member/archive/view.dart';
import 'package:pilipala/pages/member/dynamic/index.dart'; import 'package:pilipala/pages/member/dynamic/index.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'widgets/profile.dart'; import 'widgets/profile.dart';
@ -21,8 +20,7 @@ class MemberPage extends StatefulWidget {
class _MemberPageState extends State<MemberPage> class _MemberPageState extends State<MemberPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late String heroTag; final MemberController _memberController = Get.put(MemberController());
late MemberController _memberController;
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late TabController _tabController; late TabController _tabController;
@ -31,9 +29,6 @@ class _MemberPageState extends State<MemberPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
heroTag =
Get.arguments['heroTag'] ?? Utils.makeHeroTag(Get.parameters['mid']);
_memberController = Get.put(MemberController(), tag: heroTag);
_tabController = TabController(length: 3, vsync: this, initialIndex: 2); _tabController = TabController(length: 3, vsync: this, initialIndex: 2);
_futureBuilderFuture = _memberController.getInfo(); _futureBuilderFuture = _memberController.getInfo();
_extendNestCtr.addListener( _extendNestCtr.addListener(
@ -82,13 +77,11 @@ class _MemberPageState extends State<MemberPage>
children: [ children: [
Row( Row(
children: [ children: [
Obx( NetworkImgLayer(
() => NetworkImgLayer(
width: 35, width: 35,
height: 35, height: 35,
type: 'avatar', type: 'avatar',
src: _memberController.face.value, src: _memberController.face ?? '',
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx( Obx(
@ -109,78 +102,79 @@ class _MemberPageState extends State<MemberPage>
}, },
), ),
actions: [ actions: [
PopupMenuButton( IconButton(
icon: const Icon(Icons.more_vert), onPressed: () => Get.toNamed(
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ '/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
if (_memberController.ownerMid != icon: const Icon(Icons.search_outlined),
_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主'
: '分享我的主页'),
],
),
),
],
), ),
// PopupMenuButton(
// icon: const Icon(Icons.more_vert),
// itemBuilder: (BuildContext context) => <PopupMenuEntry>[
// if (_memberController.ownerMid !=
// _memberController.mid) ...[
// PopupMenuItem(
// onTap: () => _memberController.blockUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.block, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.attribute.value != 128
// ? '加入黑名单'
// : '移除黑名单'),
// ],
// ),
// )
// ],
// PopupMenuItem(
// onTap: () => _memberController.shareUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.share_outlined, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.ownerMid !=
// _memberController.mid
// ? '分享UP主'
// : '分享我的主页'),
// ],
// ),
// ),
// ],
// ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Stack( background: Stack(
children: [ children: [
Obx( // if (_memberController.face != null)
() => _memberController.face.value != '' // Positioned.fill(
? Positioned.fill( // bottom: 10,
bottom: 10, // child: Container(
child: Container( // decoration: BoxDecoration(
decoration: BoxDecoration( // image: DecorationImage(
image: DecorationImage( // fit: BoxFit.fitWidth,
fit: BoxFit.fitWidth, // image: NetworkImage(_memberController.face!),
image: NetworkImage( // alignment: Alignment.topCenter,
_memberController.face.value), // isAntiAlias: true,
alignment: Alignment.topCenter, // ),
isAntiAlias: true, // ),
), // foregroundDecoration: BoxDecoration(
), // gradient: LinearGradient(
foregroundDecoration: BoxDecoration( // colors: [
gradient: LinearGradient( // Theme.of(context)
colors: [ // .colorScheme
Theme.of(context) // .background
.colorScheme // .withOpacity(0.44),
.background // Theme.of(context).colorScheme.background,
.withOpacity(0.44), // ],
Theme.of(context).colorScheme.background, // begin: Alignment.topCenter,
], // end: Alignment.bottomCenter,
begin: Alignment.topCenter, // stops: const [0.0, 0.46],
end: Alignment.bottomCenter, // ),
stops: const [0.0, 0.46], // ),
), // ),
), // ),
),
)
: const SizedBox(),
),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -224,109 +218,109 @@ class _MemberPageState extends State<MemberPage>
fontWeight: fontWeight:
FontWeight.bold), FontWeight.bold),
)), )),
const SizedBox(width: 2), // const SizedBox(width: 2),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '女')
const Icon( // const Icon(
FontAwesomeIcons.venus, // FontAwesomeIcons.venus,
size: 14, // size: 14,
color: Colors.pink, // color: Colors.pink,
), // ),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '男')
const Icon( // const Icon(
FontAwesomeIcons.mars, // FontAwesomeIcons.mars,
size: 14, // size: 14,
color: Colors.blue, // color: Colors.blue,
), // ),
const SizedBox(width: 4), // const SizedBox(width: 4),
Image.asset( // Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png', // 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11, // height: 11,
), // ),
const SizedBox(width: 6), // const SizedBox(width: 6),
if (_memberController.memberInfo // if (_memberController.memberInfo
.value.vip!.status == // .value.vip!.status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'] != // 'img_label_uri_hans'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'], // 'img_label_uri_hans'],
height: 20, // height: 20,
), // ),
] else if (_memberController // ] else if (_memberController
.memberInfo // .memberInfo
.value // .value
.vip! // .vip!
.status == // .status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'] != // 'img_label_uri_hans_static'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'], // 'img_label_uri_hans_static'],
height: 20, // height: 20,
), // ),
] // ]
], ],
), ),
if (_memberController.memberInfo.value // if (_memberController.memberInfo.value
.official!['title'] != // .official!['title'] !=
'') ...[ // '') ...[
const SizedBox(height: 6), // const SizedBox(height: 6),
Text.rich( // Text.rich(
maxLines: 2, // maxLines: 2,
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['role'] == // .official!['role'] ==
1 // 1
? '个人认证:' // ? '个人认证:'
: '企业认证:', // : '企业认证:',
style: TextStyle( // style: TextStyle(
color: Theme.of(context) // color: Theme.of(context)
.primaryColor, // .primaryColor,
), // ),
children: [ // children: [
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['title'], // .official!['title'],
), // ),
], // ],
), // ),
softWrap: true, // softWrap: true,
), // ),
], // ],
const SizedBox(height: 4), // const SizedBox(height: 4),
if (_memberController // if (_memberController
.memberInfo.value.sign != // .memberInfo.value.sign !=
'') // '')
SelectableRegion( // SelectableRegion(
magnifierConfiguration: // magnifierConfiguration:
const TextMagnifierConfiguration(), // const TextMagnifierConfiguration(),
focusNode: FocusNode(), // focusNode: FocusNode(),
selectionControls: // selectionControls:
MaterialTextSelectionControls(), // MaterialTextSelectionControls(),
child: Text( // child: Text(
_memberController // _memberController
.memberInfo.value.sign!, // .memberInfo.value.sign!,
textAlign: TextAlign.left, // textAlign: TextAlign.left,
maxLines: 2, // maxLines: 2,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
), // ),
], ],
), ),
], ],
@ -353,28 +347,29 @@ class _MemberPageState extends State<MemberPage>
return MediaQuery.of(context).padding.top + kToolbarHeight; return MediaQuery.of(context).padding.top + kToolbarHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Column( // body: Column(
children: [ // children: [
SizedBox( // SizedBox(
width: double.infinity, // width: double.infinity,
height: 50, // height: 50,
child: TabBar(controller: _tabController, tabs: const [ // child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'), // Tab(text: '主页'),
Tab(text: '动态'), // Tab(text: '动态'),
Tab(text: '投稿'), // Tab(text: '投稿'),
]), // ]),
), // ),
Expanded( // Expanded(
child: TabBarView( // child: TabBarView(
controller: _tabController, // controller: _tabController,
children: const [ // children: const [
Text('主页'), // Text('主页'),
MemberDynamicPanel(), // MemberDynamicPanel(),
ArchivePanel(), // ArchivePanel(),
], // ],
)) // ))
], // ],
), // ),
body: ArchivePanel(),
), ),
); );
} }

View File

@ -22,7 +22,7 @@ Widget profile(ctr, {loadingStatus = false}) {
width: 90, width: 90,
height: 90, height: 90,
type: 'avatar', type: 'avatar',
src: !loadingStatus ? memberInfo.face : ctr.face.value, src: !loadingStatus ? memberInfo.face : ctr.face,
), ),
if (!loadingStatus && if (!loadingStatus &&
memberInfo.liveRoom != null && memberInfo.liveRoom != null &&
@ -70,126 +70,116 @@ Widget profile(ctr, {loadingStatus = false}) {
), ),
) )
], ],
), )),
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( // Padding(
padding: const EdgeInsets.only(left: 10, right: 10), // padding: const EdgeInsets.only(left: 10, right: 10),
child: Row( // child: Row(
mainAxisSize: MainAxisSize.max, // mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, // mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ // children: [
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? ctr.userStat!['following'].toString() // ? ctr.userStat!['following'].toString()
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold), // fontWeight: FontWeight.bold),
), // ),
Text( // Text(
'关注', // '关注',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize), // .fontSize),
) // )
], // ],
), // ),
), // ),
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? Utils.numFormat( // ? Utils.numFormat(
ctr.userStat!['follower'], // ctr.userStat!['follower'],
) // )
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold)), // fontWeight: FontWeight.bold)),
Text( // Text('粉丝',
'粉丝', // style: TextStyle(
style: TextStyle( // fontSize: Theme.of(context)
fontSize: Theme.of(context) // .textTheme
.textTheme // .labelMedium!
.labelMedium! // .fontSize))
.fontSize), // ],
) // ),
], // ),
), // Column(
), // children: [
Column( // const Text('-',
children: [ // style: TextStyle(fontWeight: FontWeight.bold)),
const Text('-', // Text(
style: TextStyle(fontWeight: FontWeight.bold)), // '获赞',
Text( // style: TextStyle(
'获赞', // fontSize: Theme.of(context)
style: TextStyle( // .textTheme
fontSize: Theme.of(context) // .labelMedium!
.textTheme // .fontSize),
.labelMedium! // )
.fontSize), // ],
) // ),
], // ],
), // ),
], // ),
), // const SizedBox(height: 10),
),
const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid) ...[ if (ctr.ownerMid != ctr.mid) ...[
Row( Row(
children: [ children: [
Obx( TextButton(
() => Expanded(
child: TextButton(
onPressed: () => ctr.actionRelationMod(), onPressed: () => ctr.actionRelationMod(),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1 padding: const EdgeInsets.only(left: 42, right: 42),
? Colors.transparent foregroundColor:
: ctr.attribute.value != 0 !loadingStatus && memberInfo.isFollowed!
? Theme.of(context).colorScheme.outline ? Theme.of(context).colorScheme.outline
: Theme.of(context) : Theme.of(context).colorScheme.onPrimary,
.colorScheme backgroundColor: !loadingStatus &&
.onPrimary, memberInfo.isFollowed!
backgroundColor: ctr.attribute.value != 0 ? Theme.of(context).colorScheme.onInverseSurface
? Theme.of(context)
.colorScheme
.onInverseSurface
: Theme.of(context) : Theme.of(context)
.colorScheme .colorScheme
.primary, // 设置按钮背景色 .primary, // 设置按钮背景色
), ),
child: Obx(() => Text(ctr.attributeText.value)), child: Text(!loadingStatus && memberInfo.isFollowed!
), ? '取关'
), : '关注'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( TextButton(
child: TextButton(
onPressed: () {}, onPressed: () {},
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Theme.of(context) padding: const EdgeInsets.only(left: 42, right: 42),
.colorScheme backgroundColor:
.onInverseSurface, Theme.of(context).colorScheme.onInverseSurface,
), ),
child: const Text('发消息'), child: const Text('发消息'),
),
) )
], ],
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ class SSearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode(); final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs; RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs; Rx<TextEditingController> controller = TextEditingController().obs;
RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs; RxList<HotSearchItem> hotSearchList = [HotSearchItem()].obs;
Box histiryWord = GStrorage.historyword; Box histiryWord = GStrorage.historyword;
List historyCacheList = []; List historyCacheList = [];
RxList historyList = [].obs; RxList historyList = [].obs;
@ -27,9 +27,9 @@ class SSearchController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { // if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault(); // searchDefault();
} // }
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) { if (Get.parameters['keyword'] != null) {
@ -51,7 +51,7 @@ class SSearchController extends GetxController {
searchSuggestList.value = []; searchSuggestList.value = [];
return; return;
} }
_debouncer.call(() => querySearchSuggest(value)); // _debouncer.call(() => querySearchSuggest(value));
} }
void onClear() { void onClear() {
@ -85,9 +85,7 @@ class SSearchController extends GetxController {
// 获取热搜关键词 // 获取热搜关键词
Future queryHotSearchList() async { Future queryHotSearchList() async {
var result = await SearchHttp.hotSearchList(); var result = await SearchHttp.hotSearchList();
if (result['status']) {
hotSearchList.value = result['data'].list; hotSearchList.value = result['data'].list;
}
return result; return result;
} }
@ -105,11 +103,9 @@ class SSearchController extends GetxController {
Future querySearchSuggest(String value) async { Future querySearchSuggest(String value) async {
var result = await SearchHttp.searchSuggest(term: value); var result = await SearchHttp.searchSuggest(term: value);
if (result['status']) { if (result['status']) {
if (result['data'] is SearchSuggestModel) {
searchSuggestList.value = result['data'].tag; searchSuggestList.value = result['data'].tag;
} }
} }
}
onSelect(word) { onSelect(word) {
searchKeyWord.value = word; searchKeyWord.value = word;

View File

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

View File

@ -7,7 +7,6 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/article_panel.dart';
import 'widgets/live_panel.dart'; import 'widgets/live_panel.dart';
import 'widgets/media_bangumi_panel.dart'; import 'widgets/media_bangumi_panel.dart';
import 'widgets/user_panel.dart'; import 'widgets/user_panel.dart';
@ -85,14 +84,12 @@ class _SearchPanelState extends State<SearchPanel>
ctr: _searchPanelController, ctr: _searchPanelController,
list: list.value, list: list.value,
); );
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return searchMbangumiPanel(context, ctr, list); // return searchMbangumiPanel(context, ctr, list);
case SearchType.bili_user: case SearchType.bili_user:
return searchUserPanel(context, ctr, list); return searchUserPanel(context, ctr, list);
case SearchType.live_room: // case SearchType.live_room:
return searchLivePanel(context, ctr, list); // return searchLivePanel(context, ctr, list);
// case SearchType.article:
// return searchArticlePanel(context, ctr, list);
default: default:
return const SizedBox(); return const SizedBox();
} }
@ -118,12 +115,12 @@ class _SearchPanelState extends State<SearchPanel>
switch (widget.searchType) { switch (widget.searchType) {
case SearchType.video: case SearchType.video:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return const MediaBangumiSkeleton(); // return const MediaBangumiSkeleton();
case SearchType.bili_user: case SearchType.bili_user:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.live_room: // case SearchType.live_room:
return const VideoCardHSkeleton(); // return const VideoCardHSkeleton();
default: default:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
} }

View File

@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
Widget searchArticlePanel(BuildContext context, ctr, list) {
TextStyle textStyle = TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline);
return ListView.builder(
controller: ctr!.scrollController,
itemCount: list.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) {
double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (list[index].imageUrls != null &&
list[index].imageUrls.isNotEmpty)
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return NetworkImgLayer(
width: maxWidth,
height: maxHeight,
src: list[index].imageUrls.first,
);
}),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 2,
text: TextSpan(
children: [
for (var i in list[index].title) ...[
TextSpan(
text: i['text'],
style: TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface,
),
),
]
],
),
),
const Spacer(),
Text(
Utils.dateFormat(list[index].pubTime,
formatType: 'detail'),
style: textStyle),
Row(
children: [
Text('${list[index].view}浏览', style: textStyle),
Text('', style: textStyle),
Text('${list[index].reply}评论', style: textStyle),
],
),
],
),
),
),
],
),
);
}),
),
);
},
);
}

View File

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

View File

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

View File

@ -13,18 +13,17 @@ import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/pages/video/detail/widgets/header_control.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import 'widgets/header_control.dart';
class VideoDetailController extends GetxController class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
/// 路由传参 /// 路由传参
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!); RxInt cid = int.parse(Get.parameters['cid']!).obs;
RxInt danmakuCid = 0.obs; RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
// 视频详情 // 视频详情
@ -109,9 +108,7 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) { localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false; enableHeart = false;
} }
danmakuCid.value = cid; danmakuCid.value = cid.value;
///
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = Floating(); floating = Floating();
} }
@ -218,19 +215,16 @@ class VideoDetailController extends GetxController
// 默认1倍速 // 默认1倍速
speed: 1.0, speed: 1.0,
bvid: bvid, bvid: bvid,
cid: cid, cid: cid.value,
enableHeart: enableHeart, enableHeart: enableHeart,
isFirstTime: isFirstTime, isFirstTime: isFirstTime,
autoplay: autoplay, autoplay: autoplay,
); );
/// 开启自动全屏时在player初始化完成后立即传入headerControl
plPlayerController.headerControl = headerControl;
} }
// 视频链接 // 视频链接
Future queryVideoUrl() async { Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];
@ -276,8 +270,8 @@ class VideoDetailController extends GetxController
currentDecodeFormats = flag currentDecodeFormats = flag
? currentDecodeFormats ? currentDecodeFormats
: VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!; : VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;
} catch (err) { } catch (e) {
SmartDialog.showToast('DecodeFormats error: $err'); print(e);
} }
/// 取出符合当前解码格式的videoItem /// 取出符合当前解码格式的videoItem
@ -289,7 +283,7 @@ class VideoDetailController extends GetxController
} }
videoUrl = firstVideo.baseUrl!; videoUrl = firstVideo.baseUrl!;
} catch (err) { } catch (err) {
SmartDialog.showToast('firstVideo error: $err'); print(err);
} }
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量 /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
@ -299,6 +293,7 @@ class VideoDetailController extends GetxController
try { try {
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
if (data.dash!.dolby?.audio?.isNotEmpty == true) { if (data.dash!.dolby?.audio?.isNotEmpty == true) {
// 杜比 // 杜比
audiosList.insert(0, data.dash!.dolby!.audio!.first); audiosList.insert(0, data.dash!.dolby!.audio!.first);
@ -312,20 +307,16 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
List<int> numbers = audiosList.map((map) => map.id!).toList(); List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers); int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
if (!numbers.contains(resultAudioQa) && if (!numbers.contains(resultAudioQa)) {
numbers.any((e) => e > resultAudioQa)) {
closestNumber = 30280; closestNumber = 30280;
} }
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
} else {
firstAudio = AudioItem();
} }
} catch (err) { } catch (e) {
firstAudio = audiosList.isNotEmpty ? audiosList.first : AudioItem(); print(e);
SmartDialog.showToast('firstAudio error: $err');
} }
audioUrl = firstAudio.baseUrl ?? ''; audioUrl = firstAudio!.baseUrl ?? '';
// //
if (firstAudio.id != null) { if (firstAudio.id != null) {
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
bool _isFabVisible = true; bool _isFabVisible = true;
String replyLevel = '1'; String replyLevel = '1';
late String heroTag;
// 添加页面缓存 // 添加页面缓存
@override @override
@ -47,29 +46,22 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override @override
void initState() { void initState() {
super.initState();
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0; int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
heroTag = Get.arguments['heroTag']; super.initState();
replyLevel = widget.replyLevel ?? '1'; replyLevel = widget.replyLevel ?? '1';
if (replyLevel == '2') { if (replyLevel == '2') {
_videoReplyController = Get.put( _videoReplyController = Get.put(
VideoReplyController(oid, widget.rpid.toString(), replyLevel), VideoReplyController(oid, widget.rpid.toString(), replyLevel),
tag: widget.rpid.toString()); tag: widget.rpid.toString());
} else { } else {
_videoReplyController = _videoReplyController = Get.put(VideoReplyController(oid, '', replyLevel),
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag); tag: Get.arguments['heroTag']);
} }
fabAnimationCtr = AnimationController( fabAnimationCtr = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300)); vsync: this, duration: const Duration(milliseconds: 300));
_futureBuilderFuture = _videoReplyController.queryReplyList(); _futureBuilderFuture = _videoReplyController.queryReplyList();
fabAnimationCtr.forward();
scrollListener();
}
void scrollListener() {
scrollController = _videoReplyController.scrollController; scrollController = _videoReplyController.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@ -89,6 +81,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
} }
}, },
); );
fabAnimationCtr.forward();
} }
void _showFab() { void _showFab() {
@ -108,7 +101,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
// 展示二级回复 // 展示二级回复
void replyReply(replyItem) { void replyReply(replyItem) {
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
if (replyItem != null) { if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid; videoDetailCtr.oid = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!; videoDetailCtr.fRpid = replyItem.rpid!;
@ -119,10 +112,9 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override @override
void dispose() { void dispose() {
scrollController.removeListener(() {}); super.dispose();
fabAnimationCtr.dispose(); fabAnimationCtr.dispose();
scrollController.dispose(); scrollController.dispose();
super.dispose();
} }
@override @override
@ -136,7 +128,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
child: Stack( child: Stack(
children: [ children: [
CustomScrollView( CustomScrollView(
controller: scrollController, controller: _videoReplyController.scrollController,
key: const PageStorageKey<String>('评论'), key: const PageStorageKey<String>('评论'),
slivers: <Widget>[ slivers: <Widget>[
SliverPersistentHeader( SliverPersistentHeader(

View File

@ -744,14 +744,11 @@ InlineSpan buildContent(
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
// 跳转到指定位置 // 跳转到指定位置
try { Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'])
.plPlayerController .plPlayerController
.seekTo( .seekTo(
Duration(seconds: Utils.duration(matchStr)), Duration(seconds: Utils.duration(matchStr)),
); );
} catch (_) {}
}, },
), ),
); );

View File

@ -26,6 +26,11 @@ class VideoReplyReplyController extends GetxController {
currentPage = 0; currentPage = 0;
} }
// 上拉加载
Future onLoad() async {
queryReplyList(type: 'onLoad');
}
Future queryReplyList({type = 'init'}) async { Future queryReplyList({type = 'init'}) async {
if (type == 'init') { if (type == 'init') {
currentPage = 0; currentPage = 0;
@ -44,11 +49,11 @@ class VideoReplyReplyController extends GetxController {
if (replyList.length == res['data'].page.count) { if (replyList.length == res['data'].page.count) {
noMore.value = '没有更多了'; noMore.value = '没有更多了';
} }
currentPage++;
} else { } else {
// 未登录状态replies可能返回null // 未登录状态replies可能返回null
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了'; noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
} }
currentPage++;
if (type == 'init') { if (type == 'init') {
// List<ReplyItemModel> replies = res['data'].replies; // List<ReplyItemModel> replies = res['data'].replies;
// 添加置顶回复 // 添加置顶回复
@ -67,10 +72,6 @@ class VideoReplyReplyController extends GetxController {
// res['data'].replies = replies; // res['data'].replies = replies;
replyList.value = replies; replyList.value = replies;
} else { } else {
// 每次回复之后,翻页请求有且只有相同的一条回复数据
if (replies.length == 1 && replies.last.rpid == replyList.last.rpid) {
return;
}
replyList.addAll(replies); replyList.addAll(replies);
// res['data'].replies.addAll(replyList); // res['data'].replies.addAll(replyList);
} }

View File

@ -1,4 +1,3 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -55,9 +54,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) { scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { if (!_videoReplyReplyController.isLoadingMore) {
_videoReplyReplyController.queryReplyList(type: 'onLoad'); _videoReplyReplyController.onLoad();
}); }
} }
}, },
); );

View File

@ -20,6 +20,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -53,7 +54,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
// 自动退出全屏 // 自动退出全屏
late bool autoExitFullcreen; late bool autoExitFullcreen;
late bool autoPlayEnable; Floating? floating;
late BangumiIntroController bangumiIntroController;
@override @override
void initState() { void initState() {
@ -61,13 +63,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag); videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag); videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
statusBarHeight = localCache.get('statusBarHeight'); statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen = autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
autoPlayEnable =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
videoSourceInit(); videoSourceInit();
appbarStreamListen(); appbarStreamListen();
if (Platform.isAndroid) {
floating = Floating();
}
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
@ -98,6 +102,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (autoExitFullcreen) { if (autoExitFullcreen) {
plPlayerController!.triggerFullScreen(status: false); plPlayerController!.triggerFullScreen(status: false);
} }
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
}
/// 单个循环
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.play();
}
// 播放完展示控制栏 // 播放完展示控制栏
try { try {
PiPStatus currentStatus = PiPStatus currentStatus =
@ -130,8 +151,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose(); plPlayerController!.dispose();
} }
if (videoDetailController.floating != null) { if (floating != null) {
videoDetailController.floating!.dispose(); floating!.dispose();
} }
super.dispose(); super.dispose();
} }
@ -143,12 +164,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)) { if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)) {
videoDetailController.brightness = plPlayerController!.brightness.value; videoDetailController.brightness = plPlayerController!.brightness.value;
} }
if (plPlayerController != null) {
videoDetailController.defaultST = plPlayerController!.position.value; videoDetailController.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
}
super.didPushNext(); super.didPushNext();
} }
@ -156,18 +175,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 返回当前页面时 // 返回当前页面时
void didPopNext() async { void didPopNext() async {
videoDetailController.isFirstTime = false; videoDetailController.isFirstTime = false;
bool autoplay = autoPlayEnable; bool autoplay =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
videoDetailController.playerInit(autoplay: autoplay); videoDetailController.playerInit(autoplay: autoplay);
videoDetailController.autoPlay.value = true;
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
videoDetailController.autoPlay.value =
!videoDetailController.isShowCover.value;
videoIntroController.isPaused = false; videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0 && autoplay) { if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
plPlayerController?.play(); plPlayerController!.play();
} }
plPlayerController?.addStatusLister(playerListener); plPlayerController!.addStatusLister(playerListener);
super.didPopNext(); super.didPopNext();
} }
@ -225,9 +242,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
? const SizedBox() ? const SizedBox()
: PLVideoPlayer( : PLVideoPlayer(
controller: plPlayerController!, controller: plPlayerController!,
headerControl: headerControl: HeaderControl(
videoDetailController controller:
.headerControl, plPlayerController,
videoDetailCtr:
videoDetailController,
floating: floating,
),
danmuWidget: Obx( danmuWidget: Obx(
() => PlDanmaku( () => PlDanmaku(
key: Key( key: Key(
@ -349,45 +370,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Container( body: Container(
key: Key(heroTag),
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: CustomScrollView(
children: [
Opacity(
opacity: 0,
child: SizedBox(
width: double.infinity,
height: 0,
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
),
Expanded(
child: TabBarView(
controller: videoDetailController.tabCtr,
children: [
Builder(
builder: (context) {
return CustomScrollView(
key: const PageStorageKey<String>('简介'), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[ slivers: <Widget>[
if (videoDetailController.videoType == if (videoDetailController.videoType ==
SearchType.video) ...[ SearchType.video) ...[
const VideoIntroPanel(), const VideoIntroPanel(),
] else if (videoDetailController.videoType == ] else
SearchType.media_bangumi) ...[ // if (videoDetailController.videoType ==
BangumiIntroPanel( // SearchType.media_bangumi) ...[
cid: videoDetailController.cid) // BangumiIntroPanel(
], // cid: videoDetailController.cid)
// ],
// if (videoDetailController.videoType == // if (videoDetailController.videoType ==
// SearchType.video) ...[ // SearchType.video) ...[
// SliverPersistentHeader( // SliverPersistentHeader(
@ -404,22 +399,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: Divider( child: Divider(
indent: 12, indent: 12,
endIndent: 12, endIndent: 12,
color: Theme.of(context) color:
.dividerColor Theme.of(context).dividerColor.withOpacity(0.06),
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
],
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
)
],
), ),
), ),
// const RelatedVideoPanel(),
], ],
), ),
), ),

View File

@ -13,6 +13,7 @@ import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
@ -149,13 +150,14 @@ class _HeaderControlState extends State<HeaderControl> {
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle), style: subTitleStyle),
), ),
// ListTile( ListTile(
// onTap: () {}, onTap: () => {Get.back(), showSetRepeat()},
// dense: true, dense: true,
// enabled: false, leading: const Icon(Icons.repeat, size: 20),
// leading: const Icon(Icons.play_circle_outline, size: 20), title: Text('播放顺序', style: titleStyle),
// title: Text('播放设置', style: titleStyle), subtitle: Text(widget.controller!.playRepeat.description,
// ), style: subTitleStyle),
),
ListTile( ListTile(
onTap: () => {Get.back(), showSetDanmaku()}, onTap: () => {Get.back(), showSetDanmaku()},
dense: true, dense: true,
@ -704,6 +706,60 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
/// 播放顺序
void showSetRepeat() async {
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择播放顺序', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in PlayRepeat.values) ...[
ListTile(
onTap: () {
widget.controller!.setPlayRepeat(i);
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: widget.controller!.playRepeat == i
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
)
],
],
),
),
),
],
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _ = widget.controller!; final _ = widget.controller!;

View File

@ -41,7 +41,7 @@ class WebviewController extends GetxController {
webviewInit() { webviewInit() {
controller controller
..setUserAgent(Request().headerUa()) ..setUserAgent(Request().headerUa('mob'))
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate( ..setNavigationDelegate(
NavigationDelegate( NavigationDelegate(
@ -99,8 +99,6 @@ class WebviewController extends GetxController {
HomeController homeCtr = Get.find<HomeController>(); HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true); homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face; homeCtr.userFace.value = result['data'].face;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true); await LoginUtils.refreshLoginStatus(true);
} catch (err) { } catch (err) {
SmartDialog.show(builder: (context) { SmartDialog.show(builder: (context) {

View File

@ -13,6 +13,7 @@ import 'package:media_kit_video/media_kit_video.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
@ -209,6 +210,9 @@ class PlPlayerController {
late double fontSizeVal; late double fontSizeVal;
late double danmakuSpeedVal; late double danmakuSpeedVal;
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
// 添加一个私有构造函数 // 添加一个私有构造函数
PlPlayerController._() { PlPlayerController._() {
_videoType = videoType; _videoType = videoType;
@ -226,6 +230,12 @@ class PlPlayerController {
// 弹幕速度 // 弹幕速度
danmakuSpeedVal = danmakuSpeedVal =
localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0); localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0);
playRepeat = PlayRepeat.values.toList().firstWhere(
(e) =>
e.value ==
videoStorage.get(VideoBoxKey.playRepeat,
defaultValue: PlayRepeat.pause.value),
);
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) { // if (status == PlayerStatus.playing) {
// WakelockPlus.enable(); // WakelockPlus.enable();
@ -449,9 +459,7 @@ class PlPlayerController {
for (var element in _statusListeners) { for (var element in _statusListeners) {
element(event ? PlayerStatus.playing : PlayerStatus.paused); element(event ? PlayerStatus.playing : PlayerStatus.paused);
} }
if (videoPlayerController!.state.position.inSeconds != 0) {
makeHeartBeat(_position.value.inSeconds, type: 'status'); makeHeartBeat(_position.value.inSeconds, type: 'status');
}
}), }),
videoPlayerController!.stream.completed.listen((event) { videoPlayerController!.stream.completed.listen((event) {
if (event) { if (event) {
@ -512,7 +520,6 @@ class PlPlayerController {
position = Duration.zero; position = Duration.zero;
} }
_position.value = position; _position.value = position;
_heartDuration = position.inSeconds;
if (duration.value.inSeconds != 0) { if (duration.value.inSeconds != 0) {
if (type != 'slider') { if (type != 'slider') {
/// 拖动进度条调节时,不等待第一帧,防止抖动 /// 拖动进度条调节时,不等待第一帧,防止抖动
@ -823,9 +830,6 @@ class PlPlayerController {
builder: (context) => Dialog.fullscreen( builder: (context) => Dialog.fullscreen(
backgroundColor: Colors.black, backgroundColor: Colors.black,
child: SafeArea( child: SafeArea(
// 忽略手机安全区域
left: false,
right: false,
bottom: bottom:
direction.value == 'vertical' || mode == FullScreenMode.vertical direction.value == 'vertical' || mode == FullScreenMode.vertical
? true ? true
@ -881,7 +885,7 @@ class PlPlayerController {
} }
// 记录播放记录 // 记录播放记录
Future makeHeartBeat(int progress, {type = 'playing'}) async { Future makeHeartBeat(progress, {type = 'playing'}) async {
if (!_enableHeart) { if (!_enableHeart) {
return false; return false;
} }
@ -908,6 +912,11 @@ class PlPlayerController {
} }
} }
setPlayRepeat(PlayRepeat type) {
playRepeat = type;
videoStorage.put(VideoBoxKey.playRepeat, type.value);
}
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) { if (type == 'single' && playerCount.value > 1) {

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

@ -48,26 +48,25 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late AnimationController animationController; late AnimationController animationController;
late VideoController videoController; late VideoController videoController;
final PLVideoPlayerController _ctr = Get.put(PLVideoPlayerController());
// bool _mountSeekBackwardButton = false; bool _mountSeekBackwardButton = false;
// bool _mountSeekForwardButton = false; bool _mountSeekForwardButton = false;
// bool _hideSeekBackwardButton = false; bool _hideSeekBackwardButton = false;
// bool _hideSeekForwardButton = false; bool _hideSeekForwardButton = false;
// double _brightnessValue = 0.0; double _brightnessValue = 0.0;
// bool _brightnessIndicator = false; bool _brightnessIndicator = false;
Timer? _brightnessTimer; Timer? _brightnessTimer;
// double _volumeValue = 0.0; double _volumeValue = 0.0;
// bool _volumeIndicator = false; bool _volumeIndicator = false;
Timer? _volumeTimer; Timer? _volumeTimer;
double _distance = 0.0; double _distance = 0.0;
// 初始手指落下位置 // 初始手指落下位置
double _initTapPositoin = 0.0; double _initTapPositoin = 0.0;
// bool _volumeInterceptEventStream = false; bool _volumeInterceptEventStream = false;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
late FullScreenMode mode; late FullScreenMode mode;
@ -76,11 +75,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late bool enableBackgroundPlay; late bool enableBackgroundPlay;
void onDoubleTapSeekBackward() { void onDoubleTapSeekBackward() {
_ctr.onDoubleTapSeekBackward(); setState(() {
_mountSeekBackwardButton = true;
});
} }
void onDoubleTapSeekForward() { void onDoubleTapSeekForward() {
_ctr.onDoubleTapSeekForward(); setState(() {
_mountSeekForwardButton = true;
});
} }
// 双击播放、暂停 // 双击播放、暂停
@ -132,10 +135,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future.microtask(() async { Future.microtask(() async {
try { try {
FlutterVolumeController.showSystemUI = true; FlutterVolumeController.showSystemUI = true;
_ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!; _volumeValue = (await FlutterVolumeController.getVolume())!;
FlutterVolumeController.addListener((value) { FlutterVolumeController.addListener((value) {
if (mounted && !_ctr.volumeInterceptEventStream.value) { if (mounted && !_volumeInterceptEventStream) {
_ctr.volumeValue.value = value; setState(() {
_volumeValue = value;
});
} }
}); });
} catch (_) {} } catch (_) {}
@ -143,10 +148,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future.microtask(() async { Future.microtask(() async {
try { try {
_ctr.brightnessValue.value = await ScreenBrightness().current; _brightnessValue = await ScreenBrightness().current;
ScreenBrightness().onCurrentBrightnessChanged.listen((value) { ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
if (mounted) { if (mounted) {
_ctr.brightnessValue.value = value; setState(() {
_brightnessValue = value;
});
} }
}); });
} catch (_) {} } catch (_) {}
@ -158,14 +165,18 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
FlutterVolumeController.showSystemUI = false; FlutterVolumeController.showSystemUI = false;
await FlutterVolumeController.setVolume(value); await FlutterVolumeController.setVolume(value);
} catch (_) {} } catch (_) {}
_ctr.volumeValue.value = value; setState(() {
_ctr.volumeIndicator.value = true; _volumeValue = value;
_ctr.volumeInterceptEventStream.value = true; _volumeIndicator = true;
_volumeInterceptEventStream = true;
});
_volumeTimer?.cancel(); _volumeTimer?.cancel();
_volumeTimer = Timer(const Duration(milliseconds: 200), () { _volumeTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) { if (mounted) {
_ctr.volumeIndicator.value = false; setState(() {
_ctr.volumeInterceptEventStream.value = false; _volumeIndicator = false;
_volumeInterceptEventStream = false;
});
} }
}); });
} }
@ -174,11 +185,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
try { try {
await ScreenBrightness().setScreenBrightness(value); await ScreenBrightness().setScreenBrightness(value);
} catch (_) {} } catch (_) {}
_ctr.brightnessIndicator.value = true; setState(() {
_brightnessIndicator = true;
});
_brightnessTimer?.cancel(); _brightnessTimer?.cancel();
_brightnessTimer = Timer(const Duration(milliseconds: 200), () { _brightnessTimer = Timer(const Duration(milliseconds: 200), () {
if (mounted) { if (mounted) {
_ctr.brightnessIndicator.value = false; setState(() {
_brightnessIndicator = false;
});
} }
}); });
widget.controller.brightness.value = value; widget.controller.brightness.value = value;
@ -307,12 +322,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 音量🔊 控制条展示 /// 音量🔊 控制条展示
Obx( Align(
() => Align(
alignment: Alignment.center, alignment: Alignment.center,
child: AnimatedOpacity( child: AnimatedOpacity(
curve: Curves.easeInOut, curve: Curves.easeInOut,
opacity: _ctr.volumeIndicator.value ? 1.0 : 0.0, opacity: _volumeIndicator ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
@ -332,9 +346,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
width: 28.0, width: 28.0,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Icon( child: Icon(
_ctr.volumeValue.value == 0.0 _volumeValue == 0.0
? Icons.volume_off ? Icons.volume_off
: _ctr.volumeValue.value < 0.5 : _volumeValue < 0.5
? Icons.volume_down ? Icons.volume_down
: Icons.volume_up, : Icons.volume_up,
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
@ -343,7 +357,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
Expanded( Expanded(
child: Text( child: Text(
'${(_ctr.volumeValue.value * 100.0).round()}%', '${(_volumeValue * 100.0).round()}%',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 13.0, fontSize: 13.0,
@ -357,15 +371,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
), ),
),
/// 亮度🌞 控制条展示 /// 亮度🌞 控制条展示
Obx( Align(
() => Align(
alignment: Alignment.center, alignment: Alignment.center,
child: AnimatedOpacity( child: AnimatedOpacity(
curve: Curves.easeInOut, curve: Curves.easeInOut,
opacity: _ctr.brightnessIndicator.value ? 1.0 : 0.0, opacity: _brightnessIndicator ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
@ -385,9 +397,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
width: 28.0, width: 28.0,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Icon( child: Icon(
_ctr.brightnessValue.value < 1.0 / 3.0 _brightnessValue < 1.0 / 3.0
? Icons.brightness_low ? Icons.brightness_low
: _ctr.brightnessValue.value < 2.0 / 3.0 : _brightnessValue < 2.0 / 3.0
? Icons.brightness_medium ? Icons.brightness_medium
: Icons.brightness_high, : Icons.brightness_high,
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
@ -397,7 +409,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
const SizedBox(width: 2.0), const SizedBox(width: 2.0),
Expanded( Expanded(
child: Text( child: Text(
'${(_ctr.brightnessValue.value * 100.0).round()}%', '${(_brightnessValue * 100.0).round()}%',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 13.0, fontSize: 13.0,
@ -411,7 +423,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
), ),
),
// Obx(() { // Obx(() {
// if (_.buffered.value == Duration.zero) { // if (_.buffered.value == Duration.zero) {
@ -514,7 +525,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
if (tapPosition < sectionWidth) { if (tapPosition < sectionWidth) {
// 左边区域 👈 // 左边区域 👈
final brightness = _ctr.brightnessValue.value - delta / 100.0; final brightness = _brightnessValue - delta / 100.0;
final result = brightness.clamp(0.0, 1.0); final result = brightness.clamp(0.0, 1.0);
setBrightness(result); setBrightness(result);
} else if (tapPosition < sectionWidth * 2) { } else if (tapPosition < sectionWidth * 2) {
@ -537,7 +548,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_distance = dy; _distance = dy;
} else { } else {
// 右边区域 👈 // 右边区域 👈
final volume = _ctr.volumeValue.value - delta / 100.0; final volume = _volumeValue - delta / 100.0;
final result = volume.clamp(0.0, 1.0); final result = volume.clamp(0.0, 1.0);
setVolume(result); setVolume(result);
} }
@ -550,14 +561,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Obx( Obx(
() => Column( () => Column(
children: [ children: [
if (widget.headerControl != null || _.headerControl != null) if (widget.headerControl != null)
ClipRect( ClipRect(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: AppBarAni( child: AppBarAni(
controller: animationController, controller: animationController,
visible: !_.controlsLock.value && _.showControls.value, visible: !_.controlsLock.value && _.showControls.value,
position: 'top', position: 'top',
child: widget.headerControl ?? _.headerControl!, child: widget.headerControl!,
), ),
), ),
const Spacer(), const Spacer(),
@ -693,20 +704,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}), }),
/// 点击 快进/快退 /// 点击 快进/快退
Obx( if (_mountSeekBackwardButton || _mountSeekForwardButton)
() => Visibility( Positioned.fill(
visible: _ctr.mountSeekBackwardButton.value ||
_ctr.mountSeekForwardButton.value,
child: Positioned.fill(
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: _ctr.mountSeekBackwardButton.value child: _mountSeekBackwardButton
? TweenAnimationBuilder<double>( ? TweenAnimationBuilder<double>(
tween: Tween<double>( tween: Tween<double>(
begin: 0.0, begin: 0.0,
end: end: _hideSeekBackwardButton ? 0.0 : 1.0,
_ctr.hideSeekBackwardButton.value ? 0.0 : 1.0,
), ),
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
builder: (context, value, child) => Opacity( builder: (context, value, child) => Opacity(
@ -714,9 +721,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: child, child: child,
), ),
onEnd: () { onEnd: () {
if (_ctr.hideSeekBackwardButton.value) { if (_hideSeekBackwardButton) {
_ctr.hideSeekBackwardButton.value = false; setState(() {
_ctr.mountSeekBackwardButton.value = false; _hideSeekBackwardButton = false;
_mountSeekBackwardButton = false;
});
} }
}, },
child: BackwardSeekIndicator( child: BackwardSeekIndicator(
@ -724,7 +733,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// _seekBarDeltaValueNotifier.value = -value; // _seekBarDeltaValueNotifier.value = -value;
}, },
onSubmitted: (value) { onSubmitted: (value) {
_ctr.hideSeekBackwardButton.value = true; setState(() {
_hideSeekBackwardButton = true;
});
Player player = Player player =
widget.controller.videoPlayerController!; widget.controller.videoPlayerController!;
var result = player.state.position - value; var result = player.state.position - value;
@ -745,11 +756,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
Expanded( Expanded(
child: _ctr.mountSeekForwardButton.value child: _mountSeekForwardButton
? TweenAnimationBuilder<double>( ? TweenAnimationBuilder<double>(
tween: Tween<double>( tween: Tween<double>(
begin: 0.0, begin: 0.0,
end: _ctr.hideSeekForwardButton.value ? 0.0 : 1.0, end: _hideSeekForwardButton ? 0.0 : 1.0,
), ),
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
builder: (context, value, child) => Opacity( builder: (context, value, child) => Opacity(
@ -757,9 +768,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: child, child: child,
), ),
onEnd: () { onEnd: () {
if (_ctr.hideSeekForwardButton.value) { if (_hideSeekForwardButton) {
_ctr.hideSeekForwardButton.value = false; setState(() {
_ctr.mountSeekForwardButton.value = false; _hideSeekForwardButton = false;
_mountSeekForwardButton = false;
});
} }
}, },
child: ForwardSeekIndicator( child: ForwardSeekIndicator(
@ -767,7 +780,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// _seekBarDeltaValueNotifier.value = value; // _seekBarDeltaValueNotifier.value = value;
}, },
onSubmitted: (value) { onSubmitted: (value) {
_ctr.hideSeekForwardButton.value = true; setState(() {
_hideSeekForwardButton = true;
});
Player player = Player player =
widget.controller.videoPlayerController!; widget.controller.videoPlayerController!;
var result = player.state.position + value; var result = player.state.position + value;
@ -785,37 +800,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
], ],
), ),
), ),
),
),
], ],
); );
} }
} }
class PLVideoPlayerController extends GetxController {
RxBool mountSeekBackwardButton = false.obs;
RxBool mountSeekForwardButton = false.obs;
RxBool hideSeekBackwardButton = false.obs;
RxBool hideSeekForwardButton = false.obs;
RxDouble brightnessValue = 0.0.obs;
RxBool brightnessIndicator = false.obs;
RxDouble volumeValue = 0.0.obs;
RxBool volumeIndicator = false.obs;
RxDouble distance = 0.0.obs;
// 初始手指落下位置
RxDouble initTapPositoin = 0.0.obs;
RxBool volumeInterceptEventStream = false.obs;
// 双击快进 展示样式
void onDoubleTapSeekForward() {
mountSeekForwardButton.value = true;
}
void onDoubleTapSeekBackward() {
mountSeekBackwardButton.value = true;
}
}

View File

@ -18,6 +18,7 @@ import 'package:pilipala/pages/html/index.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart'; import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/member_search/index.dart';
import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart'; import 'package:pilipala/pages/searchResult/index.dart';
@ -35,6 +36,8 @@ import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../pages/history_search/index.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
bool iosTransition = bool iosTransition =
setting.get(SettingBoxKey.iosTransition, defaultValue: false); setting.get(SettingBoxKey.iosTransition, defaultValue: false);
@ -42,7 +45,7 @@ bool iosTransition =
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
// 首页(推荐) // 首页(推荐)
CustomGetPage(name: '/', page: () => const HomePage()), CustomGetPage(name: '/', page: () => HomePage()),
// 热门 // 热门
CustomGetPage(name: '/hot', page: () => const HotPage()), CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情 // 视频详情
@ -86,6 +89,7 @@ class Routes {
CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()), CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
// 用户中心 // 用户中心
CustomGetPage(name: '/member', page: () => const MemberPage()), CustomGetPage(name: '/member', page: () => const MemberPage()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复 // 二级回复
CustomGetPage( CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), name: '/replyReply', page: () => const VideoReplyReplyPanel()),
@ -110,6 +114,9 @@ class Routes {
CustomGetPage(name: '/about', page: () => const AboutPage()), CustomGetPage(name: '/about', page: () => const AboutPage()),
// //
CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()), CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),
// 历史记录搜索
CustomGetPage(
name: '/historySearch', page: () => const HistorySearchPage()),
]; ];
} }

View File

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

View File

@ -2,8 +2,8 @@ import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:saver_gallery/saver_gallery.dart';
class DownloadUtils { class DownloadUtils {
// 获取存储权限 // 获取存储权限
@ -15,32 +15,25 @@ class DownloadUtils {
statuses[Permission.storage].toString(); statuses[Permission.storage].toString();
} }
static Future<bool> downloadImg(String imgUrl, static Future<bool> downloadImg(String imgUrl) async {
{String imgType = 'cover'}) async {
try {
await requestStoragePer(); await requestStoragePer();
SmartDialog.showLoading(msg: '保存中'); SmartDialog.showLoading(msg: '保存中');
var response = await Dio() var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes)); .get(imgUrl, options: Options(responseType: ResponseType.bytes));
String picName = String picName =
"plpl_${imgType}_${DateTime.now().toString().split('-').join()}"; "plpl_cover_${DateTime.now().toString().split('-').join()}.png";
final SaveResult result = await SaverGallery.saveImage( final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data), Uint8List.fromList(response.data),
quality: 60, quality: 100,
name: picName, name: picName,
// 保存到 PiliPala文件夹
androidRelativePath: "Pictures/PiliPala",
androidExistNotSave: false,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result.isSuccess) { if (result != null) {
if (result['isSuccess']) {
// ignore: avoid_print
await SmartDialog.showToast('$picName」已保存 '); await SmartDialog.showToast('$picName」已保存 ');
} }
return true; }
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());
return true; return true;
} }
} }
}

View File

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

View File

@ -156,4 +156,6 @@ class VideoBoxKey {
static const String videoBrightness = 'videoBrightness'; static const String videoBrightness = 'videoBrightness';
// 倍速 // 倍速
static const String videoSpeed = 'videoSpeed'; static const String videoSpeed = 'videoSpeed';
// 播放顺序
static const String playRepeat = 'playRepeat';
} }

View File

@ -337,14 +337,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
dio_http2_adapter:
dependency: "direct main"
description:
name: dio_http2_adapter
sha256: "3d81128cf389649ae6ac5cce23bcf5f9b254882b7f27185ca3b0d443ee9b825c"
url: "https://pub.dev"
source: hosted
version: "2.3.1+1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -629,14 +621,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
http2:
dependency: transitive
description:
name: http2
sha256: "38db0c4aa9f1cd238a5d2e86aa0cc7cc91c77e0c6c94ba64bbe85e4ff732a952"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
http_client_helper: http_client_helper:
dependency: transitive dependency: transitive
description: description:
@ -669,6 +653,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.17" version: "4.0.17"
image_gallery_saver:
dependency: "direct main"
description:
name: image_gallery_saver
sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -1086,14 +1078,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
saver_gallery:
dependency: "direct main"
description:
name: saver_gallery
sha256: "3131bba4257f69901437c0f1ebd692201ca5f34512d42667513a3802f1c171d1"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
screen_brightness: screen_brightness:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.8 version: 1.0.7
environment: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@ -45,12 +45,11 @@ dependencies:
cookie_jar: ^4.0.8 cookie_jar: ^4.0.8
dio_cookie_manager: ^3.1.0 dio_cookie_manager: ^3.1.0
connectivity_plus: ^4.0.1 connectivity_plus: ^4.0.1
dio_http2_adapter: ^2.3.1+1
# 图片 # 图片
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
extended_image: ^8.0.2 extended_image: ^8.0.2
saver_gallery: ^2.0.1 image_gallery_saver: ^2.0.3
# 存储 # 存储
path_provider: ^2.0.14 path_provider: ^2.0.14