merge main

This commit is contained in:
guozhigq
2023-11-12 12:02:31 +08:00
90 changed files with 2582 additions and 429 deletions

View File

@@ -39,9 +39,13 @@
android:label="PiliPala" android:label="PiliPala"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"> xmlns:tools="http://schemas.android.com/tools"
android:enableOnBackInvokedCallback="true"
android:allowBackup="false"
android:fullBackupContent="false"
tools:replace="android:allowBackup">
<activity <activity
android:name=".MainActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
@@ -222,6 +226,24 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
@@ -234,6 +256,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- <!--
Media access permissions. Media access permissions.
Android 13 or higher. Android 13 or higher.

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
</vector>

View File

@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
</vector>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/*" />

BIN
assets/images/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

26
change_log/1.0.11.1112.md Normal file
View File

@@ -0,0 +1,26 @@
## 1.0.11
### 新功能
+ 适配了原生媒体通知栏 @Daydreamer-riri
+ 视频主题图标 @Daydreamer-riri
+ 关闭软件后自动画中画播放
+ UP主分组管理
+ md2样式底栏
+
### 修复
+ 历史记录记忆播放
+ 部分类型视频连播
+ 播放速度选择框不支持返回手势
+ 播放速度选择框不支持返回手势
+ 视频播放速度总是显示1.0X
+ 评论页面计数错误
+ 退出视频还有声音
### 优化
+ 视频加载速度
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,8 +1,6 @@
PODS: PODS:
- appscheme (1.0.4): - appscheme (1.0.4):
- Flutter - Flutter
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
@@ -14,6 +12,10 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.8):
- Flutter
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0): - media_kit_native_event_loop (1.0.0):
@@ -54,11 +56,11 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`) - appscheme (from `.symlinks/plugins/appscheme/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/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`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@@ -80,13 +82,12 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- GT3Captcha-iOS
- ReachabilitySwift - ReachabilitySwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: appscheme:
:path: ".symlinks/plugins/appscheme/ios" :path: ".symlinks/plugins/appscheme/ios"
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@@ -95,6 +96,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_volume_controller: flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios" :path: ".symlinks/plugins/flutter_volume_controller/ios"
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop: media_kit_native_event_loop:
@@ -132,12 +135,13 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

View File

@@ -140,6 +140,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */, 5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -268,6 +269,23 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

View File

@@ -103,5 +103,9 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -70,36 +70,44 @@ class VideoCardV extends StatelessWidget {
break; break;
// 动态 // 动态
case 'picture': case 'picture':
String dynamicType = 'picture'; try {
String uri = videoItem.uri; String dynamicType = 'picture';
if (videoItem.uri.contains('bilibili://article/')) { String uri = videoItem.uri;
dynamicType = 'article'; String id = '';
RegExp regex = RegExp(r'\d+'); if (videoItem.uri.startsWith('bilibili://article/')) {
Match match = regex.firstMatch(videoItem.uri)!; // https://www.bilibili.com/read/cv27063554
String matchedNumber = match.group(0)!; dynamicType = 'read';
videoItem.param = 'cv' + matchedNumber; RegExp regex = RegExp(r'\d+');
} Match match = regex.firstMatch(videoItem.uri)!;
if (uri.startsWith('http')) { String matchedNumber = match.group(0)!;
String path = Uri.parse(uri).path; videoItem.param = int.parse(matchedNumber);
if (isStringNumeric(path.split('/')[1])) { id = 'cv${videoItem.param}';
// 请求接口
var res = await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
if (res['status']) {
Get.toNamed('/dynamicDetail', arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail'
});
}
return;
} }
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
if (isStringNumeric(path.split('/')[1])) {
// 请求接口
var res =
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
if (res['status']) {
Get.toNamed('/dynamicDetail', arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail'
});
}
return;
}
}
Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title,
'id': id,
'dynamicType': dynamicType
});
} catch (err) {
SmartDialog.showToast(err.toString());
} }
Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title,
'id': videoItem.param.toString(),
'dynamicType': dynamicType
});
break; break;
default: default:
SmartDialog.showToast(videoItem.goto); SmartDialog.showToast(videoItem.goto);

View File

@@ -360,4 +360,49 @@ class Api {
// id=849312409672744983 // id=849312409672744983
// features=itemOpusStyle // features=itemOpusStyle
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail'; static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
// AI总结
/// https://api.bilibili.com/x/web-interface/view/conclusion/get?
/// bvid=BV1ju4y1s7kn&
/// cid=1296086601&
/// up_mid=4641697&
/// w_rid=1607c6c5a4a35a1297e31992220900ae&
/// wts=1697033079
static const String aiConclusion = '/x/web-interface/view/conclusion/get';
// captcha验证码
static const String getCaptcha =
'https://passport.bilibili.com/x/passport-login/captcha?source=main_web';
// web端短信验证码
static const String smsCode =
'https://passport.bilibili.com/x/passport-login/web/sms/send';
// web端验证码登录
// web端密码登录
// app端短信验证码
static const String appSmsCode =
'https://passport.bilibili.com/x/passport-login/sms/send';
// app端验证码登录
// 获取短信验证码
// static const String appSafeSmsCode =
// 'https://passport.bilibili.com/x/safecenter/common/sms/send';
/// app端密码登录
/// username
/// password
/// key
/// rhash
static const String loginInByPwdApi =
'https://passport.bilibili.com/x/passport-login/oauth2/login';
/// 密码加密密钥
/// disable_rcmd
/// local_id
static const getWebKey =
'https://passport.bilibili.com/x/passport-login/web/key';
} }

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/io.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@@ -17,6 +18,11 @@ class Request {
static late CookieManager cookieManager; static late CookieManager cookieManager;
static late final Dio dio; static late final Dio dio;
factory Request() => _instance; factory Request() => _instance;
Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
/// 设置cookie /// 设置cookie
static setCookie() async { static setCookie() async {
@@ -41,8 +47,8 @@ class Request {
log("setCookie, ${e.toString()}"); log("setCookie, ${e.toString()}");
} }
} }
setOptionsHeaders(userInfo);
} }
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
if (cookie.isEmpty) { if (cookie.isEmpty) {
try { try {
@@ -67,8 +73,10 @@ class Request {
return token; return token;
} }
static setOptionsHeaders(userInfo) { static setOptionsHeaders(userInfo, status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
}
dio.options.headers['env'] = 'prod'; dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64'; dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
@@ -92,6 +100,13 @@ class Request {
headers: {}, headers: {},
); );
enableSystemProxy =
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
systemProxyHost =
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
systemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
dio = Dio(options) dio = Dio(options)
/// fix 第三方登录 302重定向 跟iOS代理问题冲突 /// fix 第三方登录 302重定向 跟iOS代理问题冲突
@@ -100,6 +115,29 @@ class Request {
idleTimeout: const Duration(milliseconds: 10000), idleTimeout: const Duration(milliseconds: 10000),
onClientCreate: (_, config) => config.onBadCertificate = (_) => true, onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
), ),
)
/// 设置代理
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
// Config the client.
client.findProxy = (uri) {
if (enableSystemProxy) {
print('🌹:$systemProxyHost');
print('🌹:$systemProxyPort');
// return 'PROXY host:port';
return 'PROXY $systemProxyHost:$systemProxyPort';
} else {
// 不设置代理
return 'DIRECT';
}
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
},
); );
//添加拦截器 //添加拦截器

177
lib/http/login.dart Normal file
View File

@@ -0,0 +1,177 @@
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/login/index.dart';
import 'package:pilipala/utils/login.dart';
import 'package:uuid/uuid.dart';
class LoginHttp {
static Future queryCaptcha() async {
var res = await Request().get(Api.getCaptcha);
if (res.data['code'] == 0) {
return {
'status': true,
'data': CaptchaDataModel.fromJson(res.data['data']),
};
} else {
return {'status': false, 'data': res.message};
}
}
static Future sendSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
var res = await Request().post(
Api.appSmsCode,
data: {
'cid': cid,
'tel': tel,
"source": "main_web",
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
// headers: {'user-agent': ApiConstants.userAgent}
),
);
print(res);
}
// web端验证码
static Future sendWebSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map data = {
'cid': cid,
'tel': tel,
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.smsCode,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
}
// web端验证码登录
static Future loginInByWebSmsCode() async {}
// web端密码登录
static Future liginInByWebPwd() async {}
// app端验证码
static Future sendAppSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map<String, dynamic> data = {
'cid': cid,
'tel': tel,
'login_session_id': const Uuid().v4().replaceAll('-', ''),
'recaptcha_token': token,
'gee_challenge': challenge,
'gee_validate': validate,
'gee_seccode': seccode,
'channel': 'bili',
'buvid': buvid(),
'local_id': buvid(),
// 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'statistics': {
"appId": 1,
"platform": 3,
"version": "7.52.0",
"abtest": ""
},
};
// FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.appSmsCode,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
}
static String buvid() {
var mac = <String>[];
var random = Random();
for (var i = 0; i < 6; i++) {
var min = 0;
var max = 0xff;
var num = (random.nextInt(max - min + 1) + min).toRadixString(16);
mac.add(num);
}
var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();
var md5Arr = md5Str.split('');
return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';
}
// 获取盐hash跟PubKey
static Future getWebKey() async {
var res = await Request().get(Api.getWebKey,
data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': {}, 'msg': res.data['message']};
}
}
// app端密码登录
static Future loginInByMobPwd({
required String tel,
required String password,
required String key,
required String rhash,
}) async {
dynamic publicKey = RSAKeyParser().parse(key);
String passwordEncryptyed =
Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64;
Map<String, dynamic> data = {
'username': tel,
'password': passwordEncryptyed,
'local_id': LoginUtils.generateBuvid(),
'disable_rcmd': "0",
};
var res = await Request().post(
Api.loginInByPwdApi,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
}
}

View File

@@ -39,16 +39,25 @@ class SearchHttp {
static Future searchSuggest({required term}) async { static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest, var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term}); data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) { if (res.data is String) {
if (res.data['result'] is Map) { Map<String, dynamic> resultMap = json.decode(res.data);
res.data['result']['term'] = term; if (resultMap['code'] == 0) {
if (resultMap['result'] is Map) {
resultMap['result']['term'] = term;
}
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
} }
return {
'status': true,
'data': res.data['result'] is Map
? SearchSuggestModel.fromJson(res.data['result'])
: [],
};
} else { } else {
return { return {
'status': false, 'status': false,

View File

@@ -9,9 +9,11 @@ import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/model_rec_video_item.dart'; import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/wbi_sign.dart';
/// res.data['code'] == 0 请求正常返回结果 /// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果 /// res.data['data'] 为结果
@@ -420,4 +422,23 @@ class VideoHttp {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} }
} }
static Future aiConclusion({
String? bvid,
int? cid,
int? upMid,
}) async {
Map params = await WbiSign().makSign({
'bvid': bvid,
'cid': cid,
'up_mid': upMid,
});
var res = await Request().get(Api.aiConclusion, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': AiConclusionModel.fromJson(res.data['data']),
};
}
}
} }

View File

@@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart'; import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@@ -28,6 +29,7 @@ void main() async {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async { .then((_) async {
await GStrorage.init(); await GStrorage.init();
await setupServiceLocator();
runApp(const MyApp()); runApp(const MyApp());
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

View File

