Merge branch 'main' into mod-not-login-recommend2

This commit is contained in:
guozhigq
2024-02-05 00:35:11 +08:00
committed by GitHub
43 changed files with 1546 additions and 665 deletions

View File

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

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

View File

@ -179,8 +179,14 @@ class Request {
);
return response;
} on DioException catch (e) {
print('get error: $e');
return Future.error(await ApiInterceptor.dioError(e));
Response errResponse = Response(
data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200,
requestOptions: RequestOptions(),
);
return errResponse;
}
}
@ -201,8 +207,14 @@ class Request {
// print('post success: ${response.data}');
return response;
} on DioException catch (e) {
print('post error: $e');
return Future.error(await ApiInterceptor.dioError(e));
Response errResponse = Response(
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:hive/hive.dart';
import '../utils/storage.dart';
// import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor {
@override
@ -71,35 +70,28 @@ class ApiInterceptor extends Interceptor {
return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown:
final String res = await checkConnect();
return '$res \n 网络异常,请稍后重试';
// default:
// return 'Dio异常';
return '$res,网络异常';
}
}
static Future<String> checkConnect() async {
final ConnectivityResult connectivityResult =
await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.mobile) {
return 'connected with mobile network';
} else if (connectivityResult == ConnectivityResult.wifi) {
return 'connected with wifi network';
} else if (connectivityResult == ConnectivityResult.ethernet) {
// I am connected to a ethernet network.
return '';
} else if (connectivityResult == ConnectivityResult.vpn) {
// I am connected to a vpn network.
// Note for iOS and macOS:
// There is no separate network interface type for [vpn].
// It returns [other] on any device (also simulator)
return '';
} else if (connectivityResult == ConnectivityResult.other) {
// I am connected to a network which is not in the above mentioned networks.
return '';
} else if (connectivityResult == ConnectivityResult.none) {
return 'not connected to any network';
} else {
return '';
switch (connectivityResult) {
case ConnectivityResult.mobile:
return '正在使用移动流量';
case ConnectivityResult.wifi:
return '正在使用wifi';
case ConnectivityResult.ethernet:
return '正在使用局域网';
case ConnectivityResult.vpn:
return '正在使用代理网络';
case ConnectivityResult.other:
return '正在使用其他网络';
case ConnectivityResult.none:
return '未连接到任何网络';
default:
return '';
}
}
}

View File

@ -240,10 +240,11 @@ class VideoHttp {
// 获取投币状态
static Future hasCoinVideo({required String bvid}) async {
var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
print('res: $res');
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': true, 'data': []};
return {'status': false, 'data': []};
}
}
@ -377,7 +378,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': true, 'data': []};
return {'status': false, 'data': []};
}
}
@ -393,7 +394,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': true, 'data': []};
return {'status': false, 'data': []};
}
}
@ -449,6 +450,8 @@ class VideoHttp {
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': null, 'msg': res.data['message']};
}
}
@ -463,11 +466,13 @@ class VideoHttp {
'up_mid': upMid,
});
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 {
'status': true,
'data': AiConclusionModel.fromJson(res.data['data']),
};
} else {
return {'status': false, 'data': []};
}
}
}

View File

@ -22,6 +22,9 @@ import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart';
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 {
WidgetsFlutterBinding.ensureInitialized();
@ -34,7 +37,33 @@ void main() async {
Request();
await Request.setCookie();
RecommendFilter();
runApp(const MyApp());
// 异常捕获 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.setSystemUIOverlayStyle(const SystemUiOverlayStyle(

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

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

View File

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

View File

@ -133,6 +133,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'),
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),
),
],
),
),
@ -260,4 +265,9 @@ class AboutController extends GetxController {
mode: LaunchMode.externalApplication,
);
}
// 日志
logs() {
Get.toNamed('/logs');
}
}

View File

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

View File

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

View File

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

View File

