merge main

This commit is contained in:
guozhigq
2023-11-12 12:09:16 +08:00
185 changed files with 10201 additions and 2649 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

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

BIN
assets/images/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1,4 @@
## 1.0.10
### 修复
+ 长按倍速抬起后未恢复默认倍速

26
change_log/1.0.11.1112.md Normal file
View File

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

22
change_log/1.0.7.0908.md Normal file
View File

@ -0,0 +1,22 @@
## 1.0.7
默认倍速、直播弹幕、专栏等功能开发中
### 新功能
+ 弹幕设置、屏蔽功能
+ 不是很完美的后台播放功能
+ 不是很完美的画中画(pip)功能Android端
### 修复
+ 动态页面加载异常
+ 网络异常时页面空白
+ 竖屏全屏状态栏问题
+ iOS端代理请求异常
### 优化
+ 图片预览
+ 全屏播放时自动旋转
+ 转发内容增加视频标题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

24
change_log/1.0.8.0917.md Normal file
View File

@ -0,0 +1,24 @@
## 1.0.8
直播弹幕、循环播放等功能开发中
### 新功能
+ 用户拉黑功能
+ gif图片保存
+ 删除已看历史记录
### 修复
+ 弹幕数量较少
+ 弹幕屏蔽设置自动记忆
+ 动态页面渲染
+ 用户主页数据错乱
+ 大家都在搜空白
+ 默认自动全屏,顶部操作栏丢失
### 优化
+ 全屏状态栏区域显示优化
+ 图片保存至PiliPala文件夹
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

28
change_log/1.0.9.1015.md Normal file
View File

@ -0,0 +1,28 @@
## 1.0.9
### 新功能
+ 自定义倍速、默认倍速
+ 历史记录搜索
+ 收藏夹搜索
+ 历史记录多选删除
+ 视频循环播放
+ 免登录看1080P
+ 评论区视频链接跳转
+ up主分组
+ up主投稿搜索
### 修复
+ 搜索视频标题乱码
+ 屏幕帧率
+ 动态页面渲染
### 优化
+ 快进手势
+ 视频简介链接匹配
+ 视频全屏时安全区域
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -16,10 +16,15 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- image_gallery_saver (2.0.2): - gt3_flutter_plugin (0.0.8):
- Flutter - Flutter
<<<<<<< HEAD
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
=======
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
>>>>>>> main
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0): - media_kit_native_event_loop (1.0.0):
@ -34,6 +39,8 @@ PODS:
- permission_handler_apple (9.1.1): - permission_handler_apple (9.1.1):
- Flutter - Flutter
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0): - screen_brightness_ios (0.1.0):
- Flutter - Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
@ -43,6 +50,8 @@ PODS:
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
- status_bar_control (3.2.1): - status_bar_control (3.2.1):
- Flutter - Flutter
- system_proxy (0.0.1):
- Flutter
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1): - volume_controller (0.0.1):
@ -62,18 +71,24 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
<<<<<<< HEAD
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- just_audio (from `.symlinks/plugins/just_audio/ios`) - just_audio (from `.symlinks/plugins/just_audio/ios`)
=======
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
>>>>>>> main
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`) - status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -83,6 +98,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- GT3Captcha-iOS
- ReachabilitySwift - ReachabilitySwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
@ -100,10 +116,15 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_volume_controller: flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios" :path: ".symlinks/plugins/flutter_volume_controller/ios"
<<<<<<< HEAD
image_gallery_saver: image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios" :path: ".symlinks/plugins/image_gallery_saver/ios"
just_audio: just_audio:
:path: ".symlinks/plugins/just_audio/ios" :path: ".symlinks/plugins/just_audio/ios"
=======
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
>>>>>>> main
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop: media_kit_native_event_loop:
@ -116,6 +137,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
screen_brightness_ios: screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios" :path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus: share_plus:
@ -124,6 +147,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
status_bar_control: status_bar_control:
:path: ".symlinks/plugins/status_bar_control/ios" :path: ".symlinks/plugins/status_bar_control/ios"
system_proxy:
:path: ".symlinks/plugins/system_proxy/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller: volume_controller:
@ -144,8 +169,13 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
<<<<<<< HEAD
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
=======
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
>>>>>>> main
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -153,10 +183,12 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47

View File

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

View File

@ -103,7 +103,10 @@
</array> </array>
</dict> </dict>
</array> </array>
<<<<<<< HEAD
<!-- audio service配置 --> <!-- audio service配置 -->
=======
>>>>>>> main
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>audio</string>

View File

@ -0,0 +1,96 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// ignore: must_be_immutable
class HtmlRender extends StatelessWidget {
String? htmlContent;
final int? imgCount;
final List? imgList;
HtmlRender({
this.htmlContent,
this.imgCount,
this.imgList,
super.key,
});
@override
Widget build(BuildContext context) {
return Html(
data: htmlContent,
onLinkTap: (url, buildContext, attributes) => {},
extensions: [
TagExtension(
tagsToExtend: {"img"},
builder: (extensionContext) {
try {
Map attributes = extensionContext.attributes;
List key = attributes.keys.toList();
String? imgUrl = key.contains('src')
? attributes['src']
: attributes['data-src'];
if (imgUrl!.startsWith('//')) {
imgUrl = 'https:$imgUrl';
}
if (imgUrl.startsWith('http://')) {
imgUrl = imgUrl.replaceAll('http://', 'https://');
}
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
bool isEmote = imgUrl.contains('/emote/');
bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return const SizedBox();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!);
// return Image.network(
// imgUrl,
// width: isEmote ? 22 : null,
// height: isEmote ? 22 : null,
// );
return NetworkImgLayer(
width: isEmote ? 22 : Get.size.width - 24,
height: isEmote ? 22 : 200,
src: imgUrl,
);
} catch (err) {
print(err);
return const SizedBox();
}
},
),
],
style: {
"html": Style(
fontSize: FontSize.medium,
lineHeight: LineHeight.percent(140),
),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
"a": Style(
color: Theme.of(context).colorScheme.primary,
textDecoration: TextDecoration.none,
),
"p": Style(
margin: Margins.only(bottom: 10),
),
"span": Style(
fontSize: FontSize.medium,
height: Height(1.65),
),
"div": Style(height: Height.auto()),
"li > p": Style(
display: Display.inline,
),
"li": Style(
padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify,
),
"img": Style(margin: Margins.only(top: 4, bottom: 4)),
},
);
}
}

View File

@ -5,6 +5,7 @@ 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/stat/danmu.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
@ -27,6 +28,11 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd, this.longPressEnd,
}) : super(key: key); }) : super(key: key);
bool isStringNumeric(String str) {
RegExp numericRegex = RegExp(r'^\d+$');
return numericRegex.hasMatch(str);
}
void onPushDetail(heroTag) async { void onPushDetail(heroTag) async {
String goto = videoItem.goto; String goto = videoItem.goto;
switch (goto) { switch (goto) {
@ -62,6 +68,47 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag, 'heroTag': heroTag,
}); });
break; break;
// 动态
case 'picture':
try {
String dynamicType = 'picture';
String uri = videoItem.uri;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
}
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
if (isStringNumeric(path.split('/')[1])) {
// 请求接口
var res =
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
if (res['status']) {
Get.toNamed('/dynamicDetail', arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail'
});
}
return;
}
}
Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title,
'id': id,
'dynamicType': dynamicType
});
} catch (err) {
SmartDialog.showToast(err.toString());
}
break;
default: default:
SmartDialog.showToast(videoItem.goto); SmartDialog.showToast(videoItem.goto);
Get.toNamed( Get.toNamed(
@ -112,12 +159,22 @@ class VideoCardV extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
if (crossAxisCount == 1 && videoItem.duration != null) if (videoItem.duration != null)
PBadge( if (crossAxisCount == 1) ...[
bottom: 10, PBadge(
right: 10, bottom: 10,
text: videoItem.duration, right: 10,
) text: videoItem.duration,
)
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: videoItem.duration,
)
],
], ],
); );
}), }),
@ -174,7 +231,7 @@ class VideoContent extends StatelessWidget {
], ],
), ),
if (crossAxisCount > 1) ...[ if (crossAxisCount > 1) ...[
const SizedBox(height: 3), const SizedBox(height: 2),
VideoStat( VideoStat(
videoItem: videoItem, videoItem: videoItem,
), ),
@ -247,7 +304,7 @@ class VideoContent extends StatelessWidget {
}, },
), ),
] else ...[ ] else ...[
const SizedBox(height: 26) const SizedBox(height: 24)
] ]
], ],
), ),
@ -268,23 +325,18 @@ class VideoStat extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return RichText(
children: [ maxLines: 1,
Text( text: TextSpan(
'${videoItem.stat.view}观看', style: TextStyle(
style: TextStyle( fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, color: Theme.of(context).colorScheme.outline,
color: Theme.of(context).colorScheme.outline,
),
), ),
Text( children: [
'${videoItem.stat.danmu}弹幕', TextSpan(text: '${videoItem.stat.view}观看'),
style: TextStyle( TextSpan(text: '${videoItem.stat.danmu}弹幕'),
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, ],
color: Theme.of(context).colorScheme.outline, ),
),
),
],
); );
} }
} }

View File

@ -97,6 +97,9 @@ class Api {
// 操作用户关系 // 操作用户关系
static const String relationMod = '/x/relation/modify'; static const String relationMod = '/x/relation/modify';
// 相互关系查询
static const String relationSearch = '/x/space/wbi/acc/relation';
// 评论列表 // 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11 // https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
static const String replyList = '/x/v2/reply'; static const String replyList = '/x/v2/reply';
@ -126,12 +129,14 @@ class Api {
static const String userFavFolder = '/x/v3/fav/folder/created/list'; static const String userFavFolder = '/x/v3/fav/folder/created/list';
/// 收藏夹 详情 /// 收藏夹 详情
/// media_id int 收藏夹id /// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
/// pn int 当前页 /// pn int 当前页
/// ps int pageSize /// ps int pageSize
/// keyword String 搜索词 /// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿 /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id /// tid int 分区id
/// platform web
/// type 0 当前收藏夹 1 全部收藏夹
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String userFavFolderDetail = '/x/v3/fav/resource/list'; static const String userFavFolderDetail = '/x/v3/fav/resource/list';
@ -164,6 +169,12 @@ class Api {
// 清空历史记录 // 清空历史记录
static const String clearHistory = '/x/v2/history/clear'; static const String clearHistory = '/x/v2/history/clear';
// 删除某条历史记录
static const String delHistory = '/x/v2/history/delete';
// 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search';
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword'; 'https://s.search.bilibili.com/main/hotword';
@ -239,6 +250,9 @@ class Api {
// wts=1689767832 // wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search'; static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// 用户动态 // 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space'; static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -285,6 +299,9 @@ class Api {
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
// 移除黑名单
static const String removeBlack = '/x/relation/modify';
// github 获取最新版 // github 获取最新版
static const String latestApp = static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest'; 'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
@ -294,4 +311,65 @@ class Api {
static const String onlineTotal = '/x/player/online/total'; static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
// up主分组
static const String followUpTag = '/x/relation/tags';
// 设置Up主分组
// 0 添加至默认分组 否则使用,分割tagid
static const String addUsers = '/x/relation/tags/addUsers';
// 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag';
// 获取某个动态详情
// timezone_offset=-480
// id=849312409672744983
// features=itemOpusStyle
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
// AI总结
/// https://api.bilibili.com/x/web-interface/view/conclusion/get?
/// bvid=BV1ju4y1s7kn&
/// cid=1296086601&
/// up_mid=4641697&
/// w_rid=1607c6c5a4a35a1297e31992220900ae&
/// wts=1697033079
static const String aiConclusion = '/x/web-interface/view/conclusion/get';
// captcha验证码
static const String getCaptcha =
'https://passport.bilibili.com/x/passport-login/captcha?source=main_web';
// web端短信验证码
static const String smsCode =
'https://passport.bilibili.com/x/passport-login/web/sms/send';
// web端验证码登录
// web端密码登录
// app端短信验证码
static const String appSmsCode =
'https://passport.bilibili.com/x/passport-login/sms/send';
// app端验证码登录
// 获取短信验证码
// static const String appSafeSmsCode =
// 'https://passport.bilibili.com/x/safecenter/common/sms/send';
/// app端密码登录
/// username
/// password
/// key
/// rhash
static const String loginInByPwdApi =
'https://passport.bilibili.com/x/passport-login/oauth2/login';
/// 密码加密密钥
/// disable_rcmd
/// local_id
static const getWebKey =
'https://passport.bilibili.com/x/passport-login/web/key';
} }

View File

@ -23,4 +23,31 @@ class BlackHttp {
}; };
} }
} }
// 移除黑名单
static Future removeBlack({required int fid}) async {
var res = await Request().post(
Api.removeBlack,
queryParameters: {
'act': 6,
'csrf': await Request.getCsrf(),
'fid': fid,
'jsonp': 'jsonp',
're_src': 116,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': [],
'msg': '操作成功',
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -2,4 +2,37 @@ class HttpString {
static const String baseUrl = 'https://www.bilibili.com'; static const String baseUrl = 'https://www.bilibili.com';
static const String baseApiUrl = 'https://api.bilibili.com'; static const String baseApiUrl = 'https://api.bilibili.com';
static const String tUrl = 'https://api.vc.bilibili.com'; static const String tUrl = 'https://api.vc.bilibili.com';
static const List<int> validateStatusCodes = [
302,
304,
307,
400,
401,
403,
404,
405,
409,
412,
500,
503,
504,
509,
616,
617,
625,
626,
628,
629,
632,
643,
650,
652,
658,
662,
688,
689,
701,
799,
8888
];
} }

View File

@ -17,17 +17,11 @@ class DanmakaHttp {
'oid': cid, 'oid': cid,
'segment_index': segmentIndex, 'segment_index': segmentIndex,
}; };
var response = await Request().get(
// 计算函数 Api.webDanmaku,
Future<DmSegMobileReply> computeTask(Map<String, int> params) async { data: params,
var response = await Request().get( extra: {'resType': ResponseType.bytes},
Api.webDanmaku, );
data: params, return DmSegMobileReply.fromBuffer(response.data);
extra: {'resType': ResponseType.bytes},
);
return DmSegMobileReply.fromBuffer(response.data);
}
return await compute(computeTask, params);
} }
} }

View File

@ -28,6 +28,7 @@ class DynamicsHttp {
'data': DynamicsDataModel.fromJson(res.data['data']), 'data': DynamicsDataModel.fromJson(res.data['data']),
}; };
} catch (err) { } catch (err) {
print(err);
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
@ -85,4 +86,35 @@ class DynamicsHttp {
}; };
} }
} }
//
static Future dynamicDetail({
String? id,
}) async {
var res = await Request().get(Api.dynamicDetail, data: {
'timezone_offset': -480,
'id': id,
'features': 'itemOpusStyle',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': DynamicItemModel.fromJson(res.data['data']['item']),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': err.toString(),
};
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

103
lib/http/html.dart Normal file
View File

@ -0,0 +1,103 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:pilipala/http/index.dart';
class HtmlHttp {
// article
static Future reqHtml(id, dynamicType) async {
var response = await Request().get(
"https://www.bilibili.com/opus/$id",
extra: {'ua': 'pc'},
);
if (response.data.contains('Redirecting to')) {
RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
Match match = regex.firstMatch(response.data)!;
String matchedString = match.group(0)!;
response = await Request().get(
'https:$matchedString' + '/',
extra: {'ua': 'pc'},
);
}
try {
Document rootTree = parse(response.data);
// log(response.data.body.toString());
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
// 头像
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader
.querySelector('.fixed-author-header__author__name')!
.text;
// 动态详情
Element opusDetail = appDom.querySelector('.opus-detail')!;
// 发布时间
String updateTime =
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
//
String opusContent =
opusDetail.querySelector('.opus-module-content')!.innerHtml;
String test = opusDetail
.querySelector('.horizontal-scroll-album__pic__img')!
.innerHtml;
String commentId = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': test + opusContent,
'commentId': int.parse(commentId)
};
} catch (err) {
print('err: $err');
}
}
// read
static Future reqReadHtml(id, dynamicType) async {
var response = await Request().get(
"https://www.bilibili.com/$dynamicType/$id/",
extra: {'ua': 'pc'},
);
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!;
// 头像
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// print(avatar);
// avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
return {
'status': true,
'avatar': '',
'uname': uname,
'updateTime': '',
'content': opusContent,
'commentId': int.parse(number)
};
}
}

View File

@ -4,12 +4,13 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/io.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/interceptor.dart'; import 'package:pilipala/http/interceptor.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request { class Request {
@ -17,6 +18,11 @@ class Request {
static late CookieManager cookieManager; static late CookieManager cookieManager;
static late final Dio dio; static late final Dio dio;
factory Request() => _instance; factory Request() => _instance;
Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
/// 设置cookie /// 设置cookie
static setCookie() async { static setCookie() async {
@ -41,8 +47,8 @@ class Request {
log("setCookie, ${e.toString()}"); log("setCookie, ${e.toString()}");
} }
} }
setOptionsHeaders(userInfo);
} }
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
if (cookie.isEmpty) { if (cookie.isEmpty) {
try { try {
@ -60,9 +66,6 @@ class Request {
static Future<String> getCsrf() async { static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar var cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseApiUrl)); .loadForRequest(Uri.parse(HttpString.baseApiUrl));
// for (var i in cookies) {
// print(i);
// }
String token = ''; String token = '';
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) { if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
token = cookies.firstWhere((e) => e.name == 'bili_jct').value; token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
@ -70,8 +73,10 @@ class Request {
return token; return token;
} }
static setOptionsHeaders(userInfo) { static setOptionsHeaders(userInfo, status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
}
dio.options.headers['env'] = 'prod'; dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64'; dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
@ -92,18 +97,47 @@ class Request {
//响应流上前后两次接受到数据的间隔,单位为毫秒。 //响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 12000), receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头. //Http请求头.
headers: { headers: {},
// 'cookie': '',
},
); );
enableSystemProxy =
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
systemProxyHost =
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
systemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
dio = Dio(options) dio = Dio(options)
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
..httpClientAdapter = Http2Adapter( ..httpClientAdapter = Http2Adapter(
ConnectionManager( ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000), idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true, onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
), ),
)
/// 设置代理
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
// Config the client.
client.findProxy = (uri) {
if (enableSystemProxy) {
print('🌹:$systemProxyHost');
print('🌹:$systemProxyPort');
// return 'PROXY host:port';
return 'PROXY $systemProxyHost:$systemProxyPort';
} else {
// 不设置代理
return 'DIRECT';
}
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
},
); );
//添加拦截器 //添加拦截器
@ -118,30 +152,26 @@ class Request {
dio.transformer = BackgroundTransformer(); dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) { dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || status == 304 || status == 302; return status! >= 200 && status < 300 ||
HttpString.validateStatusCodes.contains(status);
}; };
} }
/* /*
* get请求 * get请求
*/ */
get(url, {data, cacheOptions, options, cancelToken, extra}) async { get(url, {data, options, cancelToken, extra}) async {
Response response; Response response;
Options options; Options options = Options();
String ua = 'pc';
ResponseType resType = ResponseType.json; ResponseType resType = ResponseType.json;
if (extra != null) { if (extra != null) {
ua = extra!['ua'] ?? 'pc';
resType = extra!['resType'] ?? ResponseType.json; resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) {
options.headers = {'user-agent': headerUa(type: extra['ua'])};
}
} }
if (cacheOptions != null) { options.responseType = resType;
cacheOptions.headers = {'user-agent': headerUa(ua)};
options = cacheOptions;
} else {
options = Options();
options.headers = {'user-agent': headerUa(ua)};
options.responseType = resType;
}
try { try {
response = await dio.get( response = await dio.get(
url, url,
@ -208,15 +238,19 @@ class Request {
token.cancel("cancelled"); token.cancel("cancelled");
} }
String headerUa(ua) { String headerUa({type = 'mob'}) {
String headerUa = ''; String headerUa = '';
if (ua == 'mob') { if (type == 'mob') {
headerUa = Platform.isIOS if (Platform.isIOS) {
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' headerUa =
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'; 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';
} else {
headerUa =
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';
}
} else { } else {
headerUa = headerUa =
'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'; 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
} }
return headerUa; return headerUa;
} }

View File

@ -46,7 +46,10 @@ class ApiInterceptor extends Interceptor {
void onError(DioException err, ErrorInterceptorHandler handler) async { void onError(DioException err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误 // 处理网络请求错误
// handler.next(err); // handler.next(err);
SmartDialog.showToast(await dioError(err)); SmartDialog.showToast(
await dioError(err),
displayType: SmartToastType.onlyRefresh,
);
super.onError(err, handler); super.onError(err, handler);
} }

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

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

View File

@ -1,7 +1,9 @@
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/wbi_sign.dart'; import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp { class MemberHttp {
@ -18,6 +20,7 @@ class MemberHttp {
var res = await Request().get( var res = await Request().get(
Api.memberInfo, Api.memberInfo,
data: params, data: params,
extra: {'ua': 'pc'},
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -65,7 +68,7 @@ class MemberHttp {
int ps = 30, int ps = 30,
int tid = 0, int tid = 0,
int? pn, int? pn,
String keyword = '', String? keyword,
String order = 'pubdate', String order = 'pubdate',
bool orderAvoided = true, bool orderAvoided = true,
}) async { }) async {
@ -74,7 +77,7 @@ class MemberHttp {
'ps': ps, 'ps': ps,
'tid': tid, 'tid': tid,
'pn': pn, 'pn': pn,
'keyword': keyword, 'keyword': keyword ?? '',
'order': order, 'order': order,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 1550101,
@ -83,6 +86,7 @@ class MemberHttp {
var res = await Request().get( var res = await Request().get(
Api.memberArchive, Api.memberArchive,
data: params, data: params,
extra: {'ua': 'pc'},
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -119,4 +123,96 @@ class MemberHttp {
}; };
} }
} }
// 搜索用户动态
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
var res = await Request().get(Api.memberDynamic, data: {
'keyword': '海拔',
'mid': mid,
'pn': pn,
'ps': ps,
'platform': 'web'
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 查询分组
static Future followUpTags() async {
var res = await Request().get(Api.followUpTag);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 设置分组
static Future addUsers(int? fids, String? tagids) async {
var res = await Request().post(Api.addUsers, queryParameters: {
'fids': fids,
'tagids': tagids ?? '0',
'csrf': await Request.getCsrf(),
}, data: {
'cross_domain': true
});
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取某分组下的up
static Future followUpGroup(
int? mid,
int? tagid,
int? pn,
int? ps,
) async {
var res = await Request().get(Api.followUpGroup, data: {
'mid': mid,
'tagid': tagid,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) {
// FollowItemModel
return {
'status': true,
'data': res.data['data']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -26,7 +26,7 @@ class ReplyHttp {
Map errMap = { Map errMap = {
-400: '请求错误', -400: '请求错误',
-404: '无此项', -404: '无此项',
12002: '当前页面评论功能已关闭"', 12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法', 12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区', 12061: 'UP主已关闭评论区',
}; };

View File

@ -1,37 +1,63 @@
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data['code'] == 0) { if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(resultMap),
};
}
} else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': HotSearchModel.fromJson(res.data), 'data': HotSearchModel.fromJson(res.data),
}; };
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
} }
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
} }
// 获取搜索建议 // 获取搜索建议
static Future searchSuggest({required term}) async { static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest, var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term}); data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) { if (res.data is String) {
res.data['result']['term'] = term; Map<String, dynamic> resultMap = json.decode(res.data);
return { if (resultMap['code'] == 0) {
'status': true, if (resultMap['result'] is Map) {
'data': SearchSuggestModel.fromJson(res.data['result']), resultMap['result']['term'] = term;
}; }
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
} else { } else {
return { return {
'status': false, 'status': false,
@ -61,29 +87,44 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData); var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data; Object data;
switch (searchType) { try {
case SearchType.video: switch (searchType) {
data = SearchVideoModel.fromJson(res.data['data']); case SearchType.video:
break; List<int> blackMidsList =
case SearchType.live_room: setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
data = SearchLiveModel.fromJson(res.data['data']); for (var i in res.data['data']['result']) {
break; // 屏蔽推广和拉黑用户
case SearchType.bili_user: i['available'] = !blackMidsList.contains(i['mid']);
data = SearchUserModel.fromJson(res.data['data']); }
break; data = SearchVideoModel.fromJson(res.data['data']);
case SearchType.media_bangumi: break;
data = SearchMBangumiModel.fromJson(res.data['data']); case SearchType.live_room:
break; data = SearchLiveModel.fromJson(res.data['data']);
break;
case SearchType.bili_user:
data = SearchUserModel.fromJson(res.data['data']);
break;
case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']);
break;
case SearchType.article:
data = SearchArticleModel.fromJson(res.data['data']);
break;
}
return {
'status': true,
'data': data,
};
} catch (err) {
print(err);
} }
return {
'status': true,
'data': data,
};
} else { } else {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅', 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
? '没有相关数据'
: res.data['message'],
}; };
} }
} }

View File

@ -8,6 +8,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/history.dart'; import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class UserHttp { class UserHttp {
static Future<dynamic> userStat({required int mid}) async { static Future<dynamic> userStat({required int mid}) async {
@ -70,14 +71,15 @@ class UserHttp {
required int pn, required int pn,
required int ps, required int ps,
String keyword = '', String keyword = '',
String order = 'mtime'}) async { String order = 'mtime',
int type = 0}) async {
var res = await Request().get(Api.userFavFolderDetail, data: { var res = await Request().get(Api.userFavFolderDetail, data: {
'media_id': mediaId, 'media_id': mediaId,
'pn': pn, 'pn': pn,
'ps': ps, 'ps': ps,
'keyword': keyword, 'keyword': keyword,
'order': order, 'order': order,
'type': 0, 'type': type,
'tid': 0, 'tid': 0,
'platform': 'web' 'platform': 'web'
}); });
@ -231,4 +233,64 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
// 删除历史记录
static Future delHistory(kid) async {
var res = await Request().post(
Api.delHistory,
queryParameters: {
'kid': kid,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '已删除'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 相互关系查询
static Future relationSearch(int mid) async {
Map params = await WbiSign().makSign({
'mid': mid,
'token': '',
'platform': 'web',
'web_location': 1550101,
});
var res = await Request().get(
Api.relationSearch,
data: {
'mid': mid,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
// relation 主动状态
// 被动状态
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 搜索历史记录
static Future searchHistory(
{required int pn, required String keyword}) async {
var res = await Request().get(
Api.searchHistory,
data: {
'pn': pn,
'keyword': keyword,
'business': 'all',
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
} }

View File

@ -9,9 +9,11 @@ import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/model_rec_video_item.dart'; import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/wbi_sign.dart';
/// res.data['code'] == 0 请求正常返回结果 /// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果 /// res.data['data'] 为结果
@ -20,6 +22,9 @@ import 'package:pilipala/utils/storage.dart';
class VideoHttp { class VideoHttp {
static Box localCache = GStrorage.localCache; static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting; static Box setting = GStrorage.setting;
static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
static Box userInfoCache = GStrorage.userInfo;
// 首页推荐视频 // 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async { static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@ -73,6 +78,7 @@ class VideoHttp {
for (var i in res.data['data']['items']) { for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户 // 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' && if (i['card_goto'] != 'ad_av' &&
(!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)); list.add(RecVideoItemAppModel.fromJson(i));
@ -130,6 +136,11 @@ class VideoHttp {
// 'platform': '', // 'platform': '',
// 'high_quality': '' // 'high_quality': ''
}; };
// 免登录查看1080p
if (userInfoCache.get('userInfoCache') == null &&
setting.get(SettingBoxKey.p1080, defaultValue: true)) {
data['try_look'] = 1;
}
try { try {
var res = await Request().get(Api.videoUrl, data: data); var res = await Request().get(Api.videoUrl, data: data);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@ -411,4 +422,23 @@ class VideoHttp {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} }
} }
static Future aiConclusion({
String? bvid,
int? cid,
int? upMid,
}) async {
Map params = await WbiSign().makSign({
'bvid': bvid,
'cid': cid,
'up_mid': upMid,
});
var res = await Request().get(Api.aiConclusion, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': AiConclusionModel.fromJson(res.data['data']),
};
}
}
} }

View File

@ -1,5 +1,11 @@
<<<<<<< HEAD
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
=======
import 'dart:io';
>>>>>>> main
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -14,6 +20,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart'; import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -26,6 +33,7 @@ void main() async {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async { .then((_) async {
await GStrorage.init(); await GStrorage.init();
<<<<<<< HEAD
await AudioService.init<AudioHandler>( await AudioService.init<AudioHandler>(
builder: () => MyAudioHandler(), builder: () => MyAudioHandler(),
@ -38,6 +46,9 @@ void main() async {
), ),
); );
=======
await setupServiceLocator();
>>>>>>> main
runApp(const MyApp()); runApp(const MyApp());
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@ -74,6 +85,23 @@ class MyApp extends StatelessWidget {
double textScale = double textScale =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0); setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
// 强制设置高帧率
if (Platform.isAndroid) {
try {
late List modes;
FlutterDisplayMode.supported.then((value) {
modes = value;
var storageDisplay = setting.get(SettingBoxKey.displayMode);
DisplayMode f = DisplayMode.auto;
if (storageDisplay != null) {
f = modes.firstWhere((e) => e.toString() == storageDisplay);
}
DisplayMode preferred = modes.toList().firstWhere((el) => el == f);
FlutterDisplayMode.setPreferredMode(preferred);
});
} catch (_) {}
}
return DynamicColorBuilder( return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) { builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme; ColorScheme? lightColorScheme;

View File

@ -12,20 +12,20 @@ enum SearchType {
live_room, live_room,
// 主播live_user // 主播live_user
// live_user, // live_user,
// 专栏article
// article,
// 话题topic // 话题topic
// topic, // topic,
// 用户bili_user // 用户bili_user
bili_user, bili_user,
// 专栏article
article,
// 相簿photo // 相簿photo
// photo // photo
} }
extension SearchTypeExtension on SearchType { extension SearchTypeExtension on SearchType {
String get type => String get type =>
['video', 'media_bangumi', 'live_room', 'bili_user'][index]; ['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index]; String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
} }
// 搜索类型为视频、专栏及相簿时 // 搜索类型为视频、专栏及相簿时

View File

@ -244,7 +244,9 @@ class Vote {
choiceCnt = json['choice_cnt']; choiceCnt = json['choice_cnt'];
share = json['share']; share = json['share'];
defaultShare = json['default_share']; defaultShare = json['default_share'];
endTime = json['end_time']; endTime = json['end_time'] is int
? json['end_time']
: int.parse(json['end_time']);
joinNum = json['join_num']; joinNum = json['join_num'];
status = json['status']; status = json['status'];
type = json['type']; type = json['type'];

View File

@ -8,7 +8,7 @@ class FollowDataModel {
List<FollowItemModel>? list; List<FollowItemModel>? list;
FollowDataModel.fromJson(Map<String, dynamic> json) { FollowDataModel.fromJson(Map<String, dynamic> json) {
total = json['total']; total = json['total'] ?? 0;
list = json['list'] list = json['list']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e)) .map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList(); .toList();
@ -19,7 +19,7 @@ class FollowItemModel {
FollowItemModel({ FollowItemModel({
this.mid, this.mid,
this.attribute, this.attribute,
this.mtime, // this.mtime,
this.tag, this.tag,
this.special, this.special,
this.uname, this.uname,
@ -30,7 +30,7 @@ class FollowItemModel {
int? mid; int? mid;
int? attribute; int? attribute;
int? mtime; // int? mtime;
List? tag; List? tag;
int? special; int? special;
String? uname; String? uname;
@ -41,7 +41,7 @@ class FollowItemModel {
FollowItemModel.fromJson(Map<String, dynamic> json) { FollowItemModel.fromJson(Map<String, dynamic> json) {
mid = json['mid']; mid = json['mid'];
attribute = json['attribute']; attribute = json['attribute'];
mtime = json['mtime']; // mtime = json['mtime'];
tag = json['tag']; tag = json['tag'];
special = json['special']; special = json['special'];
uname = json['uname']; uname = json['uname'];

View File

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

View File

@ -0,0 +1,23 @@
class MemberTagItemModel {
MemberTagItemModel({
this.count,
this.name,
this.tagid,
this.tip,
this.checked,
});
int? count;
String? name;
int? tagid;
String? tip;
bool? checked;
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
name = json['name'];
tagid = json['tagid'];
tip = json['tip'];
checked = false;
}
}

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list; List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) { SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e)) .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList(); .toList();
} }
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id, this.id,
this.cid, this.cid,
// this.author, // this.author,
// this.mid, this.mid,
// this.typeid, // this.typeid,
// this.typename, // this.typename,
this.arcurl, this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id; int? id;
int? cid; int? cid;
// String? author; // String? author;
// String? mid; int? mid;
// String? typeid; // String? typeid;
// String? typename; // String? typename;
String? arcurl; String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl']; arcurl = json['arcurl'];
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); // title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
description = json['description']; description = json['description'];
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
indexShow = json['index_show']; indexShow = json['index_show'];
} }
} }
class SearchArticleModel {
SearchArticleModel({this.list});
List<SearchArticleItemModel>? list;
SearchArticleModel.fromJson(Map<String, dynamic> json) {
list = json['result'] != null
? json['result']
.map<SearchArticleItemModel>(
(e) => SearchArticleItemModel.fromJson(e))
.toList()
: [];
}
}
class SearchArticleItemModel {
SearchArticleItemModel({
this.pubTime,
this.like,
this.title,
this.subTitle,
this.rankOffset,
this.mid,
this.imageUrls,
this.id,
this.categoryId,
this.view,
this.reply,
this.desc,
this.rankScore,
this.type,
this.templateId,
this.categoryName,
});
int? pubTime;
int? like;
List? title;
String? subTitle;
int? rankOffset;
int? mid;
List? imageUrls;
int? id;
int? categoryId;
int? view;
int? reply;
String? desc;
int? rankScore;
String? type;
int? templateId;
String? categoryName;
SearchArticleItemModel.fromJson(Map<String, dynamic> json) {
pubTime = json['pub_time'];
like = json['like'];
title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
rankOffset = json['rank_offset'];
mid = json['mid'];
imageUrls = json['image_urls'];
id = json['id'];
categoryId = json['category_id'];
view = json['view'];
reply = json['reply'];
desc = json['desc'];
rankScore = json['rank_score'];
type = json['type'];
templateId = json['templateId'];
categoryName = json['category_name'];
}
}

View File

@ -3,17 +3,23 @@ class HistoryData {
this.cursor, this.cursor,
this.tab, this.tab,
this.list, this.list,
this.page,
}); });
Cursor? cursor; Cursor? cursor;
List<HisTabItem>? tab; List<HisTabItem>? tab;
List<HisListItem>? list; List<HisListItem>? list;
Map? page;
HistoryData.fromJson(Map<String, dynamic> json) { HistoryData.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']); cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList(); tab = json['tab'] != null
list = ? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList(); : [];
list = json['list'] != null
? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()
: [];
page = json['page'];
} }
} }
@ -79,6 +85,7 @@ class HisListItem {
this.kid, this.kid,
this.tagName, this.tagName,
this.liveStatus, this.liveStatus,
this.checked,
}); });
String? title; String? title;
@ -105,6 +112,7 @@ class HisListItem {
int? kid; int? kid;
String? tagName; String? tagName;
int? liveStatus; int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) { HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title']; title = json['title'];
@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid']; kid = json['kid'];
tagName = json['tag_name']; tagName = json['tag_name'];
liveStatus = json['live_status']; liveStatus = json['live_status'];
checked = false;
} }
} }

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

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

View File

@ -184,7 +184,7 @@ class AboutController extends GetxController {
// 获取远程版本 // 获取远程版本
Future getRemoteApp() async { Future getRemoteApp() async {
var result = await Request().get(Api.latestApp); var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
data = LatestDataModel.fromJson(result.data); data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data; remoteAppInfo = data;
remoteVersion.value = data.tagName!; remoteVersion.value = data.tagName!;

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.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/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
? int.parse(Get.parameters['seasonId']!) ? int.parse(Get.parameters['seasonId']!)
: null; : null;
var epId = Get.parameters['epId'] != null var epId = Get.parameters['epId'] != null
? int.parse(Get.parameters['epId']!) ? int.tryParse(Get.parameters['epId']!)
: null; : null;
// 是否预渲染 骨架屏 // 是否预渲染 骨架屏
@ -257,7 +258,7 @@ class BangumiIntroController extends GetxController {
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
} }
return result; return result;
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) {
nextIndex = 0;
}
}
if (nextIndex <= episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
} }

View File

@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
class _BangumiIntroPanelState extends State<BangumiIntroPanel> class _BangumiIntroPanelState extends State<BangumiIntroPanel>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final BangumiIntroController bangumiIntroController = late BangumiIntroController bangumiIntroController;
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']); late VideoDetailController videoDetailCtr;
BangumiInfoModel? bangumiDetail; BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late int cid;
late String heroTag;
// 添加页面缓存 // 添加页面缓存
@override @override
@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
heroTag = Get.arguments['heroTag'];
cid = widget.cid!;
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) { bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value; bangumiDetail = value;
}); });
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); _futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
videoDetailCtr.cid.listen((p0) {
print('🐶🐶$p0');
cid = p0;
setState(() {});
});
} }
@override @override
@ -61,22 +72,25 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) { if (snapshot.data['status']) {
// 请求成功 // 请求成功
return BangumiInfo( return BangumiInfo(
loadingStatus: false, loadingStatus: false,
bangumiDetail: bangumiDetail, bangumiDetail: bangumiDetail,
cid: cid,
); );
} else { } else {
// 请求错误 // 请求错误
return HttpError( // return HttpError(
errMsg: snapshot.data['msg'], // errMsg: snapshot.data['msg'],
fn: () => Get.back(), // fn: () => Get.back(),
); // );
return SizedBox();
} }
} else { } else {
return BangumiInfo( return BangumiInfo(
loadingStatus: true, loadingStatus: true,
bangumiDetail: bangumiDetail, bangumiDetail: bangumiDetail,
cid: widget.cid, cid: cid,
); );
} }
}, },
@ -117,6 +131,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
bangumiItem = bangumiIntroController.bangumiItem; bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!; cid = widget.cid!;
print('cid: $cid');
videoDetailCtr.cid.listen((p0) {
cid = p0;
print('cid: $cid');
setState(() {});
});
} }
// 收藏 // 收藏
@ -260,9 +280,15 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [ children: [
Text( Text(
!widget.loadingStatus !widget.loadingStatus
? widget.bangumiDetail!.areas! ? (widget.bangumiDetail!.areas!
.first['name'] .isNotEmpty
: bangumiItem!.areas!.first['name'], ? widget.bangumiDetail!.areas!
.first['name']
: '')
: (bangumiItem!.areas!.isNotEmpty
? bangumiItem!
.areas!.first['name']
: ''),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: t.colorScheme.outline, color: t.colorScheme.outline,

View File

@ -113,6 +113,9 @@ class _BangumiPageState extends State<BangumiPage>
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == if (snapshot.connectionState ==
ConnectionState.done) { ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList; List list = _bangumidController.bangumiFollowList;
if (data['status']) { if (data['status']) {
@ -198,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
}, },
), ),
), ),
const LoadingMore() LoadingMore()
], ],
), ),
); );

View File

@ -1,7 +1,9 @@
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.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget { class BangumiPanel extends StatefulWidget {
@ -30,16 +32,28 @@ class _BangumiPanelState extends State<BangumiPanel> {
dynamic userInfo; dynamic userInfo;
// 默认未开通 // 默认未开通
int vipStatus = 0; int vipStatus = 0;
late int cid;
String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!); cid = widget.cid!;
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex(); scrollToIndex();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) { if (userInfo != null) {
vipStatus = userInfo.vipStatus; vipStatus = userInfo.vipStatus;
} }
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex();
});
} }
@override @override

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false, centerTitle: false,
title: Obx( title: Obx(
() => Text( () => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000', '黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
// trailing: TextButton( trailing: TextButton(
// onPressed: () {}, onPressed: () => _blackListController
// child: const Text('移除'), .removeBlack(list[index].mid),
// ), child: const Text('移除'),
),
); );
}, },
), ),
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController { class BlackListController extends GetxController {
int currentPage = 1; int currentPage = 1;
int pageSize = 50; int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs; RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async { Future queryBlacklist({type = 'init'}) async {
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) { if (result['status']) {
if (type == 'init') { if (type == 'init') {
blackList.value = result['data'].list; blackList.value = result['data'].list;
total.value = result['data'].total;
} else { } else {
blackList.addAll(result['data'].list); blackList.addAll(result['data'].list);
} }
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
} }
return result; return result;
} }
Future removeBlack(mid) async {
var result = await BlackHttp.removeBlack(fid: mid);
if (result['status']) {
blackList.removeWhere((e) => e.mid == mid);
total.value = total.value - 1;
SmartDialog.showToast(result['msg']);
}
}
} }

View File

@ -10,22 +10,34 @@ class PlDanmakuController {
// 按 6min 分段 // 按 6min 分段
int segCount = 0; int segCount = 0;
List<DmSegMobileReply> dmSegList = []; List<DmSegMobileReply> dmSegList = [];
int currentSegIndex = 0; // 已请求的段落标记
List<int> hasrequestSeg = [];
int currentSegIndex = 1;
int currentDmIndex = 0; int currentDmIndex = 0;
void calcSegment() { void calcSegment() {
dmSegList.clear();
// 视频分段数
segCount = (videoDuration.inSeconds / (60 * 6)).ceil(); segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
dmSegList = List<DmSegMobileReply>.generate(
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
// 当前分段
try {
currentSegIndex =
(playerController.position.value.inSeconds / (60 * 6)).ceil();
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
} catch (_) {}
} }
Future<List<DmSegMobileReply>> queryDanmaku() async { Future<List<DmSegMobileReply>> queryDanmaku() async {
dmSegList.clear(); // dmSegList.clear();
for (int segIndex = 1; segIndex <= segCount; segIndex++) { DmSegMobileReply result =
DmSegMobileReply result = await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex); if (result.elems.isNotEmpty) {
if (result.elems.isNotEmpty) { result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
result.elems.sort((a, b) => (a.progress).compareTo(b.progress)); // dmSegList.add(result);
dmSegList.add(result); currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
} dmSegList[currentSegIndex - 1] = result;
} }
if (dmSegList.isNotEmpty) { if (dmSegList.isNotEmpty) {
findClosestPositionIndex(playerController.position.value.inMilliseconds); findClosestPositionIndex(playerController.position.value.inMilliseconds);

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -29,6 +30,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
bool danmuPlayStatus = true; bool danmuPlayStatus = true;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
late bool enableShowDanmaku; late bool enableShowDanmaku;
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
@override @override
void initState() { void initState() {
@ -58,6 +64,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
} }
} }
}); });
blockTypes = playerController.blockTypes;
showArea = playerController.showArea;
opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal;
danmakuSpeedVal = playerController.danmakuSpeedVal;
} }
// 播放器状态监听 // 播放器状态监听
@ -75,12 +86,23 @@ class _PlDanmakuState extends State<PlDanmaku> {
_controller!.onResume(); _controller!.onResume();
danmuPlayStatus = true; danmuPlayStatus = true;
} }
PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds;
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
return; return;
} }
PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes;
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1)) {
ctr.hasrequestSeg.add(segIndex - 1);
ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
ctr.queryDanmaku();
});
}
// 超出分段数返回 // 超出分段数返回
if (ctr.currentSegIndex >= ctr.dmSegList.length) { if (ctr.currentSegIndex >= ctr.dmSegList.length) {
return; return;
@ -99,14 +121,17 @@ class _PlDanmakuState extends State<PlDanmaku> {
var delta = currentPosition - element.progress; var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) { if (delta >= 0 && delta < 200) {
_controller!.addItems([ // 屏蔽彩色弹幕
DanmakuItem( if (blockTypes.contains(6) ? element.color == 16777215 : true) {
element.content, _controller!.addItems([
color: DmUtils.decimalToColor(element.color), DanmakuItem(
time: element.progress, element.content,
type: DmUtils.getPosition(element.mode), color: DmUtils.decimalToColor(element.color),
) time: element.progress,
]); type: DmUtils.getPosition(element.mode),
)
]);
}
ctr.currentDmIndex++; ctr.currentDmIndex++;
} else { } else {
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
@ -126,22 +151,30 @@ class _PlDanmakuState extends State<PlDanmaku> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx( return LayoutBuilder(builder: (context, box) {
() => AnimatedOpacity( double initDuration = box.maxWidth / 12;
opacity: playerController.isOpenDanmu.value ? 1 : 0, return Obx(
duration: const Duration(milliseconds: 100), () => AnimatedOpacity(
child: DanmakuView( opacity: playerController.isOpenDanmu.value ? 1 : 0,
createdController: (DanmakuController e) async { duration: const Duration(milliseconds: 100),
widget.playerController.danmakuController = _controller = e; child: DanmakuView(
}, createdController: (DanmakuController e) async {
option: DanmakuOption( widget.playerController.danmakuController = _controller = e;
fontSize: 15, },
area: 0.5, option: DanmakuOption(
duration: 5, fontSize: 15 * fontSizeVal,
area: showArea,
opacity: opacityVal,
hideTop: blockTypes.contains(5),
hideScroll: blockTypes.contains(2),
hideBottom: blockTypes.contains(4),
duration: initDuration /
(danmakuSpeedVal * widget.playerController.playbackSpeed),
),
statusChanged: (isPlaying) {},
), ),
statusChanged: (isPlaying) {},
), ),
), );
); });
} }
} }

View File

@ -149,10 +149,30 @@ class DynamicsController extends GetxController {
case 'DYNAMIC_TYPE_ARTICLE': case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title; String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl; String url = item.modules.moduleDynamic.major.opus.jumpUrl;
Get.toNamed( if (url.contains('opus') || url.contains('read')) {
'/webview', RegExp digitRegExp = RegExp(r'\d+');
parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title}, Iterable<Match> matches = digitRegExp.allMatches(url);
); String number = matches.first.group(0)!;
if (url.contains('read')) {
number = 'cv$number';
}
Get.toNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
'title': title,
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
});
} else {
Get.toNamed(
'/webview',
parameters: {
'url': 'https:$url',
'type': 'note',
'pageTitle': title
},
);
}
break; break;
case 'DYNAMIC_TYPE_PGC': case 'DYNAMIC_TYPE_PGC':
print('番剧'); print('番剧');

View File

@ -1,3 +1,4 @@
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/http/reply.dart'; import 'package:pilipala/http/reply.dart';
@ -17,6 +18,7 @@ class DynamicDetailController extends GetxController {
RxString noMore = ''.obs; RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs; RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs; RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time; ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -9,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart'; import 'package:pilipala/pages/dynamics/deatil/index.dart';
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart'; import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../widgets/dynamic_panel.dart'; import '../widgets/dynamic_panel.dart';
@ -21,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
State<DynamicDetailPage> createState() => _DynamicDetailPageState(); State<DynamicDetailPage> createState() => _DynamicDetailPageState();
} }
class _DynamicDetailPageState extends State<DynamicDetailPage> { class _DynamicDetailPageState extends State<DynamicDetailPage>
late DynamicDetailController? _dynamicDetailController; with TickerProviderStateMixin {
late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title late StreamController<bool> titleStreamC; // appBar title
final ScrollController scrollController = ScrollController(); late ScrollController scrollController;
bool _visibleTitle = false; bool _visibleTitle = false;
String? action; String? action;
// 回复类型 // 回复类型
late int type; late int type;
bool _isFabVisible = true;
@override @override
void initState() { void initState() {
@ -38,39 +45,42 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
// floor 1原创 2转发 // floor 1原创 2转发
if (Get.arguments['floor'] == 1) { if (Get.arguments['floor'] == 1) {
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']); oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
print(oid);
} else { } else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id; try {
String type = Get.arguments['item'].modules.moduleDynamic.major.type;
/// TODO
if (type == 'MAJOR_TYPE_OPUS') {
} else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
}
} catch (_) {}
} }
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11; int commentType = 11;
try {
commentType = Get.arguments['item'].basic!['comment_type'];
} catch (_) {}
type = (commentType == 0) ? 11 : commentType; type = (commentType == 0) ? 11 : commentType;
action = action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null; Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = Get.put(DynamicDetailController(oid, type)); _dynamicDetailController =
_futureBuilderFuture = _dynamicDetailController!.queryReplyList(); Get.put(DynamicDetailController(oid, type), tag: oid.toString());
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
scrollController.addListener(_listen);
if (action == 'comment') { if (action == 'comment') {
_visibleTitle = true; _visibleTitle = true;
titleStreamC.add(true); titleStreamC.add(true);
} }
}
void _listen() async { fabAnimationCtr = AnimationController(
if (scrollController.position.pixels >= vsync: this,
scrollController.position.maxScrollExtent - 300) { duration: const Duration(milliseconds: 300),
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { );
_dynamicDetailController!.queryReplyList(reqType: 'onLoad'); fabAnimationCtr.forward();
}); // 滚动事件监听
} scrollListener();
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
} }
void replyReply(replyItem) { void replyReply(replyItem) {
@ -97,9 +107,58 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
); );
} }
void scrollListener() {
scrollController = _dynamicDetailController.scrollController;
scrollController.addListener(
() {
// 分页加载
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
});
}
// 标题
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
// fab按钮
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
_hideFab();
}
},
);
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
fabAnimationCtr.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
fabAnimationCtr.reverse();
}
}
@override @override
void dispose() { void dispose() {
scrollController.removeListener(() {}); scrollController.removeListener(() {});
fabAnimationCtr.dispose();
scrollController.dispose();
super.dispose(); super.dispose();
} }
@ -118,7 +177,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
return AnimatedOpacity( return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0, opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: author(_dynamicDetailController!.item, context), child: AuthorPanel(item: _dynamicDetailController.item),
); );
}, },
), ),
@ -126,155 +185,206 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await _dynamicDetailController!.queryReplyList(); await _dynamicDetailController.queryReplyList();
}, },
child: CustomScrollView( child: Stack(
controller: scrollController, children: [
slivers: [ CustomScrollView(
if (action != 'comment') controller: scrollController,
SliverToBoxAdapter( slivers: [
child: DynamicPanel( if (action != 'comment')
item: _dynamicDetailController!.item, SliverToBoxAdapter(
source: 'detail', child: DynamicPanel(
), item: _dynamicDetailController.item,
), source: 'detail',
SliverPersistentHeader(
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context).dividerColor.withOpacity(0.05),
),
), ),
), ),
height: 45, SliverPersistentHeader(
padding: const EdgeInsets.only(left: 12, right: 6), delegate: _MySliverPersistentHeaderDelegate(
child: Row( child: Container(
children: [ decoration: BoxDecoration(
Obx( color: Theme.of(context).colorScheme.surface,
() => AnimatedSwitcher( border: Border(
duration: const Duration(milliseconds: 400), top: BorderSide(
transitionBuilder: width: 0.6,
(Widget child, Animation<double> animation) { color: Theme.of(context)
return ScaleTransition( .dividerColor
scale: animation, child: child); .withOpacity(0.05),
},
child: Text(
'${_dynamicDetailController!.acount.value}',
key: ValueKey<int>(
_dynamicDetailController!.acount.value),
), ),
), ),
), ),
const Text('条回复'), height: 45,
const Spacer(), padding: const EdgeInsets.only(left: 12, right: 6),
SizedBox( child: Row(
height: 35, children: [
child: TextButton.icon( Obx(
onPressed: () => () => AnimatedSwitcher(
_dynamicDetailController!.queryBySort(), duration: const Duration(milliseconds: 400),
icon: const Icon(Icons.sort, size: 16), transitionBuilder:
label: Obx(() => Text( (Widget child, Animation<double> animation) {
_dynamicDetailController!.sortTypeLabel.value, return ScaleTransition(
style: const TextStyle(fontSize: 13), scale: animation, child: child);
)), },
), child: Text(
) '${_dynamicDetailController.acount.value}',
], key: ValueKey<int>(
), _dynamicDetailController.acount.value),
),
),
pinned: true,
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _dynamicDetailController!.replyList.isEmpty &&
_dynamicDetailController!.isLoadingMore
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index ==
_dynamicDetailController!
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_dynamicDetailController!
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _dynamicDetailController!
.replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController!
.replyList[index].replies!
.add(replyItem);
},
);
}
},
childCount:
_dynamicDetailController!.replyList.length +
1,
), ),
), ),
),
const Text('条回复'),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () =>
_dynamicDetailController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_dynamicDetailController
.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
),
)
],
),
),
),
pinned: true,
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _dynamicDetailController.replyList.isEmpty &&
_dynamicDetailController.isLoadingMore
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index ==
_dynamicDetailController
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_dynamicDetailController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _dynamicDetailController
.replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController
.replyList[index].replies!
.add(replyItem);
},
);
}
},
childCount: _dynamicDetailController
.replyList.length +
1,
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
],
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[type],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList
.add(value['data']),
_dynamicDetailController.acount.value++
}
},
); );
} else { },
// 请求错误 tooltip: '评论动态',
return HttpError( child: const Icon(Icons.reply),
errMsg: data['msg'], ),
fn: () => setState(() {}), ),
); ),
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
], ],
), ),
), ),

View File

@ -212,6 +212,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFutureUp, future: _futureBuilderFutureUp,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data; Map data = snapshot.data;
if (data['status']) { if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value)); return Obx(() => UpPanel(_dynamicsController.upData.value));
@ -232,6 +235,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data; Map data = snapshot.data;
if (data['status']) { if (data['status']) {
List<DynamicItemModel> list = List<DynamicItemModel> list =

View File

@ -1,5 +1,8 @@
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/http/search.dart';
/// TODO 点击跳转 /// TODO 点击跳转
Widget addWidget(item, context, type, {floor = 1}) { Widget addWidget(item, context, type, {floor = 1}) {
@ -19,8 +22,27 @@ Widget addWidget(item, context, type, {floor = 1}) {
: Theme.of(context).colorScheme.background; : Theme.of(context).colorScheme.background;
switch (type) { switch (type) {
case 'ADDITIONAL_TYPE_UGC': case 'ADDITIONAL_TYPE_UGC':
// 转发的投稿
return InkWell( return InkWell(
onTap: () {}, onTap: () async {
String text = dynamicProperty[type].jumpUrl;
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
Iterable<Match> matches = bvRegex.allMatches(text);
if (matches.isNotEmpty) {
Match match = matches.first;
String bvid = match.group(0)!;
String cover = dynamicProperty[type].cover;
try {
int cid = await SearchHttp.ab2c(bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'pic': cover, 'heroTag': bvid});
} catch (err) {
SmartDialog.showToast(err.toString());
}
} else {
print("No match found.");
}
},
child: Container( child: Container(
padding: padding:
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8), const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
@ -61,101 +83,111 @@ Widget addWidget(item, context, type, {floor = 1}) {
); );
case 'ADDITIONAL_TYPE_RESERVE': case 'ADDITIONAL_TYPE_RESERVE':
return dynamicProperty[type].state != -1 return dynamicProperty[type].state != -1
? Padding( ? dynamicProperty[type].title != null
padding: const EdgeInsets.only(top: 8), ? Padding(
child: InkWell( padding: const EdgeInsets.only(top: 8),
onTap: () {}, child: InkWell(
child: Container( onTap: () {},
width: double.infinity, child: Container(
padding: const EdgeInsets.only( width: double.infinity,
left: 12, top: 10, right: 12, bottom: 10), padding: const EdgeInsets.only(
color: bgColor, left: 12, top: 10, right: 12, bottom: 10),
child: Column( color: bgColor,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
dynamicProperty[type].title, Text(
maxLines: 1, dynamicProperty[type].title,
overflow: TextOverflow.ellipsis, maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
if (dynamicProperty[type].desc1 != null)
TextSpan(
text:
dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
if (dynamicProperty[type].desc2 != null)
TextSpan(
text:
dynamicProperty[type].desc2['text']),
],
),
)
],
), ),
const SizedBox(height: 1), // TextButton(onPressed: () {}, child: Text('123'))
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
],
),
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
)
: const SizedBox();
case 'ADDITIONAL_TYPE_GOODS':
return Padding(
padding: const EdgeInsets.only(top: 6),
child: InkWell(
onTap: () {},
child: Container(
padding:
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
decoration: BoxDecoration(
color: bgColor,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
child: Row(
children: [
NetworkImgLayer(
width: 75,
height: 75,
src: dynamicProperty[type].items.first.cover,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
dynamicProperty[type].items.first.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
dynamicProperty[type].items.first.brief,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
),
),
const SizedBox(height: 2),
Text(
dynamicProperty[type].items.first.price,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
), ),
), ),
], )
), : const SizedBox()
), : const SizedBox();
)); case 'ADDITIONAL_TYPE_GOODS':
// 商品
return const SizedBox();
// return Padding(
// padding: const EdgeInsets.only(top: 6),
// child: InkWell(
// onTap: () {},
// child: Container(
// padding:
// const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
// decoration: BoxDecoration(
// color: bgColor,
// borderRadius: const BorderRadius.all(Radius.circular(6)),
// ),
// child: Row(
// children: [
// NetworkImgLayer(
// width: 75,
// height: 75,
// src: dynamicProperty[type].items.first.cover,
// ),
// const SizedBox(width: 10),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Text(
// dynamicProperty[type].items.first.name,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// Text(
// dynamicProperty[type].items.first.brief,
// maxLines: 1,
// style: TextStyle(
// color: Theme.of(context).colorScheme.outline,
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize,
// ),
// ),
// const SizedBox(height: 2),
// Text(
// dynamicProperty[type].items.first.price,
// style: TextStyle(
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ),);
case 'ADDITIONAL_TYPE_MATCH': case 'ADDITIONAL_TYPE_MATCH':
return const SizedBox(); return const SizedBox();
case 'ADDITIONAL_TYPE_COMMON': case 'ADDITIONAL_TYPE_COMMON':

View File

@ -1,65 +1,163 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget author(item, context) { class AuthorPanel extends StatelessWidget {
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid); final dynamic item;
return Row( const AuthorPanel({super.key, required this.item});
children: [
GestureDetector( @override
onTap: () { Widget build(BuildContext context) {
feedBack(); String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
Get.toNamed( return Row(
'/member?mid=${item.modules.moduleAuthor.mid}', children: [
arguments: { GestureDetector(
'face': item.modules.moduleAuthor.face, onTap: () {
'heroTag': heroTag // 番剧
}, if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
); return;
}, }
child: Hero( feedBack();
tag: heroTag, Get.toNamed(
child: NetworkImgLayer( '/member?mid=${item.modules.moduleAuthor.mid}',
width: 40, arguments: {
height: 40, 'face': item.modules.moduleAuthor.face,
type: 'avatar', 'heroTag': heroTag
src: item.modules.moduleAuthor.face, },
);
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: item.modules.moduleAuthor.face,
),
), ),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( item.modules.moduleAuthor.name,
item.modules.moduleAuthor.name, style: TextStyle(
style: TextStyle( color: item.modules.moduleAuthor!.vip != null &&
color: item.modules.moduleAuthor!.vip != null && item.modules.moduleAuthor!.vip['status'] > 0
item.modules.moduleAuthor!.vip['status'] > 0 ? const Color.fromARGB(255, 251, 100, 163)
? const Color.fromARGB(255, 251, 100, 163) : Theme.of(context).colorScheme.onBackground,
: Theme.of(context).colorScheme.onBackground, fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, ),
),
DefaultTextStyle.merge(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
),
child: Row(
children: [
Text(item.modules.moduleAuthor.pubTime),
if (item.modules.moduleAuthor.pubTime != '' &&
item.modules.moduleAuthor.pubAction != '')
const Text(' '),
Text(item.modules.moduleAuthor.pubAction),
],
),
)
],
),
const Spacer(),
if (item.type == 'DYNAMIC_TYPE_AV')
SizedBox(
width: 32,
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: item);
},
);
},
icon: const Icon(Icons.more_vert_outlined, size: 18),
), ),
), ),
DefaultTextStyle.merge( ],
style: TextStyle( );
color: Theme.of(context).colorScheme.outline, }
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, }
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
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))),
),
),
), ),
child: Row( ),
children: [ ListTile(
Text(item.modules.moduleAuthor.pubTime), onTap: () async {
if (item.modules.moduleAuthor.pubTime != '' && try {
item.modules.moduleAuthor.pubAction != '') String bvid = item.modules.moduleDynamic.major.archive.bvid;
const Text(' '), var res = await UserHttp.toViewLater(bvid: bvid);
Text(item.modules.moduleAuthor.pubAction), SmartDialog.showToast(res['msg']);
], Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
), ),
) ),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
], ],
), ),
], );
); }
} }

View File

@ -1,40 +1,183 @@
// 内容 // 内容
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'rich_node_panel.dart'; import 'rich_node_panel.dart';
Widget content(item, context, source) { // ignore: must_be_immutable
TextStyle authorStyle = class Content extends StatefulWidget {
TextStyle(color: Theme.of(context).colorScheme.primary); dynamic item;
return Container( String? source;
width: double.infinity, Content({
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), super.key,
child: Column( this.item,
crossAxisAlignment: CrossAxisAlignment.start, this.source,
children: [ });
if (item.modules.moduleDynamic.topic != null) ...[
GestureDetector( @override
child: Text( State<Content> createState() => _ContentState();
'#${item.modules.moduleDynamic.topic.name}', }
style: authorStyle,
), class _ContentState extends State<Content> {
), late bool hasPics;
], List<OpusPicsModel> pics = [];
IgnorePointer(
// 禁用SelectableRegion的触摸交互功能 @override
ignoring: source == 'detail' ? false : true, void initState() {
child: SelectableRegion( super.initState();
magnifierConfiguration: const TextMagnifierConfiguration(), hasPics = widget.item.modules.moduleDynamic.major != null &&
focusNode: FocusNode(), widget.item.modules.moduleDynamic.major.opus != null &&
selectionControls: MaterialTextSelectionControls(), widget.item.modules.moduleDynamic.major.opus.pics.isNotEmpty;
child: Text.rich( if (hasPics) {
richNode(item, context), pics = widget.item.modules.moduleDynamic.major.opus.pics;
maxLines: source == 'detail' ? 999 : 3, }
overflow: TextOverflow.ellipsis, }
),
InlineSpan picsNodes() {
List<InlineSpan> spanChilds = [];
int len = pics.length;
List<String> picList = [];
if (len == 1) {
OpusPicsModel pictureItem = pics.first;
picList.add(pictureItem.url!);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: NetworkImgLayer(
src: pictureItem.url,
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1),
),
),
);
},
), ),
), ),
], );
), }
); if (len > 1) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pics[i].url!);
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: pics[i].url,
width: box.maxWidth,
height: box.maxWidth,
),
);
},
),
);
}
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth;
double crossCount = len < 3 ? 2 : 3;
double height = maxWidth /
crossCount *
(len % crossCount == 0
? len ~/ crossCount
: len ~/ crossCount + 1) +
6;
return Container(
padding: const EdgeInsets.only(top: 6),
height: height,
child: GridView.count(
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossCount.toInt(),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
childAspectRatio: 1,
children: list,
),
);
},
),
),
);
}
return TextSpan(
children: spanChilds,
);
}
@override
Widget build(BuildContext context) {
TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary);
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.item.modules.moduleDynamic.topic != null) ...[
GestureDetector(
child: Text(
'#${widget.item.modules.moduleDynamic.topic.name}',
style: authorStyle,
),
),
],
IgnorePointer(
// 禁用SelectableRegion的触摸交互功能
ignoring: widget.source == 'detail' ? false : true,
child: SelectableRegion(
magnifierConfiguration: const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
/// fix 默认20px高度
style: const TextStyle(height: 0),
richNode(widget.item, context),
maxLines: widget.source == 'detail' ? 999 : 3,
overflow: TextOverflow.ellipsis,
),
),
),
if (hasPics) ...[
Text.rich(picsNodes()),
]
],
),
);
}
} }

View File

@ -39,10 +39,11 @@ class DynamicPanel extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: author(item, context), child: AuthorPanel(item: item),
), ),
if (item!.modules!.moduleDynamic!.desc != null) if (item!.modules!.moduleDynamic!.desc != null ||
content(item, context, source), item!.modules!.moduleDynamic!.major != null)
Content(item: item, source: source),
forWard(item, context, _dynamicsController, source), forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2), const SizedBox(height: 2),
if (source == null) ActionPanel(item: item), if (source == null) ActionPanel(item: item),

View File

@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
if (item.modules.moduleDynamic.topic != null) ...[
Padding( /// fix #话题跟content重复
padding: floor == 2 // if (item.modules.moduleDynamic.topic != null) ...[
? EdgeInsets.zero // Padding(
: const EdgeInsets.only(left: 12, right: 12), // padding: floor == 2
child: GestureDetector( // ? EdgeInsets.zero
child: Text( // : const EdgeInsets.only(left: 12, right: 12),
'#${item.modules.moduleDynamic.topic.name}', // child: GestureDetector(
style: authorStyle, // child: Text(
), // '#${item.modules.moduleDynamic.topic.name}',
), // style: authorStyle,
), // ),
], // ),
// ),
// ],
Text.rich( Text.rich(
richNode(item, context), richNode(item, context),
// 被转发状态(floor=2) 隐藏 // 被转发状态(floor=2) 隐藏
@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
: const EdgeInsets.only(left: 12, right: 12), : const EdgeInsets.only(left: 12, right: 12),
child: picWidget(item, context), child: picWidget(item, context),
), ),
/// 附加内容 商品信息、直播预约等等
if (item.modules.moduleDynamic.additional != null) if (item.modules.moduleDynamic.additional != null)
addWidget( addWidget(
item, item,
@ -133,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(item.modules.moduleDynamic.desc.text) Text.rich(
richNode(item, context),
// 被转发状态(floor=2) 隐藏
maxLines: source == 'detail' && floor != 2 ? 999 : 4,
overflow: TextOverflow.ellipsis,
),
], ],
) )
: item.modules.moduleDynamic.additional != null : item.modules.moduleDynamic.additional != null

View File

@ -1,20 +1,22 @@
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';
import 'package:pilipala/pages/preview/index.dart';
Widget picWidget(item, context) { Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type; String type = item.modules.moduleDynamic.major.type;
List pictures = []; List pictures = [];
if (type == 'MAJOR_TYPE_OPUS') { if (type == 'MAJOR_TYPE_OPUS') {
pictures = item.modules.moduleDynamic.major.opus.pics; /// fix 图片跟rich_node_panel重复
// pictures = item.modules.moduleDynamic.major.opus.pics;
return const SizedBox();
} }
if (type == 'MAJOR_TYPE_DRAW') { if (type == 'MAJOR_TYPE_DRAW') {
pictures = item.modules.moduleDynamic.major.draw.items; pictures = item.modules.moduleDynamic.major.draw.items;
} }
int len = pictures.length; int len = pictures.length;
List picList = []; List<String> picList = [];
List<Widget> list = []; List<Widget> list = [];
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url); picList.add(pictures[i].src ?? pictures[i].url);
@ -23,11 +25,14 @@ Widget picWidget(item, context) {
builder: (context, BoxConstraints box) { builder: (context, BoxConstraints box) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed('/preview', showDialog(
arguments: {'initialPage': i, 'imgList': picList}); useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
}, },
// child: Hero(
// tag: pictures[i].src ?? pictures[i].url,
child: NetworkImgLayer( child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url, src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth, width: box.maxWidth,

View File

@ -1,175 +1,324 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
// 富文本 // 富文本
InlineSpan richNode(item, context) { InlineSpan richNode(item, context) {
TextStyle authorStyle = final spacer = _VerticalSpaceSpan(0.0);
TextStyle(color: Theme.of(context).colorScheme.primary); try {
List<InlineSpan> spanChilds = []; TextStyle authorStyle =
for (var i in item.modules.moduleDynamic.desc.richTextNodes) { TextStyle(color: Theme.of(context).colorScheme.primary);
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') { List<InlineSpan> spanChilds = [];
spanChilds.add( String contentType = 'desc';
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
dynamic richTextNodes;
if (item.modules.moduleDynamic.desc != null) {
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
} else if (item.modules.moduleDynamic.major != null) {
contentType = 'major';
// 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes =
item.modules.moduleDynamic.major.opus.summary.richTextNodes;
} }
// @用户 if (richTextNodes == null || richTextNodes.isEmpty) {
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') { return spacer;
spanChilds.add( } else {
WidgetSpan( for (var i in richTextNodes) {
alignment: PlaceholderAlignment.middle, /// fix 渲染专栏时内容会重复
child: Row( // if (item.modules.moduleDynamic.major.opus.title == null &&
mainAxisSize: MainAxisSize.min, // i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
children: [ if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
GestureDetector( spanChilds.add(
onTap: () => Get.toNamed('/member?mid=${i.rid}', TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
arguments: {'face': null}), }
// @用户
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () => Get.toNamed('/member?mid=${i.rid}',
arguments: {'face': null}),
child: Text(
' ${i.text}',
style: authorStyle,
),
),
],
),
),
);
}
// 话题
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text( child: Text(
' ${i.text}', '${i.origText}',
style: authorStyle, style: authorStyle,
), ),
), ),
],
),
),
);
}
// 话题
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.origText}',
style: authorStyle,
), ),
), );
), }
); // 网页链接
} if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
// 网页链接 spanChilds.add(
if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') { WidgetSpan(
spanChilds.add( alignment: PlaceholderAlignment.middle,
WidgetSpan( child: Icon(
alignment: PlaceholderAlignment.middle, Icons.link,
child: Icon( size: 20,
Icons.link, color: Theme.of(context).colorScheme.primary,
size: 20, ),
color: Theme.of(context).colorScheme.primary,
),
),
);
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
Get.toNamed(
'/webview',
parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''},
);
},
child: Text(
i.text,
style: authorStyle,
), ),
), );
), spanChilds.add(
); WidgetSpan(
} alignment: PlaceholderAlignment.middle,
// 投票 child: GestureDetector(
if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') { onTap: () {
spanChilds.add( Get.toNamed(
WidgetSpan( '/webview',
alignment: PlaceholderAlignment.middle, parameters: {
child: GestureDetector( 'url': i.origText,
onTap: () { 'type': 'url',
String dynamicId = item.basic['comment_id_str']; 'pageTitle': ''
Get.toNamed( },
'/webview', );
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
'type': 'vote',
'pageTitle': '投票'
}, },
); child: Text(
}, i.text,
child: Text( style: authorStyle,
'投票:${i.text}', ),
style: authorStyle, ),
), ),
), );
), }
); // 投票
} if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
// 表情 spanChilds.add(
if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') { WidgetSpan(
spanChilds.add( alignment: PlaceholderAlignment.middle,
WidgetSpan( child: GestureDetector(
child: NetworkImgLayer( onTap: () {
src: i.emoji.iconUrl, try {
type: 'emote', String dynamicId = item.basic['comment_id_str'];
width: i.emoji.size * 20, Get.toNamed(
height: i.emoji.size * 20, '/webview',
), parameters: {
), 'url':
); 'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
} 'type': 'vote',
// 抽奖 'pageTitle': '投票'
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') { },
spanChilds.add( );
WidgetSpan( } catch (_) {}
alignment: PlaceholderAlignment.middle, },
child: Icon( child: Text(
Icons.redeem_rounded, '投票:${i.text}',
size: 16, style: authorStyle,
color: Theme.of(context).colorScheme.primary, ),
), ),
),
);
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.origText} ',
style: authorStyle,
), ),
), );
), }
); // 表情
} if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
spanChilds.add(
WidgetSpan(
child: NetworkImgLayer(
src: i.emoji.iconUrl,
type: 'emote',
width: i.emoji.size * 20,
height: i.emoji.size * 20,
),
),
);
}
// 抽奖
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.redeem_rounded,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
),
);
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.origText} ',
style: authorStyle,
),
),
),
);
}
/// TODO 商品 /// TODO 商品
if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') { if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
child: Icon( child: Icon(
Icons.shopping_bag_outlined, Icons.shopping_bag_outlined,
size: 16, size: 16,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
),
);
spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.text} ',
style: authorStyle,
), ),
), );
), spanChilds.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.text} ',
style: authorStyle,
),
),
),
);
}
}
// if (contentType == 'major' &&
// item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {
// // 图片可能跟其他widget重复渲染
// List<OpusPicsModel> pics = item.modules.moduleDynamic.major.opus.pics;
// int len = pics.length;
// List<String> picList = [];
// if (len == 1) {
// OpusPicsModel pictureItem = pics.first;
// picList.add(pictureItem.url!);
// spanChilds.add(const TextSpan(text: '\n'));
// spanChilds.add(
// WidgetSpan(
// child: LayoutBuilder(
// builder: (context, BoxConstraints box) {
// return GestureDetector(
// onTap: () {
// showDialog(
// useSafeArea: false,
// context: context,
// builder: (context) {
// return ImagePreview(initialPage: 0, imgList: picList);
// },
// );
// },
// child: Padding(
// padding: const EdgeInsets.only(top: 4),
// child: NetworkImgLayer(
// src: pictureItem.url,
// width: box.maxWidth / 2,
// height: box.maxWidth *
// 0.5 *
// (pictureItem.height != null &&
// pictureItem.width != null
// ? pictureItem.height! / pictureItem.width!
// : 1),
// ),
// ),
// );
// },
// ),
// ),
// );
// }
// if (len > 1) {
// List<Widget> list = [];
// for (var i = 0; i < len; i++) {
// picList.add(pics[i].url!);
// list.add(
// LayoutBuilder(
// builder: (context, BoxConstraints box) {
// return GestureDetector(
// onTap: () {
// showDialog(
// useSafeArea: false,
// context: context,
// builder: (context) {
// return ImagePreview(initialPage: i, imgList: picList);
// },
// );
// },
// child: NetworkImgLayer(
// src: pics[i].url,
// width: box.maxWidth,
// height: box.maxWidth,
// ),
// );
// },
// ),
// );
// }
// spanChilds.add(
// WidgetSpan(
// child: LayoutBuilder(
// builder: (context, BoxConstraints box) {
// double maxWidth = box.maxWidth;
// double crossCount = len < 3 ? 2 : 3;
// double height = maxWidth /
// crossCount *
// (len % crossCount == 0
// ? len ~/ crossCount
// : len ~/ crossCount + 1) +
// 6;
// return Container(
// padding: const EdgeInsets.only(top: 6),
// height: height,
// child: GridView.count(
// padding: EdgeInsets.zero,
// physics: const NeverScrollableScrollPhysics(),
// crossAxisCount: crossCount.toInt(),
// mainAxisSpacing: 4.0,
// crossAxisSpacing: 4.0,
// childAspectRatio: 1,
// children: list,
// ),
// );
// },
// ),
// ),
// );
// }
// spanChilds.add(
// WidgetSpan(
// child: NetworkImgLayer(
// src: pics.first.url,
// type: 'emote',
// width: 100,
// height: 200,
// ),
// ),
// );
// }
return TextSpan(
children: spanChilds,
); );
} }
} catch (err) {
print('❌rich_node_panel err: $err');
return spacer;
} }
return TextSpan( }
children: spanChilds,
); class _VerticalSpaceSpan extends WidgetSpan {
_VerticalSpaceSpan(double height)
: super(child: SizedBox(height: height, width: double.infinity));
} }

View File

@ -91,7 +91,10 @@ class _UpPanelState extends State<UpPanel> {
), ),
Material( Material(
child: InkWell( child: InkWell(
onTap: () => {feedBack(), Get.toNamed('/follow')}, onTap: () => {
feedBack(),
Get.toNamed('/follow?mid=${userInfo.mid}')
},
child: Container( child: Container(
height: 100, height: 100,
padding: const EdgeInsets.only(left: 10, right: 10), padding: const EdgeInsets.only(left: 10, right: 10),

View File

@ -57,20 +57,21 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
const SizedBox(height: 6), const SizedBox(height: 6),
], ],
// const SizedBox(height: 4), // const SizedBox(height: 4),
if (item.modules.moduleDynamic.topic != null) ...[ /// fix #话题跟content重复
Padding( // if (item.modules.moduleDynamic.topic != null) ...[
padding: floor == 2 // Padding(
? EdgeInsets.zero // padding: floor == 2
: const EdgeInsets.only(left: 12, right: 12), // ? EdgeInsets.zero
child: GestureDetector( // : const EdgeInsets.only(left: 12, right: 12),
child: Text( // child: GestureDetector(
'#${item.modules.moduleDynamic.topic.name}', // child: Text(
style: authorStyle, // '#${item.modules.moduleDynamic.topic.name}',
), // style: authorStyle,
), // ),
), // ),
const SizedBox(height: 6), // ),
], // const SizedBox(height: 6),
// ],
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[ if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
Text.rich(richNode(item, context)), Text.rich(richNode(item, context)),
const SizedBox(height: 6), const SizedBox(height: 6),

View File

@ -16,13 +16,16 @@ class FansPage extends StatefulWidget {
} }
class _FansPageState extends State<FansPage> { class _FansPageState extends State<FansPage> {
final FansController _fansController = Get.put(FansController()); late String mid;
late FansController _fansController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mid = Get.parameters['mid']!;
_fansController = Get.put(FansController(), tag: mid);
_futureBuilderFuture = _fansController.queryFans('init'); _futureBuilderFuture = _fansController.queryFans('init');
scrollController.addListener( scrollController.addListener(
() async { () async {

View File

@ -44,6 +44,14 @@ class _FavPageState extends State<FavPage> {
'我的收藏', '我的收藏',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
icon: const Icon(Icons.search_outlined),
),
const SizedBox(width: 6),
],
), ),
body: FutureBuilder( body: FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,

View File

@ -14,7 +14,7 @@ class FavDetailController extends GetxController {
int currentPage = 1; int currentPage = 1;
bool isLoadingMore = false; bool isLoadingMore = false;
RxMap favInfo = {}.obs; RxMap favInfo = {}.obs;
RxList<FavDetailItemData> favList = [FavDetailItemData()].obs; RxList favList = [].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
int mediaCount = 0; int mediaCount = 0;
@ -61,15 +61,13 @@ class FavDetailController extends GetxController {
aid: id, addIds: '', delIds: mediaId.toString()); aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) { if (result['status']) {
if (result['data']['prompt']) { if (result['data']['prompt']) {
List<FavDetailItemData> dataList = favDetailData.value.medias!; List dataList = favList;
for (var i in dataList) { for (var i in dataList) {
if (i.id == id) { if (i.id == id) {
dataList.remove(i); dataList.remove(i);
break; break;
} }
} }
favDetailData.value.medias = dataList;
favDetailData.refresh();
SmartDialog.showToast('取消收藏'); SmartDialog.showToast('取消收藏');
} }
} }

View File

@ -92,13 +92,18 @@ class _FavDetailPageState extends State<FavDetailPage> {
); );
}, },
), ),
// actions: [ actions: [
// IconButton( IconButton(
// onPressed: () {}, onPressed: () => Get.toNamed(
// icon: const Icon(Icons.more_vert), '/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'),
// ), icon: const Icon(Icons.search_outlined),
// const SizedBox(width: 4) ),
// ], // IconButton(
// onPressed: () {},
// icon: const Icon(Icons.more_vert),
// ),
const SizedBox(width: 6),
],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Container( background: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -168,7 +173,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Obx(
() => Text( () => Text(
'${_favDetailController.favInfo['media_count'] ?? '-'}条视频', '${_favDetailController.favList.length}条视频',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
@ -187,14 +192,20 @@ class _FavDetailPageState extends State<FavDetailPage> {
if (_favDetailController.item!.mediaCount == 0) { if (_favDetailController.item!.mediaCount == 0) {
return const NoData(); return const NoData();
} else { } else {
List favList = _favDetailController.favList;
return Obx( return Obx(
() => SliverList( () => favList.isEmpty
delegate: SliverChildBuilderDelegate((context, index) { ? const SliverToBoxAdapter(child: SizedBox())
return FavVideoCardH( : SliverList(
videoItem: _favDetailController.favList[index], delegate:
); SliverChildBuilderDelegate((context, index) {
}, childCount: _favDetailController.favList.length), return FavVideoCardH(
), videoItem: favList[index],
callFn: () => _favDetailController
.onCancelFav(favList[index].id),
);
}, childCount: favList.length),
),
); );
} }
} else { } else {

View File

@ -10,134 +10,109 @@ 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 '../controller.dart';
// 收藏视频卡片 - 水平布局 // 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget { class FavVideoCardH extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final FavDetailController _favDetailController = final Function? callFn;
Get.put(FavDetailController());
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key); const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int id = videoItem.id; int id = videoItem.id;
String bvid = videoItem.bvid ?? IdUtils.av2bv(id); String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
String heroTag = Utils.makeHeroTag(id); String heroTag = Utils.makeHeroTag(id);
return Dismissible( return InkWell(
movementDuration: const Duration(milliseconds: 300), onTap: () async {
background: Container( // int? seasonId;
decoration: BoxDecoration( String? epId;
color: Theme.of(context).colorScheme.errorContainer, if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
), videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
child: const Row( // seasonId = videoItem.ogv['season_id'];
mainAxisAlignment: MainAxisAlignment.center, epId = videoItem.epId;
children: [ } else if (videoItem.page == 0 || videoItem.page > 1) {
Icon(Icons.clear_all_rounded), var result = await VideoHttp.videoIntro(bvid: bvid);
SizedBox(width: 6), if (result['status']) {
Text('取消收藏') epId = result['data'].epId;
],
)),
direction: DismissDirection.endToStart,
key: ValueKey<int>(videoItem.id),
onDismissed: (DismissDirection direction) {
_favDetailController.onCancelFav(videoItem.id);
// widget.onDeleteNotice();
},
child: InkWell(
onTap: () async {
// int? seasonId;
String? epId;
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
// seasonId = videoItem.ogv['season_id'];
epId = videoItem.epId;
} else if (videoItem.page == 0 || videoItem.page > 1) {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
epId = result['data'].epId;
}
} }
}
Map<String, String> parameters = { Map<String, String> parameters = {
'bvid': bvid, 'bvid': bvid,
'cid': videoItem.cid.toString(), 'cid': videoItem.cid.toString(),
'epId': epId ?? '', 'epId': epId ?? '',
}; };
// if (seasonId != null) { // if (seasonId != null) {
// parameters['seasonId'] = seasonId.toString(); // parameters['seasonId'] = seasonId.toString();
// } // }
Get.toNamed('/video', parameters: parameters, arguments: { Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem, 'videoItem': videoItem,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': 'videoType':
epId != null ? SearchType.media_bangumi : SearchType.video, epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5), StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
double width = double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox( return SizedBox(
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: StyleString.aspectRatio, aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth; double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight; double maxHeight = boxConstraints.maxHeight;
return Stack( return Stack(
children: [ children: [
Hero( Hero(
tag: heroTag, tag: heroTag,
child: NetworkImgLayer( child: NetworkImgLayer(
src: videoItem.pic, src: videoItem.pic,
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
),
),
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
style: const TextStyle(
fontSize: 11, color: Colors.white),
), ),
), ),
Positioned( )
right: 4, ],
bottom: 4, );
child: Container( },
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(4),
color:
Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
style: const TextStyle(
fontSize: 11, color: Colors.white),
),
),
)
],
);
},
),
), ),
VideoContent(videoItem: videoItem) ),
], VideoContent(videoItem: videoItem, callFn: callFn)
), ],
); ),
}, );
), },
), ),
], ),
), ],
), ),
); );
} }
@ -145,7 +120,8 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const VideoContent({super.key, required this.videoItem}); final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -173,7 +149,6 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
const SizedBox(height: 2),
Row( Row(
children: [ children: [
StatView( StatView(
@ -181,7 +156,51 @@ class VideoContent extends StatelessWidget {
view: videoItem.cntInfo['play'], view: videoItem.cntInfo['play'],
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']) StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
SizedBox(
width: 26,
height: 26,
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,
),
),
),
], ],
), ),
], ],

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_detail.dart';
class FavSearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs; // 搜索词
String hintText = '请输入已收藏视频名称'; // 默认
RxBool loadingStatus = false.obs; // 加载状态
RxString loadingText = '加载中...'.obs; // 加载提示
bool hasMore = false;
late int searchType;
late int mediaId;
int currentPage = 1; // 当前页
int count = 0; // 总数
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
@override
void onInit() {
super.onInit();
searchType = int.parse(Get.parameters['searchType']!);
mediaId = int.parse(Get.parameters['mediaId']!);
}
// 清空搜索
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 = true;
currentPage = 1;
searchFav();
}
// 搜索收藏夹视频
Future searchFav({type = 'init'}) async {
var res = await await UserHttp.userFavFolderDetail(
pn: currentPage,
ps: 20,
mediaId: mediaId,
keyword: searchKeyWord.value,
type: searchType,
);
if (res['status']) {
if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias;
} else if (type == 'onLoad') {
favList.addAll(res['data'].medias);
}
hasMore = res['data'].hasMore;
}
currentPage += 1;
loadingStatus.value = false;
}
onLoad() {
if (!hasMore) return;
searchFav(type: 'onLoad');
}
}

View File

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

View File

@ -0,0 +1,116 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/favDetail/widget/fav_video_card.dart';
import 'controller.dart';
class FavSearchPage extends StatefulWidget {
final int? sourceType;
final int? mediaId;
const FavSearchPage({super.key, this.sourceType, this.mediaId});
@override
State<FavSearchPage> createState() => _FavSearchPageState();
}
class _FavSearchPageState extends State<FavSearchPage> {
final FavSearchController _favSearchCtr = Get.put(FavSearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _favSearchCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('fav', const Duration(seconds: 1), () {
_favSearchCtr.onLoad();
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _favSearchCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _favSearchCtr.searchFocusNode,
controller: _favSearchCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _favSearchCtr.onChange(value),
decoration: InputDecoration(
hintText: _favSearchCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _favSearchCtr.onClear(),
),
),
onSubmitted: (String value) => _favSearchCtr.submit(),
),
),
),
body: Obx(
() => _favSearchCtr.loadingStatus.value && _favSearchCtr.favList.isEmpty
? ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
)
: _favSearchCtr.favList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: _favSearchCtr.favList.length + 1,
itemBuilder: (context, index) {
if (index == _favSearchCtr.favList.length) {
return Container(
height: MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
);
} else {
return FavVideoCardH(
videoItem: _favSearchCtr.favList[index],
callFn: () => null,
);
}
},
)
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
),
);
}
}

View File

@ -1,20 +1,28 @@
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.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/follow.dart'; import 'package:pilipala/http/follow.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class FollowController extends GetxController { /// 查看自己的关注时,可以查看分类
/// 查看其他人的关注时,只可以看全部
class FollowController extends GetxController with GetTickerProviderStateMixin {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
int pn = 1; int pn = 1;
int ps = 20; int ps = 20;
int total = 0; int total = 0;
RxList<FollowItemModel> followList = [FollowItemModel()].obs; RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
late int mid; late int mid;
late String name; late String name;
var userInfo; var userInfo;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxBool isOwner = false.obs;
late List<MemberTagItemModel> followTags;
late TabController tabController;
@override @override
void onInit() { void onInit() {
@ -23,6 +31,7 @@ class FollowController extends GetxController {
mid = Get.parameters['mid'] != null mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!) ? int.parse(Get.parameters['mid']!)
: userInfo.mid; : userInfo.mid;
isOwner.value = mid == userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname; name = Get.parameters['name'] ?? userInfo.uname;
} }
@ -56,4 +65,20 @@ class FollowController extends GetxController {
} }
return res; return res;
} }
// 当查看当前用户的关注时,请求关注分组
Future followUpTags() async {
if (userInfo != null && mid == userInfo.mid) {
var res = await MemberHttp.followUpTags();
if (res['status']) {
followTags = res['data'];
tabController = TabController(
initialIndex: 0,
length: res['data'].length,
vsync: this,
);
}
return res;
}
}
} }

View File

@ -1,12 +1,8 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/follow/result.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/follow_item.dart'; import 'widgets/follow_list.dart';
import 'widgets/owner_follow_list.dart';
class FollowPage extends StatefulWidget { class FollowPage extends StatefulWidget {
const FollowPage({super.key}); const FollowPage({super.key});
@ -16,30 +12,15 @@ class FollowPage extends StatefulWidget {
} }
class _FollowPageState extends State<FollowPage> { class _FollowPageState extends State<FollowPage> {
final FollowController _followController = Get.put(FollowController()); late String mid;
late FollowController _followController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _followController.queryFollowings('init'); mid = Get.parameters['mid']!;
scrollController.addListener( _followController = Get.put(FollowController(), tag: mid);
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_followController.queryFollowings('onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
} }
@override @override
@ -51,73 +32,57 @@ class _FollowPageState extends State<FollowPage> {
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
title: Text( title: Text(
'${_followController.name}的关注', _followController.isOwner.value
? '我的关注'
: '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
body: RefreshIndicator( body: Obx(
onRefresh: () async => () => !_followController.isOwner.value
await _followController.queryFollowings('init'), ? FollowList(ctr: _followController)
child: FutureBuilder( : FutureBuilder(
future: _futureBuilderFuture, future: _followController.followUpTags(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data; var data = snapshot.data;
if (data['status']) { if (data['status']) {
List<FollowItemModel> list = _followController.followList; return Column(
return Obx( children: [
() => list.isNotEmpty TabBar(
? ListView.builder( controller: _followController.tabController,
controller: scrollController, isScrollable: true,
itemCount: list.length + 1, tabs: [
itemBuilder: (BuildContext context, int index) { for (var i in data['data']) ...[
if (index == list.length) { Tab(text: i.name),
return Container( ]
height: ]),
MediaQuery.of(context).padding.bottom + Expanded(
60, child: TabBarView(
padding: EdgeInsets.only( controller: _followController.tabController,
bottom: MediaQuery.of(context) children: [
.padding for (var i = 0;
.bottom), i < _followController.tabController.length;
child: Center( i++) ...[
child: Obx( OwnerFollowList(
() => Text( ctr: _followController,
_followController.loadingText.value, tagItem: _followController.followTags[i],
style: TextStyle( )
color: Theme.of(context) ]
.colorScheme ],
.outline, ),
fontSize: 13),
),
),
),
);
} else {
return followItem(item: list[index]);
}
},
)
: const CustomScrollView(
slivers: [NoData()],
), ),
); ],
} else { );
return CustomScrollView( } else {
slivers: [ return const SizedBox();
HttpError( }
errMsg: data['msg'], } else {
fn: () => _followController.queryFollowings('init'), return const SizedBox();
) }
], },
); ),
} ),
} else {
// 骨架屏
return const SizedBox();
}
},
)),
); );
} }
} }

View File

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

View File

@ -0,0 +1,114 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'follow_item.dart';
class FollowList extends StatefulWidget {
final FollowController ctr;
const FollowList({
super.key,
required this.ctr,
});
@override
State<FollowList> createState() => _FollowListState();
}
class _FollowListState extends State<FollowList> {
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = widget.ctr.queryFollowings('init');
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
widget.ctr.queryFollowings('onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async => await widget.ctr.queryFollowings('init'),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
List<FollowItemModel> list = widget.ctr.followList;
return Obx(
() => list.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: list.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return Container(
height:
MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom),
child: Center(
child: Obx(
() => Text(
widget.ctr.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return FollowItem(
item: list[index],
ctr: widget.ctr,
);
}
},
)
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
);
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
);
}
}

View File

@ -0,0 +1,134 @@
import 'dart:math';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'follow_item.dart';
class OwnerFollowList extends StatefulWidget {
final FollowController ctr;
final MemberTagItemModel? tagItem;
const OwnerFollowList({super.key, required this.ctr, this.tagItem});
@override
State<OwnerFollowList> createState() => _OwnerFollowListState();
}
class _OwnerFollowListState extends State<OwnerFollowList>
with AutomaticKeepAliveClientMixin {
late int mid;
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
int pn = 1;
int ps = 20;
late MemberTagItemModel tagItem;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
mid = widget.ctr.mid;
tagItem = widget.tagItem!;
_futureBuilderFuture = followUpGroup('init');
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
followUpGroup('onLoad');
});
}
},
);
}
// 获取分组下up
Future followUpGroup(type) async {
if (type == 'init') {
pn = 1;
}
var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps);
if (res['status']) {
if (res['data'].isNotEmpty) {
if (type == 'init') {
followList.value = res['data'];
} else {
followList.addAll(res['data']);
}
pn += 1;
}
}
return res;
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async => await followUpGroup('init'),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
return Obx(
() => followList.isNotEmpty
? ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController,
itemCount: followList.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == followList.length) {
return Container(
height:
MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom),
);
} else {
return FollowItem(
item: followList[index],
ctr: widget.ctr,
);
}
},
)
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
);
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
);
}
}

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs; RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs; RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override @override
void onInit() { void onInit() {
@ -121,4 +123,80 @@ class HistoryController extends GetxController {
}, },
); );
} }
// 删除某条历史记录
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
}
// 删除已看历史记录
Future onDelHistory() async {
/// TODO 优化
List<HisListItem> result =
historyList.where((e) => e.progress == -1).toList();
for (HisListItem i in result) {
String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
SmartDialog.showToast('操作完成');
}
// 删除选中的记录
Future onDelCheckedHistory() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选历史记录吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
/// TODO 优化
await SmartDialog.dismiss();
SmartDialog.showLoading(msg: '请求中');
List<HisListItem> result =
historyList.where((e) => e.checked!).toList();
for (HisListItem i in result) {
String str = 'archive';
try {
str = i.history!.business!;
} catch (_) {}
String resKid = '${str}_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
checkedCount.value = 0;
SmartDialog.dismiss();
enableMultiple.value = false;
},
child: const Text('确认'),
)
],
);
},
);
}
} }

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