@@ -0,0 +1,49 @@
class CaptchaDataModel {
CaptchaDataModel({
this.type,
this.token,
this.geetest,
this.tencent,
this.validate,
this.seccode,
});
String? type;
String? token;
GeetestData? geetest;
Tencent? tencent;
String? validate;
String? seccode;
CaptchaDataModel.fromJson(Map<String, dynamic> json) {
type = json["type"];
token = json["token"];
geetest =
json["geetest"] != null ? GeetestData.fromJson(json["geetest"]) : null;
tencent =
json["tencent"] != null ? Tencent.fromJson(json["tencent"]) : null;
}
}
class GeetestData {
GeetestData({
this.challenge,
this.gt,
});
String? challenge;
String? gt;
GeetestData.fromJson(Map<String, dynamic> json) {
challenge = json["challenge"];
gt = json["gt"];
}
}
class Tencent {
Tencent({this.appid});
String? appid;
Tencent.fromJson(Map<String, dynamic> json) {
appid = json["appid"];
}
}

80
lib/models/video/ai.dart Normal file
View File

@@ -0,0 +1,80 @@
class AiConclusionModel {
AiConclusionModel({
this.code,
this.modelResult,
this.stid,
this.status,
this.likeNum,
this.dislikeNum,
});
int? code;
ModelResult? modelResult;
String? stid;
int? status;
int? likeNum;
int? dislikeNum;
AiConclusionModel.fromJson(Map<String, dynamic> json) {
code = json['code'];
modelResult = ModelResult.fromJson(json['model_result']);
stid = json['stid'];
status = json['status'];
likeNum = json['like_num'];
dislikeNum = json['dislike_num'];
}
}
class ModelResult {
ModelResult({
this.resultType,
this.summary,
this.outline,
});
int? resultType;
String? summary;
List<OutlineItem>? outline;
ModelResult.fromJson(Map<String, dynamic> json) {
resultType = json['result_type'];
summary = json['summary'];
outline = json['result_type'] == 2
? json['outline']
.map<OutlineItem>((e) => OutlineItem.fromJson(e))
.toList()
: <OutlineItem>[];
}
}
class OutlineItem {
OutlineItem({
this.title,
this.partOutline,
});
String? title;
List<PartOutline>? partOutline;
OutlineItem.fromJson(Map<String, dynamic> json) {
title = json['title'];
partOutline = json['part_outline']
.map<PartOutline>((e) => PartOutline.fromJson(e))
.toList();
}
}
class PartOutline {
PartOutline({
this.timestamp,
this.content,
});
int? timestamp;
String? content;
PartOutline.fromJson(Map<String, dynamic> json) {
timestamp = json['timestamp'];
content = json['content'];
}
}

View File

@@ -201,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
}, },
), ),
), ),
const LoadingMore() LoadingMore()
], ],
), ),
); );

View File

@@ -10,6 +10,8 @@ class PlDanmakuController {
// 按 6min 分段 // 按 6min 分段
int segCount = 0; int segCount = 0;
List<DmSegMobileReply> dmSegList = []; List<DmSegMobileReply> dmSegList = [];
// 已请求的段落标记
List<int> hasrequestSeg = [];
int currentSegIndex = 1; int currentSegIndex = 1;
int currentDmIndex = 0; int currentDmIndex = 0;

View File

@@ -95,7 +95,9 @@ class _PlDanmakuState extends State<PlDanmaku> {
// 根据position判断是否有已缓存弹幕。没有则请求对应段 // 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil(); int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex; segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty) { if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1)) {
ctr.hasrequestSeg.add(segIndex - 1);
ctr.currentSegIndex = segIndex; ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () { EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
ctr.queryDanmaku(); ctr.queryDanmaku();

View File

@@ -17,6 +17,10 @@ class AuthorPanel extends StatelessWidget {
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// 番剧
if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
return;
}
feedBack(); feedBack();
Get.toNamed( Get.toNamed(
'/member?mid=${item.modules.moduleAuthor.mid}', '/member?mid=${item.modules.moduleAuthor.mid}',

View File

@@ -1,17 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class FollowItem extends StatelessWidget { class FollowItem extends StatelessWidget {
final FollowItemModel item; final FollowItemModel item;
const FollowItem({super.key, required this.item}); final FollowController? ctr;
const FollowItem({super.key, required this.item, this.ctr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item!.mid); String heroTag = Utils.makeHeroTag(item.mid);
return ListTile( return ListTile(
onTap: () { onTap: () {
feedBack(); feedBack();
@@ -39,7 +43,29 @@ class FollowItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
trailing: const SizedBox(width: 6), trailing: ctr!.isOwner.value
? SizedBox(
height: 34,
child: TextButton(
onPressed: () async {
await Get.bottomSheet(
GroupPanel(mid: item.mid!),
isScrollControlled: true,
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
backgroundColor:
Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色
),
child: const Text(
'已关注',
style: TextStyle(fontSize: 12),
),
),
)
: const SizedBox(),
); );
} }
} }

View File

@@ -84,7 +84,10 @@ class _FollowListState extends State<FollowList> {
), ),
); );
} else { } else {
return FollowItem(item: list[index]); return FollowItem(
item: list[index],
ctr: widget.ctr,
);
} }
}, },
) )

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -89,6 +91,7 @@ class _OwnerFollowListState extends State<OwnerFollowList>
return Obx( return Obx(
() => followList.isNotEmpty () => followList.isNotEmpty
? ListView.builder( ? ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,
itemCount: followList.length + 1, itemCount: followList.length + 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -101,7 +104,10 @@ class _OwnerFollowListState extends State<OwnerFollowList>
MediaQuery.of(context).padding.bottom), MediaQuery.of(context).padding.bottom),
); );
} else { } else {
return FollowItem(item: followList[index]); return FollowItem(
item: followList[index],
ctr: widget.ctr,
);
} }
}, },
) )

View File

@@ -10,6 +10,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/live_item.dart'; import 'widgets/live_item.dart';
@@ -118,7 +119,7 @@ class _LivePageState extends State<LivePage>
}, },
), ),
), ),
const LoadingMore() LoadingMore(ctr: _liveController)
], ],
), ),
), ),
@@ -180,24 +181,3 @@ class _LivePageState extends State<LivePage>
); );
} }
} }
class LoadingMore extends StatelessWidget {
const LoadingMore({super.key});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).padding.bottom + 80,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Text(
'加载中...',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
),
),
);
}
}

View File

@@ -0,0 +1,204 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:pilipala/models/login/index.dart';
class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>();
final GlobalKey passwordFormKey = GlobalKey<FormState>();
final GlobalKey msgCodeFormKey = GlobalKey<FormState>();
final TextEditingController mobTextController = TextEditingController();
final TextEditingController passwordTextController = TextEditingController();
final TextEditingController msgCodeTextController = TextEditingController();
final FocusNode mobTextFieldNode = FocusNode();
final FocusNode passwordTextFieldNode = FocusNode();
final FocusNode msgCodeTextFieldNode = FocusNode();
final PageController pageViewController = PageController();
RxInt currentIndex = 0.obs;
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
// 默认密码登录
RxInt loginType = 0.obs;
// 监听pageView切换
void onPageChange(int index) {
currentIndex.value = index;
}
// 输入手机号 下一页
void nextStep() async {
if ((mobFormKey.currentState as FormState).validate()) {
await pageViewController.animateToPage(
1,
duration: const Duration(microseconds: 3000),
curve: Curves.easeInOut,
);
passwordTextFieldNode.requestFocus();
}
}
// 上一页
void previousPage() async {
passwordTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
pageViewController.animateToPage(
0,
duration: const Duration(microseconds: 300),
curve: Curves.easeInOut,
);
}
// 切换登录方式
void changeLoginType() {
loginType.value = loginType.value == 0 ? 1 : 0;
if (loginType.value == 0) {
passwordTextFieldNode.requestFocus();
} else {
msgCodeTextFieldNode.requestFocus();
}
}
// app端密码登录
void loginInByAppPassword() async {
if ((passwordFormKey.currentState as FormState).validate()) {
var webKeyRes = await LoginHttp.getWebKey();
if (webKeyRes['status']) {
String rhash = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
LoginHttp.loginInByMobPwd(
tel: mobTextController.text,
password: passwordTextController.text,
key: key,
rhash: rhash,
);
} else {
SmartDialog.showToast(webKeyRes['msg']);
}
}
}
// 验证码登录
void loginInByCode() {
if ((msgCodeFormKey.currentState as FormState).validate()) {}
}
// app端验证码
void getMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendAppSmsCode(
cid: 86,
tel: 13734077064,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
print(res);
});
}
// 申请极验验证码
Future getCaptcha(oncall) async {
SmartDialog.showLoading(msg: '请求中...');
var result = await LoginHttp.queryCaptcha();
if (result['status']) {
CaptchaDataModel captchaData = result['data'];
var registerData = Gt3RegisterData(
challenge: captchaData.geetest!.challenge,
gt: captchaData.geetest!.gt!,
success: true,
);
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
SmartDialog.dismiss();
}, onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证');
}, onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message");
String code = message["code"];
if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功');
captchaData.validate = message['result']['geetest_validate'];
captchaData.seccode = message['result']['geetest_seccode'];
captchaData.geetest!.challenge =
message['result']['geetest_challenge'];
oncall(captchaData);
} else {
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
debugPrint("Captcha result code : $code");
}
}, onError: (Map<String, dynamic> message) async {
String code = message["code"];
// 处理验证中返回的错误 Handling errors returned in verification
if (Platform.isAndroid) {
// Android 平台
if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else if (code == "201") {
// 网络无法访问 Network inaccessible
} else if (code == "202") {
// Json 解析错误 Analysis error
} else if (code == "204") {
// WebView 加载超时,请检查是否混淆极验 SDK Load timed out
} else if (code == "204_1") {
// WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
} else if (code == "204_2") {
// WebView 加载 SSLError
} else if (code == "206") {
// gettype 接口错误或返回为 null API error or return null
} else if (code == "207") {
// getphp 接口错误或返回为 null API error or return null
} else if (code == "208") {
// ajax 接口错误或返回为 null API error or return null
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/android
}
}
if (Platform.isIOS) {
// iOS 平台
if (code == "-1009") {
// 网络无法访问 Network inaccessible
} else if (code == "-1004") {
// 无法查找到 HOST Unable to find HOST
} else if (code == "-1002") {
// 非法的 URL Illegal URL
} else if (code == "-1001") {
// 网络超时 Network timeout
} else if (code == "-999") {
// 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
} else if (code == "-21") {
// 使用了重复的 challenge Duplicate challenges are used
// 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
} else if (code == "-20") {
// 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
} else if (code == "-10") {
// 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
} else if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/ios
}
}
});
captcha.startCaptcha(registerData);
} else {}
}
}

View File

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

362
lib/pages/login/view.dart Normal file
View File

@@ -0,0 +1,362 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController());
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Obx(
() => _loginPageCtr.currentIndex.value == 0
? IconButton(
onPressed: () async {
_loginPageCtr.mobTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
Get.back();
},
icon: const Icon(Icons.close_outlined),
)
: IconButton(
onPressed: () => _loginPageCtr.previousPage(),
icon: const Icon(Icons.arrow_back),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _loginPageCtr.pageViewController,
onPageChanged: (int index) => _loginPageCtr.onPageChange(index),
children: [
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
),
child: Form(
key: _loginPageCtr.mobFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text(
'登录',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
Row(
children: [
Text(
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
GestureDetector(
onTap: () {},
child: const Icon(Icons.info_outline, size: 16),
)
],
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.mobTextController,
focusNode: _loginPageCtr.mobTextFieldNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入手机号码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
},
onSaved: (val) {
print(val);
},
onEditingComplete: () {
_loginPageCtr.nextStep();
},
),
),
GestureDetector(
onTap: () {
Get.offNamed(
'/webview',
parameters: {
'url':
'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Text(
'使用网页端登录',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(onPressed: () {}, child: const Text('中国大陆')),
TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.nextStep(),
child: const Text('下一步'),
)
],
),
],
),
),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
),
child: Obx(
() => _loginPageCtr.loginType.value == 0
? Form(
key: _loginPageCtr.passwordFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'密码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入您的 BiliBili 密码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.passwordTextController,
focusNode: _loginPageCtr.passwordTextFieldNode,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
isDense: true,
labelText: '输入密码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "密码不能为空";
},
onSaved: (val) {
print(val);
},
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () =>
_loginPageCtr.loginInByAppPassword(),
child: const Text('确认登录'),
)
],
),
],
),
)
: Form(
key: _loginPageCtr.msgCodeFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'验证码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入收到到验证码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: Stack(
children: [
TextFormField(
controller:
_loginPageCtr.msgCodeTextController,
focusNode: _loginPageCtr.msgCodeTextFieldNode,
maxLength: 6,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入验证码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty
? null
: "验证码不能为空";
},
onSaved: (val) {
print(val);
},
),
Positioned(
right: 8,
top: 4,
child: Center(
child: TextButton(
onPressed: () =>
_loginPageCtr.getMsgCode(),
child: const Text('获取验证码'),
),
),
),
],
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.loginInByCode(),
child: const Text('确认登录'),
)
],
),
],
),
),
),
),
],
),
);
}
}

