Merge branch 'main' into mod-add-time-in-rcmd-and-search

This commit is contained in:
guozhigq
2024-02-16 11:42:01 +08:00
committed by GitHub
99 changed files with 3446 additions and 1671 deletions

View File

@@ -1,84 +1,157 @@
name: build_apk name: Pilipala Release
# action事件触发 # action事件触发
on: on:
push: push:
# push tag时触发 # push tag时触发
tags: tags:
- 'v*.*.*' - "v*.*.*"
# 可以有多个jobs # 可以有多个jobs
jobs: jobs:
build_apk: android:
# 运行环境 ubuntu-latest window-latest mac-latest # 运行环境 ubuntu-latest window-latest mac-latest
runs-on: ubuntu-latest runs-on: ubuntu-latest
# 每个jobs中可以有多个steps # 每个jobs中可以有多个steps
steps: steps:
- name: 代码迁出 - name: 代码迁出
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 构建Java环境 - name: 构建Java环境
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "17" java-version: "17"
token: ${{secrets.GIT_TOKEN}} token: ${{secrets.GIT_TOKEN}}
- name: 检查缓存 - name: 检查缓存
uses: actions/cache@v2 uses: actions/cache@v2
id: cache-flutter id: cache-flutter
with: with:
path: /root/flutter-sdk # Flutter SDK 的路径 path: /root/flutter-sdk # Flutter SDK 的路径
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
- name: 安装Flutter - name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.16.5 flutter-version: 3.16.5
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
run: flutter pub get run: flutter pub get
- name: 解码生成 jks - name: 解码生成 jks
run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks
env: env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
- name: flutter build apk - name: flutter build apk
# 对应 android/app/build.gradle signingConfigs中的配置项 run: flutter build apk --release --split-per-abi
run: flutter build apk --release --split-per-abi env:
env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
- name: 获取版本号 - name: flutter build apk
id: version run: flutter build apk --release
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
# - name: 获取当前日期 - name: 获取版本号
# id: date id: version
# run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk # - name: 获取当前日期
run: | # id: date
# DATE=${{ steps.date.outputs.date }} # run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
if [[ $file =~ app-(.*)-release.apk ]]; then
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk"
mv "$file" "$new_file_name"
fi
done
- name: 构建和发布release - name: 重命名应用
uses: ncipollo/release-action@v1 run: |
with: # DATE=${{ steps.date.outputs.date }}
# release title for file in build/app/outputs/flutter-apk/app-*.apk; do
name: v${{ steps.version.outputs.version }} if [[ $file =~ app-(.?*)release.apk ]]; then
artifacts: "build/app/outputs/flutter-apk/Pili-*.apk" new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}${{ steps.version.outputs.version }}.apk"
bodyFile: "change_log/${{steps.version.outputs.version}}.md" mv "$file" "$new_file_name"
token: ${{ secrets.GIT_TOKEN }} fi
allowUpdates: true done
- name: 上传
uses: actions/upload-artifact@v3
with:
name: Pilipala-Release
path: |
build/app/outputs/flutter-apk/Pili-*.apk
iOS:
runs-on: macos-latest
steps:
- name: 代码迁出
uses: actions/checkout@v4
- name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2.10.0
with:
cache: true
flutter-version: 3.16.5
- name: flutter build ipa
run: |
flutter build ios --release --no-codesign
ln -sf ./build/ios/iphoneos Payload
zip -r9 app.ipa Payload/runner.app
- name: 获取版本号
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: 重命名应用
run: |
DATE=${{ steps.date.outputs.date }}
for file in app.ipa; do
new_file_name="build/Pili-${{ steps.version.outputs.version }}.ipa"
mv "$file" "$new_file_name"
done
- name: 上传
uses: actions/upload-artifact@v3
with:
if-no-files-found: error
name: Pilipala-Release
path: |
build/Pili-*.ipa
upload:
runs-on: ubuntu-latest
needs:
- android
- iOS
steps:
- uses: actions/download-artifact@v3
with:
name: Pilipala-Release
path: ./Pilipala-Release
- name: Install dependencies
run: sudo apt-get install tree -y
- name: Get version
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: Upload Release
uses: ncipollo/release-action@v1
with:
name: v${{ steps.version.outputs.version }}
token: ${{ secrets.GIT_TOKEN }}
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
omitPrereleaseDuringUpdate: true
allowUpdates: true
artifacts: Pilipala-Release/*

View File

@@ -58,11 +58,10 @@ android {
applicationId "com.guozhigq.pilipala" applicationId "com.guozhigq.pilipala"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
// minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
minSdkVersion 19 minSdkVersion 21
multiDexEnabled true multiDexEnabled true
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

16
change_log/1.0.18.0130.md Normal file
View File

@@ -0,0 +1,16 @@
## 1.0.18
### 功能
### 修复
### 优化
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

15
change_log/1.0.19.0131.md Normal file
View File

@@ -0,0 +1,15 @@
## 1.0.19
### 修复
+ 视频404、评论加载错误
+ bvav转换
### 优化
+ 视频详情页内存占用
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -13,8 +13,13 @@ PODS:
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_mailer (0.0.1):
- Flutter
- flutter_volume_controller (0.0.1): - flutter_volume_controller (0.0.1):
- Flutter - Flutter
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
@@ -49,6 +54,7 @@ PODS:
- Flutter - Flutter
- system_proxy (0.0.1): - system_proxy (0.0.1):
- Flutter - Flutter
- Toast (4.1.0)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1): - volume_controller (0.0.1):
@@ -68,7 +74,9 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@@ -93,6 +101,7 @@ SPEC REPOS:
- FMDB - FMDB
- GT3Captcha-iOS - GT3Captcha-iOS
- ReachabilitySwift - ReachabilitySwift
- Toast
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: appscheme:
@@ -109,8 +118,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_mailer:
:path: ".symlinks/plugins/flutter_mailer/ios"
flutter_volume_controller: flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios" :path: ".symlinks/plugins/flutter_volume_controller/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin: gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios" :path: ".symlinks/plugins/gt3_flutter_plugin/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
@@ -156,7 +169,9 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23 gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
@@ -173,6 +188,7 @@ SPEC CHECKSUMS:
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44 system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47

View File

@@ -22,20 +22,27 @@ class HttpError extends StatelessWidget {
"assets/images/error.svg", "assets/images/error.svg",
height: 200, height: 200,
), ),
const SizedBox(height: 20), const SizedBox(height: 30),
Text( Text(
errMsg ?? '请求异常', errMsg ?? '请求异常',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 30), const SizedBox(height: 20),
OutlinedButton.icon( FilledButton.tonal(
onPressed: () { onPressed: () {
fn!(); fn!();
}, },
icon: const Icon(Icons.arrow_forward_outlined, size: 20), style: ButtonStyle(
label: Text(btnText ?? '点击重试'), backgroundColor: MaterialStateProperty.resolveWith((states) {
) return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),
child: Text(
btnText ?? '点击重试',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
], ],
), ),
), ),

View File

@@ -324,8 +324,9 @@ class VideoContent extends StatelessWidget {
reSrc: 11, reSrc: 11,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast( SmartDialog.showToast(res['code'] == 0
res['msg'] ?? '成功'); ? '成功'
: res['msg']);
}, },
child: const Text('确认'), child: const Text('确认'),
) )

View File

@@ -161,12 +161,12 @@ class VideoCardV extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
if (videoItem.duration != null) if (videoItem.duration > 0)
if (crossAxisCount == 1) ...[ if (crossAxisCount == 1) ...[
PBadge( PBadge(
bottom: 10, bottom: 10,
right: 10, right: 10,
text: videoItem.duration, text: Utils.timeFormat(videoItem.duration),
) )
] else ...[ ] else ...[
PBadge( PBadge(
@@ -174,7 +174,7 @@ class VideoCardV extends StatelessWidget {
right: 7, right: 7,
size: 'small', size: 'small',
type: 'gray', type: 'gray',
text: videoItem.duration, text: Utils.timeFormat(videoItem.duration),
) )
], ],
], ],

View File

@@ -214,6 +214,9 @@ class Api {
// https://api.bilibili.com/x/relation/tags // https://api.bilibili.com/x/relation/tags
static const String followingsClass = '/x/relation/tags'; static const String followingsClass = '/x/relation/tags';
// 搜索follow
static const followSearch = '/x/relation/followings/search';
// 粉丝 // 粉丝
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc // vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention // order_type 排序规则 最近访问传空,最常访问传 attention
@@ -230,6 +233,10 @@ class Api {
static const String liveRoomInfo = static const String liveRoomInfo =
'${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo'; '${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo';
// 直播间详情 H5
static const String liveRoomInfoH5 =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getH5InfoByRoom';
// 用户信息 需要Wbi签名 // 用户信息 需要Wbi签名
// https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482 // https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482
static const String memberInfo = '/x/space/wbi/acc/info'; static const String memberInfo = '/x/space/wbi/acc/info';

View File

@@ -8,6 +8,7 @@ import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart'; // import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/utils.dart'; import '../utils/utils.dart';
import 'constants.dart'; import 'constants.dart';
@@ -77,10 +78,11 @@ class Request {
static setOptionsHeaders(userInfo, bool status) { static setOptionsHeaders(userInfo, bool status) {
if (status) { if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(userInfo.mid);
} }
dio.options.headers['env'] = 'prod'; dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64'; dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
dio.options.headers['x-bili-aurora-zone'] = 'sh001'; dio.options.headers['x-bili-aurora-zone'] = 'sh001';
dio.options.headers['referer'] = 'https://www.bilibili.com/'; dio.options.headers['referer'] = 'https://www.bilibili.com/';
} }
@@ -177,8 +179,14 @@ class Request {
); );
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
print('get error: $e'); Response errResponse = Response(
return Future.error(await ApiInterceptor.dioError(e)); data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200,
requestOptions: RequestOptions(),
);
return errResponse;
} }
} }
@@ -199,8 +207,14 @@ class Request {
// print('post success: ${response.data}'); // print('post success: ${response.data}');
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
print('post error: $e'); Response errResponse = Response(
return Future.error(await ApiInterceptor.dioError(e)); data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200,
requestOptions: RequestOptions(),
);
return errResponse;
} }
} }

View File

@@ -5,7 +5,6 @@ import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
// import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@@ -71,35 +70,28 @@ class ApiInterceptor extends Interceptor {
return '发送请求超时,请检查网络设置'; return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown: case DioExceptionType.unknown:
final String res = await checkConnect(); final String res = await checkConnect();
return '$res \n 网络异常,请稍后重试'; return '$res,网络异常';
// default:
// return 'Dio异常';
} }
} }
static Future<String> checkConnect() async { static Future<String> checkConnect() async {
final ConnectivityResult connectivityResult = final ConnectivityResult connectivityResult =
await Connectivity().checkConnectivity(); await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.mobile) { switch (connectivityResult) {
return 'connected with mobile network'; case ConnectivityResult.mobile:
} else if (connectivityResult == ConnectivityResult.wifi) { return '正在使用移动流量';
return 'connected with wifi network'; case ConnectivityResult.wifi:
} else if (connectivityResult == ConnectivityResult.ethernet) { return '正在使用wifi';
// I am connected to a ethernet network. case ConnectivityResult.ethernet:
return ''; return '正在使用局域网';
} else if (connectivityResult == ConnectivityResult.vpn) { case ConnectivityResult.vpn:
// I am connected to a vpn network. return '正在使用代理网络';
// Note for iOS and macOS: case ConnectivityResult.other:
// There is no separate network interface type for [vpn]. return '正在使用其他网络';
// It returns [other] on any device (also simulator) case ConnectivityResult.none:
return ''; return '未连接到任何网络';
} else if (connectivityResult == ConnectivityResult.other) { default:
// I am connected to a network which is not in the above mentioned networks. return '';
return '';
} else if (connectivityResult == ConnectivityResult.none) {
return 'not connected to any network';
} else {
return '';
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import '../models/live/item.dart'; import '../models/live/item.dart';
import '../models/live/room_info.dart'; import '../models/live/room_info.dart';
import '../models/live/room_info_h5.dart';
import 'api.dart'; import 'api.dart';
import 'init.dart'; import 'init.dart';
@@ -46,4 +47,22 @@ class LiveHttp {
}; };
} }
} }
static Future liveRoomInfoH5({roomId, qn}) async {
var res = await Request().get(Api.liveRoomInfoH5, data: {
'room_id': roomId,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': RoomInfoH5Model.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@@ -461,4 +461,41 @@ class MemberHttp {
}; };
} }
} }
// 搜索follow
static Future getfollowSearch({
required int mid,
required int ps,
required int pn,
required String name,
}) async {
Map<String, dynamic> data = {
'vmid': mid,
'pn': pn,
'ps': ps,
'order': 'desc',
'order_type': 'attention',
'gaia_source': 'main_web',
'name': name,
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
var res = await Request().get(Api.followSearch, data: {
...data,
'w_rid': params['w_rid'],
'wts': params['wts'],
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': FollowDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@@ -9,6 +9,7 @@ import '../models/user/fav_folder.dart';
import '../models/video/ai.dart'; import '../models/video/ai.dart';
import '../models/video/play/url.dart'; import '../models/video/play/url.dart';
import '../models/video_detail_res.dart'; import '../models/video_detail_res.dart';
import '../utils/recommend_filter.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
import 'api.dart'; import 'api.dart';
@@ -46,8 +47,13 @@ class VideoHttp {
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['item']) { for (var i in res.data['data']['item']) {
//过滤掉live与ad以及拉黑用户 //过滤掉live与ad以及拉黑用户
if (i['goto'] == 'av' && !blackMidsList.contains(i['owner']['mid'])) { if (i['goto'] == 'av' &&
list.add(RecVideoItemModel.fromJson(i)); (i['owner'] != null &&
!blackMidsList.contains(i['owner']['mid']))) {
RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
} }
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
@@ -59,7 +65,9 @@ class VideoHttp {
} }
} }
static Future rcmdVideoListApp({int? ps, required int freshIdx}) async { // 添加额外的loginState变量模拟未登录状态
static Future rcmdVideoListApp(
{bool loginStatus = true, required int freshIdx}) async {
try { try {
var res = await Request().get( var res = await Request().get(
Api.recommendListApp, Api.recommendListApp,
@@ -72,9 +80,11 @@ class VideoHttp {
'device_name': 'vivo', 'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false', 'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey, 'appkey': Constants.appKey,
'access_key': localCache 'access_key': loginStatus
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ?? ? (localCache.get(LocalCacheKey.accessKey,
'' defaultValue: {})['value'] ??
'')
: ''
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@@ -87,12 +97,15 @@ class VideoHttp {
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null && (i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) { !blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i)); RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
} }
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
return {'status': false, 'data': [], 'msg': ''}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} catch (err) { } catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()}; return {'status': false, 'data': [], 'msg': err.toString()};
@@ -117,7 +130,7 @@ class VideoHttp {
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
return {'status': false, 'data': []}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} catch (err) { } catch (err) {
return {'status': false, 'data': [], 'msg': err}; return {'status': false, 'data': [], 'msg': err};
@@ -203,7 +216,10 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<HotVideoItemModel> list = []; List<HotVideoItemModel> list = [];
for (var i in res.data['data']) { for (var i in res.data['data']) {
list.add(HotVideoItemModel.fromJson(i)); HotVideoItemModel videoItem = HotVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem, relatedVideos: true)) {
list.add(videoItem);
}
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
@@ -224,10 +240,11 @@ class VideoHttp {
// 获取投币状态 // 获取投币状态
static Future hasCoinVideo({required String bvid}) async { static Future hasCoinVideo({required String bvid}) async {
var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid}); var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
print('res: $res');
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@@ -305,7 +322,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'data': []}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} }
@@ -361,7 +378,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@@ -377,7 +394,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@@ -433,6 +450,8 @@ class VideoHttp {
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': null, 'msg': res.data['message']};
} }
} }
@@ -447,11 +466,13 @@ class VideoHttp {
'up_mid': upMid, 'up_mid': upMid,
}); });
var res = await Request().get(Api.aiConclusion, data: params); var res = await Request().get(Api.aiConclusion, data: params);
if (res.data['code'] == 0) { if (res.data['code'] == 0 && res.data['data']['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': AiConclusionModel.fromJson(res.data['data']), 'data': AiConclusionModel.fromJson(res.data['data']),
}; };
} else {
return {'status': false, 'data': []};
} }
} }
} }

View File

@@ -21,6 +21,9 @@ import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc. import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -32,7 +35,33 @@ void main() async {
await setupServiceLocator(); await setupServiceLocator();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
runApp(const MyApp()); RecommendFilter();
// 异常捕获 logo记录
final Catcher2Options debugConfig = Catcher2Options(
SilentReportMode(),
[
FileHandler(await getLogsPath()),
ConsoleHandler(
enableDeviceParameters: false,
enableApplicationParameters: false,
)
],
);
final Catcher2Options releaseConfig = Catcher2Options(
SilentReportMode(),
[FileHandler(await getLogsPath())],
);
Catcher2(
debugConfig: debugConfig,
releaseConfig: releaseConfig,
runAppFunction: () {
runApp(const MyApp());
},
);
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
@@ -41,7 +70,6 @@ void main() async {
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
)); ));
Data.init(); Data.init();
GStrorage.lazyInit();
PiliSchame.init(); PiliSchame.init();
}); });
} }

View File

@@ -0,0 +1,9 @@
enum DynamicBadgeMode { hidden, point, number }
extension DynamicBadgeModeDesc on DynamicBadgeMode {
String get description => ['隐藏', '红点', '数字'][index];
}
extension DynamicBadgeModeCode on DynamicBadgeMode {
int get code => [0, 1, 2][index];
}

View File

@@ -1,7 +1,7 @@
// 首页推荐类型 // 首页推荐类型
enum RcmdType { web, app } enum RcmdType { web, app, notLogin }
extension RcmdTypeExtension on RcmdType { extension RcmdTypeExtension on RcmdType {
String get values => ['web', 'app'][index]; String get values => ['web', 'app', 'notLogin'][index];
String get labels => ['web端', 'app端'][index]; String get labels => ['web端', 'app端', '游客模式'][index];
} }

View File

@@ -1,6 +1,6 @@
enum ReplySortType { time, like, reply } enum ReplySortType { time, like }
extension ReplySortTypeExtension on ReplySortType { extension ReplySortTypeExtension on ReplySortType {
String get titles => ['最新评论', '最热评论', '回复最多'][index]; String get titles => ['最新评论', '最热评论'][index];
String get labels => ['最新', '最热', '最多回复'][index]; String get labels => ['最新', '最热'][index];
} }

View File

@@ -17,8 +17,9 @@ class LatestDataModel {
url = json['url']; url = json['url'];
tagName = json['tag_name']; tagName = json['tag_name'];
createdAt = json['created_at']; createdAt = json['created_at'];
assets = assets = json['assets'] != null
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList(); ? json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList()
: [];
body = json['body']; body = json['body'];
} }
} }

View File

@@ -1,8 +1,3 @@
import 'package:hive/hive.dart';
part 'result.g.dart';
@HiveType(typeId: 0)
class RecVideoItemAppModel { class RecVideoItemAppModel {
RecVideoItemAppModel({ RecVideoItemAppModel({
this.id, this.id,
@@ -27,47 +22,27 @@ class RecVideoItemAppModel {
this.adInfo, this.adInfo,
}); });
@HiveField(0)
int? id; int? id;
@HiveField(1)
int? aid; int? aid;
@HiveField(2)
String? bvid; String? bvid;
@HiveField(3)
int? cid; int? cid;
@HiveField(4)
String? pic; String? pic;
@HiveField(5)
RcmdStat? stat; RcmdStat? stat;
@HiveField(6) int? duration;
String? duration;
@HiveField(7)
String? title; String? title;
@HiveField(8)
int? isFollowed; int? isFollowed;
@HiveField(9)
RcmdOwner? owner; RcmdOwner? owner;
@HiveField(10)
RcmdReason? rcmdReason; RcmdReason? rcmdReason;
@HiveField(11)
String? goto; String? goto;
@HiveField(12)
int? param; int? param;
@HiveField(13)
String? uri; String? uri;
@HiveField(14)
String? talkBack; String? talkBack;
// 番剧 // 番剧
@HiveField(15)
String? bangumiView; String? bangumiView;
@HiveField(16)
String? bangumiFollow; String? bangumiFollow;
@HiveField(17)
String? bangumiBadge; String? bangumiBadge;
@HiveField(18)
String? cardType; String? cardType;
@HiveField(19)
Map? adInfo; Map? adInfo;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) { RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {
@@ -79,13 +54,27 @@ class RecVideoItemAppModel {
cid = json['player_args'] != null ? json['player_args']['cid'] : -1; cid = json['player_args'] != null ? json['player_args']['cid'] : -1;
pic = json['cover']; pic = json['cover'];
stat = RcmdStat.fromJson(json); stat = RcmdStat.fromJson(json);
duration = json['cover_right_text']; // 改用player_args中的duration作为原始数据秒数
duration = json['player_args'] != null
? json['player_args']['duration']
: -1;
//duration = json['cover_right_text'];
title = json['title']; title = json['title'];
isFollowed = 0;
owner = RcmdOwner.fromJson(json); owner = RcmdOwner.fromJson(json);
rcmdReason = json['rcmd_reason_style'] != null rcmdReason = json['rcmd_reason_style'] != null
? RcmdReason.fromJson(json['rcmd_reason_style']) ? RcmdReason.fromJson(json['rcmd_reason_style'])
: null; : null;
// 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
isFollowed = rcmdReason != null &&
rcmdReason!.content != null &&
rcmdReason!.content!.contains('关注')
? 1
: 0;
// 如果是就无需再显示推荐原因交由view统一处理即可
if (isFollowed == 1) {
rcmdReason = null;
}
goto = json['goto']; goto = json['goto'];
param = int.parse(json['param']); param = int.parse(json['param']);
uri = json['uri']; uri = json['uri'];
@@ -102,18 +91,14 @@ class RecVideoItemAppModel {
} }
} }
@HiveType(typeId: 1)
class RcmdStat { class RcmdStat {
RcmdStat({ RcmdStat({
this.view, this.view,
this.like, this.like,
this.danmu, this.danmu,
}); });
@HiveField(0)
String? view; String? view;
@HiveField(1)
String? like; String? like;
@HiveField(2)
String? danmu; String? danmu;
RcmdStat.fromJson(Map<String, dynamic> json) { RcmdStat.fromJson(Map<String, dynamic> json) {
@@ -122,13 +107,10 @@ class RcmdStat {
} }
} }
@HiveType(typeId: 2)
class RcmdOwner { class RcmdOwner {
RcmdOwner({this.name, this.mid}); RcmdOwner({this.name, this.mid});
@HiveField(0)
String? name; String? name;
@HiveField(1)
int? mid; int? mid;
RcmdOwner.fromJson(Map<String, dynamic> json) { RcmdOwner.fromJson(Map<String, dynamic> json) {
@@ -141,13 +123,11 @@ class RcmdOwner {
} }
} }
@HiveType(typeId: 8)
class RcmdReason { class RcmdReason {
RcmdReason({ RcmdReason({
this.content, this.content,
}); });
@HiveField(0)
String? content; String? content;
RcmdReason.fromJson(Map<String, dynamic> json) { RcmdReason.fromJson(Map<String, dynamic> json) {

View File

@@ -1,209 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'result.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemAppModelAdapter extends TypeAdapter<RecVideoItemAppModel> {
@override
final int typeId = 0;
@override
RecVideoItemAppModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemAppModel(
id: fields[0] as int?,
aid: fields[1] as int?,
bvid: fields[2] as String?,
cid: fields[3] as int?,
pic: fields[4] as String?,
stat: fields[5] as RcmdStat?,
duration: fields[6] as String?,
title: fields[7] as String?,
isFollowed: fields[8] as int?,
owner: fields[9] as RcmdOwner?,
rcmdReason: fields[10] as RcmdReason?,
goto: fields[11] as String?,
param: fields[12] as int?,
uri: fields[13] as String?,
talkBack: fields[14] as String?,
bangumiView: fields[15] as String?,
bangumiFollow: fields[16] as String?,
bangumiBadge: fields[17] as String?,
cardType: fields[18] as String?,
adInfo: (fields[19] as Map?)?.cast<dynamic, dynamic>(),
);
}
@override
void write(BinaryWriter writer, RecVideoItemAppModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.aid)
..writeByte(2)
..write(obj.bvid)
..writeByte(3)
..write(obj.cid)
..writeByte(4)
..write(obj.pic)
..writeByte(5)
..write(obj.stat)
..writeByte(6)
..write(obj.duration)
..writeByte(7)
..write(obj.title)
..writeByte(8)
..write(obj.isFollowed)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.rcmdReason)
..writeByte(11)
..write(obj.goto)
..writeByte(12)
..write(obj.param)
..writeByte(13)
..write(obj.uri)
..writeByte(14)
..write(obj.talkBack)
..writeByte(15)
..write(obj.bangumiView)
..writeByte(16)
..write(obj.bangumiFollow)
..writeByte(17)
..write(obj.bangumiBadge)
..writeByte(18)
..write(obj.cardType)
..writeByte(19)
..write(obj.adInfo);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemAppModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdStatAdapter extends TypeAdapter<RcmdStat> {
@override
final int typeId = 1;
@override
RcmdStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdStat(
view: fields[0] as String?,
like: fields[1] as String?,
danmu: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdStat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdOwnerAdapter extends TypeAdapter<RcmdOwner> {
@override
final int typeId = 2;
@override
RcmdOwner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdOwner(
name: fields[0] as String?,
mid: fields[1] as int?,
);
}
@override
void write(BinaryWriter writer, RcmdOwner obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.mid);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdOwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 8;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
content: fields[0] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,130 @@
class RoomInfoH5Model {
RoomInfoH5Model({
this.roomInfo,
this.anchorInfo,
this.isRoomFeed,
this.watchedShow,
this.likeInfoV3,
this.blockInfo,
});
RoomInfo? roomInfo;
AnchorInfo? anchorInfo;
int? isRoomFeed;
Map? watchedShow;
LikeInfoV3? likeInfoV3;
Map? blockInfo;
RoomInfoH5Model.fromJson(Map<String, dynamic> json) {
roomInfo = RoomInfo.fromJson(json['room_info']);
anchorInfo = AnchorInfo.fromJson(json['anchor_info']);
isRoomFeed = json['is_room_feed'];
watchedShow = json['watched_show'];
likeInfoV3 = LikeInfoV3.fromJson(json['like_info_v3']);
blockInfo = json['block_info'];
}
}
class RoomInfo {
RoomInfo({
this.uid,
this.roomId,
this.title,
this.cover,
this.description,
this.liveStatus,
this.liveStartTime,
this.areaId,
this.areaName,
this.parentAreaId,
this.parentAreaName,
this.online,
this.background,
this.appBackground,
this.liveId,
});
int? uid;
int? roomId;
String? title;
String? cover;
String? description;
int? liveStatus;
int? liveStartTime;
int? areaId;
String? areaName;
int? parentAreaId;
String? parentAreaName;
int? online;
String? background;
String? appBackground;
String? liveId;
RoomInfo.fromJson(Map<String, dynamic> json) {
uid = json['uid'];
roomId = json['room_id'];
title = json['title'];
cover = json['cover'];
description = json['description'];
liveStatus = json['liveS_satus'];
liveStartTime = json['live_start_time'];
areaId = json['area_id'];
areaName = json['area_name'];
parentAreaId = json['parent_area_id'];
parentAreaName = json['parent_area_name'];
online = json['online'];
background = json['background'];
appBackground = json['app_background'];
liveId = json['live_id'];
}
}
class AnchorInfo {
AnchorInfo({
this.baseInfo,
this.relationInfo,
});
BaseInfo? baseInfo;
RelationInfo? relationInfo;
AnchorInfo.fromJson(Map<String, dynamic> json) {
baseInfo = BaseInfo.fromJson(json['base_info']);
relationInfo = RelationInfo.fromJson(json['relation_info']);
}
}
class BaseInfo {
BaseInfo({
this.uname,
this.face,
});
String? uname;
String? face;
BaseInfo.fromJson(Map<String, dynamic> json) {
uname = json['uname'];
face = json['face'];
}
}
class RelationInfo {
RelationInfo({this.attention});
int? attention;
RelationInfo.fromJson(Map<String, dynamic> json) {
attention = json['attention'];
}
}
class LikeInfoV3 {
LikeInfoV3({this.totalLikes});
int? totalLikes;
LikeInfoV3.fromJson(Map<String, dynamic> json) {
totalLikes = json['total_likes'];
}
}

View File

@@ -1,5 +1,3 @@
import 'package:pilipala/utils/utils.dart';
import './model_owner.dart'; import './model_owner.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@@ -38,7 +36,7 @@ class RecVideoItemModel {
@HiveField(6) @HiveField(6)
String? title = ''; String? title = '';
@HiveField(7) @HiveField(7)
String? duration = ''; int? duration = -1;
@HiveField(8) @HiveField(8)
int? pubdate = -1; int? pubdate = -1;
@HiveField(9) @HiveField(9)
@@ -58,7 +56,7 @@ class RecVideoItemModel {
uri = json["uri"]; uri = json["uri"];
pic = json["pic"]; pic = json["pic"];
title = json["title"]; title = json["title"];
duration = Utils.tampToSeektime(json["duration"]); duration = json["duration"];
pubdate = json["pubdate"]; pubdate = json["pubdate"];
owner = Owner.fromJson(json["owner"]); owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]); stat = Stat.fromJson(json["stat"]);
@@ -77,14 +75,15 @@ class Stat {
this.danmu, this.danmu,
}); });
@HiveField(0) @HiveField(0)
String? view; int? view;
@HiveField(1) @HiveField(1)
int? like; int? like;
@HiveField(2) @HiveField(2)
int? danmu; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = Utils.numFormat(json["view"]); // 无需在model中转换以保留原始数据在view层处理即可
view = json["view"];
like = json["like"]; like = json["like"];
danmu = json['danmaku']; danmu = json['danmaku'];
} }

View File

@@ -24,7 +24,7 @@ class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
uri: fields[4] as String?, uri: fields[4] as String?,
pic: fields[5] as String?, pic: fields[5] as String?,
title: fields[6] as String?, title: fields[6] as String?,
duration: fields[7] as String?, duration: fields[7] as int?,
pubdate: fields[8] as int?, pubdate: fields[8] as int?,
owner: fields[9] as Owner?, owner: fields[9] as Owner?,
stat: fields[10] as Stat?, stat: fields[10] as Stat?,
@@ -87,7 +87,7 @@ class StatAdapter extends TypeAdapter<Stat> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return Stat( return Stat(
view: fields[0] as String?, view: fields[0] as int?,
like: fields[1] as int?, like: fields[1] as int?,
danmu: fields[2] as int?, danmu: fields[2] as int?,
); );

View File

@@ -166,7 +166,7 @@ class SessionMsgDataModel {
int? hasMore; int? hasMore;
int? minSeqno; int? minSeqno;
int? maxSeqno; int? maxSeqno;
List? eInfos; List<dynamic>? eInfos;
SessionMsgDataModel.fromJson(Map<String, dynamic> json) { SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
messages = json['messages'] messages = json['messages']

View File

@@ -7,6 +7,7 @@ import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart'; import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart';
class AboutPage extends StatefulWidget { class AboutPage extends StatefulWidget {
const AboutPage({super.key}); const AboutPage({super.key});
@@ -17,6 +18,19 @@ class AboutPage extends StatefulWidget {
class _AboutPageState extends State<AboutPage> { class _AboutPageState extends State<AboutPage> {
final AboutController _aboutController = Get.put(AboutController()); final AboutController _aboutController = Get.put(AboutController());
String cacheSize = '';
@override
void initState() {
super.initState();
// 读取缓存占用
getCacheSize();
}
Future<void> getCacheSize() async {
final res = await CacheManage().loadApplicationCache();
setState(() => cacheSize = res);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -133,6 +147,22 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'), title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
), ),
ListTile(
onTap: () => _aboutController.logs(),
title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () async {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
}
},
title: const Text('清除缓存'),
subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
], ],
), ),
), ),
@@ -260,4 +290,9 @@ class AboutController extends GetxController {
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
} }
// 日志
logs() {
Get.toNamed('/logs');
}
} }

View File

@@ -9,7 +9,6 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/bangumu_card_v.dart'; import 'widgets/bangumu_card_v.dart';
@@ -199,7 +198,10 @@ class _BangumiPageState extends State<BangumiPage>
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => {}, fn: () {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
},
); );
} }
} else { } else {
@@ -208,7 +210,6 @@ class _BangumiPageState extends State<BangumiPage>
}, },
), ),
), ),
const LoadingMore()
], ],
), ),
); );

View File

@@ -151,7 +151,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
if (item.badge != null && vipStatus != 1) { if (item.badge != null && item.badge == '会员' && vipStatus != 1) {
SmartDialog.showToast('需要大会员'); SmartDialog.showToast('需要大会员');
return; return;
} }
@@ -255,11 +255,24 @@ class _BangumiPanelState extends State<BangumiPanel> {
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
if (widget.pages[i].badge != null) ...[ if (widget.pages[i].badge != null) ...[
Image.asset( if (widget.pages[i].badge == '会员') ...[
'assets/images/big-vip.png', Image.asset(
height: 16, 'assets/images/big-vip.png',
), height: 16,
], ),
],
if (widget.pages[i].badge != '会员') ...[
const Spacer(),
Text(
widget.pages[i].badge!,
style: TextStyle(
fontSize: 11,
color:
Theme.of(context).colorScheme.primary,
),
),
],
]
], ],
), ),
const SizedBox(height: 3), const SizedBox(height: 3),

View File

@@ -35,6 +35,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
late double opacityVal; late double opacityVal;
late double fontSizeVal; late double fontSizeVal;
late double danmakuDurationVal; late double danmakuDurationVal;
late double strokeWidth;
int latestAddedPosition = -1; int latestAddedPosition = -1;
@override @override
@@ -65,6 +66,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
showArea = playerController.showArea; showArea = playerController.showArea;
opacityVal = playerController.opacityVal; opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal; fontSizeVal = playerController.fontSizeVal;
strokeWidth = playerController.strokeWidth;
danmakuDurationVal = playerController.danmakuDurationVal; danmakuDurationVal = playerController.danmakuDurationVal;
} }
@@ -136,6 +138,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
hideBottom: blockTypes.contains(4), hideBottom: blockTypes.contains(4),
duration: duration:
danmakuDurationVal / playerController.playbackSpeed, danmakuDurationVal / playerController.playbackSpeed,
strokeWidth: strokeWidth,
// initDuration / // initDuration /
// (danmakuSpeedVal * widget.playerController.playbackSpeed), // (danmakuSpeedVal * widget.playerController.playbackSpeed),
), ),

View File

@@ -37,6 +37,10 @@ class DynamicDetailController extends GetxController {
} }
int deaultReplySortIndex = int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0); setting.get(SettingBoxKey.replySortType, defaultValue: 0);
if (deaultReplySortIndex == 2) {
setting.put(SettingBoxKey.replySortType, 0);
deaultReplySortIndex = 0;
}
_sortType = ReplySortType.values[deaultReplySortIndex]; _sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels; sortTypeLabel.value = _sortType.labels;
@@ -92,9 +96,6 @@ class DynamicDetailController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@@ -192,22 +192,6 @@ class _DynamicsPageState extends State<DynamicsPage>
) )
], ],
), ),
// Obx(
// () => Visibility(
// visible: _dynamicsController.userLogin.value,
// child: Positioned(
// right: 4,
// top: 0,
// bottom: 0,
// child: IconButton(
// padding: EdgeInsets.zero,
// onPressed: () =>
// {feedBack(), _dynamicsController.resetSearch()},
// icon: const Icon(Icons.history, size: 21),
// ),
// ),
// ),
// ),
], ],
), ),
), ),
@@ -229,7 +213,8 @@ class _DynamicsPageState extends State<DynamicsPage>
return Obx(() => UpPanel(_dynamicsController.upData.value)); return Obx(() => UpPanel(_dynamicsController.upData.value));
} else { } else {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: SizedBox(height: 80)); child: SizedBox(height: 80),
);
} }
} else { } else {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
@@ -240,15 +225,6 @@ class _DynamicsPageState extends State<DynamicsPage>
} }
}, },
), ),
SliverToBoxAdapter(
child: Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
),
FutureBuilder( FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {

View File

@@ -1,5 +1,6 @@
// 内容 // 内容
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
@@ -80,7 +81,7 @@ class _ContentState extends State<Content> {
height: height, height: height,
), ),
), ),
height > maxHeight height > Get.size.height * 0.9
? const PBadge( ? const PBadge(
text: '长图', text: '长图',
right: 8, right: 8,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@@ -87,7 +88,7 @@ Widget picWidget(item, context) {
childAspectRatio: aspectRatio, childAspectRatio: aspectRatio,
children: list, children: list,
), ),
if (len == 1 && origAspectRatio < 0.4) if (len == 1 && height > Get.size.height * 0.9)
const PBadge( const PBadge(
text: '长图', text: '长图',
top: null, top: null,

View File

@@ -36,8 +36,7 @@ class _UpPanelState extends State<UpPanel> {
} }
upList.insert( upList.insert(
0, 0,
UpItem( UpItem(face: '', uname: '全部动态', mid: -1),
face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1),
); );
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
upList.insert( upList.insert(
@@ -56,7 +55,7 @@ class _UpPanelState extends State<UpPanel> {
floating: true, floating: true,
pinned: false, pinned: false,
delegate: _SliverHeaderDelegate( delegate: _SliverHeaderDelegate(
height: 124, height: 126,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -121,6 +120,13 @@ class _UpPanelState extends State<UpPanel> {
], ],
), ),
), ),
Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
], ],
)), )),
); );
@@ -171,6 +177,9 @@ class _UpPanelState extends State<UpPanel> {
}, },
onLongPress: () { onLongPress: () {
feedBack(); feedBack();
if (data.mid == -1) {
return;
}
String heroTag = Utils.makeHeroTag(data.mid); String heroTag = Utils.makeHeroTag(data.mid);
Get.toNamed('/member?mid=${data.mid}', Get.toNamed('/member?mid=${data.mid}',
arguments: {'face': data.face, 'heroTag': heroTag}); arguments: {'face': data.face, 'heroTag': heroTag});
@@ -198,12 +207,19 @@ class _UpPanelState extends State<UpPanel> {
backgroundColor: data.type == 'live' backgroundColor: data.type == 'live'
? Theme.of(context).colorScheme.secondaryContainer ? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
child: NetworkImgLayer( child: data.face != ''
width: 49, ? NetworkImgLayer(
height: 49, width: 50,
src: data.face, height: 50,
type: 'avatar', src: data.face,
), type: 'avatar',
)
: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage(
'assets/images/noface.jpeg',
),
),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
@@ -271,13 +287,11 @@ class UpPanelSkeleton extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Container(
width: 49, width: 50,
height: 49, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(50),
Radius.circular(24),
),
), ),
), ),
Container( Container(

View File

@@ -24,11 +24,13 @@ class _FavDetailPageState extends State<FavDetailPage> {
Get.put(FavDetailController()); Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC; // a
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late String mediaId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
@@ -94,8 +96,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () =>
'/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'), Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
// IconButton( // IconButton(

View File

@@ -9,14 +9,20 @@ import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
// 收藏视频卡片 - 水平布局 // 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget { class FavVideoCardH extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
final int? searchType;
const FavVideoCardH({Key? key, required this.videoItem, this.callFn}) const FavVideoCardH({
: super(key: key); Key? key,
required this.videoItem,
this.callFn,
this.searchType,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -27,7 +33,9 @@ class FavVideoCardH extends StatelessWidget {
onTap: () async { onTap: () async {
// int? seasonId; // int? seasonId;
String? epId; String? epId;
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') { if (videoItem.ogv != null &&
(videoItem.ogv['type_name'] == '番剧' ||
videoItem.ogv['type_name'] == '国创')) {
videoItem.cid = await SearchHttp.ab2c(bvid: bvid); videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
// seasonId = videoItem.ogv['season_id']; // seasonId = videoItem.ogv['season_id'];
epId = videoItem.epId; epId = videoItem.epId;
@@ -84,28 +92,31 @@ class FavVideoCardH extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
Positioned( PBadge(
right: 4, text: Utils.timeFormat(videoItem.duration!),
bottom: 4, right: 6.0,
child: Container( bottom: 6.0,
padding: const EdgeInsets.symmetric( type: 'gray',
vertical: 1, horizontal: 6), ),
decoration: BoxDecoration( if (videoItem.ogv != null) ...[
borderRadius: BorderRadius.circular(4), PBadge(
color: Colors.black54.withOpacity(0.4)), text: videoItem.ogv['type_name'],
child: Text( top: 6.0,
Utils.timeFormat(videoItem.duration!), right: 6.0,
style: const TextStyle( bottom: null,
fontSize: 11, color: Colors.white), left: null,
),
), ),
) ],
], ],
); );
}, },
), ),
), ),
VideoContent(videoItem: videoItem, callFn: callFn) VideoContent(
videoItem: videoItem,
callFn: callFn,
searchType: searchType,
)
], ],
), ),
); );
@@ -121,93 +132,123 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn}); final int? searchType;
const VideoContent({
super.key,
required this.videoItem,
this.callFn,
this.searchType,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Column(
videoItem.title, crossAxisAlignment: CrossAxisAlignment.start,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(videoItem.ctime!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row(
children: [ children: [
StatView( Text(
theme: 'gray', videoItem.title,
view: videoItem.cntInfo['play'], textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(width: 8), if (videoItem.ogv != null) ...[
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']), Text(
const Spacer(), videoItem.intro,
SizedBox( style: TextStyle(
width: 26, fontSize:
height: 26, Theme.of(context).textTheme.labelMedium!.fontSize,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 18,
), ),
), ),
],
const Spacer(),
Text(
Utils.dateFormat(videoItem.favTime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.owner.name != '') ...[
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
],
),
), ),
], ],
), ),
searchType != 1
? Positioned(
right: 0,
bottom: -4,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
)
: const SizedBox(),
], ],
), ),
), ),

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_detail.dart';
import '../../http/video.dart';
class FavSearchController extends GetxController { class FavSearchController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs; Rx<TextEditingController> controller = TextEditingController().obs;
@@ -72,4 +75,21 @@ class FavSearchController extends GetxController {
if (!hasMore) return; if (!hasMore) return;
searchFav(type: 'onLoad'); searchFav(type: 'onLoad');
} }
onCancelFav(int id) async {
var result = await VideoHttp.favVideo(
aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) {
if (result['data']['prompt']) {
List dataList = favList;
for (var i in dataList) {
if (i.id == id) {
dataList.remove(i);
break;
}
}
SmartDialog.showToast('取消收藏');
}
}
}
} }

View File

@@ -8,9 +8,7 @@ import 'package:pilipala/pages/fav_detail/widget/fav_video_card.dart';
import 'controller.dart'; import 'controller.dart';
class FavSearchPage extends StatefulWidget { class FavSearchPage extends StatefulWidget {
final int? sourceType; const FavSearchPage({super.key});
final int? mediaId;
const FavSearchPage({super.key, this.sourceType, this.mediaId});
@override @override
State<FavSearchPage> createState() => _FavSearchPageState(); State<FavSearchPage> createState() => _FavSearchPageState();
@@ -19,11 +17,12 @@ class FavSearchPage extends StatefulWidget {
class _FavSearchPageState extends State<FavSearchPage> { class _FavSearchPageState extends State<FavSearchPage> {
final FavSearchController _favSearchCtr = Get.put(FavSearchController()); final FavSearchController _favSearchCtr = Get.put(FavSearchController());
late ScrollController scrollController; late ScrollController scrollController;
late int searchType;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
searchType = int.parse(Get.parameters['searchType']!);
scrollController = _favSearchCtr.scrollController; scrollController = _favSearchCtr.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@@ -100,7 +99,11 @@ class _FavSearchPageState extends State<FavSearchPage> {
} else { } else {
return FavVideoCardH( return FavVideoCardH(
videoItem: _favSearchCtr.favList[index], videoItem: _favSearchCtr.favList[index],
callFn: () => null, searchType: searchType,
callFn: () => searchType != 1
? _favSearchCtr
.onCancelFav(_favSearchCtr.favList[index].id!)
: {},
); );
} }
}, },

View File

@@ -37,6 +37,29 @@ class _FollowPageState extends State<FollowPage> {
: '${_followController.name}的关注', : '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [
IconButton(
onPressed: () => Get.toNamed('/followSearch?mid=$mid'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => Get.toNamed('/blackListPage'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.block, size: 19),
SizedBox(width: 10),
Text('黑名单管理'),
],
),
)
],
),
const SizedBox(width: 6),
],
), ),
body: Obx( body: Obx(
() => !_followController.isOwner.value () => !_followController.isOwner.value
@@ -87,3 +110,22 @@ class _FollowPageState extends State<FollowPage> {
); );
} }
} }
class _FakeAPI {
static const List<String> _kOptions = <String>[
'aardvark',
'bobcat',
'chameleon',
];
// Searches the options, but injects a fake "network" delay.
static Future<Iterable<String>> search(String query) async {
await Future<void>.delayed(
const Duration(seconds: 1)); // Fake 1 second delay.
if (query == '') {
return const Iterable<String>.empty();
}
return _kOptions.where((String option) {
return option.contains(query.toLowerCase());
});
}
}

View File

@@ -42,7 +42,7 @@ class FollowItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
trailing: ctr!.isOwner.value trailing: ctr != null && ctr!.isOwner.value
? SizedBox( ? SizedBox(
height: 34, height: 34,
child: TextButton( child: TextButton(

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import '../../models/follow/result.dart';
class FollowSearchController extends GetxController {
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
late int mid = 1;
RxString uname = ''.obs;
int ps = 20;
int pn = 1;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
RxInt total = 0.obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
// 清空搜索
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
} else {
Get.back();
}
}
void onChange(value) {
searchKeyWord.value = value;
}
// 提交搜索内容
void submit() {
loadingStatus.value = 'loading';
searchFollow();
}
Future searchFollow({type = 'init'}) async {
if (controller.value.text == '') {
return {'status': true, 'data': <FollowItemModel>[].obs};
}
if (type == 'init') {
ps = 1;
}
var res = await MemberHttp.getfollowSearch(
mid: mid,
ps: ps,
pn: pn,
name: controller.value.text,
);
if (res['status']) {
if (type == 'init') {
followList.value = res['data'].list;
} else {
followList.addAll(res['data'].list);
}
total.value = res['data'].total;
}
return res;
}
void onLoad() {
searchFollow(type: 'onLoad');
}
}

View File

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

View File

@@ -0,0 +1,121 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/follow_search/index.dart';
import '../follow/widgets/follow_item.dart';
class FollowSearchPage extends StatefulWidget {
const FollowSearchPage({super.key});
@override
State<FollowSearchPage> createState() => _FollowSearchPageState();
}
class _FollowSearchPageState extends State<FollowSearchPage> {
final FollowSearchController _followSearchController =
Get.put(FollowSearchController());
late Future? _futureBuilder;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilder = _followSearchController.searchFollow();
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () {
_followSearchController.onLoad();
});
}
},
);
}
void reRequest() {
setState(() {
_futureBuilder = _followSearchController.searchFollow();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: reRequest,
icon: const Icon(CupertinoIcons.search, size: 22),
),
const SizedBox(width: 6),
],
title: TextField(
autofocus: true,
focusNode: _followSearchController.searchFocusNode,
controller: _followSearchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _followSearchController.onChange(value),
decoration: InputDecoration(
hintText: _followSearchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _followSearchController.onClear(),
),
),
onSubmitted: (String value) => reRequest(),
),
),
body: FutureBuilder(
future: _futureBuilder,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data == null) {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
if (data['status']) {
RxList followList = _followSearchController.followList;
return Obx(
() => followList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: followList.length,
itemBuilder: ((context, index) {
return FollowItem(
item: followList[index],
);
}),
)
: CustomScrollView(
slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
} else {
return const SizedBox();
}
}),
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../http/index.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin { class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false; bool flag = false;
@@ -24,6 +25,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late bool hideSearchBar; late bool hideSearchBar;
late List defaultTabs; late List defaultTabs;
late List<String> tabbarSort; late List<String> tabbarSort;
RxString defaultSearch = ''.obs;
@override @override
void onInit() { void onInit() {
@@ -35,6 +37,9 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
setTabConfig(); setTabConfig();
hideSearchBar = hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
} }
void onRefresh() { void onRefresh() {
@@ -58,13 +63,16 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
} }
void setTabConfig() async { void setTabConfig() async {
defaultTabs = tabsConfig; defaultTabs = [...tabsConfig];
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
defaultTabs.retainWhere(
(item) => tabbarSort.contains((item['type'] as TabType).id));
defaultTabs.sort((a, b) => tabbarSort
.indexOf((a['type'] as TabType).id)
.compareTo(tabbarSort.indexOf((b['type'] as TabType).id)));
tabs.value = defaultTabs tabs.value = defaultTabs;
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.toList();
if (tabbarSort.contains(TabType.rcmd.id)) { if (tabbarSort.contains(TabType.rcmd.id)) {
initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id); initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);
@@ -94,4 +102,11 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
} }
}); });
} }
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
defaultSearch.value = res.data['data']['name'];
}
}
} }

View File

@@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import './controller.dart'; import './controller.dart';
@@ -144,6 +143,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0), padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0),
child: UserInfoWidget( child: UserInfoWidget(
top: top, top: top,
ctr: ctr,
userLogin: isUserLoggedIn, userLogin: isUserLoggedIn,
userFace: ctr?.userFace.value, userFace: ctr?.userFace.value,
callback: () => callback!(), callback: () => callback!(),
@@ -162,18 +162,20 @@ class UserInfoWidget extends StatelessWidget {
required this.userLogin, required this.userLogin,
required this.userFace, required this.userFace,
required this.callback, required this.callback,
required this.ctr,
}) : super(key: key); }) : super(key: key);
final double top; final double top;
final RxBool userLogin; final RxBool userLogin;
final String? userFace; final String? userFace;
final VoidCallback? callback; final VoidCallback? callback;
final HomeController? ctr;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
const SearchBar(), SearchBar(ctr: ctr),
if (userLogin.value) ...[ if (userLogin.value) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
ClipRect( ClipRect(
@@ -335,11 +337,15 @@ class CustomChip extends StatelessWidget {
} }
class SearchBar extends StatelessWidget { class SearchBar extends StatelessWidget {
const SearchBar({super.key}); const SearchBar({
Key? key,
required this.ctr,
}) : super(key: key);
final HomeController? ctr;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final SSearchController searchController = Get.put(SSearchController());
final ColorScheme colorScheme = Theme.of(context).colorScheme; final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Expanded( return Expanded(
child: Container( child: Container(
@@ -353,7 +359,10 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer.withOpacity(0.05), color: colorScheme.onSecondaryContainer.withOpacity(0.05),
child: InkWell( child: InkWell(
splashColor: colorScheme.primaryContainer.withOpacity(0.3), splashColor: colorScheme.primaryContainer.withOpacity(0.3),
onTap: () => Get.toNamed('/search'), onTap: () => Get.toNamed(
'/search',
parameters: {'hintText': ctr!.defaultSearch.value},
),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 14), const SizedBox(width: 14),
@@ -362,14 +371,12 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer, color: colorScheme.onSecondaryContainer,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Obx(
child: Obx( () => Text(
() => Text( ctr!.defaultSearch.value,
searchController.defaultSearch.value, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: TextStyle(color: colorScheme.outline),
style: TextStyle(color: colorScheme.outline),
),
), ),
), ),
], ],

View File

@@ -89,8 +89,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
if (data['status']) { if (data['status']) {
return Obx( return Obx(
() => SliverList( () => SliverList(
delegate: delegate: SliverChildBuilderDelegate((context, index) {
SliverChildBuilderDelegate((context, index) {
return VideoCardH( return VideoCardH(
videoItem: _hotController.videoList[index], videoItem: _hotController.videoList[index],
showPubdate: true, showPubdate: true,
@@ -110,7 +109,12 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_futureBuilderFuture =
_hotController.queryHotFeed('init');
});
},
); );
} }
} else { } else {

View File

@@ -96,9 +96,6 @@ class HtmlRenderController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@@ -10,8 +10,7 @@ class LiveController extends GetxController {
int count = 12; int count = 12;
int _currentPage = 1; int _currentPage = 1;
RxInt crossAxisCount = 2.obs; RxInt crossAxisCount = 2.obs;
RxList<LiveItemModel> liveList = [LiveItemModel()].obs; RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
bool isLoadingMore = false;
bool flag = false; bool flag = false;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@@ -39,7 +38,6 @@ class LiveController extends GetxController {
} }
_currentPage += 1; _currentPage += 1;
} }
isLoadingMore = false;
return res; return res;
} }

View File

@@ -11,7 +11,6 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/live_item.dart'; import 'widgets/live_item.dart';
@@ -45,8 +44,8 @@ class _LivePageState extends State<LivePage>
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('liveList', const Duration(seconds: 1), () { EasyThrottle.throttle('liveList', const Duration(milliseconds: 200),
_liveController.isLoadingMore = true; () {
_liveController.onLoad(); _liveController.onLoad();
}); });
} }
@@ -108,24 +107,20 @@ class _LivePageState extends State<LivePage>
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => {}, fn: () {
setState(() {
_futureBuilderFuture =
_liveController.queryLiveList('init');
});
},
); );
} }
} else { } else {
// 缓存数据 return contentGrid(_liveController, []);
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
} }
}, },
), ),
), ),
LoadingMore(ctr: _liveController)
], ],
), ),
), ),

View File

@@ -184,18 +184,32 @@ class VideoStat extends StatelessWidget {
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
), ),
), ),
child: RichText( child: Row(
maxLines: 1, mainAxisAlignment: MainAxisAlignment.spaceBetween,
textAlign: TextAlign.justify, children: [
softWrap: false, Text(
text: TextSpan( liveItem!.areaName!,
style: const TextStyle(fontSize: 11, color: Colors.white), style: const TextStyle(fontSize: 11, color: Colors.white),
children: [ ),
TextSpan(text: liveItem!.areaName!), Text(
TextSpan(text: liveItem!.watchedShow!['text_small']), liveItem!.watchedShow!['text_small'],
], style: const TextStyle(fontSize: 11, color: Colors.white),
), ),
],
), ),
// child: RichText(
// maxLines: 1,
// textAlign: TextAlign.justify,
// softWrap: false,
// text: TextSpan(
// style: const TextStyle(fontSize: 11, color: Colors.white),
// children: [
// TextSpan(text: liveItem!.areaName!),
// TextSpan(text: liveItem!.watchedShow!['text_small']),
// ],
// ),
// ),
); );
} }
} }

View File

@@ -3,6 +3,7 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart'; import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart'; import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart';
class LiveRoomController extends GetxController { class LiveRoomController extends GetxController {
String cover = ''; String cover = '';
@@ -14,13 +15,7 @@ class LiveRoomController extends GetxController {
RxBool volumeOff = false.obs; RxBool volumeOff = false.obs;
PlPlayerController plPlayerController = PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'live'); PlPlayerController.getInstance(videoType: 'live');
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
// MeeduPlayerController meeduPlayerController = MeeduPlayerController(
// colorTheme: Theme.of(Get.context!).colorScheme.primary,
// pipEnabled: true,
// controlsStyle: ControlsStyle.live,
// enabledButtons: const EnabledButtons(pip: true),
// );
@override @override
void onInit() { void onInit() {
@@ -36,11 +31,10 @@ class LiveRoomController extends GetxController {
cover = liveItem.cover; cover = liveItem.cover;
} }
} }
queryLiveInfo();
} }
playerInit(source) { playerInit(source) async {
plPlayerController.setDataSource( await plPlayerController.setDataSource(
DataSource( DataSource(
videoSource: source, videoSource: source,
audioSource: null, audioSource: null,
@@ -66,7 +60,8 @@ class LiveRoomController extends GetxController {
String videoUrl = (item.urlInfo?.first.host)! + String videoUrl = (item.urlInfo?.first.host)! +
item.baseUrl! + item.baseUrl! +
item.urlInfo!.first.extra!; item.urlInfo!.first.extra!;
playerInit(videoUrl); await playerInit(videoUrl);
return res;
} }
} }
@@ -80,4 +75,12 @@ class LiveRoomController extends GetxController {
volumeOff.value = true; volumeOff.value = true;
} }
} }
Future queryLiveInfoH5() async {
var res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
if (res['status']) {
roomInfoH5.value = res['data'];
}
return res;
}
} }

View File

@@ -19,6 +19,8 @@ class LiveRoomPage extends StatefulWidget {
class _LiveRoomPageState extends State<LiveRoomPage> { class _LiveRoomPageState extends State<LiveRoomPage> {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController()); final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
late Future? _futureBuilder;
late Future? _futureBuilderFuture;
bool isShowCover = true; bool isShowCover = true;
bool isPlay = true; bool isPlay = true;
@@ -27,18 +29,16 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
plPlayerController = _liveRoomController.plPlayerController;
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) {
if (status == PlayerStatus.playing) {
isShowCover = false;
setState(() {});
}
},
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = Floating(); floating = Floating();
} }
videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
}
Future<void> videoSourceInit() async {
_futureBuilder = _liveRoomController.queryLiveInfoH5();
plPlayerController = _liveRoomController.plPlayerController;
} }
@override @override
@@ -52,57 +52,123 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
);
} else {
return const SizedBox();
}
},
);
Widget childWhenDisabled = Scaffold( Widget childWhenDisabled = Scaffold(
primary: true, primary: true,
appBar: PreferredSize( backgroundColor: Colors.black,
preferredSize: Size.fromHeight( body: Stack(
MediaQuery.of(context).orientation == Orientation.portrait ? 56 : 0,
),
child: AppBar(
centerTitle: false,
titleSpacing: 0,
title: _liveRoomController.liveItem != null
? Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController
.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
)
: const SizedBox(),
// actions: [
// SizedBox(
// height: 34,
// child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
// ),
// const SizedBox(width: 12),
// ],
),
),
body: Column(
children: [ children: [
Stack( // Obx(
// () => Positioned.fill(
// child: Opacity(
// opacity: 0.8,
// child: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// '' &&
// _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// null
// ? NetworkImgLayer(
// width: Get.width,
// height: Get.height,
// src: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground ??
// '',
// )
// : Image.asset(
// 'assets/images/live/default_bg.webp',
// width: Get.width,
// height: Get.height,
// ),
// ),
// ),
// ),
Positioned.fill(
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
width: Get.width,
height: Get.height,
),
),
),
Column(
children: [ children: [
AppBar(
centerTitle: false,
titleSpacing: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
toolbarHeight:
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
title: FutureBuilder(
future: _futureBuilder,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController
.roomInfoH5.value.anchorInfo!.baseInfo!.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.roomInfoH5.value
.anchorInfo!.baseInfo!.uname!,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController
.roomInfoH5.value.watchedShow !=
null)
Text(
_liveRoomController.roomInfoH5.value
.watchedShow!['text_large'] ??
'',
style: const TextStyle(fontSize: 12),
),
],
),
],
),
);
} else {
return const SizedBox();
}
},
),
),
PopScope( PopScope(
canPop: plPlayerController?.isFullScreen.value != true, canPop: plPlayerController?.isFullScreen.value != true,
onPopInvoked: (bool didPop) { onPopInvoked: (bool didPop) {
@@ -120,55 +186,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
Orientation.landscape Orientation.landscape
? Get.size.height ? Get.size.height
: Get.size.width * 9 / 16, : Get.size.width * 9 / 16,
child: plPlayerController!.videoPlayerController != null child: videoPlayerPanel,
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
), ),
), ),
// if (_liveRoomController.liveItem != null &&
// _liveRoomController.liveItem.cover != null)
// Visibility(
// visible: isShowCover,
// child: Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: NetworkImgLayer(
// type: 'emote',
// src: _liveRoomController.liveItem.cover,
// width: Get.size.width,
// height: videoHeight,
// ),
// ),
// ),
], ],
), ),
], ],
), ),
); );
Widget childWhenEnabled = AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
),
)
: const SizedBox(),
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
return PiPSwitcher( return PiPSwitcher(
childWhenDisabled: childWhenDisabled, childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled, childWhenEnabled: videoPlayerPanel,
floating: floating,
); );
} else { } else {
return childWhenDisabled; return childWhenDisabled;

View File

@@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[
@@ -65,6 +66,7 @@ class MainController extends GetxController {
int selectedIndex = 0; int selectedIndex = 0;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override @override
void onInit() { void onInit() {
@@ -75,7 +77,12 @@ class MainController extends GetxController {
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
var userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
getUnreadDynamic(); dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic();
}
} }
void onBackPressed(BuildContext context) { void onBackPressed(BuildContext context) {

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamic_badge_mode.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
@@ -127,11 +128,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
destinations: <Widget>[ destinations: <Widget>[
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return NavigationDestination( return NavigationDestination(
icon: Badge( icon: Obx(
label: Text(e['count'].toString()), () => Badge(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), label:
isLabelVisible: e['count'] > 0, _mainController.dynamicBadgeType.value ==
child: e['icon'], DynamicBadgeMode.number
? Text(e['count'].toString())
: null,
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible:
_mainController.dynamicBadgeType.value !=
DynamicBadgeMode.hidden &&
e['count'] > 0,
child: e['icon'],
),
), ),
selectedIcon: e['selectIcon'], selectedIcon: e['selectIcon'],
label: e['label'], label: e['label'],
@@ -148,11 +159,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
items: [ items: [
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem( return BottomNavigationBarItem(
icon: Badge( icon: Obx(
label: Text(e['count'].toString()), () => Badge(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), label:
isLabelVisible: e['count'] > 0, _mainController.dynamicBadgeType.value ==
child: e['icon'], DynamicBadgeMode.number
? Text(e['count'].toString())
: null,
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible:
_mainController.dynamicBadgeType.value !=
DynamicBadgeMode.hidden &&
e['count'] > 0,
child: e['icon'],
),
), ),
activeIcon: e['selectIcon'], activeIcon: e['selectIcon'],
label: e['label'], label: e['label'],

View File

@@ -105,7 +105,7 @@ class _MemberPageState extends State<MemberPage>
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'), '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
PopupMenuButton( PopupMenuButton(

View File

@@ -25,7 +25,7 @@ class MemberArchiveController extends GetxController {
// 获取用户投稿 // 获取用户投稿
Future getMemberArchive(type) async { Future getMemberArchive(type) async {
if (type == 'onRefresh') { if (type == 'init') {
pn = 1; pn = 1;
} }
var res = await MemberHttp.memberArchive( var res = await MemberHttp.memberArchive(
@@ -34,7 +34,12 @@ class MemberArchiveController extends GetxController {
order: currentOrder['type']!, order: currentOrder['type']!,
); );
if (res['status']) { if (res['status']) {
archivesList.addAll(res['data'].list.vlist); if (type == 'init') {
archivesList.value = res['data'].list.vlist;
}
if (type == 'onLoad') {
archivesList.addAll(res['data'].list.vlist);
}
count = res['data'].page['count']; count = res['data'].page['count'];
pn += 1; pn += 1;
} }
@@ -42,13 +47,14 @@ class MemberArchiveController extends GetxController {
} }
toggleSort() async { toggleSort() async {
pn = 1; List<String> typeList = orderList.map((e) => e['type']!).toList();
int index = orderList.indexOf(currentOrder); int index = typeList.indexOf(currentOrder['type']!);
if (index == orderList.length - 1) { if (index == orderList.length - 1) {
currentOrder.value = orderList.first; currentOrder.value = orderList.first;
} else { } else {
currentOrder.value = orderList[index + 1]; currentOrder.value = orderList[index + 1];
} }
getMemberArchive('init');
} }
// 上拉加载 // 上拉加载

View File

@@ -25,8 +25,7 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
final String heroTag = Utils.makeHeroTag(mid); final String heroTag = Utils.makeHeroTag(mid);
_memberArchivesController = _memberArchivesController =
Get.put(MemberArchiveController(), tag: heroTag); Get.put(MemberArchiveController(), tag: heroTag);
_futureBuilderFuture = _futureBuilderFuture = _memberArchivesController.getMemberArchive('init');
_memberArchivesController.getMemberArchive('onRefresh');
scrollController = _memberArchivesController.scrollController; scrollController = _memberArchivesController.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@@ -48,39 +47,16 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium), title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium),
// actions: [ actions: [
// Obx( Obx(
// () => PopupMenuButton<String>( () => TextButton.icon(
// padding: EdgeInsets.zero, icon: const Icon(Icons.sort, size: 20),
// tooltip: '投稿排序', onPressed: _memberArchivesController.toggleSort,
// icon: Icon( label: Text(_memberArchivesController.currentOrder['label']!),
// Icons.more_vert_outlined, ),
// color: Theme.of(context).colorScheme.outline, ),
// ), const SizedBox(width: 6),
// position: PopupMenuPosition.under, ],
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
// for (var i in _memberArchivesController.orderList) ...[
// PopupMenuItem<String>(
// onTap: () {},
// value: _memberArchivesController.currentOrder['label'],
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(i['label']!),
// if (_memberArchivesController.currentOrder['label'] ==
// i['label']) ...[
// const SizedBox(width: 10),
// const Icon(Icons.done, size: 20),
// ],
// ],
// ),
// ),
// ]
// ],
// ),
// ),
// ],
), ),
body: CustomScrollView( body: CustomScrollView(
controller: _memberArchivesController.scrollController, controller: _memberArchivesController.scrollController,

View File

@@ -119,7 +119,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/follow?mid=${userInfo.value.mid}'); Get.toNamed('/follow?mid=${userInfo.value.mid}', preventDuplicates: false);
} }
pushFans() { pushFans() {
@@ -127,7 +127,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/fan?mid=${userInfo.value.mid}'); Get.toNamed('/fan?mid=${userInfo.value.mid}', preventDuplicates: false);
} }
pushDynamic() { pushDynamic() {
@@ -135,6 +135,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}'); Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}',
preventDuplicates: false);
} }
} }

View File

@@ -64,7 +64,7 @@ class _MinePageState extends State<MinePage> {
), ),
), ),
IconButton( IconButton(
onPressed: () => Get.toNamed('/setting'), onPressed: () => Get.toNamed('/setting', preventDuplicates: false),
icon: const Icon( icon: const Icon(
CupertinoIcons.slider_horizontal_3, CupertinoIcons.slider_horizontal_3,
), ),

View File

@@ -9,110 +9,77 @@ import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController { class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int _currentPage = 0; int _currentPage = 0;
RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs; // RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs;
RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs; // RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;
bool isLoadingMore = true; bool isLoadingMore = true;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
RxInt crossAxisCount = 2.obs; RxInt crossAxisCount = 2.obs;
late bool enableSaveLastData; late bool enableSaveLastData;
late String defaultRcmdType = 'web'; late String defaultRcmdType = 'web';
late RxList<dynamic> videoList;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
crossAxisCount.value = crossAxisCount.value =
setting.get(SettingBoxKey.customRows, defaultValue: 2); setting.get(SettingBoxKey.customRows, defaultValue: 2);
// 读取app端缓存内容
// if (recVideo.get('cacheList') != null &&
// recVideo.get('cacheList').isNotEmpty) {
// List<RecVideoItemAppModel> list = [];
// for (var i in recVideo.get('cacheList')) {
// list.add(i);
// }
// videoList.value = list;
// }
enableSaveLastData = enableSaveLastData =
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false); setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
defaultRcmdType = defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web'); setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
if (defaultRcmdType == 'web') {
videoList = <RecVideoItemModel>[].obs;
} else {
videoList = <RecVideoItemAppModel>[].obs;
}
} }
// 获取推荐 // 获取推荐
Future queryRcmdFeed(type) async { Future queryRcmdFeed(type) async {
print(defaultRcmdType);
if (defaultRcmdType == 'app') {
return await queryRcmdFeedApp(type);
}
if (defaultRcmdType == 'web') {
return await queryRcmdFeedWeb(type);
}
}
// 获取app端推荐
Future queryRcmdFeedApp(type) async {
if (isLoadingMore == false) { if (isLoadingMore == false) {
return; return;
} }
if (type == 'onRefresh') { if (type == 'onRefresh') {
_currentPage = 0; _currentPage = 0;
} }
var res = await VideoHttp.rcmdVideoListApp( late final Map<String, dynamic> res;
freshIdx: _currentPage, switch (defaultRcmdType) {
); case 'app':
case 'notLogin':
res = await VideoHttp.rcmdVideoListApp(
loginStatus: defaultRcmdType != 'notLogin',
freshIdx: _currentPage,
);
break;
default: //'web'
res = await VideoHttp.rcmdVideoList(
freshIdx: _currentPage,
ps: 20,
);
}
if (res['status']) { if (res['status']) {
if (type == 'init') { if (type == 'init') {
if (appVideoList.isNotEmpty) { if (videoList.isNotEmpty) {
appVideoList.addAll(res['data']); videoList.addAll(res['data']);
} else { } else {
appVideoList.value = res['data']; videoList.value = res['data'];
} }
} else if (type == 'onRefresh') { } else if (type == 'onRefresh') {
if (enableSaveLastData) { if (enableSaveLastData) {
appVideoList.insertAll(0, res['data']); videoList.insertAll(0, res['data']);
} else { } else {
appVideoList.value = res['data']; videoList.value = res['data'];
} }
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
appVideoList.addAll(res['data']); videoList.addAll(res['data']);
}
recVideo.put('cacheList', res['data']);
_currentPage += 1;
}
isLoadingMore = false;
return res;
}
// 获取web端推荐
Future queryRcmdFeedWeb(type) async {
if (isLoadingMore == false) {
return;
}
if (type == 'onRefresh') {
_currentPage = 0;
}
var res = await VideoHttp.rcmdVideoList(
ps: 20,
freshIdx: _currentPage,
);
if (res['status']) {
if (type == 'init') {
if (webVideoList.isNotEmpty) {
webVideoList.addAll(res['data']);
} else {
webVideoList.value = res['data'];
}
} else if (type == 'onRefresh') {
if (enableSaveLastData) {
webVideoList.insertAll(0, res['data']);
} else {
webVideoList.value = res['data'];
}
} else if (type == 'onLoad') {
webVideoList.addAll(res['data']);
} }
_currentPage += 1; _currentPage += 1;
// 若videoList数量太小可能会影响翻页此时再次请求
// 为避免请求到的数据太少时还在反复请求要求本次返回数据大于1条才触发
if (res['data'].length > 1 && videoList.length < 10) {
queryRcmdFeed('onLoad');
}
} }
isLoadingMore = false; isLoadingMore = false;
return res; return res;
@@ -129,7 +96,7 @@ class RcmdController extends GetxController {
queryRcmdFeed('onLoad'); queryRcmdFeed('onLoad');
} }
// 返回顶部并刷新 // 返回顶部
void animateToTop() async { void animateToTop() async {
if (scrollController.offset >= if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) { MediaQuery.of(Get.context!).size.height * 5) {

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -45,7 +44,7 @@ class _RcmdPageState extends State<RcmdPage>
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle( EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () { 'my-throttler', const Duration(milliseconds: 200), () {
_rcmdController.isLoadingMore = true; _rcmdController.isLoadingMore = true;
_rcmdController.onLoad(); _rcmdController.onLoad();
}); });
@@ -97,29 +96,24 @@ class _RcmdPageState extends State<RcmdPage>
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
if (data['status']) { if (data['status']) {
return Platform.isAndroid || Platform.isIOS return Obx(
? Obx( () {
() => contentGrid( if (_rcmdController.isLoadingMore &&
_rcmdController, _rcmdController.videoList.isEmpty) {
_rcmdController.defaultRcmdType == 'web' return contentGrid(_rcmdController, []);
? _rcmdController.webVideoList } else {
: _rcmdController.appVideoList), // 显示视频列表
) return contentGrid(
: SliverLayoutBuilder( _rcmdController, _rcmdController.videoList);
builder: (context, boxConstraints) { }
return Obx( },
() => contentGrid( );
_rcmdController,
_rcmdController.defaultRcmdType == 'web'
? _rcmdController.webVideoList
: _rcmdController.appVideoList),
);
});
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () { fn: () {
setState(() { setState(() {
_rcmdController.isLoadingMore = true;
_futureBuilderFuture = _futureBuilderFuture =
_rcmdController.queryRcmdFeed('init'); _rcmdController.queryRcmdFeed('init');
}); });
@@ -127,20 +121,11 @@ class _RcmdPageState extends State<RcmdPage>
); );
} }
} else { } else {
// 缓存数据
// if (_rcmdController.videoList.isNotEmpty) {
// return contentGrid(
// _rcmdController, _rcmdController.videoList);
// }
// // 骨架屏
// else {
return contentGrid(_rcmdController, []); return contentGrid(_rcmdController, []);
// }
} }
}, },
), ),
), ),
LoadingMore(ctr: _rcmdController)
], ],
), ),
), ),
@@ -203,33 +188,3 @@ class _RcmdPageState extends State<RcmdPage>
); );
} }
} }
class LoadingMore extends StatelessWidget {
final dynamic ctr;
const LoadingMore({super.key, this.ctr});
@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: GestureDetector(
onTap: () {
if (ctr != null) {
ctr!.isLoadingMore = true;
ctr!.onLoad();
}
},
child: Center(
child: Text(
'点击加载更多 👇',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
),
),
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
@@ -27,9 +26,6 @@ class SSearchController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) { if (Get.parameters['keyword'] != null) {
@@ -130,12 +126,4 @@ class SSearchController extends GetxController {
historyList.refresh(); historyList.refresh();
histiryWord.put('cacheList', []); histiryWord.put('cacheList', []);
} }
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
searchKeyWord.value =
hintText = defaultSearch.value = res.data['data']['name'];
}
}
} }

View File

@@ -187,9 +187,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
); );
} else { } else {
return HttpError( return CustomScrollView(
errMsg: data['msg'], slivers: [
fn: () => setState(() {}), HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
); );
} }
} else { } else {

View File

@@ -105,7 +105,11 @@ class _SearchPanelState extends State<SearchPanel>
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
), ),
], ],
); );
@@ -116,7 +120,11 @@ class _SearchPanelState extends State<SearchPanel>
slivers: [ slivers: [
HttpError( HttpError(
errMsg: '没有相关数据', errMsg: '没有相关数据',
fn: () => setState(() {}), fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
), ),
], ],
); );

View File

@@ -25,16 +25,17 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5), StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) { child: LayoutBuilder(builder: (context, boxConstraints) {
double width = (boxConstraints.maxWidth - final double width = (boxConstraints.maxWidth -
StyleString.cardSpace * StyleString.cardSpace *
6 / 6 /
MediaQuery.textScalerOf(context).scale(2.0)); MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container( return Container(
constraints: const BoxConstraints(minHeight: 88), constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
if (list[index].imageUrls != null && if (list[index].imageUrls != null &&
list[index].imageUrls.isNotEmpty) list[index].imageUrls.isNotEmpty)
AspectRatio( AspectRatio(

View File

@@ -90,7 +90,7 @@ class SearchVideoPanel extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => controller.onShowFilterDialog(), onPressed: () => controller.onShowFilterDialog(ctr),
icon: Icon( icon: Icon(
Icons.filter_list_outlined, Icons.filter_list_outlined,
size: 18, size: 18,
@@ -175,7 +175,7 @@ class VideoPanelController extends GetxController {
super.onInit(); super.onInit();
} }
onShowFilterDialog() { onShowFilterDialog(searchPanelCtr) {
SmartDialog.show( SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide, animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) { builder: (BuildContext context) {
@@ -199,7 +199,8 @@ class VideoPanelController extends GetxController {
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast("${i['label']}」的筛选结果"); SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr = SearchPanelController ctr =
Get.find<SearchPanelController>(tag: 'video'); Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value']; ctr.duration.value = i['value'];
SmartDialog.showLoading(msg: 'loading'); SmartDialog.showLoading(msg: 'loading');
await ctr.onRefresh(); await ctr.onRefresh();

View File

@@ -86,7 +86,8 @@ class _SearchResultPageState extends State<SearchResultPage>
onTap: (index) { onTap: (index) {
if (index == _searchResultController!.tabIndex) { if (index == _searchResultController!.tabIndex) {
Get.find<SearchPanelController>( Get.find<SearchPanelController>(
tag: SearchType.values[index].type) tag: SearchType.values[index].type +
_searchResultController!.keyword!)
.animateToTop(); .animateToTop();
} }

View File

@@ -7,6 +7,9 @@ import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/login.dart'; import 'package:pilipala/utils/login.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../main/index.dart';
import 'widgets/select_dialog.dart';
class SettingController extends GetxController { class SettingController extends GetxController {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
@@ -19,6 +22,7 @@ class SettingController extends GetxController {
RxInt picQuality = 10.obs; RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs; Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo; var userInfo;
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override @override
void onInit() { void onInit() {
@@ -33,6 +37,9 @@ class SettingController extends GetxController {
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)]; defaultValue: ThemeType.system.code)];
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
} }
loginOut() async { loginOut() async {
@@ -76,4 +83,31 @@ class SettingController extends GetxController {
feedBackEnable.value = !feedBackEnable.value; feedBackEnable.value = !feedBackEnable.value;
setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value); setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value);
} }
// 设置动态未读标记
setDynamicBadgeMode(BuildContext context) async {
DynamicBadgeMode? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<DynamicBadgeMode>(
title: '动态未读标记',
value: dynamicBadgeType.value,
values: DynamicBadgeMode.values.map((e) {
return {'title': e.description, 'value': e};
}).toList(),
);
},
);
if (result != null) {
dynamicBadgeType.value = result;
setting.put(SettingBoxKey.dynamicBadgeMode, result.code);
MainController mainController = Get.put(MainController());
mainController.dynamicBadgeType.value =
DynamicBadgeMode.values[result.code];
if (mainController.dynamicBadgeType.value != DynamicBadgeMode.hidden) {
mainController.getUnreadDynamic();
}
SmartDialog.showToast('设置成功');
}
}
} }

View File

@@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/common/dynamics_type.dart'; import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/rcmd_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@@ -20,26 +18,23 @@ class ExtraSetting extends StatefulWidget {
class _ExtraSettingState extends State<ExtraSetting> { class _ExtraSettingState extends State<ExtraSetting> {
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache; static Box localCache = GStrorage.localCache;
late dynamic defaultRcmdType;
late dynamic defaultReplySort; late dynamic defaultReplySort;
late dynamic defaultDynamicType; late dynamic defaultDynamicType;
late dynamic enableSystemProxy; late dynamic enableSystemProxy;
late String defaultSystemProxyHost; late String defaultSystemProxyHost;
late String defaultSystemProxyPort; late String defaultSystemProxyPort;
Box userInfoCache = GStrorage.userInfo;
var userInfo;
bool userLogin = false; bool userLogin = false;
var accessKeyInfo;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 首页默认推荐类型
defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
// 默认优先显示最新评论 // 默认优先显示最新评论
defaultReplySort = defaultReplySort =
setting.get(SettingBoxKey.replySortType, defaultValue: 0); setting.get(SettingBoxKey.replySortType, defaultValue: 0);
if (defaultReplySort == 2) {
setting.put(SettingBoxKey.replySortType, 0);
defaultReplySort = 0;
}
// 优先展示全部动态 all // 优先展示全部动态 all
defaultDynamicType = defaultDynamicType =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0); setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
@@ -49,9 +44,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: ''); localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
defaultSystemProxyPort = defaultSystemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: ''); localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: null);
} }
// 设置代理 // 设置代理
@@ -159,12 +151,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableSearchWord, setKey: SettingBoxKey.enableSearchWord,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem( const SetSwitchItem(
title: '快速收藏', title: '快速收藏',
subTitle: '点按收藏至默认,长按选择文件夹', subTitle: '点按收藏至默认,长按选择文件夹',
@@ -177,50 +163,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableWordRe, setKey: SettingBoxKey.enableWordRe,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: '首页推荐刷新',
subTitle: '下拉刷新时保留上次内容',
setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false,
),
ListTile(
dense: false,
title: Text('首页推荐类型', style: titleStyle),
subtitle: Text(
'当前使用「$defaultRcmdType端」推荐',
style: subTitleStyle,
),
onTap: () async {
String? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<String>(
title: '推荐类型',
value: defaultRcmdType,
values: RcmdType.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList(),
);
},
);
if (result != null) {
if (result == 'app') {
// app端推荐需要access_key
if (accessKeyInfo == null) {
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
await MemberHttp.cookieToKey();
}
}
defaultRcmdType = result;
setting.put(SettingBoxKey.defaultRcmdType, result);
SmartDialog.showToast('下次启动时生效');
setState(() {});
}
},
),
const SetSwitchItem( const SetSwitchItem(
title: '启用ai总结', title: '启用ai总结',
subTitle: '视频详情页开启ai总结', subTitle: '视频详情页开启ai总结',

View File

@@ -22,6 +22,17 @@ class _TabbarSetPageState extends State<TabbarSetPage> {
defaultTabs = tabsConfig; defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
// 对 tabData 进行排序
defaultTabs.sort((a, b) {
int indexA = tabbarSort.indexOf((a['type'] as TabType).id);
int indexB = tabbarSort.indexOf((b['type'] as TabType).id);
// 如果类型在 sortOrder 中不存在,则放在末尾
if (indexA == -1) indexA = tabbarSort.length;
if (indexB == -1) indexB = tabbarSort.length;
return indexA.compareTo(indexB);
});
} }
void saveEdit() { void saveEdit() {

View File

@@ -0,0 +1,201 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/loggeer.dart';
class LogsPage extends StatefulWidget {
const LogsPage({super.key});
@override
State<LogsPage> createState() => _LogsPageState();
}
class _LogsPageState extends State<LogsPage> {
late File logsPath;
late String fileContent;
List logsContent = [];
@override
void initState() {
getPath();
super.initState();
}
void getPath() async {
logsPath = await getLogsPath();
fileContent = await logsPath.readAsString();
logsContent = await parseLogs(fileContent);
setState(() {});
}
Future<List<Map<String, dynamic>>> parseLogs(String fileContent) async {
const String splitToken =
'======================================================================';
List contentList = fileContent.split(splitToken).map((item) {
return item
.replaceAll(
'============================== CATCHER 2 LOG ==============================',
'Pilipala错误日志 \n ********************')
.replaceAll('DEVICE INFO', '设备信息')
.replaceAll('APP INFO', '应用信息')
.replaceAll('ERROR', '错误信息')
.replaceAll('STACK TRACE', '错误堆栈');
}).toList();
List<Map<String, dynamic>> result = [];
for (String i in contentList) {
DateTime? date;
String body = i
.split("\n")
.map((l) {
if (l.startsWith("Crash occurred on")) {
date = DateTime.parse(
l.split("Crash occurred on")[1].trim().split('.')[0],
);
return "";
}
return l;
})
.where((dynamic l) => l.replaceAll("\n", "").trim().isNotEmpty)
.join("\n");
if (date != null || body != '') {
result.add({'date': date, 'body': body, 'expand': false});
}
}
return result.reversed.toList();
}
void copyLogs() async {
await Clipboard.setData(ClipboardData(text: fileContent));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('复制成功')),
);
}
}
void feedback() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/issues'),
// 系统自带浏览器打开
mode: LaunchMode.externalApplication,
);
}
void clearLogsHandle() async {
if (await clearLogs()) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已清空')),
);
logsContent = [];
setState(() {});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text('日志', style: Theme.of(context).textTheme.titleMedium),
actions: [
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'copy':
copyLogs();
break;
case 'feedback':
feedback();
break;
case 'clear':
clearLogsHandle();
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'copy',
child: Text('复制日志'),
),
const PopupMenuItem<String>(
value: 'feedback',
child: Text('错误反馈'),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空日志'),
),
],
),
const SizedBox(width: 6),
],
),
body: logsContent.isNotEmpty
? ListView.builder(
itemCount: logsContent.length,
itemBuilder: (context, index) {
final log = logsContent[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
log['date'].toString(),
style: Theme.of(context).textTheme.titleMedium,
),
),
TextButton.icon(
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: log['body']),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'已将 ${log['date'].toString()} 复制至剪贴板',
),
),
);
}
},
icon: const Icon(Icons.copy_outlined, size: 16),
label: const Text('复制'),
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SelectableText(log['body']),
),
),
),
const Divider(indent: 12, endIndent: 12),
],
);
},
)
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
}
}

View File

@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/common/rcmd_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
class RecommendSetting extends StatefulWidget {
const RecommendSetting({super.key});
@override
State<RecommendSetting> createState() => _RecommendSettingState();
}
class _RecommendSettingState extends State<RecommendSetting> {
Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic defaultRcmdType;
Box userInfoCache = GStrorage.userInfo;
late dynamic userInfo;
bool userLogin = false;
late dynamic accessKeyInfo;
// late int filterUnfollowedRatio;
late int minDurationForRcmd;
late int minLikeRatioForRecommend;
@override
void initState() {
super.initState();
// 首页默认推荐类型
defaultRcmdType =
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
accessKeyInfo = localCache.get(LocalCacheKey.accessKey, defaultValue: null);
// filterUnfollowedRatio = setting
// .get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);
minDurationForRcmd =
setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);
minLikeRatioForRecommend =
setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'推荐设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
ListTile(
dense: false,
title: Text('首页推荐类型', style: titleStyle),
subtitle: Text(
'当前使用「$defaultRcmdType端」推荐¹',
style: subTitleStyle,
),
onTap: () async {
String? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<String>(
title: '推荐类型',
value: defaultRcmdType,
values: RcmdType.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList(),
);
},
);
if (result != null) {
if (result == 'app') {
// app端推荐需要access_key
if (accessKeyInfo == null) {
if (!userLogin) {
SmartDialog.showToast('请先登录');
return;
}
// 显示一个确认框,告知用户可能会导致账号被风控
SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text(
'使用app端推荐需获取access_key有小概率触发风控导致账号退出在官方版本app重新登录即可解除是否继续'),
actions: [
TextButton(
onPressed: () {
result = null;
SmartDialog.dismiss();
},
child: const Text('取消'),
),
TextButton(
onPressed: () async {
SmartDialog.dismiss();
await MemberHttp.cookieToKey();
},
child: const Text('确定'),
),
],
);
});
}
}
if (result != null) {
defaultRcmdType = result;
setting.put(SettingBoxKey.defaultRcmdType, result);
SmartDialog.showToast('下次启动时生效');
setState(() {});
}
}
},
),
const SetSwitchItem(
title: '推荐动态',
subTitle: '是否在推荐内容中展示动态(仅app端)',
setKey: SettingBoxKey.enableRcmdDynamic,
defaultVal: true,
),
const SetSwitchItem(
title: '首页推荐刷新',
subTitle: '下拉刷新时保留上次内容',
setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false,
),
// 分割线
const Divider(height: 1),
ListTile(
dense: false,
title: Text('点赞率过滤', style: titleStyle),
subtitle: Text(
'过滤掉点赞数/播放量「小于$minLikeRatioForRecommend%」的推荐视频(仅web端)',
style: subTitleStyle,
),
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '选择点赞率0即不过滤',
value: minLikeRatioForRecommend,
values: [0, 1, 2, 3, 4].map((e) {
return {'title': '$e %', 'value': e};
}).toList());
},
);
if (result != null) {
minLikeRatioForRecommend = result;
setting.put(SettingBoxKey.minLikeRatioForRecommend, result);
RecommendFilter.update();
setState(() {});
}
},
),
ListTile(
dense: false,
title: Text('视频时长过滤', style: titleStyle),
subtitle: Text(
'过滤掉时长「小于$minDurationForRcmd秒」的推荐视频',
style: subTitleStyle,
),
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '选择时长0即不过滤',
value: minDurationForRcmd,
values: [0, 30, 60, 90, 120].map((e) {
return {'title': '$e', 'value': e};
}).toList());
},
);
if (result != null) {
minDurationForRcmd = result;
setting.put(SettingBoxKey.minDurationForRcmd, result);
RecommendFilter.update();
setState(() {});
}
},
),
SetSwitchItem(
title: '已关注Up豁免推荐过滤',
subTitle: '推荐中已关注用户发布的内容不会被过滤',
setKey: SettingBoxKey.exemptFilterForFollowed,
defaultVal: true,
callFn: (_) => {RecommendFilter.update},
),
// ListTile(
// dense: false,
// title: Text('按比例过滤未关注Up', style: titleStyle),
// subtitle: Text(
// '滤除推荐中占比「$filterUnfollowedRatio%」的未关注用户发布的内容',
// style: subTitleStyle,
// ),
// onTap: () async {
// int? result = await showDialog(
// context: context,
// builder: (context) {
// return SelectDialog<int>(
// title: '选择滤除比例0即不过滤',
// value: filterUnfollowedRatio,
// values: [0, 16, 32, 48, 64].map((e) {
// return {'title': '$e %', 'value': e};
// }).toList());
// },
// );
// if (result != null) {
// filterUnfollowedRatio = result;
// setting.put(
// SettingBoxKey.filterUnfollowedRatio, result);
// RecommendFilter.update();
// setState(() {});
// }
// },
// ),
SetSwitchItem(
title: '过滤器也应用于相关视频',
subTitle: '视频详情页的相关视频也进行过滤²',
setKey: SettingBoxKey.applyFilterToRelatedVideos,
defaultVal: true,
callFn: (_) => {RecommendFilter.update},
),
ListTile(
dense: true,
subtitle: Text(
'¹ 若默认web端推荐不太符合预期可尝试切换至app端。\n'
'¹ 选择“模拟未登录(notLogin)”将以空的key请求推荐接口但播放页仍会携带用户信息保证账号能正常记录进度、点赞投币等。\n\n'
'² 由于接口未提供关注信息无法豁免相关视频中的已关注Up。\n\n'
'* 其它(如热门视频、手动搜索、链接跳转等)均不受过滤器影响。\n'
'* 设定较严苛的条件可导致推荐项数锐减或多次请求,请酌情选择。\n'
'* 后续可能会增加更多过滤条件,敬请期待。',
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline.withOpacity(0.7)),
),
)
],
),
);
}
}

View File

@@ -10,6 +10,7 @@ import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/pages/setting/widgets/slide_dialog.dart'; import 'package:pilipala/pages/setting/widgets/slide_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@@ -241,6 +242,14 @@ class _StyleSettingState extends State<StyleSetting> {
'当前模式:${settingController.themeType.value.description}', '当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)), style: subTitleStyle)),
), ),
ListTile(
dense: false,
onTap: () => settingController.setDynamicBadgeMode(context),
title: Text('动态未读标记', style: titleStyle),
subtitle: Obx(() => Text(
'当前标记样式:${settingController.dynamicBadgeType.value.description}',
style: subTitleStyle)),
),
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/colorSetting'), onTap: () => Get.toNamed('/colorSetting'),

View File

@@ -24,6 +24,11 @@ class SettingPage extends StatelessWidget {
dense: false, dense: false,
title: const Text('隐私设置'), title: const Text('隐私设置'),
), ),
ListTile(
onTap: () => Get.toNamed('/recommendSetting'),
dense: false,
title: const Text('推荐设置'),
),
ListTile( ListTile(
onTap: () => Get.toNamed('/playSetting'), onTap: () => Get.toNamed('/playSetting'),
dense: false, dense: false,

View File

@@ -19,6 +19,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart'; import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
class VideoDetailController extends GetxController class VideoDetailController extends GetxController
@@ -61,7 +62,7 @@ class VideoDetailController extends GetxController
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
int oid = 0; RxInt oid = 0.obs;
// 评论id 请求楼中楼评论使用 // 评论id 请求楼中楼评论使用
int fRpid = 0; int fRpid = 0;
@@ -135,13 +136,14 @@ class VideoDetailController extends GetxController
defaultValue: VideoDecodeFormats.values.last.code); defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
} }
showReplyReplyPanel() { showReplyReplyPanel() {
PersistentBottomSheetController? ctr = PersistentBottomSheetController? ctr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) { scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel( return VideoReplyReplyPanel(
oid: oid, oid: oid.value,
rpid: fRpid, rpid: fRpid,
closePanel: () => { closePanel: () => {
fRpid = 0, fRpid = 0,

View File

@@ -148,7 +148,9 @@ class VideoIntroController extends GetxController {
// 获取投币状态 // 获取投币状态
Future queryHasCoinVideo() async { Future queryHasCoinVideo() async {
var result = await VideoHttp.hasCoinVideo(bvid: bvid); var result = await VideoHttp.hasCoinVideo(bvid: bvid);
hasCoin.value = result["data"]['multiply'] == 0 ? false : true; if (result['status']) {
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
}
} }
// 获取收藏状态 // 获取收藏状态
@@ -208,6 +210,10 @@ class VideoIntroController extends GetxController {
// (取消)点赞 // (取消)点赞
Future actionLikeVideo() async { Future actionLikeVideo() async {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) { if (result['status']) {
// hasLike.value = result["data"] == 1 ? true : false; // hasLike.value = result["data"] == 1 ? true : false;
@@ -292,7 +298,6 @@ class VideoIntroController extends GetxController {
await queryVideoInFolder(); await queryVideoInFolder();
int defaultFolderId = favFolderData.value.list!.first.id!; int defaultFolderId = favFolderData.value.list!.first.id!;
int favStatus = favFolderData.value.list!.first.favState!; int favStatus = favFolderData.value.list!.first.favState!;
print('favStatus: $favStatus');
var result = await VideoHttp.favVideo( var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid), aid: IdUtils.bv2av(bvid),
addIds: favStatus == 0 ? '$defaultFolderId' : '', addIds: favStatus == 0 ? '$defaultFolderId' : '',
@@ -304,6 +309,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功'); SmartDialog.showToast('✅ 操作成功');
} }
} else {
SmartDialog.showToast(result['msg']);
} }
return; return;
} }
@@ -334,6 +341,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo(); await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功'); SmartDialog.showToast('✅ 操作成功');
} }
} else {
SmartDialog.showToast(result['msg']);
} }
} }
@@ -470,6 +479,7 @@ class VideoIntroController extends GetxController {
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cid.value = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
@@ -570,10 +580,12 @@ class VideoIntroController extends GetxController {
cid: lastPlayCid.value, cid: lastPlayCid.value,
upMid: videoDetail.value.owner!.mid!, upMid: videoDetail.value.owner!.mid!,
); );
SmartDialog.dismiss();
if (res['status']) { if (res['status']) {
modelResult = res['data'].modelResult; modelResult = res['data'].modelResult;
} else {
SmartDialog.showToast("当前视频可能暂不支持AI视频总结");
} }
SmartDialog.dismiss();
return res; return res;
} }
} }

View File

@@ -4,7 +4,7 @@ import 'package:pilipala/http/video.dart';
class ReleatedController extends GetxController { class ReleatedController extends GetxController {
// 视频aid // 视频aid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid'] ?? "";
// 推荐视频列表 // 推荐视频列表
List relatedVideoList = []; List relatedVideoList = [];

View File

@@ -41,17 +41,25 @@ class VideoReplyController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
final int deaultReplySortIndex = int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0) as int; setting.get(SettingBoxKey.replySortType, defaultValue: 0) as int;
if (deaultReplySortIndex == 2) {
setting.put(SettingBoxKey.replySortType, 0);
deaultReplySortIndex = 0;
}
_sortType = ReplySortType.values[deaultReplySortIndex]; _sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels; sortTypeLabel.value = _sortType.labels;
} }
Future queryReplyList({type = 'init'}) async { Future queryReplyList({type = 'init'}) async {
if (isLoadingMore) {
return;
}
isLoadingMore = true; isLoadingMore = true;
if (type == 'init') { if (type == 'init') {
currentPage = 0; currentPage = 0;
noMore.value = '';
} }
if (noMore.value == '没有更多了') { if (noMore.value == '没有更多了') {
return; return;
@@ -115,9 +123,6 @@ class VideoReplyController extends GetxController {
_sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
_sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:

View File

@@ -16,11 +16,13 @@ import 'widgets/reply_item.dart';
class VideoReplyPanel extends StatefulWidget { class VideoReplyPanel extends StatefulWidget {
final String? bvid; final String? bvid;
final int? oid;
final int rpid; final int rpid;
final String? replyLevel; final String? replyLevel;
const VideoReplyPanel({ const VideoReplyPanel({
this.bvid, this.bvid,
this.oid,
this.rpid = 0, this.rpid = 0,
this.replyLevel, this.replyLevel,
super.key, super.key,
@@ -48,16 +50,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0; // int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
replyLevel = widget.replyLevel ?? '1'; replyLevel = widget.replyLevel ?? '1';
if (replyLevel == '2') { if (replyLevel == '2') {
_videoReplyController = Get.put( _videoReplyController = Get.put(
VideoReplyController(oid, widget.rpid.toString(), replyLevel), VideoReplyController(widget.oid, widget.rpid.toString(), replyLevel),
tag: widget.rpid.toString()); tag: widget.rpid.toString());
} else { } else {
_videoReplyController = _videoReplyController = Get.put(
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag); VideoReplyController(widget.oid, '', replyLevel),
tag: heroTag);
} }
fabAnimationCtr = AnimationController( fabAnimationCtr = AnimationController(
@@ -75,7 +78,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) { scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
() {
_videoReplyController.onLoad(); _videoReplyController.onLoad();
}); });
} }
@@ -110,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) { if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid; videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!; videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem; videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(); videoDetailCtr.showReplyReplyPanel();
@@ -256,7 +260,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
// 请求错误 // 请求错误
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_futureBuilderFuture =
_videoReplyController.queryReplyList();
});
},
); );
} }
} else { } else {

View File

@@ -1,6 +1,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
@@ -11,10 +12,9 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'zan.dart'; import 'zan.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@@ -47,6 +47,17 @@ class ReplyItem extends StatelessWidget {
replyReply!(replyItem); replyReply!(replyItem);
} }
}, },
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replyItem);
},
);
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@@ -122,98 +133,6 @@ class ReplyItem extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// 头像、昵称
// SizedBox(
// width: double.infinity,
// child: Stack(
// children: [
// GestureDetector(
// behavior: HitTestBehavior.opaque,
// onTap: () {
// feedBack();
// Get.toNamed('/member?mid=${replyItem!.mid}', arguments: {
// 'face': replyItem!.member!.avatar!,
// 'heroTag': heroTag
// });
// },
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// lfAvtar(context, heroTag),
// const SizedBox(width: 12),
// Text(
// replyItem!.member!.uname!,
// style: TextStyle(
// color: replyItem!.member!.vip!['vipStatus'] > 0
// ? const Color.fromARGB(255, 251, 100, 163)
// : Theme.of(context).colorScheme.outline,
// fontSize: 13,
// ),
// ),
// const SizedBox(width: 6),
// Image.asset(
// 'assets/images/lv/lv${replyItem!.member!.level}.png',
// height: 11,
// ),
// const SizedBox(width: 6),
// if (replyItem!.isUp!)
// const PBadge(
// text: 'UP',
// size: 'small',
// stack: 'normal',
// fs: 9,
// ),
// ],
// ),
// ),
// Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: Container(
// width: double.infinity,
// height: 45,
// decoration: BoxDecoration(
// image: replyItem!.member!.userSailing!.cardbg != null
// ? DecorationImage(
// alignment: Alignment.centerRight,
// fit: BoxFit.fitHeight,
// image: NetworkImage(
// replyItem!.member!.userSailing!.cardbg!['image'],
// ),
// )
// : null,
// ),
// ),
// ),
// if (replyItem!.member!.userSailing!.cardbg != null &&
// replyItem!.member!.userSailing!.cardbg!['fan']['number'] > 0)
// Positioned(
// top: 10,
// left: Get.size.width / 7 * 5.8,
// child: DefaultTextStyle(
// style: TextStyle(
// fontFamily: 'fansCard',
// fontSize: 9,
// color: Theme.of(context).colorScheme.primary,
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Text('NO.'),
// Text(
// replyItem!.member!.userSailing!.cardbg!['fan']
// ['num_desc'],
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// ),
/// fix Stack内GestureDetector onTap无效 /// fix Stack内GestureDetector onTap无效
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@@ -290,30 +209,26 @@ class ReplyItem extends StatelessWidget {
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: SelectableRegion( child: Text.rich(
focusNode: FocusNode(), style: const TextStyle(height: 1.75),
selectionControls: MaterialTextSelectionControls(), maxLines:
child: Text.rich( replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
style: const TextStyle(height: 1.75), overflow: TextOverflow.ellipsis,
maxLines: TextSpan(
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999, children: [
overflow: TextOverflow.ellipsis, if (replyItem!.isTop!)
TextSpan( const WidgetSpan(
children: [ alignment: PlaceholderAlignment.top,
if (replyItem!.isTop!) child: PBadge(
const WidgetSpan( text: 'TOP',
alignment: PlaceholderAlignment.top, size: 'small',
child: PBadge( stack: 'normal',
text: 'TOP', type: 'line',
size: 'small', fs: 9,
stack: 'normal',
type: 'line',
fs: 9,
),
), ),
buildContent(context, replyItem!, replyReply, null), ),
], buildContent(context, replyItem!, replyReply, null),
), ],
), ),
), ),
), ),
@@ -446,6 +361,17 @@ class ReplyItemRow extends StatelessWidget {
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
onTap: () => replyReply!(replyItem), onTap: () => replyReply!(replyItem),
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replies![i]);
},
);
},
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@@ -539,20 +465,7 @@ InlineSpan buildContent(
// replyReply 查看二楼回复(回复详情)回调 // replyReply 查看二楼回复(回复详情)回调
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content; final content = replyItem.content;
if (content.emote.isEmpty &&
content.atNameToMid.isEmpty &&
content.jumpUrl.isEmpty &&
content.vote.isEmpty &&
content.pictures.isEmpty) {
return TextSpan(
text: content.message,
recognizer: TapGestureRecognizer()
..onTap =
() => replyReply(replyItem.root == 0 ? replyItem : fReplyItem),
);
}
final List<InlineSpan> spanChilds = <InlineSpan>[]; final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false;
// 投票 // 投票
if (content.vote.isNotEmpty) { if (content.vote.isNotEmpty) {
@@ -582,280 +495,271 @@ InlineSpan buildContent(
}); });
} }
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); // content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
if (content.message.contains('&amp;')) { content.message = content.message
content.message = content.message.replaceAll('&amp;', '&'); .replaceAll('&amp;', '&')
} .replaceAll('&lt;', '<')
// 匹配表情 .replaceAll('&gt;', '>')
content.message.splitMapJoin( .replaceAll('&quot;', '"')
RegExp(r"\[.*?\]"), .replaceAll('&apos;', "'")
onMatch: (Match match) { .replaceAll('&nbsp;', ' ');
final String matchStr = match[0]!; // print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
if (content.emote.isNotEmpty && // 构建正则表达式
matchStr.indexOf('[') == matchStr.lastIndexOf('[') && final List<String> specialTokens = [
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) { ...content.emote.keys,
final int size = content.emote[matchStr]['meta']['size']; ...content.atNameToMid.keys.map((e) => '@$e'),
if (content.emote.keys.contains(matchStr)) { ...content.jumpUrl.keys.map((e) =>
spanChilds.add( e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
WidgetSpan( ];
child: NetworkImgLayer(
src: content.emote[matchStr]['url'],
type: 'emote',
width: size * 20,
height: size * 20,
),
),
);
} else {
spanChilds.add(TextSpan(
text: matchStr,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
return matchStr;
}
} else {
spanChilds.add(TextSpan(
text: matchStr,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
return matchStr;
}
return '';
},
onNonMatch: (String str) {
// 匹配@用户
String matchMember = str;
if (content.atNameToMid.isNotEmpty) {
final List atNameToMidKeys = content.atNameToMid.keys.toList();
RegExp reg = RegExp(atNameToMidKeys.map((key) => key).join('|'));
// if (!content.message.contains(':')) {
// reg = RegExp(r"@.*( |:)");
// }
// 只@用户没有内容 String patternStr = specialTokens.map(RegExp.escape).join('|');
if (!content.message.contains(':') || if (patternStr.isNotEmpty) {
(content.atNameToMid.length == 1 && patternStr += "|";
content.message == '@${content.members.first.uname}')) { }
reg = RegExp(r"@.*( |:|$)"); patternStr += r'(\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b)';
} final RegExp pattern = RegExp(patternStr);
matchMember = str.splitMapJoin( List<String> matchedStrs = [];
reg, void addPlainTextSpan(str) {
onMatch: (Match match) { spanChilds.add(TextSpan(
if (match[0] != null) { text: str,
hasMatchMember = true; recognizer: TapGestureRecognizer()
content.atNameToMid.forEach((key, value) { ..onTap =
if (str.contains('回复')) { () => replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
spanChilds.add( }
TextSpan(
text: '回复 ', // 分割文本并处理每个部分
style: TextStyle( content.message.splitMapJoin(
fontSize: pattern,
Theme.of(context).textTheme.titleSmall!.fontSize, onMatch: (Match match) {
), String matchStr = match[0]!;
), if (content.emote.containsKey(matchStr)) {
); // 处理表情
} final int size = content.emote[matchStr]['meta']['size'];
spanChilds.add( spanChilds.add(WidgetSpan(
TextSpan( child: NetworkImgLayer(
text: '@$key', src: content.emote[matchStr]['url'],
style: TextStyle( type: 'emote',
fontSize: width: size * 20,
Theme.of(context).textTheme.titleSmall!.fontSize, height: size * 20,
color: Theme.of(context).colorScheme.primary, ),
), ));
recognizer: TapGestureRecognizer() } else if (matchStr.startsWith("@") &&
..onTap = () { content.atNameToMid.containsKey(matchStr.substring(1))) {
final String heroTag = Utils.makeHeroTag(value); // 处理@用户
Get.toNamed( final String userName = matchStr.substring(1);
'/member?mid=$value', final int userId = content.atNameToMid[userName];
arguments: {'face': '', 'heroTag': heroTag}, spanChilds.add(
); TextSpan(
}, text: matchStr,
), style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
final String heroTag = Utils.makeHeroTag(userId);
Get.toNamed(
'/member?mid=$userId',
arguments: {'face': '', 'heroTag': heroTag},
); );
}); },
} ),
return ''; );
}, } else if (RegExp(r'^\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b$')
onNonMatch: (String str) { .hasMatch(matchStr)) {
if (!str.contains('@')) { matchStr = matchStr.replaceAll('', ':');
spanChilds.add(TextSpan(text: str)); spanChilds.add(
} TextSpan(
print(str); text: ' $matchStr ',
return str; style: TextStyle(
}, color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
}
},
),
); );
} else { } else {
matchMember = str; // print("matchStr=$matchStr");
} String appUrlSchema = '';
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
// 匹配 jumpUrl defaultValue: false) as bool;
String matchUrl = matchMember; if (content.jumpUrl[matchStr] != null &&
if (content.jumpUrl.isNotEmpty) { !matchedStrs.contains(matchStr)) {
final List urlKeys = content.jumpUrl.keys.toList().reversed.toList(); appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
for (int index = 0; index < urlKeys.length; index++) { if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
var i = urlKeys[index]; addPlainTextSpan(matchStr);
if (i.contains('?')) { return "";
urlKeys[index] = i.replaceAll('?', '\\?');
} }
if (i.contains('+')) { spanChilds.addAll(
urlKeys[index] = i.replaceAll('+', '\\+'); [
} if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
if (i.contains('*')) { WidgetSpan(
urlKeys[index] = i.replaceAll('*', '\\*'); child: Image.network(
} content.jumpUrl[matchStr]['prefix_icon'],
} height: 19,
if (hasMatchMember) { color: Theme.of(context).colorScheme.primary,
matchMember = matchMember.split('回复 @ :').length > 1
? matchMember.split('回复 @ :')[1]
: matchMember;
}
matchUrl = matchMember.splitMapJoin(
/// RegExp.escape() 转义特殊字符
RegExp(urlKeys.map((key) => key).join("|")),
// RegExp('What does the fox say\\?'),
onMatch: (Match match) {
final String matchStr = match[0]!;
String appUrlSchema = '';
if (content.jumpUrl[matchStr] != null) {
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
}
// 默认不显示关键词
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
defaultValue: false) as bool;
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 == '') { TextSpan(
final String str = Uri.parse(matchStr).pathSegments[0]; text: content.jumpUrl[matchStr]['title'],
final Map matchRes = IdUtils.matchAvorBv(input: str); style: TextStyle(
final List matchKeys = matchRes.keys.toList(); color: Theme.of(context).colorScheme.primary,
if (matchKeys.isNotEmpty) { ),
if (matchKeys.first == 'BV') { recognizer: TapGestureRecognizer()
Get.toNamed( ..onTap = () async {
'/searchResult', final String title = content.jumpUrl[matchStr]['title'];
parameters: {'keyword': matchRes['BV']}, if (appUrlSchema == '') {
); final String redirectUrl =
} await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult',
parameters: {'keyword': title});
} else if (matchStr.startsWith('https://b23.tv')) {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else { } else {
Get.toNamed( Get.toNamed(
'/webview', '/webview',
parameters: { parameters: {
'url': matchStr, 'url': redirectUrl,
'type': 'url', 'type': 'url',
'pageTitle': '' 'pageTitle': title
}, },
); );
} }
} else { } else {
if (appUrlSchema.startsWith('bilibili://search') && Get.toNamed(
enableWordRe) { '/webview',
Get.toNamed('/searchResult', parameters: { parameters: {
'keyword': content.jumpUrl[matchStr]['title'] 'url': matchStr,
}); 'type': 'url',
} 'pageTitle': title
},
);
} }
}, }
), },
); )
} ],
);
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) { // 只显示一次
spanChilds.add( matchedStrs.add(matchStr);
WidgetSpan( } else {
child: Icon( addPlainTextSpan(matchStr);
FontAwesomeIcons.magnifyingGlass, }
size: 9,
color: Theme.of(context).colorScheme.primary,
),
alignment: PlaceholderAlignment.top,
),
);
}
return '';
},
onNonMatch: (String str) {
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () => replyReply(
replyItem.root == 0 ? replyItem : fReplyItem)));
return str;
},
);
} }
str = matchUrl.splitMapJoin( return '';
RegExp(r'\b\d{2}:\d{2}\b'), },
onMatch: (Match match) { onNonMatch: (String nonMatchStr) {
String matchStr = match[0]!; addPlainTextSpan(nonMatchStr);
spanChilds.add( return nonMatchStr;
},
);
if (content.jumpUrl.keys.isNotEmpty) {
List<String> unmatchedItems = content.jumpUrl.keys
.toList()
.where((item) => !content.message.contains(item))
.toList();
if (unmatchedItems.isNotEmpty) {
for (int i = 0; i < unmatchedItems.length; i++) {
String patternStr = unmatchedItems[i];
spanChilds.addAll(
[
if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[patternStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan( TextSpan(
text: ' $matchStr ', text: content.jumpUrl[patternStr]['title'],
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
// 跳转到指定位置 Get.toNamed(
try { '/webview',
Get.find<VideoDetailController>( parameters: {
tag: Get.arguments['heroTag']) 'url': patternStr,
.plPlayerController 'type': 'url',
.seekTo( 'pageTitle': content.jumpUrl[patternStr]['title']
Duration(seconds: Utils.duration(matchStr)), },
); );
} catch (_) {}
}, },
), )
); ],
return ''; );
},
onNonMatch: (str) {
return str;
},
);
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) {
if (str != '') {
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
}
} }
return str; }
}, }
);
// 图片渲染 // 图片渲染
if (content.pictures.isNotEmpty) { if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[]; final List<String> picList = <String>[];
final int len = content.pictures.length; final int len = content.pictures.length;
spanChilds.add(const TextSpan(text: '\n'));
if (len == 1) { if (len == 1) {
Map pictureItem = content.pictures.first; Map pictureItem = content.pictures.first;
picList.add(pictureItem['img_src']); picList.add(pictureItem['img_src']);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints box) { builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度 double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble(); // double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth / double height = 100;
2 * try {
pictureItem['img_height'] / height = ((box.maxWidth /
pictureItem['img_width'])) 2 *
.truncateToDouble(); pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
@@ -880,7 +784,7 @@ InlineSpan buildContent(
height: height, height: height,
), ),
), ),
height > maxHeight height > Get.size.height * 0.9
? const PBadge( ? const PBadge(
text: '长图', text: '长图',
right: 8, right: 8,
@@ -895,8 +799,7 @@ InlineSpan buildContent(
), ),
), ),
); );
} } else if (len > 1) {
if (len > 1) {
List<Widget> list = []; List<Widget> list = [];
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
picList.add(content.pictures[i]['img_src']); picList.add(content.pictures[i]['img_src']);
@@ -914,10 +817,11 @@ InlineSpan buildContent(
); );
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
src: content.pictures[i]['img_src'], src: content.pictures[i]['img_src'],
width: box.maxWidth, width: box.maxWidth,
height: box.maxWidth, height: box.maxWidth,
), origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
); );
}, },
), ),
@@ -978,3 +882,100 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember)); // spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds); return TextSpan(children: spanChilds);
} }
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
Future<dynamic> menuActionHandler(String type) async {
String message = item.content.message ?? item.content;
switch (type) {
case 'copyAll':
await Clipboard.setData(ClipboardData(text: message));
SmartDialog.showToast('已复制');
Get.back();
break;
case 'copyFreedom':
Get.back();
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('自由复制'),
content: SelectableText(message),
);
},
);
break;
// case 'block':
// SmartDialog.showToast('加入黑名单');
// break;
// case 'report':
// SmartDialog.showToast('举报');
// break;
// case 'delete':
// SmartDialog.showToast('删除');
// break;
default:
}
}
@override
Widget build(BuildContext context) {
Color errorColor = Theme.of(context).colorScheme.error;
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
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.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
ListTile(
onTap: () async => await menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () async => await menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
// ListTile(
// onTap: () async => await menuActionHandler('block'),
// minLeadingWidth: 0,
// leading: Icon(Icons.block_outlined, color: errorColor),
// title: Text('加入黑名单', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('report'),
// minLeadingWidth: 0,
// leading: Icon(Icons.report_outlined, color: errorColor),
// title: Text('举报', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('del'),
// minLeadingWidth: 0,
// leading: Icon(Icons.delete_outline, color: errorColor),
// title: Text('删除', style: TextStyle(color: errorColor)),
// ),
],
),
);
}
}

View File

@@ -61,6 +61,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final Floating floating = Floating(); final Floating floating = Floating();
// 生命周期监听 // 生命周期监听
late final AppLifecycleListener _lifecycleListener; late final AppLifecycleListener _lifecycleListener;
bool isShowing = true;
@override @override
void initState() { void initState() {
@@ -216,15 +217,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
plPlayerController!.danmakuController?.pause();
plPlayerController!.danmakuController?.clear();
} }
setState(() => isShowing = false);
super.didPushNext(); super.didPushNext();
} }
@override @override
// 返回当前页面时 // 返回当前页面时
void didPopNext() async { void didPopNext() async {
setState(() => isShowing = true);
videoDetailController.isFirstTime = false; videoDetailController.isFirstTime = false;
final bool autoplay = autoPlayEnable; final bool autoplay = autoPlayEnable;
videoDetailController.playerInit(autoplay: autoplay); videoDetailController.playerInit(autoplay: autoplay);
@@ -280,19 +281,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final double pinnedHeaderHeight = final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight; statusBarHeight + kToolbarHeight + videoHeight;
if (MediaQuery.of(context).orientation == Orientation.landscape ||
plPlayerController?.isFullScreen.value == true) {
enterFullScreen();
} else {
exitFullScreen();
}
Widget childWhenDisabled = SafeArea( Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait && top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
bottom: MediaQuery.of(context).orientation == Orientation.portrait && bottom: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
left: plPlayerController?.isFullScreen.value != true, left: false, //plPlayerController?.isFullScreen.value != true,
right: plPlayerController?.isFullScreen.value != true, right: false, //plPlayerController?.isFullScreen.value != true,
child: Stack( child: Stack(
children: [ children: [
Scaffold( Scaffold(
@@ -309,187 +304,189 @@ class _VideoDetailPageState extends State<VideoDetailPage>
body: ExtendedNestedScrollView( body: ExtendedNestedScrollView(
controller: _extendNestCtr, controller: _extendNestCtr,
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) { (BuildContext context2, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
Obx( Obx(
() => SliverAppBar( () {
automaticallyImplyLeading: false, if (MediaQuery.of(context).orientation ==
// 假装使用一个非空变量避免Obx检测不到而罢工 Orientation.landscape ||
pinned: videoDetailController.autoPlay.value ^ plPlayerController?.isFullScreen.value == true) {
false ^ enterFullScreen();
videoDetailController.autoPlay.value, } else {
elevation: 0, exitFullScreen();
scrolledUnderElevation: 0, }
forceElevated: innerBoxIsScrolled, return SliverAppBar(
expandedHeight: MediaQuery.of(context).orientation == automaticallyImplyLeading: false,
Orientation.landscape || // 假装使用一个非空变量避免Obx检测不到而罢工
plPlayerController?.isFullScreen.value == true pinned: videoDetailController.autoPlay.value ^
? MediaQuery.sizeOf(context).height - false ^
(MediaQuery.of(context).orientation == videoDetailController.autoPlay.value,
Orientation.landscape elevation: 0,
? 0 scrolledUnderElevation: 0,
: MediaQuery.of(context).padding.top) forceElevated: innerBoxIsScrolled,
: videoHeight, expandedHeight: MediaQuery.of(context).orientation ==
backgroundColor: Colors.black, Orientation.landscape ||
flexibleSpace: FlexibleSpaceBar( plPlayerController?.isFullScreen.value == true
background: PopScope( ? MediaQuery.sizeOf(context).height -
canPop: (MediaQuery.of(context).orientation ==
plPlayerController?.isFullScreen.value != true, Orientation.landscape
onPopInvoked: (bool didPop) { ? 0
if (plPlayerController?.isFullScreen.value == : MediaQuery.of(context).padding.top)
true) { : videoHeight,
plPlayerController! backgroundColor: Colors.black,
.triggerFullScreen(status: false); flexibleSpace: FlexibleSpaceBar(
} background: PopScope(
if (MediaQuery.of(context).orientation == canPop: plPlayerController?.isFullScreen.value !=
Orientation.landscape) { true,
verticalScreen(); onPopInvoked: (bool didPop) {
} if (plPlayerController?.isFullScreen.value ==
}, true) {
child: LayoutBuilder( plPlayerController!
builder: (BuildContext context, .triggerFullScreen(status: false);
BoxConstraints boxConstraints) { }
final double maxWidth = boxConstraints.maxWidth; if (MediaQuery.of(context).orientation ==
final double maxHeight = Orientation.landscape) {
boxConstraints.maxHeight; verticalScreen();
return Stack( }
children: <Widget>[ },
FutureBuilder( child: LayoutBuilder(
future: _futureBuilderFuture, builder: (BuildContext context,
builder: (BuildContext context, BoxConstraints boxConstraints) {
AsyncSnapshot snapshot) { final double maxWidth =
if (snapshot.hasData && boxConstraints.maxWidth;
snapshot.data['status']) { final double maxHeight =
return Obx( boxConstraints.maxHeight;
() => !videoDetailController return Stack(
.autoPlay.value children: <Widget>[
? const SizedBox() if (isShowing)
: PLVideoPlayer( FutureBuilder(
controller: future: _futureBuilderFuture,
plPlayerController!, builder: (BuildContext context,
headerControl: AsyncSnapshot snapshot) {
videoDetailController if (snapshot.hasData &&
.headerControl, snapshot.data['status']) {
danmuWidget: Obx( return Obx(
() => PlDanmaku( () =>
key: Key( !videoDetailController
videoDetailController .autoPlay.value
.danmakuCid ? nil
.value : PLVideoPlayer(
.toString()), controller:
cid: plPlayerController!,
videoDetailController headerControl:
.danmakuCid videoDetailController
.value, .headerControl,
playerController: danmuWidget: Obx(
plPlayerController!, () => PlDanmaku(
), key: Key(videoDetailController
), .danmakuCid
), .value
); .toString()),
} else { cid: videoDetailController
return const SizedBox(); .danmakuCid
} .value,
}, playerController:
), plPlayerController!,
),
),
),
);
} else {
return const SizedBox();
}
},
),
/// 关闭自动播放时 手动播放 /// 关闭自动播放时 手动播放
if (!videoDetailController if (!videoDetailController
.autoPlay.value) ...<Widget>[ .autoPlay.value) ...<Widget>[
Obx( Obx(
() => Visibility( () => Visibility(
visible: videoDetailController visible: videoDetailController
.isShowCover.value, .isShowCover.value,
child: Positioned( child: Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
handlePlay(); handlePlay();
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
type: 'emote', type: 'emote',
src: videoDetailController src: videoDetailController
.videoItem['pic'], .videoItem['pic'],
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
),
), ),
), ),
), ),
), ),
), Obx(
Obx( () => Visibility(
() => Visibility( visible: videoDetailController
visible: videoDetailController .isShowCover.value &&
.isShowCover.value && videoDetailController
videoDetailController .isEffective.value,
.isEffective.value, child: Stack(
child: Stack( children: [
children: [ Positioned(
Positioned( top: 0,
top: 0, left: 0,
left: 0, right: 0,
right: 0, child: AppBar(
child: AppBar( primary: false,
primary: false, foregroundColor:
foregroundColor: Colors.white,
Colors.white, elevation: 0,
elevation: 0, scrolledUnderElevation: 0,
scrolledUnderElevation: 0,
backgroundColor:
Colors.transparent,
actions: [
IconButton(
tooltip: '稍后再看',
onPressed: () async {
var res = await UserHttp
.toViewLater(
bvid:
videoDetailController
.bvid);
SmartDialog.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined),
),
const SizedBox(width: 14)
],
),
),
Positioned(
right: 12,
bottom: 10,
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: backgroundColor:
MaterialStateProperty Colors.transparent,
.resolveWith( actions: [
(states) { IconButton(
return Colors.white tooltip: '稍后再看',
.withOpacity(0.8); onPressed: () async {
}), var res = await UserHttp
.toViewLater(
bvid: videoDetailController
.bvid);
SmartDialog
.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined),
),
const SizedBox(
width: 14)
],
), ),
onPressed: () =>
handlePlay(),
icon: const Icon(
Icons.play_circle_outline,
size: 20,
),
label: const Text('轻触封面播放'),
), ),
), Positioned(
], right: 12,
)), bottom: 10,
), child: IconButton(
] tooltip: '播放',
], onPressed: () =>
); handlePlay(),
}, icon: Image.asset(
)), 'assets/images/play.png',
), width: 60,
), height: 60,
)),
),
],
)),
),
]
],
);
},
)),
),
);
},
), ),
]; ];
}, },
@@ -500,7 +497,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// }, // },
/// 不收回 /// 不收回
pinnedHeaderSliverHeightBuilder: () { pinnedHeaderSliverHeightBuilder: () {
return plPlayerController?.isFullScreen.value == true return MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height ? MediaQuery.sizeOf(context).height
: pinnedHeaderHeight; : pinnedHeaderHeight;
}, },
@@ -571,8 +570,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
); );
}, },
), ),
VideoReplyPanel( Obx(
bvid: videoDetailController.bvid, () => VideoReplyPanel(
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
),
) )
], ],
), ),

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -437,7 +438,7 @@ class _HeaderControlState extends State<HeaderControl> {
}), }),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => Get.back(),
child: Text( child: Text(
'取消', '取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),
@@ -724,6 +725,8 @@ class _HeaderControlState extends State<HeaderControl> {
double fontSizeVal = widget.controller!.fontSizeVal; double fontSizeVal = widget.controller!.fontSizeVal;
// 弹幕速度 // 弹幕速度
double danmakuDurationVal = widget.controller!.danmakuDurationVal; double danmakuDurationVal = widget.controller!.danmakuDurationVal;
// 弹幕描边
double strokeWidth = widget.controller!.strokeWidth;
final DanmakuController danmakuController = final DanmakuController danmakuController =
widget.controller!.danmakuController!; widget.controller!.danmakuController!;
@@ -857,6 +860,44 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
), ),
Text('描边粗细 $strokeWidth'),
Padding(
padding: const EdgeInsets.only(
top: 0,
bottom: 6,
left: 10,
right: 10,
),
child: SliderTheme(
data: SliderThemeData(
trackShape: MSliderTrackShape(),
thumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
trackHeight: 10,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6.0),
),
child: Slider(
min: 0,
max: 3,
value: strokeWidth,
divisions: 6,
label: '$strokeWidth',
onChanged: (double val) {
strokeWidth = val;
widget.controller!.strokeWidth = val;
setState(() {});
try {
final DanmakuOption currentOption =
danmakuController.option;
final DanmakuOption updatedOption =
currentOption.copyWith(strokeWidth: val);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
),
),
),
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'), Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(

View File

@@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart'; import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/session.dart'; import 'package:pilipala/models/msg/session.dart';
@@ -8,6 +9,8 @@ class WhisperDetailController extends GetxController {
late String face; late String face;
late String mid; late String mid;
RxList<MessageItem> messageList = <MessageItem>[].obs; RxList<MessageItem> messageList = <MessageItem>[].obs;
//表情转换图片规则
List<dynamic>? eInfos;
@override @override
void onInit() { void onInit() {
@@ -22,6 +25,9 @@ class WhisperDetailController extends GetxController {
var res = await MsgHttp.sessionMsg(talkerId: talkerId); var res = await MsgHttp.sessionMsg(talkerId: talkerId);
if (res['status']) { if (res['status']) {
messageList.value = res['data'].messages; messageList.value = res['data'].messages;
if (messageList.isNotEmpty && res['data'].eInfos != null) {
eInfos = res['data'].eInfos;
}
} }
return res; return res;
} }

View File

@@ -110,12 +110,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
if (i == 0) { if (i == 0) {
return Column( return Column(
children: [ children: [
ChatItem(item: messageList[i]), ChatItem(
item: messageList[i],
e_infos: _whisperDetailController.eInfos),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
); );
} else { } else {
return ChatItem(item: messageList[i]); return ChatItem(
item: messageList[i],
e_infos: _whisperDetailController.eInfos);
} }
}, },
), ),

View File

@@ -1,38 +1,370 @@
// ignore_for_file: must_be_immutable // ignore_for_file: must_be_immutable
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../../http/search.dart';
enum MsgType {
invalid(value: 0, label: "空空的~"),
text(value: 1, label: "文本消息"),
pic(value: 2, label: "图片消息"),
audio(value: 3, label: "语音消息"),
share(value: 4, label: "分享消息"),
revoke(value: 5, label: "撤回消息"),
custom_face(value: 6, label: "自定义表情"),
share_v2(value: 7, label: "分享v2消息"),
sys_cancel(value: 8, label: "系统撤销"),
mini_program(value: 9, label: "小程序"),
notify_msg(value: 10, label: "业务通知"),
archive_card(value: 11, label: "投稿卡片"),
article_card(value: 12, label: "专栏卡片"),
pic_card(value: 13, label: "图片卡片"),
common_share(value: 14, label: "异形卡片"),
auto_reply_push(value: 16, label: "自动回复推送"),
notify_text(value: 18, label: "文本提示");
final int value;
final String label;
const MsgType({required this.value, required this.label});
static MsgType parse(int value) {
return MsgType.values
.firstWhere((e) => e.value == value, orElse: () => MsgType.invalid);
}
}
class ChatItem extends StatelessWidget { class ChatItem extends StatelessWidget {
dynamic item; dynamic item;
List? e_infos;
ChatItem({ ChatItem({
super.key, super.key,
this.item, this.item,
this.e_infos,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isOwner = bool isOwner =
item.senderUid == GStrorage.userInfo.get('userInfoCache').mid; item.senderUid == GStrorage.userInfo.get('userInfoCache').mid;
bool isPic = item.msgType == 2; // 图片
bool isText = item.msgType == 1; // 文本
// bool isAchive = item.msgType == 11; // 投稿
// bool isArticle = item.msgType == 12; // 专栏
bool isRevoke = item.msgType == 5; // 撤回消息
bool isSystem = bool isPic = item.msgType == MsgType.pic.value; // 图片
item.msgType == 18 || item.msgType == 10 || item.msgType == 13; bool isText = item.msgType == MsgType.text.value; // 文本
int msgType = item.msgType; // bool isArchive = item.msgType == 11; // 投稿
// bool isArticle = item.msgType == 12; // 专栏
bool isRevoke = item.msgType == MsgType.revoke.value; // 撤回消息
bool isShareV2 = item.msgType == MsgType.share_v2.value;
bool isSystem = item.msgType == MsgType.notify_text.value ||
item.msgType == MsgType.notify_msg.value ||
item.msgType == MsgType.pic_card.value ||
item.msgType == MsgType.auto_reply_push.value;
dynamic content = item.content ?? ''; dynamic content = item.content ?? '';
Color textColor(BuildContext context) {
return isOwner
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSecondaryContainer;
}
Widget richTextMessage(BuildContext context) {
var text = content['content'];
if (e_infos != null) {
final List<InlineSpan> children = [];
Map<String, String> emojiMap = {};
for (var e in e_infos!) {
emojiMap[e['text']] = e['url'];
}
text.splitMapJoin(
RegExp(r"\[.+?\]"),
onMatch: (Match match) {
final String emojiKey = match[0]!;
if (emojiMap.containsKey(emojiKey)) {
children.add(WidgetSpan(
child: NetworkImgLayer(
width: 18,
height: 18,
src: emojiMap[emojiKey]!,
),
));
}
return '';
},
onNonMatch: (String text) {
children.add(TextSpan(
text: text,
style: TextStyle(
color: textColor(context),
letterSpacing: 0.6,
height: 1.5,
)));
return '';
},
);
return RichText(
text: TextSpan(
children: children,
),
);
} else {
return Text(
text,
style: TextStyle(
letterSpacing: 0.6,
color: textColor(context),
height: 1.5,
),
);
}
}
Widget messageContent(BuildContext context) {
switch (MsgType.parse(item.msgType)) {
case MsgType.notify_msg:
return SystemNotice(item: item);
case MsgType.pic_card:
return SystemNotice2(item: item);
case MsgType.notify_text:
return Text(
jsonDecode(content['content'])
.map((m) => m['text'] as String)
.join("\n"),
style: TextStyle(
letterSpacing: 0.6,
height: 5,
color: Theme.of(context).colorScheme.outline.withOpacity(0.8),
),
);
case MsgType.text:
return richTextMessage(context);
case MsgType.pic:
return NetworkImgLayer(
width: 220,
height: 220 * content['height'] / content['width'],
src: content['url'],
);
case MsgType.share_v2:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () async {
SmartDialog.showLoading();
var bvid = content["bvid"];
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
SmartDialog.dismiss<dynamic>().then(
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': content['thumb'],
'heroTag': heroTag,
}),
);
},
child: NetworkImgLayer(
width: 220,
height: 220 * 9 / 16,
src: content['thumb'],
),
),
const SizedBox(height: 6),
Text(
content['title'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 1),
Text(
content['author'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
],
);
case MsgType.archive_card:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () async {
SmartDialog.showLoading();
var bvid = content["bvid"];
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
SmartDialog.dismiss<dynamic>().then(
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': content['thumb'],
'heroTag': heroTag,
}),
);
},
child: NetworkImgLayer(
width: 220,
height: 220 * 9 / 16,
src: content['cover'],
),
),
const SizedBox(height: 6),
Text(
content['title'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 1),
Text(
Utils.timeFormat(content['times']),
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
],
);
case MsgType.auto_reply_push:
return Container(
constraints: const BoxConstraints(
maxWidth: 300.0, // 设置最大宽度为200.0
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.4),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
bottomLeft: Radius.circular(6),
bottomRight: Radius.circular(16),
),
),
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
content['main_title'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
for (var i in content['sub_cards']) ...<Widget>[
const SizedBox(height: 6),
GestureDetector(
onTap: () async {
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}',
caseSensitive: false);
Iterable<Match> matches =
bvRegex.allMatches(i['jump_url']);
if (matches.isNotEmpty) {
Match match = matches.first;
String bvid = match.group(0)!;
try {
SmartDialog.showLoading();
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
SmartDialog.dismiss<dynamic>().then(
(e) => Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': i['cover_url'],
'heroTag': heroTag,
}),
);
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());
}
} else {
SmartDialog.showToast('未匹配到 BV 号');
Get.toNamed('/webview',
arguments: {'url': i['jump_url']});
}
},
child: Row(
children: [
NetworkImgLayer(
width: 130,
height: 130 * 9 / 16,
src: i['cover_url'],
),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
i['field1'],
maxLines: 2,
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
),
Text(
i['field2'],
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
Text(
Utils.timeFormat(int.parse(i['field3'])),
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context).withOpacity(0.6),
fontSize: 12,
),
),
],
)),
],
)),
],
],
));
default:
return Text(
content['content'] ?? content.toString(),
style: TextStyle(
letterSpacing: 0.6,
height: 1.5,
color: textColor(context),
fontWeight: FontWeight.bold,
),
);
}
}
return isSystem return isSystem
? (msgType == 10 ? messageContent(context)
? SystemNotice(item: item)
: msgType == 13
? SystemNotice2(item: item)
: const SizedBox())
: isRevoke : isRevoke
? const SizedBox() ? const SizedBox()
: Row( : Row(
@@ -66,27 +398,7 @@ class ChatItem extends StatelessWidget {
? CrossAxisAlignment.end ? CrossAxisAlignment.end
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
children: [ children: [
isText messageContent(context),
? Text(
content['content'],
style: TextStyle(
color: isOwner
? Theme.of(context)
.colorScheme
.onPrimary
: Theme.of(context)
.colorScheme
.onSecondaryContainer),
)
: isPic
? NetworkImgLayer(
width: 220,
height: 220 *
content['height'] /
content['width'],
src: content['url'],
)
: const SizedBox(),
SizedBox(height: isPic ? 7 : 2), SizedBox(height: isPic ? 7 : 2),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@@ -221,6 +221,7 @@ class PlPlayerController {
late double showArea; late double showArea;
late double opacityVal; late double opacityVal;
late double fontSizeVal; late double fontSizeVal;
late double strokeWidth;
late double danmakuDurationVal; late double danmakuDurationVal;
late List<double> speedsList; late List<double> speedsList;
// 缓存 // 缓存
@@ -275,6 +276,9 @@ class PlPlayerController {
// 弹幕时间 // 弹幕时间
danmakuDurationVal = danmakuDurationVal =
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
// 描边粗细
strokeWidth =
localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
playRepeat = PlayRepeat.values.toList().firstWhere( playRepeat = PlayRepeat.values.toList().firstWhere(
(e) => (e) =>
e.value == e.value ==
@@ -1086,6 +1090,7 @@ class PlPlayerController {
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal); localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);
localCache.put(LocalCacheKey.strokeWidth, strokeWidth);
if (_videoPlayerController != null) { if (_videoPlayerController != null) {
var pp = _videoPlayerController!.platform as NativePlayer; var pp = _videoPlayerController!.platform as NativePlayer;
await pp.setProperty('audio-files', ''); await pp.setProperty('audio-files', '');

View File

@@ -586,6 +586,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 进度条 live模式下禁用 /// 进度条 live模式下禁用
Obx( Obx(
() { () {
final int value = _.sliderPositionSeconds.value; final int value = _.sliderPositionSeconds.value;
@@ -609,7 +610,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
if (_.videoType.value == 'live') { if (_.videoType.value == 'live') {
return nil; return const SizedBox();
} }
if (value > max || max <= 0) { if (value > max || max <= 0) {
return nil; return nil;

View File

@@ -3,6 +3,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart'; import '../pages/about/index.dart';
import '../pages/blacklist/index.dart'; import '../pages/blacklist/index.dart';
@@ -38,6 +40,7 @@ import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart'; import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart';
import '../pages/setting/play_setting.dart'; import '../pages/setting/play_setting.dart';
import '../pages/setting/privacy_setting.dart'; import '../pages/setting/privacy_setting.dart';
import '../pages/setting/style_setting.dart'; import '../pages/setting/style_setting.dart';
@@ -101,7 +104,9 @@ class Routes {
// 二级回复 // 二级回复
CustomGetPage( CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// 推荐设置
CustomGetPage(
name: '/recommendSetting', page: () => const RecommendSetting()),
// 播放设置 // 播放设置
CustomGetPage(name: '/playSetting', page: () => const PlaySetting()), CustomGetPage(name: '/playSetting', page: () => const PlaySetting()),
// 外观设置 // 外观设置
@@ -151,6 +156,10 @@ class Routes {
// 用户专栏 // 用户专栏
CustomGetPage( CustomGetPage(
name: '/memberSeasons', page: () => const MemberSeasonsPage()), name: '/memberSeasons', page: () => const MemberSeasonsPage()),
// 日志
CustomGetPage(name: '/logs', page: () => const LogsPage()),
// 搜索关注
CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()),
]; ];
} }

View File

@@ -147,8 +147,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
processingState: AudioProcessingState.idle, processingState: AudioProcessingState.idle,
playing: false, playing: false,
)); ));
_item.removeLast();
if (_item.isNotEmpty) { if (_item.isNotEmpty) {
_item.removeLast();
setMediaItem(_item.last); setMediaItem(_item.last);
} }
if (_item.isEmpty) { if (_item.isEmpty) {

56
lib/services/loggeer.dart Normal file
View File

@@ -0,0 +1,56 @@
// final _loggerFactory =
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
final _loggerFactory = PiliLogger();
PiliLogger getLogger<T>() {
return _loggerFactory;
}
class PiliLogger extends Logger {
PiliLogger() : super();
@override
void log(Level level, dynamic message,
{Object? error, StackTrace? stackTrace, DateTime? time}) async {
if (level == Level.error) {
String dir = (await getApplicationDocumentsDirectory()).path;
// 创建logo文件
final String filename = p.join(dir, ".pili_logs");
// 添加至文件末尾
await File(filename).writeAsString(
"**${DateTime.now()}** \n $message \n $stackTrace",
mode: FileMode.writeOnlyAppend,
);
}
super.log(level, "$message", error: error, stackTrace: stackTrace);
}
}
Future<File> getLogsPath() async {
String dir = (await getApplicationDocumentsDirectory()).path;
final String filename = p.join(dir, ".pili_logs");
final file = File(filename);
if (!await file.exists()) {
await file.create();
}
return file;
}
Future<bool> clearLogs() async {
String dir = (await getApplicationDocumentsDirectory()).path;
final String filename = p.join(dir, ".pili_logs");
final file = File(filename);
try {
await file.writeAsString('');
} catch (e) {
print('Error clearing file: $e');
return false;
}
return true;
}

154
lib/utils/cache_manage.dart Normal file
View File

@@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class CacheManage {
CacheManage._internal();
static final CacheManage cacheManage = CacheManage._internal();
factory CacheManage() => cacheManage;
// 获取缓存目录
Future<String> loadApplicationCache() async {
/// clear all of image in memory
// clearMemoryImageCache();
/// get ImageCache
// var res = getMemoryImageCache();
// 缓存大小
double cacheSize = 0;
// cached_network_image directory
Directory tempDirectory = await getTemporaryDirectory();
// get_storage directory
Directory docDirectory = await getApplicationDocumentsDirectory();
// 获取缓存大小
if (tempDirectory.existsSync()) {
double value = await getTotalSizeOfFilesInDir(tempDirectory);
cacheSize += value;
}
/// 获取缓存大小 dioCache
if (docDirectory.existsSync()) {
double value = 0;
String dioCacheFileName =
'${docDirectory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
value = await getTotalSizeOfFilesInDir(dioCacheFile);
}
cacheSize += value;
}
return formatSize(cacheSize);
}
// 循环计算文件的大小(递归)
Future<double> getTotalSizeOfFilesInDir(final FileSystemEntity file) async {
if (file is File) {
int length = await file.length();
return double.parse(length.toString());
}
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
double total = 0;
for (final FileSystemEntity child in children) {
total += await getTotalSizeOfFilesInDir(child);
}
return total;
}
return 0;
}
// 缓存大小格式转换
String formatSize(double value) {
List<String> unitArr = ['B', 'K', 'M', 'G'];
int index = 0;
while (value > 1024) {
index++;
value = value / 1024;
}
String size = value.toStringAsFixed(2);
return size + unitArr[index];
}
// 清除缓存
Future<bool> clearCacheAll() async {
bool cleanStatus = await SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('该操作将清除图片及网络请求缓存数据,确认清除?'),
actions: [
TextButton(
onPressed: (() => {SmartDialog.dismiss()}),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
SmartDialog.dismiss();
SmartDialog.showLoading(msg: '正在清除...');
try {
// 清除缓存 图片缓存
await clearLibraryCache();
Timer(const Duration(milliseconds: 500), () {
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
});
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());
}
},
child: const Text('确认'),
)
],
);
},
).then((res) {
return true;
});
return cleanStatus;
}
/// 清除 Documents 目录下的 DioCache.db
Future clearApplicationCache() async {
Directory directory = await getApplicationDocumentsDirectory();
if (directory.existsSync()) {
String dioCacheFileName =
'${directory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
dioCacheFile.delete();
}
}
}
// 清除 Library/Caches 目录及文件缓存
Future clearLibraryCache() async {
var appDocDir = await getTemporaryDirectory();
if (appDocDir.existsSync()) {
await appDocDir.delete(recursive: true);
}
}
/// 递归方式删除目录及文件
Future deleteDirectory(FileSystemEntity file) async {
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
for (final FileSystemEntity child in children) {
await deleteDirectory(child);
}
}
await file.delete();
}
}

View File

@@ -1,50 +1,65 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names, non_constant_identifier_names
import 'dart:math'; import 'dart:convert';
import 'package:flutter/material.dart';
class IdUtils { class IdUtils {
static const String TABLE = static final XOR_CODE = BigInt.parse('23442827791579');
'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'; static final MASK_CODE = BigInt.parse('2251799813685247');
static const List<int> S = [11, 10, 3, 8, 4, 6]; // 位置编码表 static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt();
static const int XOR = 177451812; // 固定异或值 static final BASE = BigInt.from(58);
static const int ADD = 8728348608; // 固定加法值
static const List<String> r = [ static const data =
'B', 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';
'V',
'1',
'',
'',
'4',
'',
'1',
'',
'7',
'',
''
];
/// av转bv /// av转bv
static String av2bv(int av) { static String av2bv(int aid) {
int x_ = (av ^ XOR) + ADD; List<String> bytes = [
List<String> newR = []; 'B',
newR.addAll(r); 'V',
for (int i = 0; i < S.length; i++) { '1',
newR[S[i]] = '0',
TABLE.characters.elementAt((x_ / pow(58, i).toInt() % 58).toInt()); '0',
'0',
'0',
'0',
'0',
'0',
'0',
'0'
];
int bvIndex = bytes.length - 1;
BigInt tmp = (MAX_AID | BigInt.from(aid)) ^ XOR_CODE;
while (tmp > BigInt.zero) {
bytes[bvIndex] = data[(tmp % BASE).toInt()];
tmp = tmp ~/ BASE;
bvIndex -= 1;
} }
return newR.join(); String tmpSwap = bytes[3];
bytes[3] = bytes[9];
bytes[9] = tmpSwap;
tmpSwap = bytes[4];
bytes[4] = bytes[7];
bytes[7] = tmpSwap;
return bytes.join();
} }
/// bv转bv /// bv转av
static int bv2av(String bv) { static int bv2av(String bvid) {
int r = 0; List<String> bvidArr = bvid.split('');
for (int i = 0; i < S.length; i++) { final tmpValue = bvidArr[3];
r += (TABLE.indexOf(bv.characters.elementAt(S[i])).toInt()) * bvidArr[3] = bvidArr[9];
pow(58, i).toInt(); bvidArr[9] = tmpValue;
}
return (r - ADD) ^ XOR; final tmpValue2 = bvidArr[4];
bvidArr[4] = bvidArr[7];
bvidArr[7] = tmpValue2;
bvidArr.removeRange(0, 3);
BigInt tmp = bvidArr.fold(BigInt.zero,
(pre, bvidChar) => pre * BASE + BigInt.from(data.indexOf(bvidChar)));
return ((tmp & MASK_CODE) ^ XOR_CODE).toInt();
} }
// 匹配 // 匹配
@@ -72,4 +87,19 @@ class IdUtils {
} }
return result; return result;
} }
// eid生成
static String? genAuroraEid(int uid) {
if (uid == 0) {
return null;
}
String uidString = uid.toString();
List<int> resultBytes = List.generate(
uidString.length,
(i) => uidString.codeUnitAt(i) ^ "ad1va46a7lza".codeUnitAt(i % 12),
);
String auroraEid = base64Url.encode(resultBytes);
auroraEid = auroraEid.replaceAll(RegExp(r'=*$', multiLine: true), '');
return auroraEid;
}
} }

View File

@@ -0,0 +1,52 @@
import 'dart:math';
import 'storage.dart';
class RecommendFilter {
// static late int filterUnfollowedRatio;
static late int minDurationForRcmd;
static late int minLikeRatioForRecommend;
static late bool exemptFilterForFollowed;
static late bool applyFilterToRelatedVideos;
RecommendFilter() {
update();
}
static void update() {
var setting = GStrorage.setting;
// filterUnfollowedRatio =
// setting.get(SettingBoxKey.filterUnfollowedRatio, defaultValue: 0);
minDurationForRcmd =
setting.get(SettingBoxKey.minDurationForRcmd, defaultValue: 0);
minLikeRatioForRecommend =
setting.get(SettingBoxKey.minLikeRatioForRecommend, defaultValue: 0);
exemptFilterForFollowed =
setting.get(SettingBoxKey.exemptFilterForFollowed, defaultValue: true);
applyFilterToRelatedVideos = setting
.get(SettingBoxKey.applyFilterToRelatedVideos, defaultValue: true);
}
static bool filter(dynamic videoItem, {bool relatedVideos = false}) {
if (relatedVideos && !applyFilterToRelatedVideos) {
return false;
}
//由于相关视频中没有已关注标签,只能视为非关注视频
if (!relatedVideos &&
videoItem.isFollowed == 1 &&
exemptFilterForFollowed) {
return false;
}
if (videoItem.duration > 0 && videoItem.duration < minDurationForRcmd) {
return true;
}
if (videoItem.stat.view is int &&
videoItem.stat.view > -1 &&
videoItem.stat.like is int &&
videoItem.stat.like > -1 &&
videoItem.stat.like * 100 <
minLikeRatioForRecommend * videoItem.stat.view) {
return true;
}
return false;
}
}

View File

@@ -3,13 +3,11 @@ import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_owner.dart'; import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
class GStrorage { class GStrorage {
static late final Box<dynamic> recVideo;
static late final Box<dynamic> userInfo; static late final Box<dynamic> userInfo;
static late final Box<dynamic> historyword; static late final Box<dynamic> historyword;
static late final Box<dynamic> localCache; static late final Box<dynamic> localCache;
@@ -21,13 +19,6 @@ class GStrorage {
final String path = dir.path; final String path = dir.path;
await Hive.initFlutter('$path/hive'); await Hive.initFlutter('$path/hive');
regAdapter(); regAdapter();
// 首页推荐视频
recVideo = await Hive.openBox(
'recVideo',
compactionStrategy: (int entries, int deletedEntries) {
return deletedEntries > 12;
},
);
// 登录用户信息 // 登录用户信息
userInfo = await Hive.openBox( userInfo = await Hive.openBox(
'userInfo', 'userInfo',
@@ -51,13 +42,11 @@ class GStrorage {
return deletedEntries > 10; return deletedEntries > 10;
}, },
); );
// 视频设置
video = await Hive.openBox('video');
} }
static void regAdapter() { static void regAdapter() {
Hive.registerAdapter(RecVideoItemAppModelAdapter());
Hive.registerAdapter(RcmdReasonAdapter());
Hive.registerAdapter(RcmdStatAdapter());
Hive.registerAdapter(RcmdOwnerAdapter());
Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(OwnerAdapter());
Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(UserInfoDataAdapter());
Hive.registerAdapter(LevelInfoAdapter()); Hive.registerAdapter(LevelInfoAdapter());
@@ -65,16 +54,9 @@ class GStrorage {
Hive.registerAdapter(HotSearchItemAdapter()); Hive.registerAdapter(HotSearchItemAdapter());
} }
static Future<void> lazyInit() async {
// 视频设置
video = await Hive.openBox('video');
}
static Future<void> close() async { static Future<void> close() async {
// user.compact(); // user.compact();
// user.close(); // user.close();
recVideo.compact();
recVideo.close();
userInfo.compact(); userInfo.compact();
userInfo.close(); userInfo.close();
historyword.compact(); historyword.compact();
@@ -120,17 +102,24 @@ class SettingBoxKey {
/// 隐私 /// 隐私
blackMidsList = 'blackMidsList', blackMidsList = 'blackMidsList',
/// 推荐
enableRcmdDynamic = 'enableRcmdDynamic',
defaultRcmdType = 'defaultRcmdType',
enableSaveLastData = 'enableSaveLastData',
minDurationForRcmd = 'minDurationForRcmd',
minLikeRatioForRecommend = 'minLikeRatioForRecommend',
exemptFilterForFollowed = 'exemptFilterForFollowed',
//filterUnfollowedRatio = 'filterUnfollowedRatio',
applyFilterToRelatedVideos = 'applyFilterToRelatedVideos',
/// 其他 /// 其他
autoUpdate = 'autoUpdate', autoUpdate = 'autoUpdate',
defaultRcmdType = 'defaultRcmdType',
replySortType = 'replySortType', replySortType = 'replySortType',
defaultDynamicType = 'defaultDynamicType', defaultDynamicType = 'defaultDynamicType',
enableHotKey = 'enableHotKey', enableHotKey = 'enableHotKey',
enableQuickFav = 'enableQuickFav', enableQuickFav = 'enableQuickFav',
enableWordRe = 'enableWordRe', enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord', enableSearchWord = 'enableSearchWord',
enableRcmdDynamic = 'enableRcmdDynamic',
enableSaveLastData = 'enableSaveLastData',
enableSystemProxy = 'enableSystemProxy', enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi'; enableAi = 'enableAi';
@@ -145,7 +134,8 @@ class SettingBoxKey {
enableMYBar = 'enableMYBar', enableMYBar = 'enableMYBar',
hideSearchBar = 'hideSearchBar', // 收起顶栏 hideSearchBar = 'hideSearchBar', // 收起顶栏
hideTabBar = 'hideTabBar', // 收起底栏 hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort'; // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode';
} }
class LocalCacheKey { class LocalCacheKey {
@@ -158,12 +148,13 @@ class LocalCacheKey {
wbiKeys = 'wbiKeys', wbiKeys = 'wbiKeys',
timeStamp = 'timeStamp', timeStamp = 'timeStamp',
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细
danmakuBlockType = 'danmakuBlockType', danmakuBlockType = 'danmakuBlockType',
danmakuShowArea = 'danmakuShowArea', danmakuShowArea = 'danmakuShowArea',
danmakuOpacity = 'danmakuOpacity', danmakuOpacity = 'danmakuOpacity',
danmakuFontScale = 'danmakuFontScale', danmakuFontScale = 'danmakuFontScale',
danmakuDuration = 'danmakuDuration', danmakuDuration = 'danmakuDuration',
strokeWidth = 'strokeWidth',
// 代理host port // 代理host port
systemProxyHost = 'systemProxyHost', systemProxyHost = 'systemProxyHost',

61
lib/utils/url_utils.dart Normal file
View File

@@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import '../http/search.dart';
import 'id_utils.dart';
import 'utils.dart';
class UrlUtils {
// 302重定向路由截取
static Future<String> parseRedirectUrl(String url) async {
late String redirectUrl;
final dio = Dio();
dio.options.followRedirects = false;
dio.options.validateStatus = (status) {
return status == 200 || status == 301 || status == 302;
};
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}
return redirectUrl;
}
// 匹配url路由跳转
static matchUrlPush(
String pathSegment,
String title,
String redirectUrl,
) async {
final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (matchRes.containsKey('BV')) {
final String bv = matchRes['BV'];
final int cid = await SearchHttp.ab2c(bvid: bv);
final String heroTag = Utils.makeHeroTag(bv);
await Get.toNamed(
'/video?bvid=$bv&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} else {
await Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title,
},
);
}
}
}

View File

@@ -9,7 +9,6 @@ import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get_utils/get_utils.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -37,7 +36,7 @@ class Utils {
} }
final String res = (number / 10000).toString(); final String res = (number / 10000).toString();
if (int.parse(res.split('.')[0]) >= 1) { if (int.parse(res.split('.')[0]) >= 1) {
return '${(number / 10000).toPrecision(1)}'; return '${(number / 10000).toStringAsFixed(1)}';
} else { } else {
return number.toString(); return number.toString();
} }
@@ -302,16 +301,18 @@ class Utils {
// [arm64-v8a] // [arm64-v8a]
String abi = androidInfo.supportedAbis.first; String abi = androidInfo.supportedAbis.first;
late String downloadUrl; late String downloadUrl;
for (var i in data.assets) { if (data.assets.isNotEmpty) {
if (i.downloadUrl.contains(abi)) { for (var i in data.assets) {
downloadUrl = i.downloadUrl; if (i.downloadUrl.contains(abi)) {
downloadUrl = i.downloadUrl;
}
} }
// 应用外下载
launchUrl(
Uri.parse(downloadUrl),
mode: LaunchMode.externalApplication,
);
} }
// 应用外下载
launchUrl(
Uri.parse(downloadUrl),
mode: LaunchMode.externalApplication,
);
} }
} }

View File

@@ -209,6 +209,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
catcher_2:
dependency: "direct main"
description:
name: catcher_2
sha256: ca94d45ffb52bf4b16a425cdff6734ae8443d36d5f06c276f1c2a593120b11ed
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -492,10 +500,11 @@ packages:
floating: floating:
dependency: "direct main" dependency: "direct main"
description: description:
name: floating path: "."
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991 ref: main
url: "https://pub.flutter-io.cn" resolved-ref: d2d8421c4d80f6113f832404109853684721e11a
source: hosted url: "https://github.com/guozhigq/floating.git"
source: git
version: "2.0.1" version: "2.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
@@ -547,6 +556,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_mailer:
dependency: transitive
description:
name: flutter_mailer
sha256: "4fffaa35e911ff5ec2e5a4ebbca62c372e99a154eb3bb2c0bf79f09adf6ecf4c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -589,6 +606,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: transitive
description:
name: fluttertoast
sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.4"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -781,6 +806,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
logger:
dependency: "direct main"
description:
name: logger
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2+1"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -789,6 +822,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
mailer:
dependency: transitive
description:
name: mailer
sha256: "57f6dd1496699999a7bfd0aa6be0645384f477f4823e16d4321c40a434346382"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -951,7 +992,7 @@ packages:
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@@ -1214,6 +1255,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.8" version: "0.3.8"
sentry:
dependency: transitive
description:
name: sentry
sha256: "5686ed515bb620dc52b4ae99a6586fe720d443591183cf1f620ec5d1f0eec100"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.15.0"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.17+1017 version: 1.0.19+1019
environment: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@@ -51,7 +51,7 @@ dependencies:
cached_network_image: ^3.3.0 cached_network_image: ^3.3.0
extended_image: ^8.2.0 extended_image: ^8.2.0
saver_gallery: ^3.0.1 saver_gallery: ^3.0.1
# 存储 # 存储
path_provider: ^2.1.1 path_provider: ^2.1.1
hive: ^2.2.3 hive: ^2.2.3
@@ -85,14 +85,14 @@ dependencies:
encrypt: ^5.0.3 encrypt: ^5.0.3
# 视频播放器 # 视频播放器
media_kit: ^1.1.10 # Primary package. media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.4 # For video rendering. media_kit_video: ^1.2.4 # For video rendering.
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
# 媒体通知 # 媒体通知
audio_service: ^0.18.12 audio_service: ^0.18.12
audio_session: ^0.1.16 audio_session: ^0.1.16
# 音量、亮度、屏幕控制 # 音量、亮度、屏幕控制
flutter_volume_controller: ^1.3.1 flutter_volume_controller: ^1.3.1
screen_brightness: ^0.2.2+1 screen_brightness: ^0.2.2+1
@@ -103,7 +103,7 @@ dependencies:
auto_orientation: ^2.3.1 auto_orientation: ^2.3.1
protobuf: ^3.0.0 protobuf: ^3.0.0
animations: ^2.0.8 animations: ^2.0.8
# 获取appx信息 # 获取appx信息
package_info_plus: ^4.1.0 package_info_plus: ^4.1.0
url_launcher: ^6.1.14 url_launcher: ^6.1.14
@@ -116,7 +116,7 @@ dependencies:
appscheme: ^1.0.8 appscheme: ^1.0.8
# 弹幕 # 弹幕
ns_danmaku: ns_danmaku:
git: git:
url: https://github.com/guozhigq/flutter_ns_danmaku.git url: https://github.com/guozhigq/flutter_ns_danmaku.git
ref: master ref: master
# 状态栏图标控制 # 状态栏图标控制
@@ -124,7 +124,10 @@ dependencies:
# 代理 # 代理
system_proxy: ^0.1.0 system_proxy: ^0.1.0
# pip # pip
floating: ^2.0.1 floating:
git:
url: https://github.com/guozhigq/floating.git
ref: main
# html解析 # html解析
html: ^0.15.4 html: ^0.15.4
# html渲染 # html渲染
@@ -134,8 +137,10 @@ dependencies:
uuid: ^3.0.7 uuid: ^3.0.7
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
nil: ^1.1.1 nil: ^1.1.1
catcher_2: ^1.1.0
logger: ^2.0.2+1
path: 1.8.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -183,6 +188,7 @@ flutter:
- assets/images/ - assets/images/
- assets/images/lv/ - assets/images/lv/
- assets/images/logo/ - assets/images/logo/
- assets/images/live/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware
@@ -204,7 +210,6 @@ flutter:
# - family: HarmonyOS # - family: HarmonyOS
# fonts: # fonts:
# - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages