Compare commits

..

35 Commits

Author SHA1 Message Date
90e811489b v1.0.3 更新 2023-08-21 09:41:07 +08:00
706bb0f924 mod: 一些样式修改 2023-08-21 09:03:34 +08:00
01d6308350 fix: richNode img preview 2023-08-20 21:58:38 +08:00
332d5dc38c feat: 底部播放进度条设置 2023-08-20 21:01:15 +08:00
8f84c6a6f9 mod: 图片预览页面样式 2023-08-20 20:34:25 +08:00
cc8753e8de fix: 用户信息字段 2023-08-20 19:37:37 +08:00
8c8ddc9d93 fix: 动态渲染异常 2023-08-20 18:10:54 +08:00
5f03244085 Merge branch 'main' into feature-media_kit 2023-08-20 15:48:51 +08:00
50a5653516 fix: 没有audio 资源的视频异常 2023-08-20 15:47:59 +08:00
7bb7159d48 mod: history 2023-08-20 15:26:10 +08:00
83341cd62b mod: 本地缓存字段修改、登录状态 2023-08-20 14:28:50 +08:00
8627869309 mod: 控制器监听事件移除 2023-08-20 09:12:31 +08:00
6bbbdd7710 fix:issues #26; mod: 部分样式 2023-08-19 23:24:17 +08:00
599d6983fc v1.0.2 更新 2023-08-19 21:33:39 +08:00
b7eed8578a mod: 请求节流 2023-08-19 20:37:42 +08:00
ec9d9739fe mod: 异常状态样式 2023-08-19 17:50:26 +08:00
740116e873 feat: 检查更新 2023-08-19 16:33:39 +08:00
2b0dc9d285 fix: 登录/退出登录页面刷新 2023-08-19 15:25:00 +08:00
7c2518bcd2 mod: 首页推荐picture类型 2023-08-19 11:34:36 +08:00
9db76e99db fix: 登录成功跳转 mod: 退出登录提示 2023-08-19 11:33:39 +08:00
40849cb68d opt: 全屏方式优化 2023-08-19 08:35:17 +08:00
b435023c99 feat: 直播、推荐、热门封面图片保存 2023-08-18 20:16:15 +08:00
b55568ef2a mod: 首页推荐代码 2023-08-18 18:22:20 +08:00
47a3c964c0 fix: 番剧记忆播放 2023-08-18 12:11:26 +08:00
85ff7c7a92 feat: 动态跳转番剧 2023-08-18 11:12:18 +08:00
083d05ed7e feat: 观看记录增加稍后再看功能 2023-08-18 10:24:09 +08:00
08862e7f72 feat: 稍后再看页面完善 2023-08-18 10:11:50 +08:00
04d953a3a2 fix: dio cookie读写 2023-08-18 09:30:31 +08:00
be1cc25d12 mod: 登录后状态 2023-08-17 22:44:01 +08:00
adff2f2828 fix: mac音量控制 2023-08-17 20:14:31 +08:00
592b32fc7a fix: 分p切换cid未更改 2023-08-17 16:00:37 +08:00
d9a758464c Merge branch 'main' of github.com:guozhigq/pilipala 2023-08-17 14:32:38 +08:00
0c067f7ca6 feat: 视频视图比例调整 2023-08-17 14:32:22 +08:00
b6aa144205 Update issue templates 2023-08-17 12:21:57 +08:00
438a94e298 Update main.yml
取消重命名拼接日期
2023-08-17 10:51:45 +08:00
108 changed files with 2129 additions and 1033 deletions

23
.github/ISSUE_TEMPLATE/bug-反馈.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug 反馈
about: 描述你所遇到的bug
title: ''
labels: 问题反馈
assignees: guozhigq
---
### 问题描述
请提供一个清晰而简明的问题描述。
### 复现步骤
请提供复现该问题所需的具体步骤。
### 预期行为
请描述你期望的正确行为或结果。
### 系统信息
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
### 相关截图或日志
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。

20
.github/ISSUE_TEMPLATE/功能请求.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: 功能请求
about: 对于功能的一些建议
title: ''
labels: 功能
assignees: guozhigq
---
### 功能描述
请提供对所请求功能的清晰描述。
### 目标
请描述你希望通过这个功能实现的目标。
### 解决方案
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
### 其他
请提供已实现该功能或类似功能的应用

View File

@ -59,16 +59,16 @@ jobs:
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: 获取当前日期
id: date
run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
# - name: 获取当前日期
# id: date
# run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
- name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk
run: |
DATE=${{ steps.date.outputs.date }}
# DATE=${{ steps.date.outputs.date }}
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
if [[ $file =~ app-(.*)-release.apk ]]; then
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}($DATE).apk"
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk"
mv "$file" "$new_file_name"
fi
done

1
assets/images/error.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

19
change_log/1.0.2.0819.md Normal file
View File

@ -0,0 +1,19 @@
## 1.0.2
### 新功能
+ 自动检查更新
+ 封面图片保存
+ 动态跳转番剧
+ 历史记录番剧记忆播放
+ 一键清空稍后再看
### 修复
+ 切换分P cid未切换
+ cookie存储问题
+ 登录/退出登录问题
### 优化
+ 页面空/异常状态样式
+ 退出登录提示
+ 请求节流
+ 全屏播放

19
change_log/1.0.3.0821.md Normal file
View File

@ -0,0 +1,19 @@
## 1.0.3
建议卸载1.0.2版本,重新安装
### 新功能
+ 底部播放进度条设置
+ 复制图片链接
### 修复
+ 用户数据格式修改
+ video Fit
+ 没有audio 资源的视频异常
+ 评论区域图片无法点击
+ 视频进度条拖动问题
### 优化
+ 页面空/异常状态样式
+ 部分页面样式
+ 图片预览页面样式

View File

@ -5,6 +5,8 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_volume_controller (0.0.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
@ -46,6 +48,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -74,6 +77,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
media_kit_libs_ios_video:
@ -109,6 +114,7 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget {
const AnimatedDialog({Key? key, required this.child}) : super(key: key);
const AnimatedDialog({Key? key, required this.child, this.closeFn})
: super(key: key);
final Widget child;
final Function? closeFn;
@override
State<StatefulWidget> createState() => AnimatedDialogState();
@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(opacityAnimation!.value),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
child: InkWell(
splashColor: Colors.transparent,
onTap: () => widget.closeFn!(),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
),
),
),
),

View File

@ -1,31 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class HttpError extends StatelessWidget {
const HttpError({required this.errMsg, required this.fn, super.key});
const HttpError(
{required this.errMsg, required this.fn, this.btnText, super.key});
final String? errMsg;
final Function()? fn;
final String? btnText;
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 150,
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/error.svg",
height: 200,
),
const SizedBox(height: 20),
Text(
errMsg ?? '请求异常',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
fn!();
},
child: const Text('点击重试'))
const SizedBox(height: 30),
OutlinedButton.icon(
onPressed: () {
fn!();
},
icon: const Icon(Icons.arrow_forward_outlined, size: 20),
label: Text(btnText ?? '点击重试'),
)
],
),
),

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/utils.dart';
class LiveCard extends StatelessWidget {
@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: Get.find<RcmdController>().crossAxisCount,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NoData extends StatelessWidget {
const NoData({super.key});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/error.svg",
height: 200,
),
const SizedBox(height: 20),
Text(
'没有数据',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
);
}
}

View File

@ -1,42 +1,82 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/download.dart';
class OverlayPop extends StatelessWidget {
final dynamic videoItem;
const OverlayPop({super.key, this.videoItem});
final Function? closeFn;
const OverlayPop({super.key, this.videoItem, this.closeFn});
@override
Widget build(BuildContext context) {
double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6.0),
borderRadius: BorderRadius.circular(10.0),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
width: (MediaQuery.of(context).size.width - 16),
height: (MediaQuery.of(context).size.width - 16) /
StyleString.aspectRatio,
src: videoItem.pic!,
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
child: Text(
videoItem.title!,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
NetworkImgLayer(
width: imgWidth,
height: imgWidth / StyleString.aspectRatio,
src: videoItem.pic!,
quality: 100,
),
),
],
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius:
const BorderRadius.all(Radius.circular(20))),
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => closeFn!(),
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: Text(
videoItem.title!,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
await DownloadUtils.downloadImg(
videoItem.pic ?? videoItem.cover);
// closeFn!();
},
icon: const Icon(Icons.download, size: 20),
)
],
)),
],
),
);
}

View File

@ -37,11 +37,11 @@ class VideoCardH extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
@ -55,7 +55,7 @@ class VideoCardH extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =

View File

@ -14,8 +14,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final dynamic videoItem;
final Function()? longPress;
final Function()? longPressEnd;
@ -61,6 +60,16 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag,
});
break;
default:
SmartDialog.showToast(videoItem.goto);
Get.toNamed(
'/webview',
parameters: {
'url': videoItem.uri,
'type': 'url',
'pageTitle': videoItem.title,
},
);
}
}
@ -80,57 +89,29 @@ class VideoCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async => onPushDetail(heroTag),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
// if (videoItem.stat.view is int &&
// videoItem.stat.danmaku is int)
// Positioned(
// left: 0,
// right: 0,
// bottom: 0,
// child: AnimatedOpacity(
// opacity: 1,
// duration: const Duration(milliseconds: 200),
// child: VideoStat(
// view: videoItem.stat.view,
// danmaku: videoItem.stat.danmaku,
// duration: videoItem.duration,
// ),
// ),
// ),
],
);
}),
),
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
);
}),
),
VideoContent(videoItem: videoItem)
],
@ -155,7 +136,6 @@ class VideoContent extends StatelessWidget {
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@ -181,22 +161,25 @@ class VideoContent extends StatelessWidget {
type: 'color',
)
],
if (videoItem.goto == 'picture') ...[
const PBadge(
text: '动态',
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
Expanded(
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
return SizedBox(
width: constraints.maxWidth,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
);
}),
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
if (videoItem.goto == 'av')
SizedBox(

View File

@ -248,6 +248,9 @@ class Api {
// 移除已观看
static const String toViewDel = '/x/v2/history/toview/del';
// 清空稍后再看
static const String toViewClear = '/x/v2/history/toview/clear';
// 追番
static const String bangumiAdd = '/pgc/web/follow/add';

View File

@ -15,21 +15,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
static Dio dio = Dio()
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
/// 设置cookie
static setCookie() async {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
var cookiePath = await Utils.getCookiePath();
var cookieJar = PersistCookieJar(
ignoreExpires: true,
@ -39,7 +30,8 @@ class Request {
dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
if (user.get(UserBoxKey.userMid) != null) {
var userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
var cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (cookie2.isEmpty) {
@ -63,16 +55,6 @@ class Request {
dio.options.headers['cookie'] = cookieString;
}
// 移除cookie
static removeCookie() async {
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), []);
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
cookieManager.cookieJar.deleteAll();
dio.interceptors.add(cookieManager);
}
// 从cookie中获取 csrf token
static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar
@ -105,16 +87,26 @@ class Request {
},
);
Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) != null) {
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString();
Box userInfoCache = GStrorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
options.headers['x-bili-mid'] = userInfo.mid.toString();
options.headers['env'] = 'prod';
options.headers['app-key'] = 'android64';
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
options.headers['x-bili-aurora-zone'] = 'sh001';
options.headers['referer'] = 'https://www.bilibili.com/';
}
dio.options = options;
dio = Dio(options)
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
//添加拦截器
dio.interceptors.add(ApiInterceptor());

View File

@ -17,7 +17,7 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
Box setting = GStrorage.setting;
Box localCache = GStrorage.localCache;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
@ -29,7 +29,8 @@ class ApiInterceptor extends Interceptor {
final uri = Uri.parse(locations.first);
final accessKey = uri.queryParameters['access_key'];
final mid = uri.queryParameters['mid'];
setting.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey});
localCache
.put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
@ -85,6 +86,12 @@ class UserHttp {
static Future<dynamic> seeYouLater() async {
var res = await Request().get(Api.seeYouLater);
if (res.data['code'] == 0) {
if (res.data['data']['count'] == 0) {
return {
'status': true,
'data': {'list': [], 'count': 0}
};
}
List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
@ -191,8 +198,28 @@ class UserHttp {
'sign': Constants.thirdSign,
},
);
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
try {
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
} catch (err) {
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
}
}
// 清空稍后再看
static Future toViewClear() async {
var res = await Request().post(
Api.toViewClear,
queryParameters: {
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '操作完成'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@ -18,6 +18,7 @@ import 'package:pilipala/utils/storage.dart';
/// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑
class VideoHttp {
static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting;
// 首页推荐视频
@ -60,8 +61,9 @@ class VideoHttp {
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key':
setting.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
'access_key': localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
''
},
);
if (res.data['code'] == 0) {
@ -136,7 +138,12 @@ class VideoHttp {
'data': PlayUrlModel.fromJson(res.data['data'])
};
} else {
return {'status': false, 'data': []};
return {
'status': false,
'data': [],
'code': res.data['code'],
'msg': res.data['message'],
};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err};
@ -153,13 +160,14 @@ class VideoHttp {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'code': result.code,
'msg': errMap[result.code] ?? '请求异常',
};
}

View File

@ -478,6 +478,8 @@ class DynamicArchiveModel {
this.stat,
this.title,
this.type,
this.epid,
this.seasonId,
});
int? aid;
@ -491,6 +493,8 @@ class DynamicArchiveModel {
Stat? stat;
String? title;
int? type;
int? epid;
int? seasonId;
DynamicArchiveModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];
@ -503,6 +507,8 @@ class DynamicArchiveModel {
stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;
title = json['title'];
type = json['type'];
epid = json['epid'];
seasonId = json['season_id'];
}
}

View File

@ -8,7 +8,9 @@ class FollowUpModel {
List<UpItem>? upList;
FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = LiveUsers.fromJson(json['live_users']);
liveUsers = json['live_users'] != null
? LiveUsers.fromJson(json['live_users'])
: null;
upList = json['up_list'] != null
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
: [];

View File

@ -4,12 +4,14 @@ class LatestDataModel {
this.tagName,
this.createdAt,
this.assets,
this.body,
});
String? url;
String? tagName;
String? createdAt;
List? assets;
String? body;
LatestDataModel.fromJson(Map<String, dynamic> json) {
url = json['url'];
@ -17,6 +19,7 @@ class LatestDataModel {
createdAt = json['created_at'];
assets =
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList();
body = json['body'];
}
}

View File

@ -121,7 +121,7 @@ class HisListItem {
viewAt = json['view_at'];
progress = json['progress'];
badge = json['badge'];
showTitle = json['show_title'];
showTitle = json['show_title'] == '' ? null : json['show_title'];
duration = json['duration'];
current = json['current'];
total = json['total'];

View File

@ -43,7 +43,7 @@ class UserInfoData {
@HiveField(5)
int? mobileVerified;
@HiveField(6)
int? money;
double? money;
@HiveField(7)
int? moral;
@HiveField(8)
@ -88,7 +88,7 @@ class UserInfoData {
: LevelInfo();
mid = json['mid'];
mobileVerified = json['mobile_verified'];
money = json['money'];
money = json['money'] is int ? json['money'].toDouble() : json['money'];
moral = json['moral'];
official = json['official'];
officialVerify = json['officialVerify'];
@ -130,6 +130,7 @@ class LevelInfo {
currentLevel = json['current_level'];
currentMin = json['current_min'];
currentExp = json['current_exp'];
nextExp = json['next_exp'];
nextExp =
json['current_level'] == 6 ? json['current_exp'] : json['next_exp'];
}
}

View File

@ -23,7 +23,7 @@ class UserInfoDataAdapter extends TypeAdapter<UserInfoData> {
levelInfo: fields[3] as LevelInfo?,
mid: fields[4] as int?,
mobileVerified: fields[5] as int?,
money: fields[6] as int?,
money: fields[6] as double?,
moral: fields[7] as int?,
official: (fields[8] as Map?)?.cast<dynamic, dynamic>(),
officialVerify: (fields[9] as Map?)?.cast<dynamic, dynamic>(),

View File

@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart';
@ -165,7 +164,7 @@ class AboutController extends GetxController {
}
}
// 获取当前版本
// 获取当前版本
Future getCurrentApp() async {
var result = await PackageInfo.fromPlatform();
currentVersion.value = result.version;

View File

@ -11,17 +11,19 @@ class BangumiController extends GetxController {
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
late int mid;
var userInfo;
@override
void onInit() {
super.onInit();
if (user.get(UserBoxKey.userMid) != null) {
mid = int.parse(user.get(UserBoxKey.userMid).toString());
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
mid = userInfo.mid;
}
userLogin.value = user.get(UserBoxKey.userLogin) != null;
userLogin.value = userInfo != null;
}
Future queryBangumiListFeed({type = 'init'}) async {
@ -48,7 +50,11 @@ class BangumiController extends GetxController {
// 我的订阅
Future queryBangumiFollow() async {
var result = await BangumiHttp.bangumiFollow(mid: 17340771);
userInfo = userInfo ?? userInfoCache.get('userInfoCache');
if (userInfo == null) {
return;
}
var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid);
if (result['status']) {
bangumiFollowList.value = result['data'].list;
} else {}

View File

@ -49,7 +49,7 @@ class BangumiIntroController extends GetxController {
RxBool hasCoin = false.obs;
// 是否收藏
RxBool hasFav = false.obs;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = [];
@ -57,6 +57,7 @@ class BangumiIntroController extends GetxController {
// 关注状态 默认未关注
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
var userInfo;
@override
void onInit() {
@ -82,7 +83,8 @@ class BangumiIntroController extends GetxController {
// videoItem!['owner'] = args.owner;
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
}
// 获取番剧简介&选集
@ -142,7 +144,7 @@ class BangumiIntroController extends GetxController {
// 投币
Future actionCoinVideo() async {
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -283,7 +285,7 @@ class BangumiIntroController extends GetxController {
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
mid: userInfo.mid, rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}

View File

@ -22,7 +22,11 @@ import 'controller.dart';
import 'widgets/intro_detail.dart';
class BangumiIntroPanel extends StatefulWidget {
const BangumiIntroPanel({super.key});
final int? cid;
const BangumiIntroPanel({
Key? key,
this.cid,
}) : super(key: key);
@override
State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();
@ -69,7 +73,11 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
);
}
} else {
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
return BangumiInfo(
loadingStatus: true,
bangumiDetail: bangumiDetail,
cid: widget.cid,
);
}
},
);
@ -79,11 +87,13 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
class BangumiInfo extends StatefulWidget {
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail;
final int? cid;
const BangumiInfo({
Key? key,
this.loadingStatus = false,
this.bangumiDetail,
this.cid,
}) : super(key: key);
@override
@ -97,6 +107,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
Box localCache = GStrorage.localCache;
late final BangumiInfoModel? bangumiItem;
late double sheetHeight;
int? cid;
@override
void initState() {
@ -105,11 +116,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!;
}
// 收藏
showFavBottomSheet() {
if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) {
if (bangumiIntroController.userInfo.mid == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -320,9 +332,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
pages: bangumiItem != null
? bangumiItem!.episodes!
: widget.bangumiDetail!.episodes!,
cid: bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid,
cid: cid ??
(bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid),
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid),

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -22,24 +23,28 @@ class _BangumiPageState extends State<BangumiPage>
with AutomaticKeepAliveClientMixin {
final BangumiController _bangumidController = Get.put(BangumiController());
late Future? _futureBuilderFuture;
late Future? _futureBuilderFutureFollow;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
ScrollController scrollController = _bangumidController.scrollController;
scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_bangumidController.isLoadingMore) {
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
_bangumidController.isLoadingMore = true;
await _bangumidController.onLoad();
}
_bangumidController.onLoad();
});
}
final ScrollDirection direction =
@ -53,6 +58,12 @@ class _BangumiPageState extends State<BangumiPage>
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -80,43 +91,61 @@ class _BangumiPageState extends State<BangumiPage>
'最近追番',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () {
setState(() {
_futureBuilderFutureFollow =
_bangumidController.queryBangumiFollow();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
],
),
),
SizedBox(
height: 258,
child: FutureBuilder(
future: _bangumidController.queryBangumiFollow(),
future: _futureBuilderFutureFollow,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList;
if (data['status']) {
return Obx(
() => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _bangumidController
.bangumiFollowList.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
() => list.isNotEmpty
? ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: list.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
),
);
},
)
: const SizedBox(
child: Center(
child: Text('还没有追番'),
),
),
);
},
),
);
} else {
return const SizedBox();

View File

@ -22,77 +22,106 @@ class BangumiPanel extends StatefulWidget {
class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex;
final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController();
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
scrollToIndex();
}
@override
void dispose() {
listViewScrollCtr.dispose();
listViewScrollCtr_2.dispose();
super.dispose();
}
void showBangumiPanel() {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 200));
listViewScrollCtr_2.animateTo(currentIndex * 56,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
});
// 在这里使用 setState 更新状态
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
titleSpacing: 10,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
Expanded(
child: Material(
child: ListView.builder(
controller: listViewScrollCtr_2,
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
setState(() {
changeFucCall(widget.pages[index], index);
});
},
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color:
Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
titleSpacing: 10,
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () => changeFucCall(widget.pages[index], index),
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
),
);
},
);
},
);
}
@ -108,6 +137,15 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
currentIndex = i;
setState(() {});
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
listViewScrollCtr.animateTo(currentIndex * 150,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
});
}
@override
@ -150,6 +188,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
SizedBox(
height: 60,
child: ListView.builder(
controller: listViewScrollCtr,
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
@ -222,87 +261,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
})),
)
// SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: ConstrainedBox(
// constraints: BoxConstraints(
// minWidth: MediaQuery.of(context).size.width,
// ),
// child: Row(
// children: [
// for (int i = 0; i < widget.pages.length; i++) ...[
// Container(
// width: 150,
// margin: const EdgeInsets.only(right: 10),
// child: Material(
// color: Theme.of(context).colorScheme.onInverseSurface,
// borderRadius: BorderRadius.circular(6),
// clipBehavior: Clip.hardEdge,
// child: InkWell(
// onTap: () => changeFucCall(widget.pages[i], i),
// child: Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 8, horizontal: 10),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// if (i == currentIndex) ...[
// Image.asset(
// 'assets/images/live.gif',
// color:
// Theme.of(context).colorScheme.primary,
// height: 12,
// ),
// const SizedBox(width: 6)
// ],
// Text(
// '第${i + 1}话',
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context)
// .colorScheme
// .primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// ),
// const SizedBox(width: 2),
// if (widget.pages[i].badge != null) ...[
// Image.asset(
// 'assets/images/big-vip.png',
// height: 16,
// ),
// ],
// ],
// ),
// const SizedBox(height: 3),
// Text(
// widget.pages[i].longTitle!,
// maxLines: 1,
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context).colorScheme.primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// overflow: TextOverflow.ellipsis,
// )
// ],
// ),
// ),
// ),
// ),
// ),
// ]
// ],
// ),
// ),
// )
],
);
}

View File

@ -46,6 +46,7 @@ class _BlackListPageState extends State<BlackListPage> {
List<int> blackMidsList =
_blackListController.blackList.map<int>((e) => e.mid!).toList();
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
scrollController.removeListener(() {});
super.dispose();
}

View File

@ -3,13 +3,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@ -46,17 +51,33 @@ class DynamicsController extends GetxController {
];
bool flag = false;
RxInt initialValue = 1.obs;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
var userInfo;
RxBool isLoadingDynamic = false.obs;
@override
void onInit() {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
super.onInit();
}
Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
dynamicsList.clear();
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: dynamicsType.value.values,
offset: offset,
mid: mid.value,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'init') {
dynamicsList.value = res['data'].items;
@ -142,13 +163,49 @@ class DynamicsController extends GetxController {
/// TODO
case 'DYNAMIC_TYPE_UGC_SEASON':
print('合集');
break;
case 'DYNAMIC_TYPE_PGC_UNION':
print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
SmartDialog.dismiss();
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
}
break;
}
}
Future queryFollowUp() async {
Future queryFollowUp({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
upData.value.upList = [];
upData.value.liveUsers = LiveUsers();
}
var res = await DynamicsHttp.followUp();
if (res['status']) {
upData.value = res['data'];
if (upData.value.upList!.isEmpty) {
mid.value = -1;
}
}
return res;
}
@ -162,7 +219,7 @@ class DynamicsController extends GetxController {
onRefresh() async {
page = 1;
queryFollowUp();
await queryFollowUp();
await queryFollowDynamic();
}
@ -182,7 +239,7 @@ class DynamicsController extends GetxController {
mid.value = -1;
dynamicsType.value = DynamicsType.values[0];
initialValue.value = 1;
SmartDialog.showToast('还原默认加载', alignment: Alignment.topCenter);
SmartDialog.showToast('还原默认加载');
dynamicsList.value = [DynamicItemModel()];
queryFollowDynamic();
}

View File

@ -95,6 +95,12 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -7,8 +7,10 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@ -29,7 +31,9 @@ class _DynamicsPageState extends State<DynamicsPage>
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
EventBus eventBus = EventBus();
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@ -39,7 +43,7 @@ class _DynamicsPageState extends State<DynamicsPage>
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
ScrollController scrollController = _dynamicsController.scrollController;
scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
@ -62,6 +66,20 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
);
eventBus.on(EventName.loginEvent, (args) {
_dynamicsController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
});
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
@ -107,73 +125,82 @@ class _DynamicsPageState extends State<DynamicsPage>
return const SizedBox();
}
}),
Obx(() => Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue: _dynamicsController.initialValue.value,
children: {
1: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
Obx(
() => _dynamicsController.userLogin.value
? Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue:
_dynamicsController.initialValue.value,
children: {
1: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
2: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
4: Text('专栏',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
2: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
// 4: Text(
// '专栏',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// ),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
))
)
: Text('动态',
style: Theme.of(context).textTheme.titleMedium),
)
],
),
Positioned(
right: 4,
top: 0,
bottom: 0,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () =>
{feedBack(), _dynamicsController.resetSearch()},
icon: const Icon(Icons.history, size: 21),
),
),
// Obx(
// () => Visibility(
// visible: _dynamicsController.userLogin.value,
// child: Positioned(
// right: 4,
// top: 0,
// bottom: 0,
// child: IconButton(
// padding: EdgeInsets.zero,
// onPressed: () =>
// {feedBack(), _dynamicsController.resetSearch()},
// icon: const Icon(Icons.history, size: 21),
// ),
// ),
// ),
// ),
],
),
),
@ -212,19 +239,36 @@ class _DynamicsPageState extends State<DynamicsPage>
List<DynamicItemModel> list =
_dynamicsController.dynamicsList;
return Obx(
() => list.isEmpty
? skeleton()
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
() {
if (list.isEmpty) {
if (_dynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
}, childCount: list.length),
},
childCount: list.length,
),
);
}
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => _dynamicsController.onRefresh(),
fn: () {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
},
);
}
} else {

View File

@ -36,7 +36,8 @@ Widget author(item, context) {
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
color: item.modules.moduleAuthor!.vip!['status'] > 0
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,

View File

@ -24,24 +24,28 @@ class _UpPanelState extends State<UpPanel> {
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
var userInfo;
@override
void initState() {
super.initState();
upList = widget.upData!.upList!;
liveList = widget.upData!.liveUsers!.items!;
if (widget.upData!.liveUsers != null) {
liveList = widget.upData!.liveUsers!.items!;
}
upList.insert(
0,
UpItem(
face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1),
);
userInfo = userInfoCache.get('userInfoCache');
upList.insert(
1,
UpItem(
face: user.get(UserBoxKey.userFace),
face: userInfo.face,
uname: '',
mid: user.get(UserBoxKey.userMid),
mid: userInfo.mid,
),
);
}
@ -64,15 +68,20 @@ class _UpPanelState extends State<UpPanel> {
controller: scrollController,
children: [
const SizedBox(width: 10),
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
),
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context).primaryColor.withOpacity(0.5),
),
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i)
],
@ -123,7 +132,8 @@ class _UpPanelState extends State<UpPanel> {
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.of(context).size.width;
double moveDistance = 0.0;
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance =
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {

View File

@ -5,19 +5,22 @@ import 'package:pilipala/models/fans/result.dart';
import 'package:pilipala/utils/storage.dart';
class FansController extends GetxController {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
int pn = 1;
int total = 0;
RxList<FansItemModel> fansList = [FansItemModel()].obs;
late int mid;
late String name;
var userInfo;
@override
void onInit() {
super.onInit();
mid = int.parse(
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
userInfo = userInfoCache.get('userInfoCache');
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname;
}
Future queryFans(type) async {

View File

@ -37,6 +37,12 @@ class _FansPageState extends State<FansPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -1,16 +1,24 @@
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
Future<dynamic> queryFavFolder() async {
userInfo = userInfoCache.get('userInfoCache');
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'};
}
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 10,
mid: GStrorage.user.get(UserBoxKey.userMid) ?? 0,
mid: userInfo!.mid!,
);
if (res['status']) {
favFolderData.value = res['data'];

View File

@ -61,6 +61,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
SliverAppBar(
expandedHeight: 260 - MediaQuery.of(context).padding.top,
pinned: true,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream,
initialData: false,

View File

@ -5,19 +5,22 @@ import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/utils/storage.dart';
class FollowController extends GetxController {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
int pn = 1;
int total = 0;
RxList<FollowItemModel> followList = [FollowItemModel()].obs;
late int mid;
late String name;
var userInfo;
@override
void onInit() {
super.onInit();
mid = int.parse(
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
userInfo = userInfoCache.get('userInfoCache');
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname;
}
Future queryFollowings(type) async {

View File

@ -37,6 +37,12 @@ class _FollowPageState extends State<FollowPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -9,9 +9,10 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs;
bool isLoadingMore = false;
RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs;
@override
void onInit() {
@ -26,9 +27,9 @@ class HistoryController extends GetxController {
max = historyList.last.history!.oid!;
viewAt = historyList.last.viewAt!;
}
isLoadingMore = true;
isLoadingMore.value = true;
var res = await UserHttp.historyList(max, viewAt);
isLoadingMore = false;
isLoadingMore.value = false;
if (res['status']) {
if (type == 'onload') {
historyList.addAll(res['data'].list);

View File

@ -1,7 +1,9 @@
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/index.dart';
import 'widgets/item.dart';
@ -16,25 +18,33 @@ class HistoryPage extends StatefulWidget {
class _HistoryPageState extends State<HistoryPage> {
final HistoryController _historyController = Get.put(HistoryController());
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
_futureBuilderFuture = _historyController.queryHistoryList();
super.initState();
_historyController.scrollController.addListener(
scrollController = _historyController.scrollController;
scrollController.addListener(
() {
if (_historyController.scrollController.position.pixels >=
_historyController.scrollController.position.maxScrollExtent -
300) {
if (!_historyController.isLoadingMore) {
_historyController.onLoad();
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
if (!_historyController.isLoadingMore.value) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_historyController.onLoad();
});
}
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -92,13 +102,8 @@ class _HistoryPageState extends State<HistoryPage> {
Map data = snapshot.data;
if (data['status']) {
return Obx(
() => _historyController.historyList.isEmpty
? const SliverToBoxAdapter(
child: Center(
child: Text('没数据'),
),
)
: SliverList(
() => _historyController.historyList.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return HistoryItem(
@ -108,7 +113,12 @@ class _HistoryPageState extends State<HistoryPage> {
},
childCount:
_historyController.historyList.length),
),
)
: _historyController.isLoadingMore.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(

View File

@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
@ -36,20 +37,23 @@ class HistoryItem extends StatelessWidget {
'pageTitle': videoItem.title
},
);
} else if (videoItem.history.business == 'live' &&
videoItem.liveStatus == 1) {
LiveItemModel liveItem = LiveItemModel.fromJson({
'face': videoItem.authorFace,
'roomid': videoItem.history.oid,
'pic': videoItem.cover,
'title': videoItem.title,
'uname': videoItem.authorName,
'cover': videoItem.cover,
});
Get.toNamed(
'/liveRoom?roomid=${videoItem.history.oid}',
arguments: {'liveItem': liveItem},
);
} else if (videoItem.history.business == 'live') {
if (videoItem.liveStatus == 1) {
LiveItemModel liveItem = LiveItemModel.fromJson({
'face': videoItem.authorFace,
'roomid': videoItem.history.oid,
'pic': videoItem.cover,
'title': videoItem.title,
'uname': videoItem.authorName,
'cover': videoItem.cover,
});
Get.toNamed(
'/liveRoom?roomid=${videoItem.history.oid}',
arguments: {'liveItem': liveItem},
);
} else {
SmartDialog.showToast('直播未开播');
}
} else if (videoItem.badge == '番剧' ||
videoItem.tagName.contains('动画')) {
/// hack
@ -115,7 +119,7 @@ class HistoryItem extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 5, StyleString.cardSpace, 5),
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
@ -232,6 +236,7 @@ class VideoContent extends StatelessWidget {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
Utils.dateFormat(videoItem.viewAt!),
@ -239,7 +244,46 @@ class VideoContent extends StatelessWidget {
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
)
),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
bvid: videoItem.history.bvid);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
],
),
],

View File

@ -11,16 +11,17 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late TabController tabController;
late List tabsCtrList;
late List<Widget> tabsPageList;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
var userInfo;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : '';
// 进行tabs配置
tabs = tabsConfig;
@ -48,7 +49,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
// 更新登录状态
void updateLoginStatus(val) {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = val ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
userFace.value = userInfo != null ? userInfo.face : '';
}
}

View File

@ -6,13 +6,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/mine/view.dart';
import 'package:pilipala/utils/storage.dart';
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
class HomeAppBar extends StatelessWidget {
const HomeAppBar({super.key});
@override
Widget build(BuildContext context) {
var userInfo = userInfoCache.get('userInfoCache');
return SliverAppBar(
// forceElevated: true,
scrolledUnderElevation: 0,
@ -55,7 +56,7 @@ class HomeAppBar extends StatelessWidget {
const SizedBox(width: 6),
/// TODO
if (user.get(UserBoxKey.userLogin)) ...[
if (userInfo != null) ...[
GestureDetector(
onTap: () => showModalBottomSheet(
context: context,
@ -70,7 +71,7 @@ class HomeAppBar extends StatelessWidget {
type: 'avatar',
width: 32,
height: 32,
src: user.get(UserBoxKey.userMid),
src: userInfo.face,
),
),
const SizedBox(width: 10),

View File

@ -23,6 +23,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
final HotController _hotController = Get.put(HotController());
List videoList = [];
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@ -31,7 +32,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
void initState() {
super.initState();
_futureBuilderFuture = _hotController.queryHotFeed('init');
ScrollController scrollController = _hotController.scrollController;
scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
@ -55,6 +56,12 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -126,7 +133,9 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
closeFn: _hotController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}

View File

@ -6,15 +6,20 @@ import 'package:pilipala/models/model_hot_video_item.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = [HotVideoItemModel()].obs;
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0;
RxBool isLoading = false.obs;
Future queryLaterList() async {
isLoading.value = true;
var res = await UserHttp.seeYouLater();
if (res['status']) {
laterList.value = res['data']['list'];
count = res['data']['count'];
if (count > 0) {
laterList.value = res['data']['list'];
}
}
isLoading.value = false;
return res;
}
@ -47,4 +52,34 @@ class LaterController extends GetxController {
},
);
}
// 一键清空
Future toViewClear() 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: const Text('取消')),
TextButton(
onPressed: () async {
var res = await UserHttp.toViewClear();
if (res['status']) {
laterList.clear();
}
SmartDialog.dismiss();
SmartDialog.showToast(res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/later/index.dart';
@ -29,25 +30,38 @@ class _LaterPageState extends State<LaterPage> {
titleSpacing: 0,
centerTitle: false,
title: Obx(
() => Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
),
() => _laterController.laterList.isNotEmpty
? Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
)
: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
Obx(
() => _laterController.laterList.isNotEmpty
? TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
)
: const SizedBox(),
),
Obx(
() => _laterController.laterList.isNotEmpty
? IconButton(
tooltip: '一键清空',
onPressed: () => _laterController.toViewClear(),
icon: Icon(
Icons.clear_all_outlined,
size: 21,
color: Theme.of(context).colorScheme.primary,
),
)
: const SizedBox(),
),
// IconButton(
// tooltip: '一键清空',
// onPressed: () {},
// icon: Icon(
// Icons.clear_all_outlined,
// size: 21,
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
const SizedBox(width: 8),
],
),
@ -61,19 +75,29 @@ class _LaterPageState extends State<LaterPage> {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
source: 'later',
);
}, childCount: _laterController.laterList.length),
),
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
source: 'later',
);
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
fn: () => setState(() {
_futureBuilderFuture = _laterController.queryLaterList();
}),
);
}
} else {
@ -81,7 +105,7 @@ class _LaterPageState extends State<LaterPage> {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
}, childCount: 10),
);
}
},

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -23,22 +24,23 @@ class LivePage extends StatefulWidget {
class _LivePageState extends State<LivePage> {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
ScrollController scrollController = _liveController.scrollController;
scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_liveController.isLoadingMore) {
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
_liveController.isLoadingMore = true;
_liveController.onLoad();
}
});
}
final ScrollDirection direction =
@ -52,6 +54,12 @@ class _LivePageState extends State<LivePage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
@ -113,7 +121,9 @@ class _LivePageState extends State<LivePage> {
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: liveItem),
closeFn: _liveController.popupDialog?.remove,
child: OverlayPop(
videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}

View File

@ -35,11 +35,11 @@ class LiveCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',

View File

@ -74,12 +74,10 @@ class LiveRoomController extends GetxController {
if (value == 0) {
// 设置音量
volumeOff.value = false;
// meeduPlayerController.setVolume(volume);
} else {
// 取消音量
volume = value;
volumeOff.value = true;
// meeduPlayerController.setVolume(0);
}
}
}

View File

@ -2,9 +2,12 @@ import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
@ -49,4 +52,13 @@ class MainController extends GetxController {
].obs;
final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;
@override
void onInit() {
super.onInit();
if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
Utils.checkUpdata();
}
}
}

View File

@ -4,6 +4,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@ -96,6 +97,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
@override
void dispose() async {
await GStrorage.close();
EventBus().off(EventName.loginEvent);
super.dispose();
}

View File

@ -8,7 +8,7 @@ import 'package:pilipala/utils/storage.dart';
class MediaController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
List list = [
{
@ -34,21 +34,23 @@ class MediaController extends GetxController {
'onTap': () => Get.toNamed('/later'),
},
];
var userInfo;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
}
Future<dynamic> queryFavFolder() async {
if (!userLogin.value || GStrorage.user.get(UserBoxKey.userMid) == null) {
if (!userLogin.value || GStrorage.userInfo.get('userInfoCache') == null) {
return {'status': false, 'data': [], 'msg': '未登录'};
}
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 5,
mid: GStrorage.user.get(UserBoxKey.userMid),
mid: GStrorage.userInfo.get('userInfoCache').mid,
);
favFolderData.value = res['data'];
return res;

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget {
@ -16,6 +17,7 @@ class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
late MediaController mediaController;
late Future _futureBuilderFuture;
EventBus eventBus = EventBus();
@override
bool get wantKeepAlive => true;
@ -25,6 +27,12 @@ class _MediaPageState extends State<MediaPage>
super.initState();
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
eventBus.on(EventName.loginEvent, (args) {
mediaController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
}
@override
@ -68,7 +76,7 @@ class _MediaPageState extends State<MediaPage>
),
),
],
Obx(() => mediaController.userLogin.value == true
Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
],
@ -116,7 +124,11 @@ class _MediaPageState extends State<MediaPage>
),
),
trailing: IconButton(
onPressed: () => _futureBuilderFuture,
onPressed: () {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
@ -149,11 +161,25 @@ class _MediaPageState extends State<MediaPage>
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).primaryColor,
color: Theme.of(context)
.colorScheme
.primary,
),
),
));

View File

@ -14,16 +14,18 @@ class MemberController extends GetxController {
Map? userStat;
String? face;
String? heroTag;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
late int ownerMid;
// 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
ownerMid = user.get(UserBoxKey.userMid) ?? -1;
userInfo = userInfoCache.get('userInfoCache');
ownerMid = userInfo != null ? userInfo.mid : -1;
face = Get.arguments['face'] ?? '';
heroTag = Get.arguments['heroTag'] ?? '';
}
@ -57,7 +59,7 @@ class MemberController extends GetxController {
// 关注/取关up
Future actionRelationMod() async {
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}

View File

@ -43,6 +43,12 @@ class _MemberPageState extends State<MemberPage>
);
}
@override
void dispose() {
_extendNestCtr.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -13,9 +13,8 @@ class MineController extends GetxController {
// 用户状态 动态、关注、粉丝
Rx<UserStat> userStat = UserStat().obs;
RxBool userLogin = false.obs;
Box user = GStrorage.user;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
Box setting = GStrorage.setting;
Rx<ThemeType> themeType = ThemeType.system.obs;
@override
@ -24,6 +23,7 @@ class MineController extends GetxController {
if (userInfoCache.get('userInfoCache') != null) {
userInfo.value = userInfoCache.get('userInfoCache');
userLogin.value = true;
}
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
@ -32,9 +32,6 @@ class MineController extends GetxController {
onLogin() async {
if (!userLogin.value) {
/// TODO
Get.back();
await Future.delayed(const Duration(milliseconds: 150));
Get.toNamed(
'/webview',
parameters: {
@ -44,8 +41,8 @@ class MineController extends GetxController {
},
);
} else {
int mid = user.get(UserBoxKey.userMid);
String face = user.get(UserBoxKey.userFace);
int mid = userInfo.value.mid!;
String face = userInfo.value.face!;
Get.toNamed(
'/member?mid=$mid',
arguments: {'face': face},
@ -54,7 +51,7 @@ class MineController extends GetxController {
}
Future queryUserInfo() async {
if (user.get(UserBoxKey.userLogin) == null) {
if (!userLogin.value) {
return {'status': false};
}
var res = await UserHttp.userInfo();
@ -62,18 +59,12 @@ class MineController extends GetxController {
if (res['data'].isLogin) {
userInfo.value = res['data'];
userInfoCache.put('userInfoCache', res['data']);
user.put(UserBoxKey.userName, res['data'].uname);
user.put(UserBoxKey.userFace, res['data'].face);
user.put(UserBoxKey.userMid, res['data'].mid);
user.put(UserBoxKey.userLogin, true);
userLogin.value = true;
// Get.find<MainController>().readuUserFace();
} else {
resetUserInfo();
}
} else {
resetUserInfo();
// SmartDialog.showToast(res['msg']);
}
await queryUserStatOwner();
return res;
@ -90,12 +81,8 @@ class MineController extends GetxController {
Future resetUserInfo() async {
userInfo.value = UserInfoData();
userStat.value = UserStat();
await user.delete(UserBoxKey.userName);
await user.delete(UserBoxKey.userFace);
await user.delete(UserBoxKey.userMid);
await user.delete(UserBoxKey.userLogin);
userInfoCache.delete('userInfoCache');
userLogin.value = false;
// Get.find<MainController>().resetLast();
}
onChangeTheme() {

View File

@ -6,15 +6,38 @@ import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'controller.dart';
class MinePage extends StatelessWidget {
class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
Widget build(BuildContext context) {
final MineController mineController = Get.put(MineController());
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage> {
final MineController mineController = Get.put(MineController());
late Future _futureBuilderFuture;
EventBus eventBus = EventBus();
@override
void initState() {
super.initState();
_futureBuilderFuture = mineController.queryUserInfo();
eventBus.on(EventName.loginEvent, (args) {
mineController.userLogin.value = args['status'];
if (mounted) {
setState(() {
_futureBuilderFuture = mineController.queryUserInfo();
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
@ -61,7 +84,7 @@ class MinePage extends StatelessWidget {
children: [
const SizedBox(height: 10),
FutureBuilder(
future: mineController.queryUserInfo(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@ -138,10 +161,11 @@ class MinePage extends StatelessWidget {
]))
],
),
const SizedBox(height: 5),
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,
@ -150,48 +174,27 @@ class MinePage extends StatelessWidget {
Positioned(
top: 0,
right: 0,
child: SizedBox(
height: 22,
bottom: 0,
child: Container(
color: Theme.of(context).colorScheme.primary,
height: 24,
constraints:
const BoxConstraints(minWidth: 100), // 设置最小宽度为100
width: box.maxWidth *
(1 -
(_mineController
.userInfo.value.levelInfo!.currentExp! /
_mineController
.userInfo.value.levelInfo!.nextExp!)),
(1 - (levelInfo.currentExp! / levelInfo.nextExp!)),
child: Center(
child: Text(
(_mineController
.userInfo.value.levelInfo!.nextExp! -
_mineController
.userInfo.value.levelInfo!.currentExp!)
.toString(),
'${levelInfo.currentExp!}/${levelInfo.nextExp!}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
),
),
),
),
),
],
),
);
},
),
LayoutBuilder(
builder: (context, BoxConstraints box) {
return Container(
width: box.maxWidth,
height: 1,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.onInverseSurface,
),
child: Stack(
children: [
Positioned(
top: 0,
top: 23,
left: 0,
bottom: 0,
child: Container(
@ -202,7 +205,6 @@ class MinePage extends StatelessWidget {
.userInfo.value.levelInfo!.nextExp!),
height: 1,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.primary,
),
),
@ -212,6 +214,36 @@ class MinePage extends StatelessWidget {
);
},
),
// LayoutBuilder(
// builder: (context, BoxConstraints box) {
// return Container(
// width: box.maxWidth,
// height: 1,
// color: Theme.of(context).colorScheme.onInverseSurface,
// child: Stack(
// children: [
// Positioned(
// top: 0,
// left: 0,
// bottom: 0,
// child: Container(
// width: box.maxWidth *
// (_mineController
// .userInfo.value.levelInfo!.currentExp! /
// _mineController
// .userInfo.value.levelInfo!.nextExp!),
// height: 1,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(4),
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
// ),
// ],
// ),
// );
// },
// ),
],
const SizedBox(height: 30),
Padding(

View File

@ -2,11 +2,9 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:get/get.dart';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:share_plus/share_plus.dart';
class PreviewController extends GetxController {
@ -17,7 +15,7 @@ class PreviewController extends GetxController {
bool storage = true;
bool videos = true;
bool photos = true;
bool visiable = true;
String currentImgUrl = '';
@override
void onInit() {
@ -26,6 +24,7 @@ class PreviewController extends GetxController {
initialPage.value = Get.arguments['initialPage']!;
currentPage.value = Get.arguments['initialPage']! + 1;
imgList.value = Get.arguments['imgList'];
currentImgUrl = imgList[initialPage.value];
}
}
@ -39,22 +38,6 @@ class PreviewController extends GetxController {
// final photosInfo = statuses[Permission.photos].toString();
}
// 图片保存
void onSaveImg() async {
var response = await Dio().get(imgList[initialPage.value],
options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 100,
name: "pic_vvex${DateTime.now().toString().split('-').join()}");
if (result != null) {
if (result['isSuccess']) {
// ignore: avoid_print
print('已保存到相册');
}
}
}
// 图片分享
void onShareImg() async {
requestPermission();
@ -62,9 +45,15 @@ class PreviewController extends GetxController {
options: Options(responseType: ResponseType.bytes));
final temp = await getTemporaryDirectory();
String imgName =
"pic_plpl${DateTime.now().toString().split('-').join()}.jpg";
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
var path = '${temp.path}/$imgName';
File(path).writeAsBytesSync(response.data);
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
}
void onChange(int index) {
initialPage.value = index;
currentPage.value = index + 1;
currentImgUrl = imgList[index];
}
}

View File

@ -2,9 +2,12 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/download.dart';
import 'controller.dart';
typedef DoubleClickAnimationListener = void Function();
@ -35,6 +38,56 @@ class _ImagePreviewState extends State<ImagePreview>
duration: const Duration(milliseconds: 250), vsync: this);
}
onOpenMenu() {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
_previewController.onShareImg();
SmartDialog.dismiss();
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Clipboard.setData(
ClipboardData(text: _previewController.currentImgUrl))
.then((value) {
SmartDialog.showToast('已复制到粘贴板');
SmartDialog.dismiss();
}).catchError((err) {
SmartDialog.showNotify(
msg: err.toString(),
notifyType: NotifyType.error,
);
});
},
dense: true,
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
DownloadUtils.downloadImg(_previewController.currentImgUrl);
},
dense: true,
title: const Text('保存到手机', style: TextStyle(fontSize: 14)),
),
],
),
);
},
);
}
@override
void dispose() {
// animationController.dispose();
@ -51,7 +104,7 @@ class _ImagePreviewState extends State<ImagePreview>
primary: false,
toolbarHeight: 0,
backgroundColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle.light,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
body: Stack(
children: [
@ -69,19 +122,14 @@ class _ImagePreviewState extends State<ImagePreview>
tag: _previewController
.imgList[_previewController.initialPage.value],
child: GestureDetector(
onTap: () {
_previewController.visiable = !_previewController.visiable;
setState(() {});
},
onLongPress: () => onOpenMenu(),
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) {
_previewController.initialPage.value = index;
_previewController.currentPage.value = index + 1;
},
onPageChanged: (int index) =>
_previewController.onChange(index),
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
preloadPagesCount: 2,
@ -149,8 +197,10 @@ class _ImagePreviewState extends State<ImagePreview>
children: <Widget>[
SizedBox(
width: 150.0,
child:
LinearProgressIndicator(value: progress),
child: LinearProgressIndicator(
value: progress,
color: Colors.white,
),
),
const SizedBox(height: 10.0),
Text('${((progress ?? 0.0) * 100).toInt()}%'),
@ -179,7 +229,6 @@ class _ImagePreviewState extends State<ImagePreview>
right: 0,
bottom: 0,
child: Container(
// height: 45,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom, top: 20),
decoration: const BoxDecoration(
@ -193,36 +242,18 @@ class _ImagePreviewState extends State<ImagePreview>
tileMode: TileMode.mirror,
),
),
child: Padding(
padding: const EdgeInsets.only(left: 20, right: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text.rich(
child: Obx(
() => Text.rich(
textAlign: TextAlign.center,
TextSpan(
style: const TextStyle(color: Colors.white, fontSize: 15),
children: [
TextSpan(
style: const TextStyle(
color: Colors.white, fontSize: 18),
children: [
TextSpan(
text: _previewController.currentPage
.toString()),
const TextSpan(text: ' / '),
TextSpan(
text: _previewController.imgList.length
.toString()),
]),
),
),
const Spacer(),
ElevatedButton(
onPressed: () => _previewController.onShareImg(),
child: const Text('分享')),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () => _previewController.onSaveImg(),
child: const Text('保存'))
],
text: _previewController.currentPage.toString()),
const TextSpan(text: ' / '),
TextSpan(
text: _previewController.imgList.length.toString()),
]),
),
),
),

View File

@ -8,9 +8,8 @@ import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController();
int _currentPage = 0;
int crossAxisCount = 2;
RxList<RecVideoItemAppModel> videoList = [RecVideoItemAppModel()].obs;
bool isLoadingMore = false;
RxList<RecVideoItemAppModel> videoList = <RecVideoItemAppModel>[].obs;
bool isLoadingMore = true;
OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
@ -29,6 +28,9 @@ class RcmdController extends GetxController {
// 获取推荐
Future queryRcmdFeed(type) async {
if (isLoadingMore == false) {
return;
}
if (type == 'onRefresh') {
_currentPage = 0;
}
@ -37,7 +39,7 @@ class RcmdController extends GetxController {
);
if (res['status']) {
if (type == 'init') {
if (videoList.length > 1) {
if (videoList.isNotEmpty) {
videoList.addAll(res['data']);
} else {
videoList.value = res['data'];
@ -56,6 +58,7 @@ class RcmdController extends GetxController {
// 下拉刷新
Future onRefresh() async {
isLoadingMore = true;
queryRcmdFeed('onRefresh');
}

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -39,10 +41,11 @@ class _RcmdPageState extends State<RcmdPage>
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_rcmdController.isLoadingMore) {
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () {
_rcmdController.isLoadingMore = true;
_rcmdController.onLoad();
}
});
}
final ScrollDirection direction =
@ -56,6 +59,12 @@ class _RcmdPageState extends State<RcmdPage>
);
}
@override
void dispose() {
_rcmdController.scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -74,30 +83,36 @@ class _RcmdPageState extends State<RcmdPage>
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return SliverLayoutBuilder(
builder: (context, boxConstraints) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
});
return Platform.isAndroid || Platform.isIOS
? Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList))
: SliverLayoutBuilder(
builder: (context, boxConstraints) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
});
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
fn: () {
setState(() {
_futureBuilderFuture =
_rcmdController.queryRcmdFeed('init');
});
},
);
}
} else {
// 缓存数据
if (_rcmdController.videoList.length > 1) {
if (_rcmdController.videoList.isNotEmpty) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
@ -118,9 +133,12 @@ class _RcmdPageState extends State<RcmdPage>
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
));
builder: (context) => AnimatedDialog(
closeFn: _rcmdController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _rcmdController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, videoList) {

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/media_bangumi.dart';
@ -29,6 +30,7 @@ class _SearchPanelState extends State<SearchPanel>
bool _isLoadingMore = false;
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@ -43,20 +45,24 @@ class _SearchPanelState extends State<SearchPanel>
),
tag: widget.searchType!.type,
);
ScrollController scrollController = _searchPanelController.scrollController;
scrollController = _searchPanelController.scrollController;
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
if (!_isLoadingMore) {
_isLoadingMore = true;
await _searchPanelController.onSearch(type: 'onLoad');
_isLoadingMore = false;
}
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_searchPanelController.onSearch(type: 'onLoad');
});
}
});
_futureBuilderFuture = _searchPanelController.onSearch();
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);

View File

@ -1,26 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/controller.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class SettingController extends GetxController {
Box user = GStrorage.user;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
Box setting = GStrorage.setting;
// Box userInfoCache = GStrorage.userInfo;
Box localCache = GStrorage.localCache;
RxBool userLogin = false.obs;
RxBool feedBackEnable = false.obs;
RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
feedBackEnable.value =
setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);
picQuality.value =
@ -30,12 +36,48 @@ class SettingController extends GetxController {
}
loginOut() async {
await Request.removeCookie();
await Get.find<MineController>().resetUserInfo();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfoCache.put('userInfoCache', null);
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(false);
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: const Text('点错了'),
),
TextButton(
onPressed: () async {
// 清空cookie
await Request.cookieManager.cookieJar.deleteAll();
Request.dio.options.headers['cookie'] = '';
// 清空本地存储的用户标识
userInfoCache.put('userInfoCache', null);
localCache
.put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''});
// 更改我的页面登录状态
await Get.find<MineController>().resetUserInfo();
// 更改主页登录状态
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(false);
// 事件通知
EventBus eventBus = EventBus();
eventBus.emit(EventName.loginEvent, {'status': false});
SmartDialog.dismiss().then((value) => Get.back());
},
child: const Text('确认'),
)
],
);
},
);
}
// 开启关闭震动反馈

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
class ExtraSetting extends StatefulWidget {
const ExtraSetting({super.key});
@override
State<ExtraSetting> createState() => _ExtraSettingState();
}
class _ExtraSettingState extends State<ExtraSetting> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'其他设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: const [
SetSwitchItem(
title: '检查更新',
subTitle: '每次启动时检查是否需要更新',
setKey: SettingBoxKey.autoUpdate,
defaultVal: false,
),
],
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
@ -19,6 +19,7 @@ class _PlaySettingState extends State<PlaySetting> {
late dynamic defaultAudioQa;
late dynamic defaultDecode;
late int defaultFullScreenMode;
late int defaultBtmProgressBehavior;
@override
void initState() {
@ -31,6 +32,8 @@ class _PlaySettingState extends State<PlaySetting> {
defaultValue: VideoDecodeFormats.values.last.code);
defaultFullScreenMode = setting.get(SettingBoxKey.fullScreenMode,
defaultValue: FullScreenMode.values.first.code);
defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,
defaultValue: BtmProgresBehavior.values.first.code);
}
@override
@ -163,6 +166,31 @@ class _PlaySettingState extends State<PlaySetting> {
],
),
),
ListTile(
dense: false,
title: Text('底部进度条展示', style: titleStyle),
subtitle: Text(
'当前展示方式:${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultBtmProgressBehavior,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultBtmProgressBehavior = item;
setting.put(SettingBoxKey.btmProgressBehavior, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in BtmProgresBehavior.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
],
),
);

View File

@ -13,12 +13,14 @@ class PrivacySetting extends StatefulWidget {
class _PrivacySettingState extends State<PrivacySetting> {
bool userLogin = false;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
var userInfo;
@override
void initState() {
super.initState();
userLogin = user.get(UserBoxKey.userLogin) ?? false;
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
}
@override

View File

@ -34,11 +34,11 @@ class SettingPage extends StatelessWidget {
dense: false,
title: const Text('外观设置'),
),
// ListTile(
// onTap: () {},
// dense: false,
// title: const Text('其他设置'),
// ),
ListTile(
onTap: () => Get.toNamed('/extraSetting'),
dense: false,
title: const Text('其他设置'),
),
Obx(
() => Visibility(
visible: settingController.userLogin.value,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class SetSwitchItem extends StatefulWidget {
final String? title;
@ -61,6 +62,9 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
onChanged: (value) {
val = value;
Setting.put(widget.setKey, value);
if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {
Utils.checkUpdata();
}
setState(() {});
}),
),

View File

@ -38,7 +38,7 @@ class VideoDetailController extends GetxController
/// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa;
late AudioQuality currentAudioQa;
AudioQuality? currentAudioQa;
late VideoDecodeFormats currentDecodeFormats;
// PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
@ -51,7 +51,7 @@ class VideoDetailController extends GetxController
RxBool enableHA = true.obs;
/// 本地存储
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
@ -70,11 +70,13 @@ class VideoDetailController extends GetxController
late Duration defaultST;
// 默认记录历史记录
bool enableHeart = true;
var userInfo;
@override
void onInit() {
super.onInit();
Map argMap = Get.arguments;
userInfo = userInfoCache.get('userInfoCache');
var keys = argMap.keys.toList();
if (keys.isNotEmpty) {
if (keys.contains('videoItem')) {
@ -92,7 +94,7 @@ class VideoDetailController extends GetxController
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
if (user.get(UserBoxKey.userMid) == null ||
if (userInfo == null ||
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
}
@ -140,9 +142,11 @@ class VideoDetailController extends GetxController
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
audioUrl = firstAudio.baseUrl ?? '';
if (currentAudioQa != null) {
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa!.code);
audioUrl = firstAudio.baseUrl ?? '';
}
playerInit();
}
@ -270,6 +274,9 @@ class VideoDetailController extends GetxController
// duration: data.timeLength ?? 0,
// );
} else {
if (result['code'] == -404) {
isShowCover.value = false;
}
SmartDialog.showToast(result['msg'].toString());
}
return result;

View File

@ -30,9 +30,6 @@ class VideoIntroController extends GetxController {
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// 请求返回的信息
String responseMsg = '请求异常';
// up主粉丝数
Map userStat = {'follower': '-'};
@ -42,7 +39,7 @@ class VideoIntroController extends GetxController {
RxBool hasCoin = false.obs;
// 是否收藏
RxBool hasFav = false.obs;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = [];
@ -52,10 +49,12 @@ class VideoIntroController extends GetxController {
int _tempThemeValue = -1;
RxInt lastPlayCid = 0.obs;
var userInfo;
@override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache');
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
@ -77,7 +76,7 @@ class VideoIntroController extends GetxController {
videoItem!['owner'] = args.owner;
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
userLogin = userInfo != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!);
}
@ -94,8 +93,6 @@ class VideoIntroController extends GetxController {
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
// 获取到粉丝数再返回
await queryUserStat();
} else {
responseMsg = result['msg'];
}
if (userLogin) {
// 获取点赞状态
@ -143,7 +140,7 @@ class VideoIntroController extends GetxController {
// 一键三连
Future actionOneThree() async {
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -206,7 +203,7 @@ class VideoIntroController extends GetxController {
// 投币
Future actionCoinVideo() async {
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -302,7 +299,7 @@ class VideoIntroController extends GetxController {
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
mid: userInfo.mid, rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}
@ -327,6 +324,9 @@ class VideoIntroController extends GetxController {
// 查询关注状态
Future queryFollowStatus() async {
if (videoDetail.value.owner == null) {
return;
}
var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!);
if (result['status']) {
followStatus.value = result['data'];
@ -337,7 +337,7 @@ class VideoIntroController extends GetxController {
// 关注/取关up
Future actionRelationMod() async {
feedBack();
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -414,6 +414,7 @@ class VideoIntroController extends GetxController {
videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {}
this.bvid = bvid;
lastPlayCid.value = cid;
await queryVideoIntro();
}
}

View File

@ -71,6 +71,10 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
// 请求错误
return HttpError(
errMsg: snapshot.data['msg'],
btnText: snapshot.data['code'] == -404 ||
snapshot.data['code'] == 62002
? '返回上一页'
: null,
fn: () => Get.back(),
);
}
@ -127,7 +131,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 收藏
showFavBottomSheet() {
if (videoIntroController.user.get(UserBoxKey.userMid) == null) {
if (videoIntroController.userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -270,12 +274,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 合集
if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
cid: widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => videoIntroController
.changeSeasonOrbangu(bvid, cid, aid),
Obx(
() => SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
cid: videoIntroController.lastPlayCid.value != 0
? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => videoIntroController
.changeSeasonOrbangu(bvid, cid, aid),
),
)
],
if (!loadingStatus &&

View File

@ -38,6 +38,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
item.cid,
item.aid,
);
currentIndex = i;
Get.back();
}

View File

@ -6,71 +6,74 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart';
class RelatedVideoPanel extends GetView<ReleatedController> {
class RelatedVideoPanel extends StatefulWidget {
const RelatedVideoPanel({super.key});
@override
State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();
}
class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
final ReleatedController _releatedController =
Get.put(ReleatedController(), tag: Get.arguments['heroTag']);
@override
Widget build(BuildContext context) {
return GetBuilder(
init: ReleatedController(),
id: Get.arguments['heroTag'],
builder: (context) {
return FutureBuilder(
future: ReleatedController().queryRelatedVideo(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data!['status']) {
// 请求成功
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == snapshot.data['data'].length) {
return SizedBox(
height: MediaQuery.of(context).padding.bottom);
} else {
return Material(
child: VideoCardH(
videoItem: snapshot.data['data'][index],
longPress: () {
try {
ReleatedController().popupDialog =
_createPopupDialog(
snapshot.data['data'][index]);
Overlay.of(context)
.insert(ReleatedController().popupDialog!);
} catch (_) {
return {};
}
},
longPressEnd: () {
ReleatedController().popupDialog?.remove();
},
),
);
}
}, childCount: snapshot.data['data'].length + 1));
} else {
// 请求错误
return const Center(
child: Text('出错了'),
);
}
return FutureBuilder(
future: _releatedController.queryRelatedVideo(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data!['status']) {
// 请求成功
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == snapshot.data['data'].length) {
return SizedBox(height: MediaQuery.of(context).padding.bottom);
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
return Material(
child: VideoCardH(
videoItem: snapshot.data['data'][index],
longPress: () {
try {
_releatedController.popupDialog =
_createPopupDialog(snapshot.data['data'][index]);
Overlay.of(context)
.insert(_releatedController.popupDialog!);
} catch (err) {
return {};
}
},
longPressEnd: () {
_releatedController.popupDialog?.remove();
},
),
);
}
},
}, childCount: snapshot.data['data'].length + 1));
} else {
// 请求错误
return const Center(
child: Text('出错了'),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
);
});
}
},
);
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
closeFn: _releatedController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem,
closeFn: _releatedController.popupDialog?.remove),
),
);
}

View File

@ -37,6 +37,9 @@ class VideoReplyController extends GetxController {
if (type == 'init') {
currentPage = 0;
}
if (noMore.value == '没有更多了') {
return;
}
var res = await ReplyHttp.replyList(
oid: aid!,
pageNum: currentPage + 1,

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -33,6 +34,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
late VideoReplyController _videoReplyController;
late AnimationController fabAnimationCtr;
late ScrollController scrollController;
Future? _futureBuilderFuture;
bool _isFabVisible = true;
@ -60,18 +62,18 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
vsync: this, duration: const Duration(milliseconds: 300));
_futureBuilderFuture = _videoReplyController.queryReplyList();
_videoReplyController.scrollController.addListener(
scrollController = _videoReplyController.scrollController;
scrollController.addListener(
() {
if (_videoReplyController.scrollController.position.pixels >=
_videoReplyController.scrollController.position.maxScrollExtent -
300) {
if (!_videoReplyController.isLoadingMore) {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_videoReplyController.onLoad();
}
});
}
final ScrollDirection direction =
_videoReplyController.scrollController.position.userScrollDirection;
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
@ -112,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
void dispose() {
super.dispose();
fabAnimationCtr.dispose();
_videoReplyController.scrollController.dispose();
scrollController.dispose();
}
@override

View File

@ -657,11 +657,13 @@ InlineSpan buildContent(
);
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) {
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
if (str != '') {
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
}
}
return str;
},

View File

@ -38,6 +38,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
Box localCache = GStrorage.localCache;
late double sheetHeight;
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
@ -48,12 +49,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
super.initState();
// 上拉加载更多
_videoReplyReplyController.scrollController.addListener(
scrollController = _videoReplyReplyController.scrollController;
scrollController.addListener(
() {
if (_videoReplyReplyController.scrollController.position.pixels >=
_videoReplyReplyController
.scrollController.position.maxScrollExtent -
300) {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
if (!_videoReplyReplyController.isLoadingMore) {
_videoReplyReplyController.onLoad();
}
@ -69,7 +69,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
@override
void dispose() {
// _videoReplyReplyController.scrollController.dispose();
// scrollController.dispose();
super.dispose();
}

View File

@ -322,7 +322,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
const VideoIntroPanel(),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
const BangumiIntroPanel()
BangumiIntroPanel(
cid: videoDetailController.cid)
],
// if (videoDetailController.videoType ==
// SearchType.video) ...[

View File

@ -120,15 +120,16 @@ class _HeaderControlState extends State<HeaderControl> {
'当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: Text('选择音质', style: titleStyle),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}',
style: subTitleStyle),
),
if (widget.videoDetailCtr!.currentAudioQa != null)
ListTile(
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: Text('选择音质', style: titleStyle),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}',
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
@ -319,7 +320,7 @@ class _HeaderControlState extends State<HeaderControl> {
/// 选择音质
void showSetAudioQa() {
AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa;
AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa!;
List<AudioItem> audio = videoInfo.dash!.audio!;
showModalBottomSheet(

View File

@ -1,46 +1,45 @@
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/cookie.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebviewController extends GetxController {
String url = '';
String type = '';
RxString type = ''.obs;
String pageTitle = '';
final WebViewController controller = WebViewController();
RxInt loadProgress = 0.obs;
RxBool loadShow = true.obs;
EventBus eventBus = EventBus();
@override
void onInit() {
super.onInit();
url = Get.parameters['url']!;
type = Get.parameters['type']!;
type.value = Get.parameters['type']!;
pageTitle = Get.parameters['pageTitle']!;
webviewInit();
if (type == 'login') {
if (type.value == 'login') {
controller.clearCache();
controller.clearLocalStorage();
WebViewCookieManager().clearCookies();
controller.setUserAgent(Request().headerUa('mob'));
}
webviewInit();
}
webviewInit() {
controller
..setUserAgent(Request().headerUa('mob'))
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
@ -51,51 +50,14 @@ class WebviewController extends GetxController {
},
onPageStarted: (String url) {},
// 加载完成
onPageFinished: (String url) async {
onUrlChange: (UrlChange urlChange) async {
loadShow.value = false;
if (type == 'login' &&
String url = urlChange.url ?? '';
if (type.value == 'login' &&
(url.startsWith(
'https://passport.bilibili.com/web/sso/exchange_cookie') ||
url.startsWith('https://m.bilibili.com/'))) {
try {
var cookies =
await WebviewCookieManager().getCookies(HttpString.baseUrl);
var apiCookies = await WebviewCookieManager()
.getCookies(HttpString.baseApiUrl);
var tCookies =
await WebviewCookieManager().getCookies(HttpString.tUrl);
await SetCookie.onSet(cookies, HttpString.baseUrl);
await SetCookie.onSet(apiCookies, HttpString.baseApiUrl);
await SetCookie.onSet(tCookies, HttpString.tUrl);
await UserHttp.userInfo();
var result = await UserHttp.userInfo();
UserHttp.thirdLogin();
print('网页登录: $result');
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
try {
Box user = GStrorage.user;
user.put(UserBoxKey.userLogin, true);
user.put(UserBoxKey.userName, result['data'].uname);
user.put(UserBoxKey.userFace, result['data'].face);
user.put(UserBoxKey.userMid, result['data'].mid);
Box userInfoCache = GStrorage.userInfo;
userInfoCache.put('userInfoCache', result['data']);
Get.find<MineController>().userInfo.value = result['data'];
Get.find<MineController>().onInit();
Get.find<RcmdController>().queryRcmdFeed('onRefresh');
Get.find<DynamicsController>().queryFollowDynamic();
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
} catch (_) {}
Get.back();
}
} catch (e) {
print(e);
}
confirmLogin(url);
}
},
onWebResourceError: (WebResourceError error) {},
@ -109,4 +71,51 @@ class WebviewController extends GetxController {
)
..loadRequest(Uri.parse(url));
}
confirmLogin(url) async {
var content = '';
if (url != null) {
content = '${content + url}; \n';
}
try {
await SetCookie.onSet();
var result = await UserHttp.userInfo();
UserHttp.thirdLogin();
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
try {
Box userInfoCache = GStrorage.userInfo;
await userInfoCache.put('userInfoCache', result['data']);
// 通知更新
eventBus.emit(EventName.loginEvent, {'status': true});
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
} catch (err) {
SmartDialog.show(builder: (context) {
return AlertDialog(
title: const Text('登录遇到问题'),
content: Text(err.toString()),
actions: [
TextButton(
onPressed: () => controller.reload(),
child: const Text('确认'),
)
],
);
});
}
Get.back();
} else {
// 获取用户信息失败
SmartDialog.showToast(result.msg);
Clipboard.setData(ClipboardData(text: result.msg.toString()));
}
} catch (e) {
SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning);
content = content + e.toString();
}
Clipboard.setData(ClipboardData(text: content));
}
}

View File

@ -18,19 +18,25 @@ class _WebviewPageState extends State<WebviewPage> {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
_webviewController.pageTitle,
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
IconButton(
TextButton(
onPressed: () {
_webviewController.controller.reload();
},
icon: const Icon(
Icons.refresh,
size: 22,
),
child: const Text('刷新'),
),
Obx(
() => _webviewController.type.value == 'login'
? TextButton(
onPressed: () => _webviewController.confirmLogin(null),
child: const Text('刷新登录状态'),
)
: const SizedBox(),
),
const SizedBox(width: 10)
],
@ -48,6 +54,14 @@ class _WebviewPageState extends State<WebviewPage> {
),
),
),
if (_webviewController.type.value == 'login')
Container(
width: double.infinity,
color: Theme.of(context).colorScheme.onInverseSurface,
padding: const EdgeInsets.only(
left: 12, right: 12, top: 6, bottom: 6),
child: const Text('登录成功未自动跳转? 请点击右上角「刷新登录状态」'),
),
Expanded(
child: WebViewWidget(controller: _webviewController.controller),
),

View File

@ -4,6 +4,8 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
@ -14,7 +16,6 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:volume_controller/volume_controller.dart';
// import 'package:wakelock_plus/wakelock_plus.dart';
import 'models/data_status.dart';
@ -45,6 +46,7 @@ class PlPlayerController {
// 播放位置
final Rx<Duration> _position = Rx(Duration.zero);
final Rx<Duration> _sliderPosition = Rx(Duration.zero);
// 展示使用
final Rx<Duration> _sliderTempPosition = Rx(Duration.zero);
final Rx<Duration> _duration = Rx(Duration.zero);
final Rx<Duration> _buffered = Rx(Duration.zero);
@ -68,7 +70,7 @@ class PlPlayerController {
final Rx<String> _direction = 'horizontal'.obs;
Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
final Rx<BoxFit> _videoFit = Rx(BoxFit.contain);
///
// ignore: prefer_final_fields
@ -93,13 +95,13 @@ class PlPlayerController {
// final Durations durations;
List<BoxFit> fits = [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
BoxFit.fitHeight,
BoxFit.fitWidth,
BoxFit.scaleDown
List<Map<String, dynamic>> videoFitType = [
{'attr': BoxFit.contain, 'desc': '包含'},
{'attr': BoxFit.cover, 'desc': '覆盖'},
{'attr': BoxFit.fill, 'desc': '填充'},
{'attr': BoxFit.fitHeight, 'desc': '高度适应'},
{'attr': BoxFit.fitWidth, 'desc': '宽度适应'},
{'attr': BoxFit.scaleDown, 'desc': '缩小适应'},
];
PreferredSizeWidget? headerControl;
@ -449,7 +451,7 @@ class PlPlayerController {
}
/// 跳转至指定位置
Future<void> seekTo(Duration position) async {
Future<void> seekTo(Duration position, {type = 'seek'}) async {
// if (position >= duration.value) {
// position = duration.value - const Duration(milliseconds: 100);
// }
@ -458,7 +460,10 @@ class PlPlayerController {
}
_position.value = position;
if (duration.value.inSeconds != 0) {
await _videoPlayerController!.stream.buffer.first;
if (type != 'slider') {
/// 拖动进度条调节时,不等待第一帧,防止抖动
await _videoPlayerController!.stream.buffer.first;
}
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
// play();
@ -568,7 +573,7 @@ class PlPlayerController {
Future<void> getCurrentVolume() async {
// mac try...catch
try {
_currentVolume.value = await VolumeController().getVolume();
_currentVolume.value = (await FlutterVolumeController.getVolume())!;
} catch (_) {}
}
@ -585,7 +590,8 @@ class PlPlayerController {
volume.value = volumeNew;
try {
VolumeController().setVolume(volumeNew, showSystemUI: false);
FlutterVolumeController.showSystemUI = false;
await FlutterVolumeController.setVolume(volumeNew);
} catch (err) {
print(err);
}
@ -631,10 +637,17 @@ class PlPlayerController {
void toggleVideoFit() {
videoFitChangedTimer?.cancel();
videoFitChanged.value = true;
if (fits.indexOf(_videoFit.value) < fits.length - 1) {
_videoFit.value = fits[fits.indexOf(_videoFit.value) + 1];
// 范围内
List attrs = videoFitType.map((e) => e['attr']).toList();
if (attrs.indexOf(_videoFit.value) < attrs.length - 1) {
int index = attrs.indexOf(_videoFit.value);
_videoFit.value = attrs[index + 1];
print(videoFitType[index + 1]['desc']);
SmartDialog.showToast(videoFitType[index + 1]['desc']);
} else {
_videoFit.value = fits[0];
// 默认 contain
_videoFit.value = videoFitType.first['attr'];
SmartDialog.showToast(videoFitType.first['desc']);
}
videoFitChangedTimer = Timer(const Duration(seconds: 1), () {
videoFitChangedTimer = null;
@ -655,9 +668,10 @@ class PlPlayerController {
/// 读取fit
Future<void> getVideoFit() async {
String fitValue = videoStorage.get(VideoBoxKey.videoBrightness,
defaultValue: 'fitHeight');
_videoFit.value = fits.firstWhere((element) => element.name == fitValue);
String fitValue =
videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain');
_videoFit.value = videoFitType
.firstWhere((element) => element['attr'] == fitValue)['attr'];
}
/// 缓存亮度

View File

@ -7,6 +7,8 @@ export './models/play_status.dart';
export './models/data_status.dart';
export './widgets/common_btn.dart';
export './models/play_speed.dart';
export './models/fullscreen_mode.dart';
export './models/bottom_progress_behavior.dart';
export './widgets/app_bar_ani.dart';
export './utils/fullscreen.dart';
export './utils.dart';

View File

@ -0,0 +1,23 @@
// ignore: camel_case_types
enum BtmProgresBehavior {
alwaysShow,
alwaysHide,
onlyShowFullScreen,
}
extension BtmProgresBehaviorDesc on BtmProgresBehavior {
String get description => ['始终展示', '始终隐藏', '仅全屏时展示'][index];
}
extension BtmProgresBehaviorCode on BtmProgresBehavior {
static final List<int> _codeList = [0, 1, 2];
int get code => _codeList[index];
static BtmProgresBehavior? fromCode(int code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return BtmProgresBehavior.values[index];
}
return null;
}
}

View File

@ -1,14 +1,34 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
//横屏
/// 低版本xcode不支持auto_orientation
Future<void> landScape() async {
if (Platform.isAndroid || Platform.isIOS) {
await AutoOrientation.landscapeAutoMode(forceSensor: true);
dynamic document;
try {
if (kIsWeb) {
await document.documentElement?.requestFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
);
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.EnterNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
@ -27,14 +47,24 @@ Future<void> enterFullScreen() async {
//退出全屏显示
Future<void> exitFullScreen() async {
late SystemUiMode mode;
if ((Platform.isAndroid &&
(await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) ||
!Platform.isAndroid) {
mode = SystemUiMode.edgeToEdge;
} else {
mode = SystemUiMode.manual;
dynamic document;
try {
if (kIsWeb) {
document.exitFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
await SystemChrome.setPreferredOrientations([]);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.ExitNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
await SystemChrome.setEnabledSystemUIMode(mode,
overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
}

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@ -16,8 +17,8 @@ import 'package:pilipala/plugin/pl_player/utils.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:volume_controller/volume_controller.dart';
import 'models/bottom_progress_behavior.dart';
import 'utils/fullscreen.dart';
import 'widgets/app_bar_ani.dart';
import 'widgets/backward_seek.dart';
@ -67,6 +68,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Box setting = GStrorage.setting;
late FullScreenMode mode;
late int defaultBtmProgressBehavior;
void onDoubleTapSeekBackward() {
setState(() {
@ -87,12 +89,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior,
defaultValue: BtmProgresBehavior.values.first.code);
Future.microtask(() async {
try {
VolumeController().showSystemUI = false;
_volumeValue = await VolumeController().getVolume();
VolumeController().listener((value) {
FlutterVolumeController.showSystemUI = true;
_volumeValue = (await FlutterVolumeController.getVolume())!;
FlutterVolumeController.addListener((value) {
if (mounted && !_volumeInterceptEventStream) {
setState(() {
_volumeValue = value;
@ -118,7 +122,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future<void> setVolume(double value) async {
try {
VolumeController().setVolume(value);
FlutterVolumeController.showSystemUI = false;
await FlutterVolumeController.setVolume(value);
} catch (_) {}
setState(() {
_volumeValue = value;
@ -177,7 +182,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 进入全屏
await enterFullScreen();
// 横屏
await landScape();
await verticalScreen();
break;
case FullScreenMode.horizontal:
@ -202,7 +207,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
systemOverlayStyle: SystemUiOverlayStyle.light,
),
body: SafeArea(
bottom: true,
bottom: false,
child: PLVideoPlayer(
controller: _,
headerControl: _.headerControl,
@ -228,6 +233,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
@override
void dispose() {
animationController.dispose();
FlutterVolumeController.removeListener();
super.dispose();
}
@ -252,13 +258,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
clipBehavior: Clip.hardEdge,
fit: StackFit.passthrough,
children: [
Video(
controller: videoController,
controls: NoVideoControls,
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
textAlign: TextAlign.center,
padding: const EdgeInsets.all(24.0),
Obx(
() => Video(
controller: videoController,
controls: NoVideoControls,
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
textAlign: TextAlign.center,
padding: const EdgeInsets.all(24.0),
),
fit: _.videoFit.value,
),
),
@ -311,38 +320,40 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
curve: Curves.easeInOut,
opacity: _.isSliderMoving.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
width: 100.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
return Text(
_.sliderTempPosition.value.inMinutes >= 60
? printDurationWithHours(
_.sliderTempPosition.value)
: printDuration(_.sliderTempPosition.value),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.duration.value.inMinutes >= 60
? printDurationWithHours(_.duration.value)
: printDuration(_.duration.value),
style: textStyle,
child: IntrinsicWidth(
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0),
),
height: 34.0,
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
return Text(
_.sliderTempPosition.value.inMinutes >= 60
? printDurationWithHours(
_.sliderTempPosition.value)
: printDuration(_.sliderTempPosition.value),
style: textStyle,
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const SizedBox(width: 2),
Obx(
() => Text(
_.duration.value.inMinutes >= 60
? printDurationWithHours(_.duration.value)
: printDuration(_.duration.value),
style: textStyle,
),
),
),
],
],
),
),
),
),
@ -538,7 +549,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
return;
}
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value);
_.seekTo(_.sliderPosition.value, type: 'slider');
},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) async {
@ -619,6 +630,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
final int value = _.sliderPosition.value.inSeconds;
final int max = _.duration.value.inSeconds;
final int buffer = _.buffered.value.inSeconds;
if (defaultBtmProgressBehavior ==
BtmProgresBehavior.alwaysHide.code) {
return Container();
}
if (defaultBtmProgressBehavior ==
BtmProgresBehavior.onlyShowFullScreen.code &&
!_.isFullScreen.value) {
return Container();
}
if (value > max || max <= 0) {
return Container();
}
@ -694,9 +714,19 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Obx(() {
if (_.dataStatus.loading || _.isBuffering.value) {
return Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
child: Container(
padding: const EdgeInsets.all(30),
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
center: Alignment.center,
colors: [Colors.black26, Colors.transparent],
),
),
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
),
);
} else {

View File

@ -44,29 +44,33 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
if (value > max || max <= 0) {
return Container();
}
return ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: colorTheme.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.0,
thumbRadius: 5.5,
onDragStart: (duration) {
feedBack();
_.onChangedSliderStart();
},
onDragUpdate: (duration) {
_.onUodatedSliderProgress(duration.timeStamp);
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(Duration(seconds: duration.inSeconds));
},
return Padding(
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5),
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: colorTheme.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.0,
thumbRadius: 5.5,
onDragStart: (duration) {
feedBack();
_.onChangedSliderStart();
},
onDragUpdate: (duration) {
_.onUodatedSliderProgress(duration.timeStamp);
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(Duration(seconds: duration.inSeconds),
type: 'slider');
},
),
);
},
),
@ -130,15 +134,15 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
// ),
// ),
// ),
// ComBtn(
// icon: const Icon(
// Icons.fit_screen_sharp,
// size: 18,
// color: Colors.white,
// ),
// fuc: () => _.toggleVideoFit(),
// ),
// const SizedBox(width: 4),
ComBtn(
icon: const Icon(
Icons.settings_overscan_outlined,
size: 18,
color: Colors.white,
),
fuc: () => _.toggleVideoFit(),
),
const SizedBox(width: 4),
// 全屏
Obx(
() => ComBtn(

View File

@ -16,6 +16,7 @@ import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart';
import 'package:pilipala/pages/setting/extra_setting.dart';
import 'package:pilipala/pages/setting/play_setting.dart';
import 'package:pilipala/pages/setting/privacy_setting.dart';
import 'package:pilipala/pages/setting/style_setting.dart';
@ -80,7 +81,8 @@ class Routes {
GetPage(name: '/styleSetting', page: () => const StyleSetting()),
// 隐私设置
GetPage(name: '/privacySetting', page: () => const PrivacySetting()),
// 其他设置
GetPage(name: '/extraSetting', page: () => const ExtraSetting()),
//
GetPage(name: '/blackListPage', page: () => const BlackListPage()),
// 关于

View File

@ -1,27 +1,22 @@
import 'package:cookie_jar/cookie_jar.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
class SetCookie {
static onSet(List cookiesList, String url) async {
// domain url
List<Cookie> jarCookies = [];
if (cookiesList.isNotEmpty) {
for (var i in cookiesList) {
Cookie jarCookie = Cookie(i.name, i.value);
jarCookies.add(jarCookie);
}
}
String cookiePath = await Utils.getCookiePath();
PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(cookiePath),
);
await cookieJar.saveFromResponse(Uri.parse(url), jarCookies);
// 重新设置 cookie
Request.setCookie();
Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(url), jarCookies);
return true;
static onSet() async {
var cookies = await WebviewCookieManager().getCookies(HttpString.baseUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), cookies);
var cookieString =
cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
Request.dio.options.headers['cookie'] = cookieString;
cookies = await WebviewCookieManager().getCookies(HttpString.baseApiUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), cookies);
cookies = await WebviewCookieManager().getCookies(HttpString.tUrl);
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.tUrl), cookies);
}
}

View File

@ -10,8 +10,8 @@ class Data {
static Future historyStatus() async {
Box localCache = GStrorage.localCache;
Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) == null) {
Box userInfoCache = GStrorage.userInfo;
if (userInfoCache.get('userInfoCache') == null) {
return;
}
var res = await UserHttp.historyStatus();

39
lib/utils/download.dart Normal file
View File

@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:dio/dio.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';
class DownloadUtils {
// 获取存储权限
static requestStoragePer() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
Permission.photos,
].request();
statuses[Permission.storage].toString();
}
static Future<bool> downloadImg(String imgUrl) async {
await requestStoragePer();
SmartDialog.showLoading(msg: '保存中');
var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
String picName =
"plpl_cover_${DateTime.now().toString().split('-').join()}.png";
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 100,
name: picName,
);
SmartDialog.dismiss();
if (result != null) {
if (result['isSuccess']) {
// ignore: avoid_print
await SmartDialog.showToast('$picName」已保存 ');
}
}
return true;
}
}

53
lib/utils/event_bus.dart Normal file
View File

@ -0,0 +1,53 @@
// 订阅者回调签名
typedef void EventCallback(arg);
class EventBus {
// 私有构造函数
EventBus._internal();
// 保存单例
static final EventBus _singleton = EventBus._internal();
// 工厂构造函数
factory EventBus() => _singleton;
// 保存事件订阅者队列key:事件名(id)value: 对应事件的订阅者队列
final _emap = <dynamic, List<EventCallback>>{};
// 添加订阅者
void on(eventName, EventCallback f) {
_emap[eventName] ??= <EventCallback>[];
_emap[eventName]!.add(f);
}
// 移除订阅者
void off(eventName, [EventCallback? f]) {
var list = _emap[eventName];
if (eventName == null || list == null) return;
if (f == null) {
_emap[eventName] = [];
} else {
list.remove(f);
}
}
// 触发事件,事件触发后该事件所有订阅者会被调用
void emit(eventName, [arg]) {
var list = _emap[eventName];
if (list == null) return;
List<EventCallback> tempList = List.from(list);
for (var callback in tempList) {
callback(arg);
}
}
// 获取订阅者数量
int getSubscriberCount(eventName) {
var list = _emap[eventName];
return list?.length ?? 0;
}
}
class EventName {
static const String loginEvent = 'loginEvent';
}

View File

@ -7,7 +7,6 @@ import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart';
class GStrorage {
static late final Box user;
static late final Box recVideo;
static late final Box userInfo;
static late final Box hotKeyword;
@ -21,17 +20,20 @@ class GStrorage {
final path = dir.path;
await Hive.initFlutter('$path/hive');
regAdapter();
// 用户信息
user = await Hive.openBox('user');
// 首页推荐视频
recVideo = await Hive.openBox(
'recVideo',
compactionStrategy: (entries, deletedEntries) {
return deletedEntries > 20;
return deletedEntries > 12;
},
);
// 登录用户信息
userInfo = await Hive.openBox('userInfo');
userInfo = await Hive.openBox(
'userInfo',
compactionStrategy: (entries, deletedEntries) {
return deletedEntries > 2;
},
);
// 本地缓存
localCache = await Hive.openBox('localCache');
// 设置
@ -70,8 +72,8 @@ class GStrorage {
}
static Future<void> close() async {
user.compact();
user.close();
// user.compact();
// user.close();
recVideo.compact();
recVideo.close();
userInfo.compact();
@ -89,19 +91,6 @@ class GStrorage {
}
}
// 约定 key
class UserBoxKey {
static const String userName = 'userName';
// 头像
static const String userFace = 'userFace';
// mid
static const String userMid = 'userMid';
// 登录状态
static const String userLogin = 'userLogin';
// 凭证
static const String accessKey = 'accessKey';
}
class SettingBoxKey {
static const String themeMode = 'themeMode';
static const String feedBackEnable = 'feedBackEnable';
@ -119,11 +108,19 @@ class SettingBoxKey {
static const String fullScreenMode = 'fullScreenMode';
static const String blackMidsList = 'blackMidsList';
static const String autoUpdate = 'autoUpdate';
static const String btmProgressBehavior = 'btmProgressBehavior';
}
class LocalCacheKey {
// 历史记录暂停状态 默认false 记录
static const String historyPause = 'historyPause';
// access_key
static const String accessKey = 'accessKey';
//
static const String wbiKeys = 'wbiKeys';
static const String timeStamp = 'timeStamp';
}
class VideoBoxKey {

View File

@ -4,8 +4,14 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get_utils/get_utils.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:url_launcher/url_launcher.dart';
class Utils {
static Future<String> getCookiePath() async {
@ -194,4 +200,55 @@ class Utils {
}
return false;
}
// 检查更新
static Future<bool> checkUpdata() async {
SmartDialog.dismiss();
var currentInfo = await PackageInfo.fromPlatform();
var result = await Request().get(Api.latestApp);
LatestDataModel data = LatestDataModel.fromJson(result.data);
bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!);
if (isUpdate) {
SmartDialog.show(
builder: (context) {
return AlertDialog(
title: const Text('🎉 发现新版本 '),
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.tagName!,
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 8),
Text(data.body!),
],
),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'稍后',
style:
TextStyle(color: Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () async {
await SmartDialog.dismiss();
launchUrl(
Uri.parse(
'https://github.com/guozhigq/pilipala/releases'),
mode: LaunchMode.externalApplication,
);
},
child: const Text('去下载')),
],
);
},
);
}
return true;
}
}

View File

@ -109,8 +109,10 @@ class WbiSign {
// 获取最新的 img_key 和 sub_key 可以从缓存中获取
static Future<Map<String, dynamic>> getWbiKeys() async {
DateTime nowDate = DateTime.now();
if (localCache.get('wbiKeys') != null &&
DateTime.fromMillisecondsSinceEpoch(localCache.get('timeStamp')).day ==
if (localCache.get(LocalCacheKey.wbiKeys) != null &&
DateTime.fromMillisecondsSinceEpoch(
localCache.get(LocalCacheKey.timeStamp))
.day ==
nowDate.day) {
Map cacheWbiKeys = localCache.get('wbiKeys');
return Map<String, dynamic>.from(cacheWbiKeys);
@ -129,8 +131,8 @@ class WbiSign {
.substring(subUrl.lastIndexOf('/') + 1, subUrl.length)
.split('.')[0]
};
localCache.put('wbiKeys', wbiKeys);
localCache.put('timeStamp', nowDate.millisecondsSinceEpoch);
localCache.put(LocalCacheKey.wbiKeys, wbiKeys);
localCache.put(LocalCacheKey.timeStamp, nowDate.millisecondsSinceEpoch);
return wbiKeys;
}

Some files were not shown because too many files have changed in this diff Show More