View File

@@ -29,6 +29,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
late Animation<double>? _slideAnimation; late Animation<double>? _slideAnimation;
int selectedIndex = 0; int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间 int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting;
late bool enableMYBar;
@override @override
void initState() { void initState() {
@@ -45,6 +47,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
Tween(begin: 0.8, end: 1.0).animate(_animationController!); Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch; _lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex); _pageController = PageController(initialPage: selectedIndex);
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
} }
void setIndex(int value) async { void setIndex(int value) async {
@@ -144,21 +147,38 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide( return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized, curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1), offset: Offset(0, snapshot.data ? 0 : 1),
child: NavigationBar( child: enableMYBar
onDestinationSelected: (value) => setIndex(value), ? NavigationBar(
selectedIndex: selectedIndex, onDestinationSelected: (value) => setIndex(value),
destinations: <Widget>[ selectedIndex: selectedIndex,
..._mainController.navigationBars.map((e) { destinations: <Widget>[
return NavigationDestination( ..._mainController.navigationBars.map((e) {
icon: e['icon'], return NavigationDestination(
selectedIcon: e['selectIcon'], icon: e['icon'],
label: e['label'], selectedIcon: e['selectIcon'],
); label: e['label'],
}).toList(), );
], }).toList(),
), ],
)
: BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: e['icon'],
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
); );
}, },
), ),

View File

@@ -6,6 +6,7 @@ class MemberDynamicPanelController extends GetxController {
int? mid; int? mid;
String offset = ''; String offset = '';
int count = 0; int count = 0;
bool hasMore = true;
@override @override
void onInit() { void onInit() {
@@ -14,12 +15,16 @@ class MemberDynamicPanelController extends GetxController {
} }
Future getMemberDynamic() async { Future getMemberDynamic() async {
if (!hasMore) {
return {'status': false};
}
var res = await MemberHttp.memberDynamic( var res = await MemberHttp.memberDynamic(
offset: offset, offset: offset,
mid: mid, mid: mid,
); );
if (res['status']) { if (res['status']) {
offset = res['data'].offset; offset = res['data'].offset;
hasMore = res['data'].hasMore;
} }
return res; return res;
} }

View File

@@ -139,11 +139,14 @@ class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
if (res['status']) { if (res['status']) {
addAll(res['data'].items); addAll(res['data'].items);
} }
if (res['data'].hasMore) { try {
isSuccess = true; if (res['data'].hasMore) {
} else { isSuccess = true;
isSuccess = false; } else {
} isSuccess = false;
}
} catch (_) {}
return isSuccess; return isSuccess;
} }
} }

View File

@@ -41,6 +41,7 @@ class MineController extends GetxController {
'pageTitle': '登录bilibili', 'pageTitle': '登录bilibili',
}, },
); );
// Get.toNamed('/loginPage');
} else { } else {
int mid = userInfo.value.mid!; int mid = userInfo.value.mid!;
String face = userInfo.value.face!; String face = userInfo.value.face!;

View File

@@ -125,7 +125,7 @@ class _RcmdPageState extends State<RcmdPage>
}, },
), ),
), ),
const LoadingMore() LoadingMore(ctr: _rcmdController)
], ],
), ),
), ),
@@ -191,7 +191,8 @@ class _RcmdPageState extends State<RcmdPage>
} }
class LoadingMore extends StatelessWidget { class LoadingMore extends StatelessWidget {
const LoadingMore({super.key}); dynamic ctr;
LoadingMore({super.key, this.ctr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -199,11 +200,18 @@ class LoadingMore extends StatelessWidget {
child: Container( child: Container(
height: MediaQuery.of(context).padding.bottom + 80, height: MediaQuery.of(context).padding.bottom + 80,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Center( child: GestureDetector(
child: Text( onTap: () {
'加载中...', if (ctr != null) {
style: TextStyle( ctr!.onLoad();
color: Theme.of(context).colorScheme.outline, fontSize: 13), }
},
child: Center(
child: Text(
'加载更多 👇',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
), ),
), ),
), ),

View File

@@ -117,6 +117,13 @@ class SSearchController extends GetxController {
submit(); submit();
} }
onLongSelect(word) {
int index = historyList.indexOf(word);
historyList.value = historyList.removeAt(index);
historyList.refresh();
histiryWord.put('cacheList', historyList);
}
onClearHis() { onClearHis() {
historyList.value = []; historyList.value = [];
historyCacheList = []; historyCacheList = [];

View File

@@ -299,20 +299,24 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
), ),
// if (_searchController.historyList.isNotEmpty) // if (_searchController.historyList.isNotEmpty)
Wrap( Obx(() => Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
direction: Axis.horizontal, direction: Axis.horizontal,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: [ children: [
for (int i = 0; i < _searchController.historyList.length; i++) for (int i = 0;
SearchText( i < _searchController.historyList.length;
searchText: _searchController.historyList[i], i++)
searchTextIdx: i, SearchText(
onSelect: (value) => _searchController.onSelect(value), searchText: _searchController.historyList[i],
) searchTextIdx: i,
], onSelect: (value) => _searchController.onSelect(value),
), onLongSelect: (value) =>
_searchController.onLongSelect(value),
)
],
)),
], ],
), ),
), ),

View File

@@ -4,8 +4,14 @@ class SearchText extends StatelessWidget {
final String? searchText; final String? searchText;
final Function? onSelect; final Function? onSelect;
final int? searchTextIdx; final int? searchTextIdx;
const SearchText( final Function? onLongSelect;
{super.key, this.searchText, this.onSelect, this.searchTextIdx}); const SearchText({
super.key,
this.searchText,
this.onSelect,
this.searchTextIdx,
this.onLongSelect,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -18,6 +24,9 @@ class SearchText extends StatelessWidget {
onTap: () { onTap: () {
onSelect!(searchText); onSelect!(searchText);
}, },
onLongPress: () {
onLongSelect!(searchText);
},
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
child: Padding( child: Padding(
padding: padding:

View File

@@ -16,8 +16,12 @@ class ExtraSetting extends StatefulWidget {
class _ExtraSettingState extends State<ExtraSetting> { class _ExtraSettingState extends State<ExtraSetting> {
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic defaultReplySort; late dynamic defaultReplySort;
late dynamic defaultDynamicType; late dynamic defaultDynamicType;
late dynamic enableSystemProxy;
late String defaultSystemProxyHost;
late String defaultSystemProxyPort;
@override @override
void initState() { void initState() {
@@ -28,6 +32,86 @@ class _ExtraSettingState extends State<ExtraSetting> {
// 优先展示全部动态 all // 优先展示全部动态 all
defaultDynamicType = defaultDynamicType =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0); setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
enableSystemProxy =
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
defaultSystemProxyHost =
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
defaultSystemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
}
// 设置代理
void twoFADialog() {
var systemProxyHost = '';
var systemProxyPort = '';
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('设置代理'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 6),
TextField(
decoration: InputDecoration(
isDense: true,
labelText: defaultSystemProxyHost != ''
? defaultSystemProxyHost
: '请输入Host使用 . 分割',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
hintText: defaultSystemProxyHost,
),
onChanged: (e) {
systemProxyHost = e;
},
),
const SizedBox(height: 10),
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: defaultSystemProxyPort != ''
? defaultSystemProxyPort
: '请输入Port',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
hintText: defaultSystemProxyPort,
),
onChanged: (e) {
systemProxyPort = e;
},
),
],
),
actions: [
TextButton(
onPressed: () async {
SmartDialog.dismiss();
},
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
localCache.put(LocalCacheKey.systemProxyHost, systemProxyHost);
localCache.put(LocalCacheKey.systemProxyPort, systemProxyPort);
SmartDialog.dismiss();
// Request.dio;
},
child: const Text('确认'),
)
],
);
},
);
} }
@override @override
@@ -135,6 +219,33 @@ class _ExtraSettingState extends State<ExtraSetting> {
], ],
), ),
), ),
ListTile(
enableFeedback: true,
onTap: () => twoFADialog(),
title: Text('设置代理', style: titleStyle),
subtitle: Text('设置代理 host:port', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: enableSystemProxy,
onChanged: (val) {
setting.put(
SettingBoxKey.enableSystemProxy, !enableSystemProxy);
setState(() {
enableSystemProxy = !enableSystemProxy;
});
},
),
),
),
const SetSwitchItem( const SetSwitchItem(
title: '检查更新', title: '检查更新',
subTitle: '每次启动时检查是否需要更新', subTitle: '每次启动时检查是否需要更新',

View File

@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@@ -37,6 +38,14 @@ class _PlaySettingState extends State<PlaySetting> {
defaultValue: BtmProgresBehavior.values.first.code); defaultValue: BtmProgresBehavior.values.first.code);
} }
@override
void dispose() {
super.dispose();
// 重新验证媒体通知后台播放设置
videoPlayerServiceHandler.revalidateSetting();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
@@ -67,6 +76,12 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.p1080, setKey: SettingBoxKey.p1080,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: 'CDN优化',
subTitle: '使用优质CDN线路',
setKey: SettingBoxKey.enableCDN,
defaultVal: true,
),
const SetSwitchItem( const SetSwitchItem(
title: '自动播放', title: '自动播放',
subTitle: '进入详情页自动播放', subTitle: '进入详情页自动播放',
@@ -79,6 +94,12 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.enableBackgroundPlay, setKey: SettingBoxKey.enableBackgroundPlay,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: '自动PiP播放',
subTitle: 'app切换至后台时画中画播放',
setKey: SettingBoxKey.autoPiP,
defaultVal: false,
),
const SetSwitchItem( const SetSwitchItem(
title: '自动全屏', title: '自动全屏',
subTitle: '视频开始播放时进入全屏', subTitle: '视频开始播放时进入全屏',

View File

@@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
@@ -78,6 +77,12 @@ class _StyleSettingState extends State<StyleSetting> {
setKey: SettingBoxKey.iosTransition, setKey: SettingBoxKey.iosTransition,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: 'MD3样式底栏',
subTitle: '符合Material You设计规范的底栏',
setKey: SettingBoxKey.enableMYBar,
defaultVal: true,
),
// SetSwitchItem( // SetSwitchItem(
// title: '首页单列', // title: '首页单列',
// subTitle: '每行展示一个内容卡片', // subTitle: '每行展示一个内容卡片',

View File

@@ -16,6 +16,7 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@@ -83,6 +84,11 @@ class VideoDetailController extends GetxController
Floating? floating; Floating? floating;
late PreferredSizeWidget headerControl; late PreferredSizeWidget headerControl;
late bool enableCDN;
late int? cacheVideoQa;
late String cacheDecode;
late int cacheAudioQa;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -120,6 +126,15 @@ class VideoDetailController extends GetxController
videoDetailCtr: this, videoDetailCtr: this,
floating: floating, floating: floating,
); );
// CDN优化
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
// 预设的画质
cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa);
// 预设的解码格式
cacheDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code);
} }
showReplyReplyPanel() { showReplyReplyPanel() {
@@ -231,22 +246,19 @@ class VideoDetailController extends GetxController
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];
List<VideoItem> allVideosList = data.dash!.video!; List<VideoItem> allVideosList = data.dash!.video!;
try { try {
// 当前可播放的最高质量视频 // 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code; int currentHighVideoQa = allVideosList.first.quality!.code;
// 使用预设的画质 当前可用的最高质量 // 预设的画质为null当前可用的最高质量
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa, cacheVideoQa ??= currentHighVideoQa;
defaultValue: currentHighVideoQa);
int resVideoQa = currentHighVideoQa; int resVideoQa = currentHighVideoQa;
if (cacheVideoQa <= currentHighVideoQa) { if (cacheVideoQa! <= currentHighVideoQa) {
// 如果预设的画质低于当前最高 // 如果预设的画质低于当前最高
List<int> numbers = data.acceptQuality! List<int> numbers = data.acceptQuality!
.where((e) => e <= currentHighVideoQa) .where((e) => e <= currentHighVideoQa)
.toList(); .toList();
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers); resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers);
} }
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!; currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
@@ -260,9 +272,7 @@ class VideoDetailController extends GetxController
List supportDecodeFormats = List supportDecodeFormats =
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!; supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
// 默认从设置中取AVC // 默认从设置中取AVC
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!;
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
try { try {
// 当前视频没有对应格式返回第一个 // 当前视频没有对应格式返回第一个
bool flag = false; bool flag = false;
@@ -285,7 +295,9 @@ class VideoDetailController extends GetxController
} catch (_) { } catch (_) {
firstVideo = videosList.first; firstVideo = videosList.first;
} }
videoUrl = firstVideo.baseUrl!; videoUrl = enableCDN
? VideoUtils.getCdnUrl(firstVideo)
: (firstVideo.backupUrl ?? firstVideo.baseUrl!);
} catch (err) { } catch (err) {
SmartDialog.showToast('firstVideo error: $err'); SmartDialog.showToast('firstVideo error: $err');
} }
@@ -295,8 +307,6 @@ class VideoDetailController extends GetxController
List<AudioItem> audiosList = data.dash!.audio!; List<AudioItem> audiosList = data.dash!.audio!;
try { try {
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code);
if (data.dash!.dolby?.audio?.isNotEmpty == true) { if (data.dash!.dolby?.audio?.isNotEmpty == true) {
// 杜比 // 杜比
audiosList.insert(0, data.dash!.dolby!.audio!.first); audiosList.insert(0, data.dash!.dolby!.audio!.first);
@@ -309,9 +319,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
List<int> numbers = audiosList.map((map) => map.id!).toList(); List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers); int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
if (!numbers.contains(resultAudioQa) && if (!numbers.contains(cacheAudioQa) &&
numbers.any((e) => e > resultAudioQa)) { numbers.any((e) => e > cacheAudioQa)) {
closestNumber = 30280; closestNumber = 30280;
} }
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
@@ -323,7 +333,9 @@ class VideoDetailController extends GetxController
SmartDialog.showToast('firstAudio error: $err'); SmartDialog.showToast('firstAudio error: $err');
} }
audioUrl = firstAudio.baseUrl ?? ''; audioUrl = enableCDN
? VideoUtils.getCdnUrl(firstAudio)
: (firstAudio.backupUrl ?? firstAudio.baseUrl!);
// //
if (firstAudio.id != null) { if (firstAudio.id != null) {
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;

View File

@@ -8,6 +8,7 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
@@ -61,12 +62,16 @@ class VideoIntroController extends GetxController {
RxString total = '1'.obs; RxString total = '1'.obs;
Timer? timer; Timer? timer;
bool isPaused = false; bool isPaused = false;
String heroTag = Get.arguments['heroTag']; String heroTag = '';
late ModelResult modelResult;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
try {
heroTag = Get.arguments['heroTag'];
} catch (_) {}
if (Get.arguments.isNotEmpty) { if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) { if (Get.arguments.containsKey('videoItem')) {
preRender = true; preRender = true;
@@ -509,19 +514,7 @@ class VideoIntroController extends GetxController {
/// 列表循环或者顺序播放时,自动播放下一个 /// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() { void nextPlay() {
late List episodes; late List episodes;
// if (videoDetail.value.ugcSeason != null) { bool isPages = false;
// UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
// List<SectionItem> sections = ugcSeason.sections!;
// for (int i = 0; i < sections.length; i++) {
// List<EpisodeItem> episodesList = sections[i].episodes!;
// for (int j = 0; j < episodesList.length; j++) {
// if (episodesList[j].cid == lastPlayCid.value) {
// episodes = episodesList;
// continue;
// }
// }
// }
// }
if (videoDetail.value.ugcSeason != null) { if (videoDetail.value.ugcSeason != null) {
UgcSeason ugcSeason = videoDetail.value.ugcSeason!; UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
List<SectionItem> sections = ugcSeason.sections!; List<SectionItem> sections = ugcSeason.sections!;
@@ -531,6 +524,11 @@ class VideoIntroController extends GetxController {
List<EpisodeItem> episodesList = sections[i].episodes!; List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList); episodes.addAll(episodesList);
} }
} else if (videoDetail.value.pages != null) {
isPages = true;
List<Part> pages = videoDetail.value.pages!;
episodes = [];
episodes.addAll(pages);
} }
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value); int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
@@ -549,9 +547,9 @@ class VideoIntroController extends GetxController {
} }
} }
int cid = episodes[nextIndex].cid!; int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!; String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
int aid = episodes[nextIndex].aid!; int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid); changeSeasonOrbangu(rBvid, cid, rAid);
} }
// 设置关注分组 // 设置关注分组
@@ -561,4 +559,25 @@ class VideoIntroController extends GetxController {
isScrollControlled: true, isScrollControlled: true,
); );
} }
// ai总结
Future aiConclusion() async {
SmartDialog.showLoading(msg: '正在生产ai总结');
var res = await VideoHttp.aiConclusion(
bvid: bvid,
cid: lastPlayCid.value,
upMid: videoDetail.value.owner!.mid!,
);
if (res['status']) {
if (res['data'].modelResult.resultType == 0) {
SmartDialog.showToast('该视频不支持ai总结');
}
if (res['data'].modelResult.resultType == 2 ||
res['data'].modelResult.resultType == 1) {
modelResult = res['data'].modelResult;
}
}
SmartDialog.dismiss();
return res;
}
} }

View File

@@ -11,6 +11,8 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/introduction/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@@ -226,6 +228,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
arguments: {'face': face, 'heroTag': memberHeroTag}); arguments: {'face': face, 'heroTag': memberHeroTag});
} }
// ai总结
showAiBottomSheet() {
showBottomSheet(
context: context,
enableDrag: true,
builder: (BuildContext context) {
return AiDetail(modelResult: videoIntroController.modelResult);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData t = Theme.of(context); ThemeData t = Theme.of(context);
@@ -238,70 +251,91 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
!loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)),
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
child: Row( child: Text(
children: [ !loadingStatus
StatView( ? widget.videoDetail!.title
theme: 'gray', : videoItem['title'],
view: !widget.loadingStatus style: const TextStyle(
? widget.videoDetail!.stat!.view fontSize: 18,
: videoItem['stat'].view, fontWeight: FontWeight.bold,
size: 'medium', ),
), maxLines: 2,
const SizedBox(width: 10), overflow: TextOverflow.ellipsis,
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal)
Obx(
() => Text(
'${videoIntroController.total.value}人在看',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
],
), ),
), ),
const SizedBox(height: 7), Stack(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Padding(
padding: const EdgeInsets.only(top: 7, bottom: 6),
child: Row(
children: [
StatView(
theme: 'gray',
view: !widget.loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal)
Obx(
() => Text(
'${videoIntroController.total.value}人在看',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
],
),
),
),
Positioned(
right: 10,
top: 6,
child: GestureDetector(
onTap: () async {
var res = await videoIntroController.aiConclusion();
if (res['status']) {
if (res['data'].modelResult.resultType == 2 ||
res['data'].modelResult.resultType == 1) {
showAiBottomSheet();
}
}
},
child:
Image.asset('assets/images/ai.png', height: 22),
),
)
],
),
// 点赞收藏转发 布局样式1 // 点赞收藏转发 布局样式1
// SingleChildScrollView( // SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7), // padding: const EdgeInsets.only(top: 7, bottom: 7),

View File

@@ -92,11 +92,11 @@ class VideoReplyController extends GetxController {
} }
} }
replies.insertAll(0, res['data'].topReplies); replies.insertAll(0, res['data'].topReplies);
count.value = res['data'].page.count;
replyList.value = replies; replyList.value = replies;
} else { } else {
replyList.addAll(replies); replyList.addAll(replies);
} }
count.value = res['data'].page.count;
} }
isLoadingMore = false; isLoadingMore = false;
return res; return res;

View File