@ -63,13 +63,16 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
}
void setTabConfig() async {
defaultTabs = tabsConfig;
defaultTabs = [...tabsConfig];
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
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
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.toList();
tabs.value = defaultTabs;
if (tabbarSort.contains(TabType.rcmd.id)) {
initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);

View File

@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
@ -65,6 +66,7 @@ class MainController extends GetxController {
int selectedIndex = 0;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override
void onInit() {
@ -75,7 +77,12 @@ class MainController extends GetxController {
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
var userInfo = userInfoCache.get('userInfoCache');
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) {

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.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/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
@ -127,11 +128,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: Badge(
label: Text(e['count'].toString()),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: e['count'] > 0,
child: e['icon'],
icon: Obx(
() => Badge(
label:
_mainController.dynamicBadgeType.value ==
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'],
label: e['label'],
@ -148,11 +159,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: Badge(
label: Text(e['count'].toString()),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: e['count'] > 0,
child: e['icon'],
icon: Obx(
() => Badge(
label:
_mainController.dynamicBadgeType.value ==
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'],
label: e['label'],

View File

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

View File

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

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/login.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 {
Box userInfoCache = GStrorage.userInfo;
@ -19,6 +22,7 @@ class SettingController extends GetxController {
RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo;
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override
void onInit() {
@ -33,6 +37,9 @@ class SettingController extends GetxController {
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)];
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
}
loginOut() async {
@ -76,4 +83,31 @@ class SettingController extends GetxController {
feedBackEnable.value = !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

@ -22,6 +22,17 @@ class _TabbarSetPageState extends State<TabbarSetPage> {
defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
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() {

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

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

View File

@ -148,7 +148,9 @@ class VideoIntroController extends GetxController {
// 获取投币状态
Future queryHasCoinVideo() async {
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 {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) {
// hasLike.value = result["data"] == 1 ? true : false;
@ -570,10 +576,12 @@ class VideoIntroController extends GetxController {
cid: lastPlayCid.value,
upMid: videoDetail.value.owner!.mid!,
);
SmartDialog.dismiss();
if (res['status']) {
modelResult = res['data'].modelResult;
} else {
SmartDialog.showToast("当前视频可能暂不支持AI视频总结");
}
SmartDialog.dismiss();
return res;
}
}

View File

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

View File

@ -256,7 +256,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
// 请求错误
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
fn: () {
setState(() {
_futureBuilderFuture =
_videoReplyController.queryReplyList();
});
},
);
}
} else {

View File

@ -1,5 +1,6 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@ -539,18 +540,6 @@ InlineSpan buildContent(
// replyReply 查看二楼回复(回复详情)回调
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
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>[];
bool hasMatchMember = false;
@ -582,258 +571,171 @@ InlineSpan buildContent(
});
}
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
if (content.message.contains('&amp;')) {
content.message = content.message.replaceAll('&amp;', '&');
}
// 匹配表情
content.message.splitMapJoin(
RegExp(r"\[.*?\]"),
onMatch: (Match match) {
final String matchStr = match[0]!;
if (content.emote.isNotEmpty &&
matchStr.indexOf('[') == matchStr.lastIndexOf('[') &&
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) {
final int size = content.emote[matchStr]['meta']['size'];
if (content.emote.keys.contains(matchStr)) {
spanChilds.add(
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"@.*( |:)");
// }
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
...content.atNameToMid.keys.map((e) => '@$e'),
...content.jumpUrl.keys.map((e) =>
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
];
// 只@用户没有内容
if (!content.message.contains(':') ||
(content.atNameToMid.length == 1 &&
content.message == '@${content.members.first.uname}')) {
reg = RegExp(r"@.*( |:|$)");
}
matchMember = str.splitMapJoin(
reg,
onMatch: (Match match) {
if (match[0] != null) {
hasMatchMember = true;
content.atNameToMid.forEach((key, value) {
if (str.contains('回复')) {
spanChilds.add(
TextSpan(
text: '回复 ',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
),
),
);
}
spanChilds.add(
TextSpan(
text: '@$key',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
final String heroTag = Utils.makeHeroTag(value);
Get.toNamed(
'/member?mid=$value',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
String patternStr = specialTokens.map(RegExp.escape).join('|');
if (patternStr.isNotEmpty) {
patternStr += "|";
}
patternStr += r'(\b\d{1,2}[:]\d{2}\b)';
final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = [];
void addPlainTextSpan(str) {
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap =
() => replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
}
// 分割文本并处理每个部分
content.message.splitMapJoin(
pattern,
onMatch: (Match match) {
String matchStr = match[0]!;
if (content.emote.containsKey(matchStr)) {
// 处理表情
final int size = content.emote[matchStr]['meta']['size'];
spanChilds.add(WidgetSpan(
child: NetworkImgLayer(
src: content.emote[matchStr]['url'],
type: 'emote',
width: size * 20,
height: size * 20,
),
));
} else if (matchStr.startsWith("@") &&
content.atNameToMid.containsKey(matchStr.substring(1))) {
// 处理@用户
final String userName = matchStr.substring(1);
final int userId = content.atNameToMid[userName];
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 '';
},
onNonMatch: (String str) {
if (!str.contains('@')) {
spanChilds.add(TextSpan(text: str));
}
print(str);
return str;
},
},
),
);
} else if (RegExp(r'^\b[0-9]{1,2}[:][0-9]{2}\b$').hasMatch(matchStr)) {
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
}
},
),
);
} else {
matchMember = str;
}
// 匹配 jumpUrl
String matchUrl = matchMember;
if (content.jumpUrl.isNotEmpty) {
final List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
for (int index = 0; index < urlKeys.length; index++) {
var i = urlKeys[index];
if (i.contains('?')) {
urlKeys[index] = i.replaceAll('?', '\\?');
// print("matchStr=$matchStr");
String appUrlSchema = '';
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
defaultValue: false) as bool;
if (content.jumpUrl[matchStr] != null &&
!matchedStrs.contains(matchStr)) {
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
addPlainTextSpan(matchStr);
return "";
}
if (i.contains('+')) {
urlKeys[index] = i.replaceAll('+', '\\+');
}
if (i.contains('*')) {
urlKeys[index] = i.replaceAll('*', '\\*');
}
}
if (hasMatchMember) {
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,
spanChilds.addAll(
[
if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[matchStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
if (appUrlSchema == '') {
final String str = Uri.parse(matchStr).pathSegments[0];
final Map matchRes = IdUtils.matchAvorBv(input: str);
final List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty) {
if (matchKeys.first == 'BV') {
Get.toNamed(
'/searchResult',
parameters: {'keyword': matchRes['BV']},
);
}
} else {
)
],
TextSpan(
text: content.jumpUrl[matchStr]['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
if (appUrlSchema == '') {
final String str = Uri.parse(matchStr).pathSegments[0];
final Map matchRes = IdUtils.matchAvorBv(input: str);
final List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty) {
if (matchKeys.first == 'BV') {
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
'/searchResult',
parameters: {'keyword': matchRes['BV']},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search') &&
enableWordRe) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
),
);
}
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
spanChilds.add(
WidgetSpan(
child: Icon(
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(
RegExp(r'\b\d{2}:\d{2}\b'),
onMatch: (Match match) {
String matchStr = match[0]!;
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
);
} catch (_) {}
},
),
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
)
],
);
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)));
// 只显示一次
matchedStrs.add(matchStr);
} else {
addPlainTextSpan(matchStr);
}
}
return str;
return '';
},
onNonMatch: (String nonMatchStr) {
addPlainTextSpan(nonMatchStr);
return nonMatchStr;
},
);
@ -841,10 +743,10 @@ InlineSpan buildContent(
if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[];
final int len = content.pictures.length;
spanChilds.add(const TextSpan(text: '\n'));
if (len == 1) {
Map pictureItem = content.pictures.first;
picList.add(pictureItem['img_src']);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
@ -880,7 +782,7 @@ InlineSpan buildContent(
height: height,
),
),
height > maxHeight
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,

View File

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

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
@ -724,6 +725,8 @@ class _HeaderControlState extends State<HeaderControl> {
double fontSizeVal = widget.controller!.fontSizeVal;
// 弹幕速度
double danmakuDurationVal = widget.controller!.danmakuDurationVal;
// 弹幕描边
double strokeWidth = widget.controller!.strokeWidth;
final DanmakuController 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)}%'),
Padding(
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:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/session.dart';
@ -8,6 +9,8 @@ class WhisperDetailController extends GetxController {
late String face;
late String mid;
RxList<MessageItem> messageList = <MessageItem>[].obs;
//表情转换图片规则
List<dynamic>? eInfos;
@override
void onInit() {
@ -22,6 +25,9 @@ class WhisperDetailController extends GetxController {
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
if (res['status']) {
messageList.value = res['data'].messages;
if (messageList.isNotEmpty && res['data'].eInfos != null) {
eInfos = res['data'].eInfos;
}
}
return res;
}

View File

@ -110,12 +110,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
if (i == 0) {
return Column(
children: [
ChatItem(item: messageList[i]),
ChatItem(
item: messageList[i],
e_infos: _whisperDetailController.eInfos),
const SizedBox(height: 12),
],
);
} 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
import 'dart:convert';
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/utils/utils.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 {
dynamic item;
List? e_infos;
ChatItem({
super.key,
this.item,
this.e_infos,
});
@override
Widget build(BuildContext context) {
bool isOwner =
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 =
item.msgType == 18 || item.msgType == 10 || item.msgType == 13;
int msgType = item.msgType;
bool isPic = item.msgType == MsgType.pic.value; // 图片
bool isText = item.msgType == MsgType.text.value; // 文本
// 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 ?? '';
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
? (msgType == 10
? SystemNotice(item: item)
: msgType == 13
? SystemNotice2(item: item)
: const SizedBox())
? messageContent(context)
: isRevoke
? const SizedBox()
: Row(
@ -66,27 +398,7 @@ class ChatItem extends StatelessWidget {
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
isText
? 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(),
messageContent(context),
SizedBox(height: isPic ? 7 : 2),
Row(
mainAxisSize: MainAxisSize.min,

View File

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

View File

@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart';
import '../pages/blacklist/index.dart';
@ -153,6 +154,8 @@ class Routes {
// 用户专栏
CustomGetPage(
name: '/memberSeasons', page: () => const MemberSeasonsPage()),
// 日志
CustomGetPage(name: '/logs', page: () => const LogsPage()),
];
}

View File

@ -147,8 +147,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
processingState: AudioProcessingState.idle,
playing: false,
));
_item.removeLast();
if (_item.isNotEmpty) {
_item.removeLast();
setMediaItem(_item.last);
}
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;
}

View File

@ -1,51 +1,65 @@
// ignore_for_file: constant_identifier_names
// ignore_for_file: constant_identifier_names, non_constant_identifier_names
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
class IdUtils {
static const String TABLE =
'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF';
static const List<int> S = [11, 10, 3, 8, 4, 6]; // 位置编码表
static const int XOR = 177451812; // 固定异或值
static const int ADD = 8728348608; // 固定加法值
static const List<String> r = [
'B',
'V',
'1',
'',
'',
'4',
'',
'1',
'',
'7',
'',
''
];
static final XOR_CODE = BigInt.parse('23442827791579');
static final MASK_CODE = BigInt.parse('2251799813685247');
static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt();
static final BASE = BigInt.from(58);
static const data =
'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';
/// av转bv
static String av2bv(int av) {
int x_ = (av ^ XOR) + ADD;
List<String> newR = [];
newR.addAll(r);
for (int i = 0; i < S.length; i++) {
newR[S[i]] =
TABLE.characters.elementAt((x_ / pow(58, i).toInt() % 58).toInt());
static String av2bv(int aid) {
List<String> bytes = [
'B',
'V',
'1',
'0',
'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
static int bv2av(String bv) {
int r = 0;
for (int i = 0; i < S.length; i++) {
r += (TABLE.indexOf(bv.characters.elementAt(S[i])).toInt()) *
pow(58, i).toInt();
}
return (r - ADD) ^ XOR;
/// bv转av
static int bv2av(String bvid) {
List<String> bvidArr = bvid.split('');
final tmpValue = bvidArr[3];
bvidArr[3] = bvidArr[9];
bvidArr[9] = tmpValue;
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();
}
// 匹配

View File

@ -137,7 +137,8 @@ class SettingBoxKey {
enableMYBar = 'enableMYBar',
hideSearchBar = 'hideSearchBar', // 收起顶栏
hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort'; // 首页tabbar
tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode';
}
class LocalCacheKey {
@ -150,12 +151,13 @@ class LocalCacheKey {
wbiKeys = 'wbiKeys',
timeStamp = 'timeStamp',
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细
danmakuBlockType = 'danmakuBlockType',
danmakuShowArea = 'danmakuShowArea',
danmakuOpacity = 'danmakuOpacity',
danmakuFontScale = 'danmakuFontScale',
danmakuDuration = 'danmakuDuration',
strokeWidth = 'strokeWidth',
// 代理host port
systemProxyHost = 'systemProxyHost',

View File

@ -281,16 +281,18 @@ class Utils {
// [arm64-v8a]
String abi = androidInfo.supportedAbis.first;
late String downloadUrl;
for (var i in data.assets) {
if (i.downloadUrl.contains(abi)) {
downloadUrl = i.downloadUrl;
if (data.assets.isNotEmpty) {
for (var i in data.assets) {
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"
source: hosted
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:
dependency: transitive
description:
@ -492,10 +500,11 @@ packages:
floating:
dependency: "direct main"
description:
name: floating
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
url: "https://pub.flutter-io.cn"
source: hosted
path: "."
ref: main
resolved-ref: d2d8421c4d80f6113f832404109853684721e11a
url: "https://github.com/guozhigq/floating.git"
source: git
version: "2.0.1"
flutter:
dependency: "direct main"
@ -547,6 +556,14 @@ packages:
description: flutter
source: sdk
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:
dependency: transitive
description:
@ -589,6 +606,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct main"
description:
@ -781,6 +806,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -789,6 +822,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -951,7 +992,7 @@ packages:
source: hosted
version: "2.0.1"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@ -1214,6 +1255,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct main"
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
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.17+1017
version: 1.0.19+1019
environment:
sdk: ">=2.19.6 <3.0.0"
@ -51,7 +51,7 @@ dependencies:
cached_network_image: ^3.3.0
extended_image: ^8.2.0
saver_gallery: ^3.0.1
# 存储
path_provider: ^2.1.1
hive: ^2.2.3
@ -85,14 +85,14 @@ dependencies:
encrypt: ^5.0.3
# 视频播放器
media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.4 # For video rendering.
media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.4 # For video rendering.
media_kit_libs_video: ^1.0.4
# 媒体通知
audio_service: ^0.18.12
audio_session: ^0.1.16
# 音量、亮度、屏幕控制
flutter_volume_controller: ^1.3.1
screen_brightness: ^0.2.2+1
@ -103,7 +103,7 @@ dependencies:
auto_orientation: ^2.3.1
protobuf: ^3.0.0
animations: ^2.0.8
# 获取appx信息
package_info_plus: ^4.1.0
url_launcher: ^6.1.14
@ -116,7 +116,7 @@ dependencies:
appscheme: ^1.0.8
# 弹幕
ns_danmaku:
git:
git:
url: https://github.com/guozhigq/flutter_ns_danmaku.git
ref: master
# 状态栏图标控制
@ -124,7 +124,10 @@ dependencies:
# 代理
system_proxy: ^0.1.0
# pip
floating: ^2.0.1
floating:
git:
url: https://github.com/guozhigq/floating.git
ref: main
# html解析
html: ^0.15.4
# html渲染
@ -134,8 +137,10 @@ dependencies:
uuid: ^3.0.7
scrollable_positioned_list: ^0.3.8
nil: ^1.1.1
catcher_2: ^1.1.0
logger: ^2.0.2+1
path: 1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
@ -204,7 +209,6 @@ flutter:
# - family: HarmonyOS
# fonts:
# - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages