merge main
This commit is contained in:
@ -39,9 +39,13 @@
|
||||
android:label="PiliPala"
|
||||
android:name="${applicationName}"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
@ -222,6 +226,24 @@
|
||||
|
||||
</intent-filter>
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
@ -234,6 +256,8 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_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.
|
||||
Android 13 or higher.
|
||||
|
BIN
android/app/src/main/res/drawable-hdpi/ic_notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-hdpi/ic_notification_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-mdpi/ic_notification_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 337 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification_icon.png
Normal file
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 |
@ -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>
|
@ -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>
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
3
android/app/src/main/res/raw/keep.xml
Normal file
3
android/app/src/main/res/raw/keep.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/*" />
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/images/ai.png
Normal file
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
26
change_log/1.0.11.1112.md
Normal file
@ -0,0 +1,26 @@
|
||||
## 1.0.11
|
||||
|
||||
### 新功能
|
||||
+ 适配了原生媒体通知栏 @Daydreamer-riri
|
||||
+ 视频主题图标 @Daydreamer-riri
|
||||
+ 关闭软件后自动画中画播放
|
||||
+ UP主分组管理
|
||||
+ md2样式底栏
|
||||
+
|
||||
|
||||
|
||||
### 修复
|
||||
+ 历史记录记忆播放
|
||||
+ 部分类型视频连播
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 视频播放速度总是显示1.0X
|
||||
+ 评论页面计数错误
|
||||
+ 退出视频还有声音
|
||||
|
||||
|
||||
### 优化
|
||||
+ 视频加载速度
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
@ -1,8 +1,6 @@
|
||||
PODS:
|
||||
- appscheme (1.0.4):
|
||||
- Flutter
|
||||
- auto_orientation (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
@ -14,6 +12,10 @@ PODS:
|
||||
- FMDB (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):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
@ -54,11 +56,11 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||
- 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_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
@ -80,13 +82,12 @@ DEPENDENCIES:
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- GT3Captcha-iOS
|
||||
- ReachabilitySwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
appscheme:
|
||||
:path: ".symlinks/plugins/appscheme/ios"
|
||||
auto_orientation:
|
||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
@ -95,6 +96,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
gt3_flutter_plugin:
|
||||
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
@ -132,12 +135,13 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
||||
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
|
@ -140,6 +140,7 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
|
||||
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -268,6 +269,23 @@
|
||||
shellPath = /bin/sh;
|
||||
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 */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
@ -103,5 +103,9 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -70,36 +70,44 @@ class VideoCardV extends StatelessWidget {
|
||||
break;
|
||||
// 动态
|
||||
case 'picture':
|
||||
String dynamicType = 'picture';
|
||||
String uri = videoItem.uri;
|
||||
if (videoItem.uri.contains('bilibili://article/')) {
|
||||
dynamicType = 'article';
|
||||
RegExp regex = RegExp(r'\d+');
|
||||
Match match = regex.firstMatch(videoItem.uri)!;
|
||||
String matchedNumber = match.group(0)!;
|
||||
videoItem.param = 'cv' + matchedNumber;
|
||||
}
|
||||
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;
|
||||
try {
|
||||
String dynamicType = 'picture';
|
||||
String uri = videoItem.uri;
|
||||
String id = '';
|
||||
if (videoItem.uri.startsWith('bilibili://article/')) {
|
||||
// https://www.bilibili.com/read/cv27063554
|
||||
dynamicType = 'read';
|
||||
RegExp regex = RegExp(r'\d+');
|
||||
Match match = regex.firstMatch(videoItem.uri)!;
|
||||
String matchedNumber = match.group(0)!;
|
||||
videoItem.param = int.parse(matchedNumber);
|
||||
id = 'cv${videoItem.param}';
|
||||
}
|
||||
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;
|
||||
default:
|
||||
SmartDialog.showToast(videoItem.goto);
|
||||
|
@ -360,4 +360,49 @@ class Api {
|
||||
// id=849312409672744983
|
||||
// features=itemOpusStyle
|
||||
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';
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -17,6 +18,11 @@ class Request {
|
||||
static late CookieManager cookieManager;
|
||||
static late final Dio dio;
|
||||
factory Request() => _instance;
|
||||
Box setting = GStrorage.setting;
|
||||
static Box localCache = GStrorage.localCache;
|
||||
late dynamic enableSystemProxy;
|
||||
late String systemProxyHost;
|
||||
late String systemProxyPort;
|
||||
|
||||
/// 设置cookie
|
||||
static setCookie() async {
|
||||
@ -41,8 +47,8 @@ class Request {
|
||||
log("setCookie, ${e.toString()}");
|
||||
}
|
||||
}
|
||||
setOptionsHeaders(userInfo);
|
||||
}
|
||||
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
|
||||
|
||||
if (cookie.isEmpty) {
|
||||
try {
|
||||
@ -67,8 +73,10 @@ class Request {
|
||||
return token;
|
||||
}
|
||||
|
||||
static setOptionsHeaders(userInfo) {
|
||||
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
|
||||
static setOptionsHeaders(userInfo, status) {
|
||||
if (status) {
|
||||
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
|
||||
}
|
||||
dio.options.headers['env'] = 'prod';
|
||||
dio.options.headers['app-key'] = 'android64';
|
||||
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
|
||||
@ -92,6 +100,13 @@ class Request {
|
||||
headers: {},
|
||||
);
|
||||
|
||||
enableSystemProxy =
|
||||
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
|
||||
systemProxyHost =
|
||||
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
|
||||
systemProxyPort =
|
||||
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
|
||||
|
||||
dio = Dio(options)
|
||||
|
||||
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
|
||||
@ -100,6 +115,29 @@ class Request {
|
||||
idleTimeout: const Duration(milliseconds: 10000),
|
||||
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
177
lib/http/login.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -39,16 +39,25 @@ class SearchHttp {
|
||||
static Future searchSuggest({required term}) async {
|
||||
var res = await Request().get(Api.serachSuggest,
|
||||
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
||||
if (res.data['code'] == 0) {
|
||||
if (res.data['result'] is Map) {
|
||||
res.data['result']['term'] = term;
|
||||
if (res.data is String) {
|
||||
Map<String, dynamic> resultMap = json.decode(res.data);
|
||||
if (resultMap['code'] == 0) {
|
||||
if (resultMap['result'] is Map) {
|
||||
resultMap['result']['term'] = term;
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': resultMap['result'] is Map
|
||||
? SearchSuggestModel.fromJson(resultMap['result'])
|
||||
: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['result'] is Map
|
||||
? SearchSuggestModel.fromJson(res.data['result'])
|
||||
: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
|
@ -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_rec_video_item.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_detail_res.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/wbi_sign.dart';
|
||||
|
||||
/// res.data['code'] == 0 请求正常返回结果
|
||||
/// res.data['data'] 为结果
|
||||
@ -420,4 +422,23 @@ class VideoHttp {
|
||||
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']),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/router/app_pages.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/data.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -28,6 +29,7 @@ void main() async {
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||
.then((_) async {
|
||||
await GStrorage.init();
|
||||
await setupServiceLocator();
|
||||
runApp(const MyApp());
|
||||
// 小白条、导航栏沉浸
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
49
lib/models/login/index.dart
Normal file
49
lib/models/login/index.dart
Normal 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
80
lib/models/video/ai.dart
Normal 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'];
|
||||
}
|
||||
}
|
@ -201,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
},
|
||||
),
|
||||
),
|
||||
const LoadingMore()
|
||||
LoadingMore()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -10,6 +10,8 @@ class PlDanmakuController {
|
||||
// 按 6min 分段
|
||||
int segCount = 0;
|
||||
List<DmSegMobileReply> dmSegList = [];
|
||||
// 已请求的段落标记
|
||||
List<int> hasrequestSeg = [];
|
||||
int currentSegIndex = 1;
|
||||
int currentDmIndex = 0;
|
||||
|
||||
|
@ -95,7 +95,9 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
// 根据position判断是否有已缓存弹幕。没有则请求对应段
|
||||
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
|
||||
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;
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
ctr.queryDanmaku();
|
||||
|
@ -17,6 +17,10 @@ class AuthorPanel extends StatelessWidget {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// 番剧
|
||||
if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
Get.toNamed(
|
||||
'/member?mid=${item.modules.moduleAuthor.mid}',
|
||||
|
@ -1,17 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.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/utils.dart';
|
||||
|
||||
class FollowItem extends StatelessWidget {
|
||||
final FollowItemModel item;
|
||||
const FollowItem({super.key, required this.item});
|
||||
final FollowController? ctr;
|
||||
const FollowItem({super.key, required this.item, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(item!.mid);
|
||||
String heroTag = Utils.makeHeroTag(item.mid);
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
@ -39,7 +43,29 @@ class FollowItem extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,10 @@ class _FollowListState extends State<FollowList> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return FollowItem(item: list[index]);
|
||||
return FollowItem(
|
||||
item: list[index],
|
||||
ctr: widget.ctr,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -89,6 +91,7 @@ class _OwnerFollowListState extends State<OwnerFollowList>
|
||||
return Obx(
|
||||
() => followList.isNotEmpty
|
||||
? ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
itemCount: followList.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
@ -101,7 +104,10 @@ class _OwnerFollowListState extends State<OwnerFollowList>
|
||||
MediaQuery.of(context).padding.bottom),
|
||||
);
|
||||
} else {
|
||||
return FollowItem(item: followList[index]);
|
||||
return FollowItem(
|
||||
item: followList[index],
|
||||
ctr: widget.ctr,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -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/overlay_pop.dart';
|
||||
import 'package:pilipala/pages/main/index.dart';
|
||||
import 'package:pilipala/pages/rcmd/index.dart';
|
||||
|
||||
import 'controller.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
204
lib/pages/login/controller.dart
Normal file
204
lib/pages/login/controller.dart
Normal 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 {}
|
||||
}
|
||||
}
|
4
lib/pages/login/index.dart
Normal file
4
lib/pages/login/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library login;
|
||||
|
||||
export './controller.dart';
|
||||
export 'view.dart';
|
362
lib/pages/login/view.dart
Normal file
362
lib/pages/login/view.dart
Normal 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('确认登录'),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
late Animation<double>? _slideAnimation;
|
||||
int selectedIndex = 0;
|
||||
int? _lastSelectTime; //上次点击时间
|
||||
Box setting = GStrorage.setting;
|
||||
late bool enableMYBar;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -45,6 +47,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
|
||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||
_pageController = PageController(initialPage: selectedIndex);
|
||||
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
||||
}
|
||||
|
||||
void setIndex(int value) async {
|
||||
@ -144,21 +147,38 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
builder: (context, AsyncSnapshot snapshot) {
|
||||
return AnimatedSlide(
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
offset: Offset(0, snapshot.data ? 0 : 1),
|
||||
child: NavigationBar(
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: <Widget>[
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
child: enableMYBar
|
||||
? NavigationBar(
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: <Widget>[
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
)
|
||||
: 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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -6,6 +6,7 @@ class MemberDynamicPanelController extends GetxController {
|
||||
int? mid;
|
||||
String offset = '';
|
||||
int count = 0;
|
||||
bool hasMore = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -14,12 +15,16 @@ class MemberDynamicPanelController extends GetxController {
|
||||
}
|
||||
|
||||
Future getMemberDynamic() async {
|
||||
if (!hasMore) {
|
||||
return {'status': false};
|
||||
}
|
||||
var res = await MemberHttp.memberDynamic(
|
||||
offset: offset,
|
||||
mid: mid,
|
||||
);
|
||||
if (res['status']) {
|
||||
offset = res['data'].offset;
|
||||
hasMore = res['data'].hasMore;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -139,11 +139,14 @@ class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
|
||||
if (res['status']) {
|
||||
addAll(res['data'].items);
|
||||
}
|
||||
if (res['data'].hasMore) {
|
||||
isSuccess = true;
|
||||
} else {
|
||||
isSuccess = false;
|
||||
}
|
||||
try {
|
||||
if (res['data'].hasMore) {
|
||||
isSuccess = true;
|
||||
} else {
|
||||
isSuccess = false;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return isSuccess;
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ class MineController extends GetxController {
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
// Get.toNamed('/loginPage');
|
||||
} else {
|
||||
int mid = userInfo.value.mid!;
|
||||
String face = userInfo.value.face!;
|
||||
|
@ -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 {
|
||||
const LoadingMore({super.key});
|
||||
dynamic ctr;
|
||||
LoadingMore({super.key, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -199,11 +200,18 @@ class LoadingMore extends StatelessWidget {
|
||||
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),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (ctr != null) {
|
||||
ctr!.onLoad();
|
||||
}
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
'加载更多 👇',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline, fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -117,6 +117,13 @@ class SSearchController extends GetxController {
|
||||
submit();
|
||||
}
|
||||
|
||||
onLongSelect(word) {
|
||||
int index = historyList.indexOf(word);
|
||||
historyList.value = historyList.removeAt(index);
|
||||
historyList.refresh();
|
||||
histiryWord.put('cacheList', historyList);
|
||||
}
|
||||
|
||||
onClearHis() {
|
||||
historyList.value = [];
|
||||
historyCacheList = [];
|
||||
|
@ -299,20 +299,24 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
),
|
||||
),
|
||||
// if (_searchController.historyList.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (int i = 0; i < _searchController.historyList.length; i++)
|
||||
SearchText(
|
||||
searchText: _searchController.historyList[i],
|
||||
searchTextIdx: i,
|
||||
onSelect: (value) => _searchController.onSelect(value),
|
||||
)
|
||||
],
|
||||
),
|
||||
Obx(() => Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (int i = 0;
|
||||
i < _searchController.historyList.length;
|
||||
i++)
|
||||
SearchText(
|
||||
searchText: _searchController.historyList[i],
|
||||
searchTextIdx: i,
|
||||
onSelect: (value) => _searchController.onSelect(value),
|
||||
onLongSelect: (value) =>
|
||||
_searchController.onLongSelect(value),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -4,8 +4,14 @@ class SearchText extends StatelessWidget {
|
||||
final String? searchText;
|
||||
final Function? onSelect;
|
||||
final int? searchTextIdx;
|
||||
const SearchText(
|
||||
{super.key, this.searchText, this.onSelect, this.searchTextIdx});
|
||||
final Function? onLongSelect;
|
||||
const SearchText({
|
||||
super.key,
|
||||
this.searchText,
|
||||
this.onSelect,
|
||||
this.searchTextIdx,
|
||||
this.onLongSelect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -18,6 +24,9 @@ class SearchText extends StatelessWidget {
|
||||
onTap: () {
|
||||
onSelect!(searchText);
|
||||
},
|
||||
onLongPress: () {
|
||||
onLongSelect!(searchText);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding:
|
||||
|
@ -16,8 +16,12 @@ class ExtraSetting extends StatefulWidget {
|
||||
|
||||
class _ExtraSettingState extends State<ExtraSetting> {
|
||||
Box setting = GStrorage.setting;
|
||||
static Box localCache = GStrorage.localCache;
|
||||
late dynamic defaultReplySort;
|
||||
late dynamic defaultDynamicType;
|
||||
late dynamic enableSystemProxy;
|
||||
late String defaultSystemProxyHost;
|
||||
late String defaultSystemProxyPort;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -28,6 +32,86 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
// 优先展示全部动态 all
|
||||
defaultDynamicType =
|
||||
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
|
||||
@ -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(
|
||||
title: '检查更新',
|
||||
subTitle: '每次启动时检查是否需要更新',
|
||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/switch_item.dart';
|
||||
@ -37,6 +38,14 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
defaultValue: BtmProgresBehavior.values.first.code);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
// 重新验证媒体通知后台播放设置
|
||||
videoPlayerServiceHandler.revalidateSetting();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
|
||||
@ -67,6 +76,12 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
setKey: SettingBoxKey.p1080,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: 'CDN优化',
|
||||
subTitle: '使用优质CDN线路',
|
||||
setKey: SettingBoxKey.enableCDN,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动播放',
|
||||
subTitle: '进入详情页自动播放',
|
||||
@ -79,6 +94,12 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
setKey: SettingBoxKey.enableBackgroundPlay,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动PiP播放',
|
||||
subTitle: 'app切换至后台时画中画播放',
|
||||
setKey: SettingBoxKey.autoPiP,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动全屏',
|
||||
subTitle: '视频开始播放时进入全屏',
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/common/theme_type.dart';
|
||||
@ -78,6 +77,12 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
setKey: SettingBoxKey.iosTransition,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: 'MD3样式底栏',
|
||||
subTitle: '符合Material You设计规范的底栏',
|
||||
setKey: SettingBoxKey.enableMYBar,
|
||||
defaultVal: true,
|
||||
),
|
||||
// SetSwitchItem(
|
||||
// title: '首页单列',
|
||||
// subTitle: '每行展示一个内容卡片',
|
||||
|
@ -16,6 +16,7 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/utils/video_utils.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'widgets/header_control.dart';
|
||||
@ -83,6 +84,11 @@ class VideoDetailController extends GetxController
|
||||
Floating? floating;
|
||||
late PreferredSizeWidget headerControl;
|
||||
|
||||
late bool enableCDN;
|
||||
late int? cacheVideoQa;
|
||||
late String cacheDecode;
|
||||
late int cacheAudioQa;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -120,6 +126,15 @@ class VideoDetailController extends GetxController
|
||||
videoDetailCtr: this,
|
||||
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() {
|
||||
@ -231,22 +246,19 @@ class VideoDetailController extends GetxController
|
||||
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
||||
if (result['status']) {
|
||||
data = result['data'];
|
||||
|
||||
List<VideoItem> allVideosList = data.dash!.video!;
|
||||
|
||||
try {
|
||||
// 当前可播放的最高质量视频
|
||||
int currentHighVideoQa = allVideosList.first.quality!.code;
|
||||
// 使用预设的画质 | 当前可用的最高质量
|
||||
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
|
||||
defaultValue: currentHighVideoQa);
|
||||
// 预设的画质为null,则当前可用的最高质量
|
||||
cacheVideoQa ??= currentHighVideoQa;
|
||||
int resVideoQa = currentHighVideoQa;
|
||||
if (cacheVideoQa <= currentHighVideoQa) {
|
||||
if (cacheVideoQa! <= currentHighVideoQa) {
|
||||
// 如果预设的画质低于当前最高
|
||||
List<int> numbers = data.acceptQuality!
|
||||
.where((e) => e <= currentHighVideoQa)
|
||||
.toList();
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers);
|
||||
}
|
||||
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
|
||||
|
||||
@ -260,9 +272,7 @@ class VideoDetailController extends GetxController
|
||||
List supportDecodeFormats =
|
||||
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
|
||||
// 默认从设置中取AVC
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
|
||||
SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code))!;
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!;
|
||||
try {
|
||||
// 当前视频没有对应格式返回第一个
|
||||
bool flag = false;
|
||||
@ -285,7 +295,9 @@ class VideoDetailController extends GetxController
|
||||
} catch (_) {
|
||||
firstVideo = videosList.first;
|
||||
}
|
||||
videoUrl = firstVideo.baseUrl!;
|
||||
videoUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstVideo)
|
||||
: (firstVideo.backupUrl ?? firstVideo.baseUrl!);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast('firstVideo error: $err');
|
||||
}
|
||||
@ -295,8 +307,6 @@ class VideoDetailController extends GetxController
|
||||
List<AudioItem> audiosList = data.dash!.audio!;
|
||||
|
||||
try {
|
||||
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
if (data.dash!.dolby?.audio?.isNotEmpty == true) {
|
||||
// 杜比
|
||||
audiosList.insert(0, data.dash!.dolby!.audio!.first);
|
||||
@ -309,9 +319,9 @@ class VideoDetailController extends GetxController
|
||||
|
||||
if (audiosList.isNotEmpty) {
|
||||
List<int> numbers = audiosList.map((map) => map.id!).toList();
|
||||
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
|
||||
if (!numbers.contains(resultAudioQa) &&
|
||||
numbers.any((e) => e > resultAudioQa)) {
|
||||
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
|
||||
if (!numbers.contains(cacheAudioQa) &&
|
||||
numbers.any((e) => e > cacheAudioQa)) {
|
||||
closestNumber = 30280;
|
||||
}
|
||||
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
|
||||
@ -323,7 +333,9 @@ class VideoDetailController extends GetxController
|
||||
SmartDialog.showToast('firstAudio error: $err');
|
||||
}
|
||||
|
||||
audioUrl = firstAudio.baseUrl ?? '';
|
||||
audioUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstAudio)
|
||||
: (firstAudio.backupUrl ?? firstAudio.baseUrl!);
|
||||
//
|
||||
if (firstAudio.id != null) {
|
||||
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
|
||||
|
@ -8,6 +8,7 @@ import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.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/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
@ -61,12 +62,16 @@ class VideoIntroController extends GetxController {
|
||||
RxString total = '1'.obs;
|
||||
Timer? timer;
|
||||
bool isPaused = false;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
String heroTag = '';
|
||||
late ModelResult modelResult;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
try {
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
} catch (_) {}
|
||||
if (Get.arguments.isNotEmpty) {
|
||||
if (Get.arguments.containsKey('videoItem')) {
|
||||
preRender = true;
|
||||
@ -509,19 +514,7 @@ class VideoIntroController extends GetxController {
|
||||
/// 列表循环或者顺序播放时,自动播放下一个
|
||||
void nextPlay() {
|
||||
late List episodes;
|
||||
// if (videoDetail.value.ugcSeason != null) {
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
bool isPages = false;
|
||||
if (videoDetail.value.ugcSeason != null) {
|
||||
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
|
||||
List<SectionItem> sections = ugcSeason.sections!;
|
||||
@ -531,6 +524,11 @@ class VideoIntroController extends GetxController {
|
||||
List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
episodes.addAll(episodesList);
|
||||
}
|
||||
} else if (videoDetail.value.pages != null) {
|
||||
isPages = true;
|
||||
List<Part> pages = videoDetail.value.pages!;
|
||||
episodes = [];
|
||||
episodes.addAll(pages);
|
||||
}
|
||||
|
||||
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
|
||||
@ -549,9 +547,9 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
}
|
||||
int cid = episodes[nextIndex].cid!;
|
||||
String bvid = episodes[nextIndex].bvid!;
|
||||
int aid = episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(bvid, cid, aid);
|
||||
String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
|
||||
int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(rBvid, cid, rAid);
|
||||
}
|
||||
|
||||
// 设置关注分组
|
||||
@ -561,4 +559,25 @@ class VideoIntroController extends GetxController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/models/video_detail_res.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/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -226,6 +228,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
arguments: {'face': face, 'heroTag': memberHeroTag});
|
||||
}
|
||||
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
enableDrag: true,
|
||||
builder: (BuildContext context) {
|
||||
return AiDetail(modelResult: videoIntroController.modelResult);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData t = Theme.of(context);
|
||||
@ -238,70 +251,91 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (videoIntroController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${videoIntroController.total.value}人在看',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
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
|
||||
// SingleChildScrollView(
|
||||
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
|
@ -92,11 +92,11 @@ class VideoReplyController extends GetxController {
|
||||
}
|
||||
}
|
||||
replies.insertAll(0, res['data'].topReplies);
|
||||
count.value = res['data'].page.count;
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
replyList.addAll(replies);
|
||||
}
|
||||
count.value = res['data'].page.count;
|
||||
}
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
|
@ -669,58 +669,70 @@ InlineSpan buildContent(
|
||||
String matchUrl = matchMember;
|
||||
if (content.jumpUrl.isNotEmpty && hasMatchMember) {
|
||||
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(
|
||||
/// RegExp.escape() 转义特殊字符
|
||||
RegExp(urlKeys.map((key) => key).join("|")),
|
||||
// RegExp(RegExp.escape(urlKeys.join("|"))),
|
||||
// RegExp('What does the fox say\\?'),
|
||||
onMatch: (Match match) {
|
||||
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 =
|
||||
setting.get(SettingBoxKey.enableWordRe, defaultValue: false);
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
String str = Uri.parse(matchStr).pathSegments[0];
|
||||
Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
String str = Uri.parse(matchStr).pathSegments[0];
|
||||
Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
|
@ -3,27 +3,24 @@ import 'dart:io';
|
||||
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.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/models/common/search_type.dart';
|
||||
import 'package:pilipala/pages/bangumi/introduction/index.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/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/app_bar.dart';
|
||||
import 'widgets/header_control.dart';
|
||||
|
||||
class VideoDetailPage extends StatefulWidget {
|
||||
@ -36,7 +33,7 @@ class VideoDetailPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
with TickerProviderStateMixin, RouteAware {
|
||||
with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
|
||||
late VideoDetailController videoDetailController;
|
||||
PlPlayerController? plPlayerController;
|
||||
final ScrollController _extendNestCtr = ScrollController();
|
||||
@ -56,6 +53,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// 自动退出全屏
|
||||
late bool autoExitFullcreen;
|
||||
late bool autoPlayEnable;
|
||||
late bool autoPiP;
|
||||
final floating = Floating();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -63,14 +62,29 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
videoDetailController = Get.put(VideoDetailController(), 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.bangumiDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
videoDetailController.cid.listen((p0) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
bangumiIntroController.bangumiDetail.value, p0);
|
||||
});
|
||||
statusBarHeight = localCache.get('statusBarHeight');
|
||||
autoExitFullcreen =
|
||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||
autoPlayEnable =
|
||||
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
|
||||
autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false);
|
||||
|
||||
videoSourceInit();
|
||||
appbarStreamListen();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
// 获取视频资源,初始化播放器
|
||||
@ -153,6 +167,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
if (videoDetailController.floating != null) {
|
||||
videoDetailController.floating!.dispose();
|
||||
}
|
||||
videoPlayerServiceHandler.onVideoDetailDispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
floating.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -199,6 +216,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
.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
|
||||
Widget build(BuildContext context) {
|
||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||
@ -497,6 +525,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
return PiPSwitcher(
|
||||
childWhenDisabled: childWhenDisabled,
|
||||
childWhenEnabled: childWhenEnabled,
|
||||
floating: floating,
|
||||
);
|
||||
} else {
|
||||
return childWhenDisabled;
|
||||
|
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
Box localCache = GStrorage.localCache;
|
||||
Box videoStorage = GStrorage.video;
|
||||
late List speedsList;
|
||||
double buttonSpace = 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -88,7 +89,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
@ -182,8 +182,8 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
/// 选择倍速
|
||||
void showSetSpeedSheet() {
|
||||
double currentSpeed = widget.controller!.playbackSpeed;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('播放速度'),
|
||||
@ -196,12 +196,20 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
for (var i in speedsList) ...[
|
||||
if (i == currentSpeed) ...[
|
||||
FilledButton(
|
||||
onPressed: () => {setState(() => currentSpeed = i)},
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
] else ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () => {setState(() => currentSpeed = i)},
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
]
|
||||
@ -219,10 +227,10 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await SmartDialog.dismiss();
|
||||
widget.controller!.setPlaybackSpeed(currentSpeed);
|
||||
await widget.controller!.setDefaultSpeed();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
child: const Text('默认速度'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -276,7 +284,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('选择画质', style: titleStyle),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
@ -793,7 +801,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
fuc: () => Get.back(),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.house,
|
||||
@ -838,7 +846,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
if (Platform.isAndroid) ...[
|
||||
SizedBox(
|
||||
width: 34,
|
||||
@ -870,7 +878,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
],
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
@ -888,7 +896,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.sliders,
|
||||
|
@ -3,6 +3,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/plugin/pl_player/index.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/storage.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
@ -73,6 +75,7 @@ class PlPlayerController {
|
||||
|
||||
Rx<bool> videoFitChanged = false.obs;
|
||||
final Rx<BoxFit> _videoFit = Rx(BoxFit.contain);
|
||||
final Rx<String> _videoFitDesc = Rx('包含');
|
||||
|
||||
///
|
||||
// ignore: prefer_final_fields
|
||||
@ -183,6 +186,7 @@ class PlPlayerController {
|
||||
|
||||
/// 视频比例
|
||||
Rx<BoxFit> get videoFit => _videoFit;
|
||||
Rx<String> get videoFitDEsc => _videoFitDesc;
|
||||
|
||||
/// 是否长按倍速
|
||||
Rx<bool> get doubleSpeedStatus => _doubleSpeedStatus;
|
||||
@ -214,6 +218,8 @@ class PlPlayerController {
|
||||
late double fontSizeVal;
|
||||
late double danmakuSpeedVal;
|
||||
late List speedsList;
|
||||
// 缓存
|
||||
double? defaultDuration;
|
||||
|
||||
// 播放顺序相关
|
||||
PlayRepeat playRepeat = PlayRepeat.pause;
|
||||
@ -264,14 +270,6 @@ class PlPlayerController {
|
||||
// 获取实例 传参
|
||||
static PlPlayerController getInstance({
|
||||
String videoType = 'archive',
|
||||
List<BoxFit> fits = const [
|
||||
BoxFit.contain,
|
||||
BoxFit.cover,
|
||||
BoxFit.fill,
|
||||
BoxFit.fitHeight,
|
||||
BoxFit.fitWidth,
|
||||
BoxFit.scaleDown
|
||||
],
|
||||
}) {
|
||||
// 如果实例尚未创建,则创建一个新实例
|
||||
_instance ??= PlPlayerController._();
|
||||
@ -324,6 +322,9 @@ class PlPlayerController {
|
||||
await pause(notify: false);
|
||||
}
|
||||
|
||||
if (_playerCount.value == 0) {
|
||||
return;
|
||||
}
|
||||
// 配置Player 音轨、字幕等等
|
||||
_videoPlayerController = await _createVideoController(
|
||||
dataSource, _looping, enableHA, width, height);
|
||||
@ -332,12 +333,11 @@ class PlPlayerController {
|
||||
// 数据加载完成
|
||||
dataStatus.status.value = DataStatus.loaded;
|
||||
|
||||
await _initializePlayer(seekTo: seekTo);
|
||||
|
||||
// listen the video player events
|
||||
if (!_listenersInitialized) {
|
||||
startListeners();
|
||||
}
|
||||
await _initializePlayer(seekTo: seekTo);
|
||||
bool autoEnterFullcreen =
|
||||
setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
|
||||
if (autoEnterFullcreen && _isFirstTime) {
|
||||
@ -379,6 +379,10 @@ class PlPlayerController {
|
||||
var pp = player.platform as NativePlayer;
|
||||
// 解除倍速限制
|
||||
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) {
|
||||
await pp.setProperty(
|
||||
@ -407,6 +411,7 @@ class PlPlayerController {
|
||||
player,
|
||||
configuration: VideoControllerConfiguration(
|
||||
enableHardwareAcceleration: enableHA,
|
||||
androidAttachSurfaceAfterVideoParameters: false,
|
||||
),
|
||||
);
|
||||
|
||||
@ -437,22 +442,22 @@ class PlPlayerController {
|
||||
Future _initializePlayer({
|
||||
Duration seekTo = Duration.zero,
|
||||
}) async {
|
||||
// 跳转播放
|
||||
if (seekTo != Duration.zero) {
|
||||
await this.seekTo(seekTo);
|
||||
}
|
||||
|
||||
// 设置倍速
|
||||
if (_playbackSpeed.value != 1.0) {
|
||||
await setPlaybackSpeed(_playbackSpeed.value);
|
||||
} else {
|
||||
await setPlaybackSpeed(1.0);
|
||||
}
|
||||
|
||||
getVideoFit();
|
||||
// if (_looping) {
|
||||
// await setLooping(_looping);
|
||||
// }
|
||||
|
||||
// 跳转播放
|
||||
if (seekTo != Duration.zero) {
|
||||
await this.seekTo(seekTo);
|
||||
}
|
||||
|
||||
// 自动播放
|
||||
if (_autoPlay) {
|
||||
await play();
|
||||
@ -515,12 +520,24 @@ class PlPlayerController {
|
||||
}),
|
||||
videoPlayerController!.stream.buffering.listen((event) {
|
||||
isBuffering.value = event;
|
||||
videoPlayerServiceHandler.onStatusChange(
|
||||
playerStatus.status.value, event);
|
||||
}),
|
||||
// videoPlayerController!.stream.volume.listen((event) {
|
||||
// if (!mute.value && _volumeBeforeMute != event) {
|
||||
// _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();
|
||||
// }
|
||||
} else {
|
||||
print('seek duration else');
|
||||
_timerForSeek?.cancel();
|
||||
_timerForSeek =
|
||||
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
||||
//_timerForSeek = null;
|
||||
if (duration.value.inSeconds != 0) {
|
||||
await _videoPlayerController!.stream.buffer.first;
|
||||
await _videoPlayerController?.seek(position);
|
||||
// if (playerStatus.stopped) {
|
||||
// if (playerStatus.status.value == PlayerStatus.paused) {
|
||||
// play();
|
||||
// }
|
||||
t.cancel();
|
||||
//_timerForSeek = null;
|
||||
_timerForSeek = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -573,28 +592,41 @@ class PlPlayerController {
|
||||
await _videoPlayerController?.setRate(speed);
|
||||
try {
|
||||
DanmakuOption currentOption = danmakuController!.option;
|
||||
defaultDuration ??= currentOption.duration;
|
||||
DanmakuOption updatedOption = currentOption.copyWith(
|
||||
duration: (currentOption.duration / speed) * playbackSpeed);
|
||||
duration: (defaultDuration! / speed) * playbackSpeed);
|
||||
danmakuController!.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
// fix 长按倍速后放开不恢复
|
||||
// _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]);
|
||||
if (!doubleSpeedStatus.value) {
|
||||
_playbackSpeed.value = speed;
|
||||
}
|
||||
}
|
||||
|
||||
// 还原默认速度
|
||||
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 {
|
||||
// 播放时自动隐藏控制条
|
||||
controls = !hideControls;
|
||||
// repeat为true,将从头播放
|
||||
if (repeat) {
|
||||
await seekTo(Duration.zero);
|
||||
@ -606,17 +638,18 @@ class PlPlayerController {
|
||||
|
||||
playerStatus.status.value = PlayerStatus.playing;
|
||||
// screenManager.setOverlays(false);
|
||||
|
||||
// 播放时自动隐藏控制条
|
||||
if (hideControls) {
|
||||
_hideTaskControls();
|
||||
}
|
||||
audioSessionHandler.setActive(true);
|
||||
}
|
||||
|
||||
/// 暂停播放
|
||||
Future<void> pause({bool notify = true}) async {
|
||||
Future<void> pause({bool notify = true, bool isInterrupt = false}) async {
|
||||
await _videoPlayerController?.pause();
|
||||
playerStatus.status.value = PlayerStatus.paused;
|
||||
|
||||
// 主动暂停时让出音频焦点
|
||||
if (!isInterrupt) {
|
||||
audioSessionHandler.setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更改播放状态
|
||||
@ -725,44 +758,61 @@ class PlPlayerController {
|
||||
|
||||
/// Toggle Change the videofit accordingly
|
||||
void toggleVideoFit() {
|
||||
videoFitChangedTimer?.cancel();
|
||||
videoFitChanged.value = true;
|
||||
// 范围内
|
||||
List attrs = videoFitType.map((e) => e['attr']).toList();
|
||||
if (attrs.indexOf(_videoFit.value) < attrs.length - 1) {
|
||||
int index = attrs.indexOf(_videoFit.value);
|
||||
_videoFit.value = attrs[index + 1];
|
||||
print(videoFitType[index + 1]['desc']);
|
||||
SmartDialog.showToast(videoFitType[index + 1]['desc']);
|
||||
} else {
|
||||
// 默认 contain
|
||||
_videoFit.value = videoFitType.first['attr'];
|
||||
SmartDialog.showToast(videoFitType.first['desc']);
|
||||
}
|
||||
videoFitChangedTimer = Timer(const Duration(seconds: 1), () {
|
||||
videoFitChangedTimer = null;
|
||||
videoFitChanged.value = false;
|
||||
});
|
||||
print(_videoFit.value);
|
||||
}
|
||||
|
||||
/// Change Video Fit accordingly
|
||||
void onVideoFitChange(BoxFit fit) {
|
||||
_videoFit.value = fit;
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('画面比例'),
|
||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (var i in videoFitType) ...[
|
||||
if (_videoFit.value == i['attr']) ...[
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
_videoFit.value = i['attr'];
|
||||
_videoFitDesc.value = i['desc'];
|
||||
setVideoFit();
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i['desc']),
|
||||
),
|
||||
] else ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
_videoFit.value = i['attr'];
|
||||
_videoFitDesc.value = i['desc'];
|
||||
setVideoFit();
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i['desc']),
|
||||
),
|
||||
]
|
||||
]
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 缓存fit
|
||||
// Future<void> setVideoFit() async {
|
||||
// videoStorage.put(VideoBoxKey.videoBrightness, _videoFit.value.name);
|
||||
// }
|
||||
Future<void> setVideoFit() async {
|
||||
List attrs = videoFitType.map((e) => e['attr']).toList();
|
||||
int index = attrs.indexOf(_videoFit.value);
|
||||
videoStorage.put(VideoBoxKey.cacheVideoFit, index);
|
||||
}
|
||||
|
||||
/// 读取fit
|
||||
// Future<void> getVideoFit() async {
|
||||
// String fitValue =
|
||||
// videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain');
|
||||
// _videoFit.value = videoFitType
|
||||
// .firstWhere((element) => element['attr'] == fitValue)['attr'];
|
||||
// }
|
||||
Future<void> getVideoFit() async {
|
||||
int fitValue = videoStorage.get(VideoBoxKey.cacheVideoFit, defaultValue: 0);
|
||||
_videoFit.value = videoFitType[fitValue]['attr'];
|
||||
_videoFitDesc.value = videoFitType[fitValue]['desc'];
|
||||
}
|
||||
|
||||
/// 读取亮度
|
||||
// Future<void> getVideoBrightness() async {
|
||||
@ -795,6 +845,7 @@ class PlPlayerController {
|
||||
if (val) {
|
||||
setPlaybackSpeed(longPressSpeed);
|
||||
} else {
|
||||
print(playbackSpeed);
|
||||
setPlaybackSpeed(playbackSpeed);
|
||||
}
|
||||
}
|
||||
@ -980,12 +1031,15 @@ class PlPlayerController {
|
||||
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
|
||||
localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal);
|
||||
|
||||
var pp = _videoPlayerController!.platform as NativePlayer;
|
||||
await pp.setProperty('audio-files', '');
|
||||
removeListeners();
|
||||
await _videoPlayerController?.dispose();
|
||||
_videoPlayerController = null;
|
||||
_instance = null;
|
||||
// 关闭所有视频页面恢复亮度
|
||||
resetBrightness();
|
||||
videoPlayerServiceHandler.clear();
|
||||
} catch (err) {
|
||||
print(err);
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
late int defaultBtmProgressBehavior;
|
||||
late bool enableQuickDouble;
|
||||
late bool enableBackgroundPlay;
|
||||
late double screenWidth;
|
||||
|
||||
void onDoubleTapSeekBackward() {
|
||||
_ctr.onDoubleTapSeekBackward();
|
||||
@ -116,6 +117,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
screenWidth = Get.size.width;
|
||||
animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 300));
|
||||
videoController = widget.controller.videoController!;
|
||||
@ -128,7 +130,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true);
|
||||
enableBackgroundPlay =
|
||||
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
FlutterVolumeController.showSystemUI = true;
|
||||
@ -217,6 +218,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
controller: videoController,
|
||||
controls: NoVideoControls,
|
||||
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
|
||||
resumeUponEnteringForegroundMode: true,
|
||||
subtitleViewConfiguration: SubtitleViewConfiguration(
|
||||
style: subTitleStyle,
|
||||
textAlign: TextAlign.center,
|
||||
@ -508,7 +510,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
}
|
||||
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);
|
||||
setBrightness(result);
|
||||
} else if (tapPosition < sectionWidth * 2) {
|
||||
@ -531,7 +537,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
_distance = dy;
|
||||
} 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);
|
||||
setVolume(result);
|
||||
}
|
||||
|
@ -115,15 +115,22 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
Icons.settings_overscan_outlined,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () => _.toggleVideoFit(),
|
||||
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(
|
||||
() => ComBtn(
|
||||
@ -139,7 +146,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -19,6 +19,7 @@ import 'package:pilipala/pages/hot/index.dart';
|
||||
import 'package:pilipala/pages/html/index.dart';
|
||||
import 'package:pilipala/pages/later/index.dart';
|
||||
import 'package:pilipala/pages/liveRoom/view.dart';
|
||||
import 'package:pilipala/pages/login/index.dart';
|
||||
import 'package:pilipala/pages/member/index.dart';
|
||||
import 'package:pilipala/pages/member_search/index.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
@ -129,6 +130,8 @@ class Routes {
|
||||
// 私信详情
|
||||
CustomGetPage(
|
||||
name: '/whisperDetail', page: () => const WhisperDetailPage()),
|
||||
// 登录页面
|
||||
CustomGetPage(name: '/loginPage', page: () => const LoginPage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
180
lib/services/audio_handler.dart
Normal file
180
lib/services/audio_handler.dart
Normal 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,
|
||||
));
|
||||
}
|
||||
}
|
53
lib/services/audio_session.dart
Normal file
53
lib/services/audio_session.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
11
lib/services/service_locator.dart
Normal file
11
lib/services/service_locator.dart
Normal 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();
|
||||
}
|
@ -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:get/get.dart';
|
||||
import 'package:pilipala/pages/dynamics/index.dart';
|
||||
import 'package:pilipala/pages/home/index.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
import 'package:pilipala/pages/mine/index.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class LoginUtils {
|
||||
static Future refreshLoginStatus(bool status) async {
|
||||
@ -27,4 +32,29 @@ class LoginUtils {
|
||||
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
28
lib/utils/proxy.dart
Normal 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;";
|
||||
};
|
||||
}
|
||||
}
|
@ -105,6 +105,8 @@ class SettingBoxKey {
|
||||
static const String enableAutoEnter = 'enableAutoEnter';
|
||||
static const String enableAutoExit = 'enableAutoExit';
|
||||
static const String p1080 = 'p1080';
|
||||
static const String enableCDN = 'enableCDN';
|
||||
static const String autoPiP = 'autoPiP';
|
||||
|
||||
// youtube 双击快进快退
|
||||
static const String enableQuickDouble = 'enableQuickDouble';
|
||||
@ -124,6 +126,7 @@ class SettingBoxKey {
|
||||
static const String enableSearchWord = 'enableSearchWord';
|
||||
static const String enableRcmdDynamic = 'enableRcmdDynamic';
|
||||
static const String enableSaveLastData = 'enableSaveLastData';
|
||||
static const String enableSystemProxy = 'enableSystemProxy';
|
||||
|
||||
/// 外观
|
||||
static const String themeMode = 'themeMode';
|
||||
@ -134,6 +137,7 @@ class SettingBoxKey {
|
||||
static const String enableSingleRow = 'enableSingleRow'; // 首页单列
|
||||
static const String displayMode = 'displayMode';
|
||||
static const String customRows = 'customRows'; // 自定义列
|
||||
static const String enableMYBar = 'enableMYBar';
|
||||
}
|
||||
|
||||
class LocalCacheKey {
|
||||
@ -152,6 +156,10 @@ class LocalCacheKey {
|
||||
static const String danmakuOpacity = 'danmakuOpacity';
|
||||
static const String danmakuFontScale = 'danmakuFontScale';
|
||||
static const String danmakuSpeed = 'danmakuSpeed';
|
||||
|
||||
// 代理host port
|
||||
static const String systemProxyHost = 'systemProxyHost';
|
||||
static const String systemProxyPort = 'systemProxyPort';
|
||||
}
|
||||
|
||||
class VideoBoxKey {
|
||||
@ -169,4 +177,6 @@ class VideoBoxKey {
|
||||
static const String longPressSpeedDefault = 'longPressSpeedDefault';
|
||||
// 自定义倍速集合
|
||||
static const String customSpeedsList = 'customSpeedsList';
|
||||
// 画面填充比例
|
||||
static const String cacheVideoFit = 'cacheVideoFit';
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
36
lib/utils/video_utils.dart
Normal file
36
lib/utils/video_utils.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import audio_service
|
||||
import audio_session
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import dynamic_color
|
||||
@ -20,6 +22,8 @@ import url_launcher_macos
|
||||
import wakelock_plus
|
||||
|
||||
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"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||
|
170
pubspec.lock
170
pubspec.lock
@ -21,10 +21,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animations
|
||||
sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164
|
||||
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
appscheme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -57,14 +65,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_video_progress_bar
|
||||
sha256: "67f3a5ea70d48b48caaf29f5a0606284a6aa3a393736daf9e82bec985d2f9b70"
|
||||
sha256: "3384875247cdbea748bd9ae8330631cd06a6cabfcda4945d45c9b406da92bc66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "2.0.1"
|
||||
auto_orientation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -149,26 +189,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
|
||||
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
version: "3.3.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
|
||||
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "3.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
|
||||
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -325,18 +365,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.2"
|
||||
version: "5.3.3"
|
||||
dio_cookie_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio_cookie_manager
|
||||
sha256: c4b7a693aa09efd694a5c5e12065daa5e026647b106245281ed1042b3ebefb8f
|
||||
sha256: e79498b0f632897ff0c28d6e8178b4bc6e9087412401f618c31fa0904ace050d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
dio_http2_adapter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -357,10 +397,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
|
||||
sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.6"
|
||||
version: "1.6.8"
|
||||
easy_debounce:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -369,6 +409,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -454,14 +502,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -519,10 +559,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_smart_dialog
|
||||
sha256: "8ba9eeb5b0b380bec368c5c8a324e1dab0cd88965c2dd83e64237441140bc599"
|
||||
sha256: "8ffa51d55591227dbfe9fc2b1ff396b37bec7d09c241d875b9b932db99d2d5ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.3+2"
|
||||
version: "4.9.4"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -540,10 +580,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_volume_controller
|
||||
sha256: "7f88cb046b00fd80e98bcb7926b9e3879f004f30905109fdf6c5d09b8d28eb2e"
|
||||
sha256: "1161957826183b46916adb4f1c9f91befce0d8415bd3fcd781f7faed9df62d46"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.7"
|
||||
version: "1.3.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -589,6 +629,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -761,18 +809,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: d652c2bdb0cd876bf1046e24d0b614651fefe59f7c3a2d9b7ed57217b9e7db94
|
||||
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.8+1"
|
||||
version: "1.1.10+1"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: a7ef60926ac528e2fabe9ee7084e648e385422a881ba914c978a7a81e6595dee
|
||||
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
version: "1.3.6"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -801,10 +849,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_video
|
||||
sha256: f130964bd4c0907d0af645ba03c8080a914776bfd2e23761a5e22ac3c0c0906a
|
||||
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.4"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -825,10 +873,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: b1a427f0540c5f052dfab73e4b76a5eb8efa7ebb5d83179cb23fc3932afc315a
|
||||
sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -866,10 +914,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
|
||||
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "2.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -914,66 +962,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.1.1"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.2.0"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.3.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.2.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
|
||||
sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.3"
|
||||
version: "11.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221"
|
||||
sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.3"
|
||||
version: "11.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -986,10 +1034,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
version: "3.12.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1098,10 +1146,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: screen_brightness
|
||||
sha256: "62fd61a64e68b32b98b840bad7d8b6822bbc40e63c2b569a5f85528484c86b41"
|
||||
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.2.2+1"
|
||||
screen_brightness_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1335,10 +1383,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.12"
|
||||
version: "6.1.14"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1396,7 +1444,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
|
40
pubspec.yaml
40
pubspec.yaml
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.10
|
||||
version: 1.0.11
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.6 <3.0.0"
|
||||
@ -36,31 +36,31 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.5
|
||||
# 动态取色
|
||||
dynamic_color: ^1.6.6
|
||||
dynamic_color: ^1.6.8
|
||||
|
||||
get: ^4.6.5
|
||||
|
||||
# 网络
|
||||
dio: ^5.3.0
|
||||
dio: ^5.3.3
|
||||
cookie_jar: ^4.0.8
|
||||
dio_cookie_manager: ^3.1.0
|
||||
dio_cookie_manager: ^3.1.1
|
||||
connectivity_plus: ^4.0.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
|
||||
saver_gallery: ^2.0.1
|
||||
|
||||
# 存储
|
||||
path_provider: ^2.0.14
|
||||
path_provider: ^2.1.1
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
|
||||
# 设备信息
|
||||
device_info_plus: ^9.0.2
|
||||
# 权限
|
||||
permission_handler: ^10.4.3
|
||||
permission_handler: ^11.0.1
|
||||
# 分享
|
||||
share_plus: ^7.0.2
|
||||
# cookie 管理
|
||||
@ -76,32 +76,37 @@ dependencies:
|
||||
# 图标
|
||||
font_awesome_flutter: ^10.4.0
|
||||
# toast
|
||||
flutter_smart_dialog: ^4.9.3+2
|
||||
flutter_smart_dialog: ^4.9.4
|
||||
# 下滑关闭
|
||||
dismissible_page: ^1.0.2
|
||||
custom_sliding_segmented_control: ^1.7.5
|
||||
# 加密
|
||||
crypto: ^3.0.3
|
||||
encrypt: ^5.0.3
|
||||
|
||||
# 视频播放器
|
||||
media_kit: ^1.1.8 # Primary package.
|
||||
media_kit_video: ^1.2.1 # For video rendering.
|
||||
media_kit_libs_video: ^1.0.3
|
||||
media_kit: ^1.1.10 # Primary package.
|
||||
media_kit_video: ^1.2.4 # For video rendering.
|
||||
media_kit_libs_video: ^1.0.4
|
||||
|
||||
# 媒体通知
|
||||
audio_service: ^0.18.12
|
||||
audio_session: ^0.1.16
|
||||
|
||||
# 音量、亮度、屏幕控制
|
||||
flutter_volume_controller: ^1.2.7
|
||||
screen_brightness: ^0.2.2
|
||||
flutter_volume_controller: ^1.3.0
|
||||
screen_brightness: ^0.2.2+1
|
||||
wakelock_plus: ^1.1.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
|
||||
protobuf: ^3.0.0
|
||||
animations: ^2.0.7
|
||||
animations: ^2.0.8
|
||||
|
||||
# 获取appx信息
|
||||
package_info_plus: ^4.1.0
|
||||
url_launcher: ^6.1.12
|
||||
url_launcher: ^6.1.14
|
||||
flutter_svg: ^2.0.7
|
||||
# 防抖节流
|
||||
easy_debounce: ^2.0.3
|
||||
@ -124,6 +129,9 @@ dependencies:
|
||||
html: ^0.15.4
|
||||
# html渲染
|
||||
flutter_html: ^3.0.0-beta.2
|
||||
# 极验
|
||||
gt3_flutter_plugin: ^0.0.8
|
||||
uuid: ^3.0.7
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
Reference in New Issue
Block a user