@@ -669,58 +669,70 @@ InlineSpan buildContent(
String matchUrl = matchMember; String matchUrl = matchMember;
if (content.jumpUrl.isNotEmpty && hasMatchMember) { if (content.jumpUrl.isNotEmpty && hasMatchMember) {
List urlKeys = content.jumpUrl.keys.toList().reversed.toList(); List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
for (var index = 0; index < urlKeys.length; index++) {
var i = urlKeys[index];
if (i.contains('?')) {
urlKeys[index] = i.replaceAll('?', '\\?');
}
}
matchUrl = matchMember.splitMapJoin( matchUrl = matchMember.splitMapJoin(
/// RegExp.escape() 转义特殊字符 /// RegExp.escape() 转义特殊字符
RegExp(urlKeys.map((key) => key).join("|")), RegExp(urlKeys.map((key) => key).join("|")),
// RegExp(RegExp.escape(urlKeys.join("|"))), // RegExp('What does the fox say\\?'),
onMatch: (Match match) { onMatch: (Match match) {
String matchStr = match[0]!; String matchStr = match[0]!;
String appUrlSchema = content.jumpUrl[matchStr]['app_url_schema']; String appUrlSchema = '';
if (content.jumpUrl[matchStr] != null) {
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
}
// 默认不显示关键词 // 默认不显示关键词
bool enableWordRe = bool enableWordRe =
setting.get(SettingBoxKey.enableWordRe, defaultValue: false); setting.get(SettingBoxKey.enableWordRe, defaultValue: false);
spanChilds.add( if (content.jumpUrl[matchStr] != null) {
TextSpan( spanChilds.add(
text: content.jumpUrl[matchStr]['title'], TextSpan(
style: TextStyle( text: content.jumpUrl[matchStr]['title'],
color: enableWordRe style: TextStyle(
? Theme.of(context).colorScheme.primary color: enableWordRe
: null, ? Theme.of(context).colorScheme.primary
), : null,
recognizer: TapGestureRecognizer() ),
..onTap = () { recognizer: TapGestureRecognizer()
if (appUrlSchema == '') { ..onTap = () {
String str = Uri.parse(matchStr).pathSegments[0]; if (appUrlSchema == '') {
Map matchRes = IdUtils.matchAvorBv(input: str); String str = Uri.parse(matchStr).pathSegments[0];
List matchKeys = matchRes.keys.toList(); Map matchRes = IdUtils.matchAvorBv(input: str);
if (matchKeys.isNotEmpty) { List matchKeys = matchRes.keys.toList();
if (matchKeys.first == 'BV') { if (matchKeys.isNotEmpty) {
if (matchKeys.first == 'BV') {
Get.toNamed(
'/searchResult',
parameters: {'keyword': matchRes['BV']},
);
}
} else {
Get.toNamed( Get.toNamed(
'/searchResult', '/webview',
parameters: {'keyword': matchRes['BV']}, parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
); );
} }
} else { } else {
Get.toNamed( if (appUrlSchema.startsWith('bilibili://search') &&
'/webview', enableWordRe) {
parameters: { Get.toNamed('/searchResult', parameters: {
'url': matchStr, 'keyword': content.jumpUrl[matchStr]['title']
'type': 'url', });
'pageTitle': '' }
},
);
} }
} else { },
if (appUrlSchema.startsWith('bilibili://search') && ),
enableWordRe) { );
Get.toNamed('/searchResult', parameters: { }
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
),
);
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) { if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(

View File

@@ -3,27 +3,24 @@ import 'dart:io';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart'; import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/danmaku/view.dart'; import 'package:pilipala/pages/danmaku/view.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
class VideoDetailPage extends StatefulWidget { class VideoDetailPage extends StatefulWidget {
@@ -36,7 +33,7 @@ class VideoDetailPage extends StatefulWidget {
} }
class _VideoDetailPageState extends State<VideoDetailPage> class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware { with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
late VideoDetailController videoDetailController; late VideoDetailController videoDetailController;
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
@@ -56,6 +53,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 自动退出全屏 // 自动退出全屏
late bool autoExitFullcreen; late bool autoExitFullcreen;
late bool autoPlayEnable; late bool autoPlayEnable;
late bool autoPiP;
final floating = Floating();
@override @override
void initState() { void initState() {
@@ -63,14 +62,29 @@ class _VideoDetailPageState extends State<VideoDetailPage>
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag); videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag); videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
});
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
});
videoDetailController.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0);
});
statusBarHeight = localCache.get('statusBarHeight'); statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen = autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
autoPlayEnable = autoPlayEnable =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false);
videoSourceInit(); videoSourceInit();
appbarStreamListen(); appbarStreamListen();
WidgetsBinding.instance.addObserver(this);
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
@@ -153,6 +167,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (videoDetailController.floating != null) { if (videoDetailController.floating != null) {
videoDetailController.floating!.dispose(); videoDetailController.floating!.dispose();
} }
videoPlayerServiceHandler.onVideoDetailDispose();
WidgetsBinding.instance.removeObserver(this);
floating.dispose();
super.dispose(); super.dispose();
} }
@@ -199,6 +216,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.subscribe(this, ModalRoute.of(context) as PageRoute); .subscribe(this, ModalRoute.of(context) as PageRoute);
} }
@override
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
if (lifecycleState == AppLifecycleState.inactive && autoPiP) {
floating.enable(
aspectRatio: Rational(
videoDetailController.data.dash!.video!.first.width!,
videoDetailController.data.dash!.video!.first.height!,
));
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
@@ -497,6 +525,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return PiPSwitcher( return PiPSwitcher(
childWhenDisabled: childWhenDisabled, childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled, childWhenEnabled: childWhenEnabled,
floating: floating,
); );
} else { } else {
return childWhenDisabled; return childWhenDisabled;

View File

@@ -0,0 +1,236 @@
import 'package:flutter/gestures.dart';
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/models/video/ai.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class AiDetail extends StatelessWidget {
final ModelResult? modelResult;
const AiDetail({
Key? key,
this.modelResult,
}) : super(key: key);
@override
Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight,
child: Column(
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
),
),
),
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Text(
modelResult!.summary!,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 20),
ListView.builder(
shrinkWrap: true,
itemCount: modelResult!.outline!.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Column(
children: [
Text(
modelResult!.outline![index].title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 6),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult!
.outline![index].partOutline!.length,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.onBackground,
height: 1.5,
),
children: [
TextSpan(
text: Utils.tampToSeektime(
modelResult!
.outline![index]
.partOutline![i]
.timestamp!),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
Get.find<VideoDetailController>(
tag: Get.arguments[
'heroTag'])
.plPlayerController
.seekTo(
Duration(
seconds:
Utils.duration(
Utils.tampToSeektime(modelResult!
.outline![
index]
.partOutline![
i]
.timestamp!)
.toString(),
),
),
);
} catch (_) {}
},
),
const TextSpan(text: ' '),
TextSpan(
text: modelResult!
.outline![index]
.partOutline![i]
.content!),
],
),
),
],
),
],
);
},
),
const SizedBox(height: 20),
],
);
},
)
],
),
),
),
],
),
);
}
InlineSpan buildContent(BuildContext context, content) {
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
List<InlineSpan> spanChildren = [];
RegExp urlRegExp = RegExp(r'https?://\S+\b');
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
int previousEndIndex = 0;
for (Match match in matches) {
if (match.start > previousEndIndex) {
spanChildren.add(TextSpan(
text: currentDesc.rawText
.substring(previousEndIndex, match.start)));
}
spanChildren.add(
TextSpan(
text: match.group(0),
style: TextStyle(
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
try {
Get.toNamed(
'/webview',
parameters: {
'url': match.group(0)!,
'type': 'url',
'pageTitle': match.group(0)!,
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
),
);
previousEndIndex = match.end;
}
if (previousEndIndex < currentDesc.rawText.length) {
spanChildren.add(TextSpan(
text: currentDesc.rawText.substring(previousEndIndex)));
}
TextSpan result = TextSpan(children: spanChildren);
return result;
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
default:
return const TextSpan();
}
});
return TextSpan(children: spanChilds);
}
}

View File

@@ -43,6 +43,7 @@ class _HeaderControlState extends State<HeaderControl> {
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
Box videoStorage = GStrorage.video; Box videoStorage = GStrorage.video;
late List speedsList; late List speedsList;
double buttonSpace = 8;
@override @override
void initState() { void initState() {
@@ -88,7 +89,6 @@ class _HeaderControlState extends State<HeaderControl> {
Expanded( Expanded(
child: Material( child: Material(
child: ListView( child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [ children: [
ListTile( ListTile(
onTap: () {}, onTap: () {},
@@ -182,8 +182,8 @@ class _HeaderControlState extends State<HeaderControl> {
/// 选择倍速 /// 选择倍速
void showSetSpeedSheet() { void showSetSpeedSheet() {
double currentSpeed = widget.controller!.playbackSpeed; double currentSpeed = widget.controller!.playbackSpeed;
SmartDialog.show( showDialog(
animationType: SmartAnimationType.centerFade_otherSlide, context: Get.context!,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('播放速度'), title: const Text('播放速度'),
@@ -196,12 +196,20 @@ class _HeaderControlState extends State<HeaderControl> {
for (var i in speedsList) ...[ for (var i in speedsList) ...[
if (i == currentSpeed) ...[ if (i == currentSpeed) ...[
FilledButton( FilledButton(
onPressed: () => {setState(() => currentSpeed = i)}, onPressed: () async {
// setState(() => currentSpeed = i),
await widget.controller!.setPlaybackSpeed(i);
Get.back();
},
child: Text(i.toString()), child: Text(i.toString()),
), ),
] else ...[ ] else ...[
FilledButton.tonal( FilledButton.tonal(
onPressed: () => {setState(() => currentSpeed = i)}, onPressed: () async {
// setState(() => currentSpeed = i),
await widget.controller!.setPlaybackSpeed(i);
Get.back();
},
child: Text(i.toString()), child: Text(i.toString()),
), ),
] ]
@@ -219,10 +227,10 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await SmartDialog.dismiss(); await widget.controller!.setDefaultSpeed();
widget.controller!.setPlaybackSpeed(currentSpeed); Get.back();
}, },
child: const Text('确定'), child: const Text('默认速度'),
), ),
], ],
); );
@@ -276,7 +284,7 @@ class _HeaderControlState extends State<HeaderControl> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('选择画质', style: titleStyle), Text('选择画质', style: titleStyle),
const SizedBox(width: 4), SizedBox(width: buttonSpace),
Icon( Icon(
Icons.info_outline, Icons.info_outline,
size: 16, size: 16,
@@ -793,7 +801,7 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
fuc: () => Get.back(), fuc: () => Get.back(),
), ),
const SizedBox(width: 4), SizedBox(width: buttonSpace),
ComBtn( ComBtn(
icon: const Icon( icon: const Icon(
FontAwesomeIcons.house, FontAwesomeIcons.house,
@@ -838,7 +846,7 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
), ),
const SizedBox(width: 4), SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...[ if (Platform.isAndroid) ...[
SizedBox( SizedBox(
width: 34, width: 34,
@@ -870,7 +878,7 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
), ),
const SizedBox(width: 4), SizedBox(width: buttonSpace),
], ],
Obx( Obx(
() => SizedBox( () => SizedBox(
@@ -888,7 +896,7 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
), ),
const SizedBox(width: 4), SizedBox(width: buttonSpace),
ComBtn( ComBtn(
icon: const Icon( icon: const Icon(
FontAwesomeIcons.sliders, FontAwesomeIcons.sliders,

View File

@@ -3,6 +3,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart';
@@ -14,6 +15,7 @@ import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
@@ -73,6 +75,7 @@ class PlPlayerController {
Rx<bool> videoFitChanged = false.obs; Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.contain); final Rx<BoxFit> _videoFit = Rx(BoxFit.contain);
final Rx<String> _videoFitDesc = Rx('包含');
/// ///
// ignore: prefer_final_fields // ignore: prefer_final_fields
@@ -183,6 +186,7 @@ class PlPlayerController {
/// 视频比例 /// 视频比例
Rx<BoxFit> get videoFit => _videoFit; Rx<BoxFit> get videoFit => _videoFit;
Rx<String> get videoFitDEsc => _videoFitDesc;
/// 是否长按倍速 /// 是否长按倍速
Rx<bool> get doubleSpeedStatus => _doubleSpeedStatus; Rx<bool> get doubleSpeedStatus => _doubleSpeedStatus;
@@ -214,6 +218,8 @@ class PlPlayerController {
late double fontSizeVal; late double fontSizeVal;
late double danmakuSpeedVal; late double danmakuSpeedVal;
late List speedsList; late List speedsList;
// 缓存
double? defaultDuration;
// 播放顺序相关 // 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause; PlayRepeat playRepeat = PlayRepeat.pause;
@@ -264,14 +270,6 @@ class PlPlayerController {
// 获取实例 传参 // 获取实例 传参
static PlPlayerController getInstance({ static PlPlayerController getInstance({
String videoType = 'archive', String videoType = 'archive',
List<BoxFit> fits = const [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
BoxFit.fitHeight,
BoxFit.fitWidth,
BoxFit.scaleDown
],
}) { }) {
// 如果实例尚未创建,则创建一个新实例 // 如果实例尚未创建,则创建一个新实例
_instance ??= PlPlayerController._(); _instance ??= PlPlayerController._();
@@ -324,6 +322,9 @@ class PlPlayerController {
await pause(notify: false); await pause(notify: false);
} }
if (_playerCount.value == 0) {
return;
}
// 配置Player 音轨、字幕等等 // 配置Player 音轨、字幕等等
_videoPlayerController = await _createVideoController( _videoPlayerController = await _createVideoController(
dataSource, _looping, enableHA, width, height); dataSource, _looping, enableHA, width, height);
@@ -332,12 +333,11 @@ class PlPlayerController {
// 数据加载完成 // 数据加载完成
dataStatus.status.value = DataStatus.loaded; dataStatus.status.value = DataStatus.loaded;
await _initializePlayer(seekTo: seekTo);
// listen the video player events // listen the video player events
if (!_listenersInitialized) { if (!_listenersInitialized) {
startListeners(); startListeners();
} }
await _initializePlayer(seekTo: seekTo);
bool autoEnterFullcreen = bool autoEnterFullcreen =
setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false); setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
if (autoEnterFullcreen && _isFirstTime) { if (autoEnterFullcreen && _isFirstTime) {
@@ -379,6 +379,10 @@ class PlPlayerController {
var pp = player.platform as NativePlayer; var pp = player.platform as NativePlayer;
// 解除倍速限制 // 解除倍速限制
await pp.setProperty("af", "scaletempo2=max-speed=8"); await pp.setProperty("af", "scaletempo2=max-speed=8");
// 音量不一致
await pp.setProperty("volume-max", "100");
await pp.setProperty("ao", "audiotrack,opensles");
// 音轨 // 音轨
if (dataSource.audioSource != '' && dataSource.audioSource != null) { if (dataSource.audioSource != '' && dataSource.audioSource != null) {
await pp.setProperty( await pp.setProperty(
@@ -407,6 +411,7 @@ class PlPlayerController {
player, player,
configuration: VideoControllerConfiguration( configuration: VideoControllerConfiguration(
enableHardwareAcceleration: enableHA, enableHardwareAcceleration: enableHA,
androidAttachSurfaceAfterVideoParameters: false,
), ),
); );
@@ -437,22 +442,22 @@ class PlPlayerController {
Future _initializePlayer({ Future _initializePlayer({
Duration seekTo = Duration.zero, Duration seekTo = Duration.zero,
}) async { }) async {
// 跳转播放
if (seekTo != Duration.zero) {
await this.seekTo(seekTo);
}
// 设置倍速 // 设置倍速
if (_playbackSpeed.value != 1.0) { if (_playbackSpeed.value != 1.0) {
await setPlaybackSpeed(_playbackSpeed.value); await setPlaybackSpeed(_playbackSpeed.value);
} else { } else {
await setPlaybackSpeed(1.0); await setPlaybackSpeed(1.0);
} }
getVideoFit();
// if (_looping) { // if (_looping) {
// await setLooping(_looping); // await setLooping(_looping);
// } // }
// 跳转播放
if (seekTo != Duration.zero) {
await this.seekTo(seekTo);
}
// 自动播放 // 自动播放
if (_autoPlay) { if (_autoPlay) {
await play(); await play();
@@ -515,12 +520,24 @@ class PlPlayerController {
}), }),
videoPlayerController!.stream.buffering.listen((event) { videoPlayerController!.stream.buffering.listen((event) {
isBuffering.value = event; isBuffering.value = event;
videoPlayerServiceHandler.onStatusChange(
playerStatus.status.value, event);
}), }),
// videoPlayerController!.stream.volume.listen((event) { // videoPlayerController!.stream.volume.listen((event) {
// if (!mute.value && _volumeBeforeMute != event) { // if (!mute.value && _volumeBeforeMute != event) {
// _volumeBeforeMute = event / 100; // _volumeBeforeMute = event / 100;
// } // }
// }), // }),
// 媒体通知监听
onPlayerStatusChanged.listen((event) {
videoPlayerServiceHandler.onStatusChange(event, isBuffering.value);
}),
onPositionChanged.listen((event) {
EasyThrottle.throttle(
'mediaServicePositon',
const Duration(seconds: 1),
() => videoPlayerServiceHandler.onPositionChange(event));
}),
], ],
); );
} }
@@ -552,17 +569,19 @@ class PlPlayerController {
// play(); // play();
// } // }
} else { } else {
print('seek duration else');
_timerForSeek?.cancel(); _timerForSeek?.cancel();
_timerForSeek = _timerForSeek =
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
//_timerForSeek = null; //_timerForSeek = null;
if (duration.value.inSeconds != 0) { if (duration.value.inSeconds != 0) {
await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController?.seek(position); await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) { // if (playerStatus.status.value == PlayerStatus.paused) {
// play(); // play();
// } // }
t.cancel(); t.cancel();
//_timerForSeek = null; _timerForSeek = null;
} }
}); });
} }
@@ -573,28 +592,41 @@ class PlPlayerController {
await _videoPlayerController?.setRate(speed); await _videoPlayerController?.setRate(speed);
try { try {
DanmakuOption currentOption = danmakuController!.option; DanmakuOption currentOption = danmakuController!.option;
defaultDuration ??= currentOption.duration;
DanmakuOption updatedOption = currentOption.copyWith( DanmakuOption updatedOption = currentOption.copyWith(
duration: (currentOption.duration / speed) * playbackSpeed); duration: (defaultDuration! / speed) * playbackSpeed);
danmakuController!.updateOption(updatedOption); danmakuController!.updateOption(updatedOption);
} catch (_) {} } catch (_) {}
// fix 长按倍速后放开不恢复 // fix 长按倍速后放开不恢复
// _playbackSpeed.value = speed; if (!doubleSpeedStatus.value) {
} _playbackSpeed.value = speed;
/// 设置倍速
Future<void> togglePlaybackSpeed() async {
List<double> allowedSpeeds =
PlaySpeed.values.map<double>((e) => e.value).toList();
int index = allowedSpeeds.indexOf(_playbackSpeed.value);
if (index < allowedSpeeds.length - 1) {
setPlaybackSpeed(allowedSpeeds[index + 1]);
} else {
setPlaybackSpeed(allowedSpeeds[0]);
} }
} }
// 还原默认速度
Future<void> setDefaultSpeed() async {
double speed =
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
await _videoPlayerController?.setRate(speed);
_playbackSpeed.value = speed;
}
/// 设置倍速
// Future<void> togglePlaybackSpeed() async {
// List<double> allowedSpeeds =
// PlaySpeed.values.map<double>((e) => e.value).toList();
// int index = allowedSpeeds.indexOf(_playbackSpeed.value);
// if (index < allowedSpeeds.length - 1) {
// setPlaybackSpeed(allowedSpeeds[index + 1]);
// } else {
// setPlaybackSpeed(allowedSpeeds[0]);
// }
// }
/// 播放视频 /// 播放视频
Future<void> play({bool repeat = false, bool hideControls = true}) async { Future<void> play({bool repeat = false, bool hideControls = true}) async {
// 播放时自动隐藏控制条
controls = !hideControls;
// repeat为true将从头播放 // repeat为true将从头播放
if (repeat) { if (repeat) {
await seekTo(Duration.zero); await seekTo(Duration.zero);
@@ -606,17 +638,18 @@ class PlPlayerController {
playerStatus.status.value = PlayerStatus.playing; playerStatus.status.value = PlayerStatus.playing;
// screenManager.setOverlays(false); // screenManager.setOverlays(false);
audioSessionHandler.setActive(true);
// 播放时自动隐藏控制条
if (hideControls) {
_hideTaskControls();
}
} }
/// 暂停播放 /// 暂停播放
Future<void> pause({bool notify = true}) async { Future<void> pause({bool notify = true, bool isInterrupt = false}) async {
await _videoPlayerController?.pause(); await _videoPlayerController?.pause();
playerStatus.status.value = PlayerStatus.paused; playerStatus.status.value = PlayerStatus.paused;
// 主动暂停时让出音频焦点
if (!isInterrupt) {
audioSessionHandler.setActive(false);
}
} }
/// 更改播放状态 /// 更改播放状态
@@ -725,44 +758,61 @@ class PlPlayerController {
/// Toggle Change the videofit accordingly /// Toggle Change the videofit accordingly
void toggleVideoFit() { void toggleVideoFit() {
videoFitChangedTimer?.cancel(); showDialog(
videoFitChanged.value = true; context: Get.context!,
// 范围内 builder: (context) {
List attrs = videoFitType.map((e) => e['attr']).toList(); return AlertDialog(
if (attrs.indexOf(_videoFit.value) < attrs.length - 1) { title: const Text('画面比例'),
int index = attrs.indexOf(_videoFit.value); content: StatefulBuilder(builder: (context, StateSetter setState) {
_videoFit.value = attrs[index + 1]; return Wrap(
print(videoFitType[index + 1]['desc']); alignment: WrapAlignment.start,
SmartDialog.showToast(videoFitType[index + 1]['desc']); spacing: 8,
} else { runSpacing: 2,
// 默认 contain children: [
_videoFit.value = videoFitType.first['attr']; for (var i in videoFitType) ...[
SmartDialog.showToast(videoFitType.first['desc']); if (_videoFit.value == i['attr']) ...[
} FilledButton(
videoFitChangedTimer = Timer(const Duration(seconds: 1), () { onPressed: () async {
videoFitChangedTimer = null; _videoFit.value = i['attr'];
videoFitChanged.value = false; _videoFitDesc.value = i['desc'];
}); setVideoFit();
print(_videoFit.value); Get.back();
} },
child: Text(i['desc']),
/// Change Video Fit accordingly ),
void onVideoFitChange(BoxFit fit) { ] else ...[
_videoFit.value = fit; FilledButton.tonal(
onPressed: () async {
_videoFit.value = i['attr'];
_videoFitDesc.value = i['desc'];
setVideoFit();
Get.back();
},
child: Text(i['desc']),
),
]
]
],
);
}),
);
},
);
} }
/// 缓存fit /// 缓存fit
// Future<void> setVideoFit() async { Future<void> setVideoFit() async {
// videoStorage.put(VideoBoxKey.videoBrightness, _videoFit.value.name); List attrs = videoFitType.map((e) => e['attr']).toList();
// } int index = attrs.indexOf(_videoFit.value);
videoStorage.put(VideoBoxKey.cacheVideoFit, index);
}
/// 读取fit /// 读取fit
// Future<void> getVideoFit() async { Future<void> getVideoFit() async {
// String fitValue = int fitValue = videoStorage.get(VideoBoxKey.cacheVideoFit, defaultValue: 0);
// videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain'); _videoFit.value = videoFitType[fitValue]['attr'];
// _videoFit.value = videoFitType _videoFitDesc.value = videoFitType[fitValue]['desc'];
// .firstWhere((element) => element['attr'] == fitValue)['attr']; }
// }
/// 读取亮度 /// 读取亮度
// Future<void> getVideoBrightness() async { // Future<void> getVideoBrightness() async {
@@ -795,6 +845,7 @@ class PlPlayerController {
if (val) { if (val) {
setPlaybackSpeed(longPressSpeed); setPlaybackSpeed(longPressSpeed);
} else { } else {
print(playbackSpeed);
setPlaybackSpeed(playbackSpeed); setPlaybackSpeed(playbackSpeed);
} }
} }
@@ -980,12 +1031,15 @@ class PlPlayerController {
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal); localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal);
var pp = _videoPlayerController!.platform as NativePlayer;
await pp.setProperty('audio-files', '');
removeListeners(); removeListeners();
await _videoPlayerController?.dispose(); await _videoPlayerController?.dispose();
_videoPlayerController = null; _videoPlayerController = null;
_instance = null; _instance = null;
// 关闭所有视频页面恢复亮度 // 关闭所有视频页面恢复亮度
resetBrightness(); resetBrightness();
videoPlayerServiceHandler.clear();
} catch (err) { } catch (err) {
print(err); print(err);
} }

View File

@@ -74,6 +74,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late int defaultBtmProgressBehavior; late int defaultBtmProgressBehavior;
late bool enableQuickDouble; late bool enableQuickDouble;
late bool enableBackgroundPlay; late bool enableBackgroundPlay;
late double screenWidth;
void onDoubleTapSeekBackward() { void onDoubleTapSeekBackward() {
_ctr.onDoubleTapSeekBackward(); _ctr.onDoubleTapSeekBackward();
@@ -116,6 +117,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
screenWidth = Get.size.width;
animationController = AnimationController( animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300)); vsync: this, duration: const Duration(milliseconds: 300));
videoController = widget.controller.videoController!; videoController = widget.controller.videoController!;
@@ -128,7 +130,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true); setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true);
enableBackgroundPlay = enableBackgroundPlay =
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false); setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
Future.microtask(() async { Future.microtask(() async {
try { try {
FlutterVolumeController.showSystemUI = true; FlutterVolumeController.showSystemUI = true;
@@ -217,6 +218,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
controller: videoController, controller: videoController,
controls: NoVideoControls, controls: NoVideoControls,
pauseUponEnteringBackgroundMode: !enableBackgroundPlay, pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
resumeUponEnteringForegroundMode: true,
subtitleViewConfiguration: SubtitleViewConfiguration( subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle, style: subTitleStyle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -508,7 +510,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
if (tapPosition < sectionWidth) { if (tapPosition < sectionWidth) {
// 左边区域 👈 // 左边区域 👈
final brightness = _ctr.brightnessValue.value - delta / 100.0; double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16) *
3;
final brightness = _ctr.brightnessValue.value - delta / level;
final result = brightness.clamp(0.0, 1.0); final result = brightness.clamp(0.0, 1.0);
setBrightness(result); setBrightness(result);
} else if (tapPosition < sectionWidth * 2) { } else if (tapPosition < sectionWidth * 2) {
@@ -531,7 +537,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_distance = dy; _distance = dy;
} else { } else {
// 右边区域 👈 // 右边区域 👈
final volume = _ctr.volumeValue.value - delta / 100.0; double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16) *
3;
final volume = _ctr.volumeValue.value - delta / level;
final result = volume.clamp(0.0, 1.0); final result = volume.clamp(0.0, 1.0);
setVolume(result); setVolume(result);
} }

View File

@@ -115,15 +115,22 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
// ), // ),
// ), // ),
// ), // ),
ComBtn( SizedBox(
icon: const Icon( height: 30,
Icons.settings_overscan_outlined, child: TextButton(
size: 18, onPressed: () => _.toggleVideoFit(),
color: Colors.white, style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: Obx(
() => Text(
_.videoFitDEsc.value,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
), ),
fuc: () => _.toggleVideoFit(),
), ),
const SizedBox(width: 4), const SizedBox(width: 10),
// 全屏 // 全屏
Obx( Obx(
() => ComBtn( () => ComBtn(
@@ -139,7 +146,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
), ),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 12),
], ],
), ),
); );

View File

@@ -19,6 +19,7 @@ import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/html/index.dart'; import 'package:pilipala/pages/html/index.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart'; import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/login/index.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/member_search/index.dart'; import 'package:pilipala/pages/member_search/index.dart';
import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/preview/index.dart';
@@ -129,6 +130,8 @@ class Routes {
// 私信详情 // 私信详情
CustomGetPage( CustomGetPage(
name: '/whisperDetail', page: () => const WhisperDetailPage()), name: '/whisperDetail', page: () => const WhisperDetailPage()),
// 登录页面
CustomGetPage(name: '/loginPage', page: () => const LoginPage()),
]; ];
} }

View File

@@ -0,0 +1,180 @@
import 'package:audio_service/audio_service.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
Future<VideoPlayerServiceHandler> initAudioService() async {
return await AudioService.init(
builder: () => VideoPlayerServiceHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.guozhigq.pilipala.audio',
androidNotificationChannelName: 'Audio Service Pilipala',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
fastForwardInterval: Duration(seconds: 10),
rewindInterval: Duration(seconds: 10),
androidNotificationChannelDescription: 'Media notification channel',
androidNotificationIcon: 'drawable/ic_notification_icon',
),
);
}
class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
static final List<MediaItem> _item = [];
Box setting = GStrorage.setting;
bool enableBackgroundPlay = false;
VideoPlayerServiceHandler() {
revalidateSetting();
}
revalidateSetting() {
enableBackgroundPlay =
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
}
@override
Future<void> play() async {
PlPlayerController.getInstance().play();
}
@override
Future<void> pause() async {
PlPlayerController.getInstance().pause();
}
@override
Future<void> seek(Duration position) async {
playbackState.add(playbackState.value.copyWith(
updatePosition: position,
));
await PlPlayerController.getInstance().seekTo(position);
}
Future<void> setMediaItem(MediaItem newMediaItem) async {
if (!enableBackgroundPlay) return;
mediaItem.add(newMediaItem);
}
Future<void> setPlaybackState(PlayerStatus status, bool isBuffering) async {
if (!enableBackgroundPlay) return;
final AudioProcessingState processingState;
final playing = status == PlayerStatus.playing;
if (status == PlayerStatus.completed) {
processingState = AudioProcessingState.completed;
} else if (isBuffering) {
processingState = AudioProcessingState.buffering;
} else {
processingState = AudioProcessingState.ready;
}
playbackState.add(playbackState.value.copyWith(
processingState:
isBuffering ? AudioProcessingState.buffering : processingState,
controls: [
MediaControl.rewind
.copyWith(androidIcon: 'drawable/ic_baseline_replay_10_24'),
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.fastForward
.copyWith(androidIcon: 'drawable/ic_baseline_forward_10_24'),
],
playing: playing,
systemActions: const {
MediaAction.seek,
},
));
}
onStatusChange(PlayerStatus status, bool isBuffering) {
if (!enableBackgroundPlay) return;
if (_item.isEmpty) return;
setPlaybackState(status, isBuffering);
}
onVideoDetailChange(dynamic data, int cid) {
if (!enableBackgroundPlay) return;
if (data == null) return;
Map argMap = Get.arguments;
final heroTag = argMap['heroTag'];
late MediaItem? mediaItem;
if (data is VideoDetailData) {
if ((data.pages?.length ?? 0) > 1) {
final current = data.pages?.firstWhere((element) => element.cid == cid);
mediaItem = MediaItem(
id: heroTag,
title: current?.pagePart ?? "",
artist: data.title ?? "",
album: data.title ?? "",
duration: Duration(seconds: current?.duration ?? 0),
artUri: Uri.parse(data.pic ?? ""),
);
} else {
mediaItem = MediaItem(
id: heroTag,
title: data.title ?? "",
artist: data.owner?.name ?? "",
duration: Duration(seconds: data.duration ?? 0),
artUri: Uri.parse(data.pic ?? ""),
);
}
} else if (data is BangumiInfoModel) {
final current =
data.episodes?.firstWhere((element) => element.cid == cid);
mediaItem = MediaItem(
id: heroTag,
title: current?.longTitle ?? "",
artist: data.title ?? "",
duration: Duration(milliseconds: current?.duration ?? 0),
artUri: Uri.parse(data.cover ?? ""),
);
}
if (mediaItem == null) return;
setMediaItem(mediaItem);
_item.add(mediaItem);
}
onVideoDetailDispose() {
if (!enableBackgroundPlay) return;
playbackState.add(playbackState.value.copyWith(
processingState: AudioProcessingState.idle,
playing: false,
));
_item.removeLast();
if (_item.isNotEmpty) {
setMediaItem(_item.last);
}
if (_item.isEmpty) {
playbackState
.add(playbackState.value.copyWith(updatePosition: Duration.zero));
}
stop();
}
clear() {
if (!enableBackgroundPlay) return;
mediaItem.add(null);
playbackState.add(PlaybackState(
processingState: AudioProcessingState.idle,
playing: false,
));
_item.clear();
stop();
}
onPositionChange(Duration position) {
if (!enableBackgroundPlay) return;
playbackState.add(playbackState.value.copyWith(
updatePosition: position,
));
}
}

View File

@@ -0,0 +1,53 @@
import 'package:audio_session/audio_session.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
class AudioSessionHandler {
late AudioSession session;
bool _playInterrupted = false;
setActive(bool active) {
session.setActive(active);
}
AudioSessionHandler() {
initSession();
}
Future<void> initSession() async {
session = await AudioSession.instance;
session.configure(const AudioSessionConfiguration.music());
session.interruptionEventStream.listen((event) {
final player = PlPlayerController.getInstance();
if (event.begin) {
switch (event.type) {
case AudioInterruptionType.duck:
player.setVolume(player.volume.value * 0.5);
break;
case AudioInterruptionType.pause:
case AudioInterruptionType.unknown:
player.pause(isInterrupt: true);
_playInterrupted = true;
break;
}
} else {
switch (event.type) {
case AudioInterruptionType.duck:
player.setVolume(player.volume.value * 2);
break;
case AudioInterruptionType.pause:
if (_playInterrupted) PlPlayerController.getInstance().play();
break;
case AudioInterruptionType.unknown:
break;
}
_playInterrupted = false;
}
});
// 耳机拔出暂停
session.becomingNoisyEventStream.listen((_) {
PlPlayerController.getInstance().pause();
});
}
}

View File

@@ -0,0 +1,11 @@
import 'audio_handler.dart';
import 'audio_session.dart';
late VideoPlayerServiceHandler videoPlayerServiceHandler;
late AudioSessionHandler audioSessionHandler;
Future<void> setupServiceLocator() async {
final audio = await initAudioService();
videoPlayerServiceHandler = audio;
audioSessionHandler = AudioSessionHandler();
}

View File

@@ -1,9 +1,14 @@
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:uuid/uuid.dart';
class LoginUtils { class LoginUtils {
static Future refreshLoginStatus(bool status) async { static Future refreshLoginStatus(bool status) async {
@@ -27,4 +32,29 @@ class LoginUtils {
SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}'); SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}');
} }
} }
static String buvid() {
var mac = <String>[];
var random = Random();
for (var i = 0; i < 6; i++) {
var min = 0;
var max = 0xff;
var num = (random.nextInt(max - min + 1) + min).toRadixString(16);
mac.add(num);
}
var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();
var md5Arr = md5Str.split('');
return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';
}
static String getUUID() {
return const Uuid().v4().replaceAll('-', '');
}
static String generateBuvid() {
String uuid = getUUID() + getUUID();
return 'XY${uuid.substring(0, 35).toUpperCase()}';
}
} }

28
lib/utils/proxy.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'dart:io';
import 'package:system_proxy/system_proxy.dart';
class CustomProxy {
init() async {
Map<String, String>? proxy = await SystemProxy.getProxySettings();
if (proxy != null) {
HttpOverrides.global =
ProxiedHttpOverrides(proxy['host']!, proxy['port']!);
}
}
}
class ProxiedHttpOverrides extends HttpOverrides {
final String _port;
final String _host;
ProxiedHttpOverrides(this._host, this._port);
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
// set proxy
..findProxy = (uri) {
return "PROXY $_host:$_port;";
};
}
}

View File

@@ -105,6 +105,8 @@ class SettingBoxKey {
static const String enableAutoEnter = 'enableAutoEnter'; static const String enableAutoEnter = 'enableAutoEnter';
static const String enableAutoExit = 'enableAutoExit'; static const String enableAutoExit = 'enableAutoExit';
static const String p1080 = 'p1080'; static const String p1080 = 'p1080';
static const String enableCDN = 'enableCDN';
static const String autoPiP = 'autoPiP';
// youtube 双击快进快退 // youtube 双击快进快退
static const String enableQuickDouble = 'enableQuickDouble'; static const String enableQuickDouble = 'enableQuickDouble';
@@ -124,6 +126,7 @@ class SettingBoxKey {
static const String enableSearchWord = 'enableSearchWord'; static const String enableSearchWord = 'enableSearchWord';
static const String enableRcmdDynamic = 'enableRcmdDynamic'; static const String enableRcmdDynamic = 'enableRcmdDynamic';
static const String enableSaveLastData = 'enableSaveLastData'; static const String enableSaveLastData = 'enableSaveLastData';
static const String enableSystemProxy = 'enableSystemProxy';
/// 外观 /// 外观
static const String themeMode = 'themeMode'; static const String themeMode = 'themeMode';
@@ -134,6 +137,7 @@ class SettingBoxKey {
static const String enableSingleRow = 'enableSingleRow'; // 首页单列 static const String enableSingleRow = 'enableSingleRow'; // 首页单列
static const String displayMode = 'displayMode'; static const String displayMode = 'displayMode';
static const String customRows = 'customRows'; // 自定义列 static const String customRows = 'customRows'; // 自定义列
static const String enableMYBar = 'enableMYBar';
} }
class LocalCacheKey { class LocalCacheKey {
@@ -152,6 +156,10 @@ class LocalCacheKey {
static const String danmakuOpacity = 'danmakuOpacity'; static const String danmakuOpacity = 'danmakuOpacity';
static const String danmakuFontScale = 'danmakuFontScale'; static const String danmakuFontScale = 'danmakuFontScale';
static const String danmakuSpeed = 'danmakuSpeed'; static const String danmakuSpeed = 'danmakuSpeed';
// 代理host port
static const String systemProxyHost = 'systemProxyHost';
static const String systemProxyPort = 'systemProxyPort';
} }
class VideoBoxKey { class VideoBoxKey {
@@ -169,4 +177,6 @@ class VideoBoxKey {
static const String longPressSpeedDefault = 'longPressSpeedDefault'; static const String longPressSpeedDefault = 'longPressSpeedDefault';
// 自定义倍速集合 // 自定义倍速集合
static const String customSpeedsList = 'customSpeedsList'; static const String customSpeedsList = 'customSpeedsList';
// 画面填充比例
static const String cacheVideoFit = 'cacheVideoFit';
} }

View File

@@ -286,4 +286,15 @@ class Utils {
); );
} }
} }
// 时间戳转时间
static tampToSeektime(number) {
int hours = number ~/ 60;
int minutes = number % 60;
String formattedHours = hours.toString().padLeft(2, '0');
String formattedMinutes = minutes.toString().padLeft(2, '0');
return '$formattedHours:$formattedMinutes';
}
} }

View File

@@ -0,0 +1,36 @@
import 'package:pilipala/models/video/play/url.dart';
class VideoUtils {
static String getCdnUrl(dynamic item) {
var backupUrl = "";
var videoUrl = "";
/// 先获取backupUrl 一般是upgcxcode地址 播放更稳定
if (item is VideoItem) {
backupUrl = item.backupUrl ?? "";
videoUrl = backupUrl.contains("http") ? backupUrl : (item.baseUrl ?? "");
} else if (item is AudioItem) {
backupUrl = item.backupUrl ?? "";
videoUrl = backupUrl.contains("http") ? backupUrl : (item.baseUrl ?? "");
} else {
return "";
}
/// issues #70
if (videoUrl.contains(".mcdn.bilivideo") ||
videoUrl.contains("/upgcxcode/")) {
//CDN列表
var cdnList = {
'ali': 'upos-sz-mirrorali.bilivideo.com',
'cos': 'upos-sz-mirrorcos.bilivideo.com',
'hw': 'upos-sz-mirrorhw.bilivideo.com',
};
//取一个CDN
var cdn = cdnList['ali'] ?? "";
var reg = RegExp(r'(http|https)://(.*?)/upgcxcode/');
videoUrl = videoUrl.replaceAll(reg, "https://$cdn/upgcxcode/");
}
return videoUrl;
}
}

View File

@@ -5,6 +5,8 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audio_service
import audio_session
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
import dynamic_color import dynamic_color
@@ -20,6 +22,8 @@ import url_launcher_macos
import wakelock_plus import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))

View File

@@ -21,10 +21,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: animations name: animations
sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" version: "2.0.8"
appscheme: appscheme:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -49,6 +49,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.2"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -57,14 +65,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
audio_service:
dependency: "direct main"
description:
name: audio_service
sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4
url: "https://pub.dev"
source: hosted
version: "0.18.12"
audio_service_platform_interface:
dependency: transitive
description:
name: audio_service_platform_interface
sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
audio_service_web:
dependency: transitive
description:
name: audio_service_web
sha256: "523e64ddc914c714d53eec2da85bba1074f08cf26c786d4efb322de510815ea7"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
audio_session:
dependency: "direct main"
description:
name: audio_session
sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
url: "https://pub.dev"
source: hosted
version: "0.1.16"
audio_video_progress_bar: audio_video_progress_bar:
dependency: "direct main" dependency: "direct main"
description: description:
name: audio_video_progress_bar name: audio_video_progress_bar
sha256: "67f3a5ea70d48b48caaf29f5a0606284a6aa3a393736daf9e82bec985d2f9b70" sha256: "3384875247cdbea748bd9ae8330631cd06a6cabfcda4945d45c9b406da92bc66"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "2.0.1"
auto_orientation: auto_orientation:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -149,26 +189,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.3" version: "3.3.0"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "3.0.0"
cached_network_image_web: cached_network_image_web:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.1.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -325,18 +365,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.2" version: "5.3.3"
dio_cookie_manager: dio_cookie_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio_cookie_manager name: dio_cookie_manager
sha256: c4b7a693aa09efd694a5c5e12065daa5e026647b106245281ed1042b3ebefb8f sha256: e79498b0f632897ff0c28d6e8178b4bc6e9087412401f618c31fa0904ace050d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
dio_http2_adapter: dio_http2_adapter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -357,10 +397,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.6" version: "1.6.8"
easy_debounce: easy_debounce:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -369,6 +409,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.3"
encrypt:
dependency: "direct main"
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
extended_image: extended_image:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -454,14 +502,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@@ -519,10 +559,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_smart_dialog name: flutter_smart_dialog
sha256: "8ba9eeb5b0b380bec368c5c8a324e1dab0cd88965c2dd83e64237441140bc599" sha256: "8ffa51d55591227dbfe9fc2b1ff396b37bec7d09c241d875b9b932db99d2d5ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.3+2" version: "4.9.4"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -540,10 +580,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_volume_controller name: flutter_volume_controller
sha256: "7f88cb046b00fd80e98bcb7926b9e3879f004f30905109fdf6c5d09b8d28eb2e" sha256: "1161957826183b46916adb4f1c9f91befce0d8415bd3fcd781f7faed9df62d46"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.7" version: "1.3.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -589,6 +629,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.1"
gt3_flutter_plugin:
dependency: "direct main"
description:
name: gt3_flutter_plugin
sha256: f12bff2bfbcf27467833f8d564dcc24ee2f1b3254a7c7cf5eb2c4590baf11cc1
url: "https://pub.dev"
source: hosted
version: "0.0.8"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -761,18 +809,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit name: media_kit
sha256: d652c2bdb0cd876bf1046e24d0b614651fefe59f7c3a2d9b7ed57217b9e7db94 sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.8+1" version: "1.1.10+1"
media_kit_libs_android_video: media_kit_libs_android_video:
dependency: transitive dependency: transitive
description: description:
name: media_kit_libs_android_video name: media_kit_libs_android_video
sha256: a7ef60926ac528e2fabe9ee7084e648e385422a881ba914c978a7a81e6595dee sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.5" version: "1.3.6"
media_kit_libs_ios_video: media_kit_libs_ios_video:
dependency: transitive dependency: transitive
description: description:
@@ -801,10 +849,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_libs_video name: media_kit_libs_video
sha256: f130964bd4c0907d0af645ba03c8080a914776bfd2e23761a5e22ac3c0c0906a sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "1.0.4"
media_kit_libs_windows_video: media_kit_libs_windows_video:
dependency: transitive dependency: transitive
description: description:
@@ -825,10 +873,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_video name: media_kit_video
sha256: b1a427f0540c5f052dfab73e4b76a5eb8efa7ebb5d83179cb23fc3932afc315a sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -866,10 +914,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: octo_image name: octo_image
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "2.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -914,66 +962,66 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.15" version: "2.1.1"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.27" version: "2.2.0"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.4" version: "2.3.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.11" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.6" version: "2.1.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.7" version: "2.2.1"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.4.3" version: "11.0.1"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.3" version: "11.1.0"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@@ -986,10 +1034,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.11.3" version: "3.12.0"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1098,10 +1146,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: screen_brightness name: screen_brightness
sha256: "62fd61a64e68b32b98b840bad7d8b6822bbc40e63c2b569a5f85528484c86b41" sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2+1"
screen_brightness_android: screen_brightness_android:
dependency: transitive dependency: transitive
description: description:
@@ -1335,10 +1383,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.12" version: "6.1.14"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@@ -1396,7 +1444,7 @@ packages:
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
uuid: uuid:
dependency: transitive dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.10 version: 1.0.11
environment: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@@ -36,31 +36,31 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
# 动态取色 # 动态取色
dynamic_color: ^1.6.6 dynamic_color: ^1.6.8
get: ^4.6.5 get: ^4.6.5
# 网络 # 网络
dio: ^5.3.0 dio: ^5.3.3
cookie_jar: ^4.0.8 cookie_jar: ^4.0.8
dio_cookie_manager: ^3.1.0 dio_cookie_manager: ^3.1.1
connectivity_plus: ^4.0.1 connectivity_plus: ^4.0.1
dio_http2_adapter: ^2.3.1+1 dio_http2_adapter: ^2.3.1+1
# 图片 # 图片
cached_network_image: ^3.2.3 cached_network_image: ^3.3.0
extended_image: ^8.0.2 extended_image: ^8.0.2
saver_gallery: ^2.0.1 saver_gallery: ^2.0.1
# 存储 # 存储
path_provider: ^2.0.14 path_provider: ^2.1.1
hive: ^2.2.3 hive: ^2.2.3
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
# 设备信息 # 设备信息
device_info_plus: ^9.0.2 device_info_plus: ^9.0.2
# 权限 # 权限
permission_handler: ^10.4.3 permission_handler: ^11.0.1
# 分享 # 分享
share_plus: ^7.0.2 share_plus: ^7.0.2
# cookie 管理 # cookie 管理
@@ -76,32 +76,37 @@ dependencies:
# 图标 # 图标
font_awesome_flutter: ^10.4.0 font_awesome_flutter: ^10.4.0
# toast # toast
flutter_smart_dialog: ^4.9.3+2 flutter_smart_dialog: ^4.9.4
# 下滑关闭 # 下滑关闭
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
custom_sliding_segmented_control: ^1.7.5 custom_sliding_segmented_control: ^1.7.5
# 加密 # 加密
crypto: ^3.0.3 crypto: ^3.0.3
encrypt: ^5.0.3
# 视频播放器 # 视频播放器
media_kit: ^1.1.8 # Primary package. media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.1 # For video rendering. media_kit_video: ^1.2.4 # For video rendering.
media_kit_libs_video: ^1.0.3 media_kit_libs_video: ^1.0.4
# 媒体通知
audio_service: ^0.18.12
audio_session: ^0.1.16
# 音量、亮度、屏幕控制 # 音量、亮度、屏幕控制
flutter_volume_controller: ^1.2.7 flutter_volume_controller: ^1.3.0
screen_brightness: ^0.2.2 screen_brightness: ^0.2.2+1
wakelock_plus: ^1.1.1 wakelock_plus: ^1.1.1
universal_platform: ^1.0.0+1 universal_platform: ^1.0.0+1
# 进度条 # 进度条
audio_video_progress_bar: ^1.0.1 audio_video_progress_bar: ^2.0.1
auto_orientation: ^2.3.1 auto_orientation: ^2.3.1
protobuf: ^3.0.0 protobuf: ^3.0.0
animations: ^2.0.7 animations: ^2.0.8
# 获取appx信息 # 获取appx信息
package_info_plus: ^4.1.0 package_info_plus: ^4.1.0
url_launcher: ^6.1.12 url_launcher: ^6.1.14
flutter_svg: ^2.0.7 flutter_svg: ^2.0.7
# 防抖节流 # 防抖节流
easy_debounce: ^2.0.3 easy_debounce: ^2.0.3
@@ -124,6 +129,9 @@ dependencies:
html: ^0.15.4 html: ^0.15.4
# html渲染 # html渲染
flutter_html: ^3.0.0-beta.2 flutter_html: ^3.0.0-beta.2
# 极验
gt3_flutter_plugin: ^0.0.8
uuid: ^3.0.7
dev_dependencies: dev_dependencies: