Compare commits

..

2 Commits

Author SHA1 Message Date
67c9ff699c feat: sentry 异常捕获 2024-03-23 14:15:51 +08:00
74f31a818c Merge branch 'main' into feature-sentry 2024-03-23 00:41:12 +08:00
389 changed files with 12082 additions and 27773 deletions

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- "never" - "main"
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "**.txt" - "**.txt"
@ -95,7 +95,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.19.6 flutter-version: 3.16.5
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
@ -205,4 +205,4 @@ jobs:
method: sendFile method: sendFile
path: Pilipala-Beta/* path: Pilipala-Beta/*
parse_mode: Markdown parse_mode: Markdown
context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}]" context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}](${{ github.event.head_commit.url }})"

View File

@ -36,7 +36,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.19.6 flutter-version: 3.16.5
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
@ -98,7 +98,7 @@ jobs:
uses: subosito/flutter-action@v2.10.0 uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: 3.19.6 flutter-version: 3.16.5
- name: flutter build ipa - name: flutter build ipa
run: | run: |

View File

@ -26,50 +26,15 @@
Xcode 13.4 不支持**auto_orientation**,请注释相关代码 Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash ```bash
[!] Flutter (Channel [user-branch], 3.19.6, on macOS 14.6.1 23G93 darwin-arm64, [] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale
locale zh-Hans-CN) zh-Hans-CN)
! Flutter version 3.19.6 on channel [user-branch] at
/Users/rr/Documents/sdk/flutter
Currently on an unknown channel. Run `flutter channel` to switch to an
official channel.
If that doesn't fix the issue, reinstall Flutter by following instructions
at https://flutter.dev/docs/get-started/install.
! Upstream repository unknown source is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to unknown source to dismiss
this error.
• Framework revision 54e66469a9 (8 months ago), 2024-04-17 13:08:03 -0700
• Engine revision c4cd48e186
• Dart version 3.3.4
• DevTools version 2.31.1
• Pub download mirror https://pub.flutter-io.cn
• Flutter download mirror https://storage.flutter-io.cn
• If those were intentional, you can disregard the above warnings; however
it is recommended to use "git" directly to perform update checks and
upgrades.
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/rr/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• Java binary at: /Applications/Android
Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
• All Android licenses accepted.
[] Xcode - develop for iOS and macOS (Xcode 15.1) [] Xcode - develop for iOS and macOS (Xcode 15.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 15C65
• CocoaPods version 1.14.3
[] Chrome - develop for the web [] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [] Android Studio (version 2022.3)
[] VS Code (version 1.85.1)
[] Android Studio (version 2024.2) [] Connected device (3 available)
• Android Studio at /Applications/Android Studio.app/Contents [] Network resources
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
``` ```
@ -79,9 +44,6 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
## 技术交流 ## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1 Telegram: https://t.me/+lm_oOVmF0RJiODk1
Tg Beta版本@PiliPala_Beta
QQ频道: https://pd.qq.com/s/365esodk3 QQ频道: https://pd.qq.com/s/365esodk3
@ -129,7 +91,7 @@ QQ频道: https://pd.qq.com/s/365esodk3
- [x] 音质选择(视视频而定) - [x] 音质选择(视视频而定)
- [x] 解码格式选择(视视频而定) - [x] 解码格式选择(视视频而定)
- [x] 弹幕 - [x] 弹幕
- [x] 字幕 - [ ] 字幕
- [x] 记忆播放 - [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等 - [x] 视频比例:高度/宽度适应、填充、包含等

View File

@ -1,9 +1,3 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -12,6 +6,10 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
@ -23,6 +21,9 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@ -36,9 +37,8 @@ def _keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties["keyAlias"]
def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"] def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"]
android { android {
namespace "com.guozhigq.pilipala"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion "27.0.12077973" ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -91,6 +91,9 @@ flutter {
source '../..' source '../..'
} }
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3] ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
import com.android.build.OutputFile import com.android.build.OutputFile

View File

@ -1,4 +0,0 @@
# missing R8 error
-dontwarn javax.annotation.Nullable
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.OpenSSLProvider

View File

@ -12,6 +12,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
<queries> <queries>
<intent> <intent>
@ -19,6 +20,7 @@
"android.support.customtabs.action.CustomTabsService" /> "android.support.customtabs.action.CustomTabsService" />
</intent> </intent>
</queries> </queries>
<queries> <queries>
<!-- If your app checks for http support --> <!-- If your app checks for http support -->
<intent> <intent>
@ -32,6 +34,7 @@
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
<application <application
android:label="PiliPala" android:label="PiliPala"
android:name="${applicationName}" android:name="${applicationName}"
@ -42,16 +45,15 @@
android:fullBackupContent="false" android:fullBackupContent="false"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity <activity
android:name="com.guozhigq.pilipala.MainActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:autoVerify="true"
> >
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
@ -61,21 +63,10 @@
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<!-- Deep Link -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="pili"/>
<data android:scheme="pilipala"/>
</intent-filter>
<!-- App Link -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
@ -141,55 +132,102 @@
<data android:scheme="bilibili" android:host="assistant" /> <data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="feedback" /> <data android:scheme="bilibili" android:host="feedback" />
<data android:scheme="bilibili" android:host="auth" android:path="/launch" /> <data android:scheme="bilibili" android:host="auth" android:path="/launch" />
<data android:scheme="http" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="https" android:host="live.bilibili.com" <data android:scheme="https" android:host="live.bilibili.com"
android:pathPattern="/live/.*" /> android:pathPattern="/live/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.tv" <data android:scheme="https" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn" <data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" /> android:pathPattern="/mobile/video/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/story/.*" /> android:pathPattern="/story/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" /> android:pathPattern="/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" /> android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="http" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="https" android:host="bangumi.bilibili.com" <data android:scheme="https" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" /> android:pathPattern="/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" /> android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" /> android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" /> android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" /> android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/play/ss.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ss.*" /> android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" /> android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" /> android:pathPattern="/read/cv.*" />
<data android:scheme="http" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" /> <data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="http" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.cn" <data android:scheme="https" android:host="bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.com" <data android:scheme="https" android:host="bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn" <data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" /> android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="b23.tv" <data android:scheme="https" android:host="b23.tv"
android:pathPattern="/*" /> android:pathPattern="/*" />
<data android:scheme="https" android:host="space.bilibili.com" <data android:scheme="https" android:host="space.bilibili.com"
android:pathPattern="/*" /> android:pathPattern="/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
@ -201,6 +239,7 @@
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver <receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver" android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true" android:exported="true"
@ -208,7 +247,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
</receiver> </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
@ -230,4 +269,4 @@
--> -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
</manifest> </manifest>

View File

@ -1,8 +1,6 @@
package com.guozhigq.pilipala package com.guozhigq.pilipala
// import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import com.ryanheise.audioservice.AudioServiceActivity;
class MainActivity: AudioServiceActivity() { class MainActivity: FlutterActivity() {
} }

View File

@ -1,3 +1,15 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
@ -8,15 +20,6 @@ allprojects {
rootProject.buildDir = '../build' rootProject.buildDir = '../build'
subprojects { subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
if (namespace == null) {
namespace project.group
}
}
}
}
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
} }
subprojects { subprojects {

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View File

@ -1,25 +1,11 @@
pluginManagement { include ':app'
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
repositories { assert localPropertiesFile.exists()
google() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
mavenCentral()
gradlePluginPortal()
}
}
plugins { def flutterSdkPath = properties.getProperty("flutter.sdk")
id "dev.flutter.flutter-plugin-loader" version "1.0.0" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
id "com.android.application" version "8.7.2" apply false apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}
include ":app"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24"><path fill-rule="evenodd" d="m8.085 4.891-.999-1.499a1.008 1.008 0 0 1 1.679-1.118l1.709 2.566c.54-.008 1.045-.012 1.515-.012h.13c.345 0 .707.003 1.088.007l1.862-2.59a1.008 1.008 0 0 1 1.637 1.177l-1.049 1.46c.788.02 1.631.046 2.53.078 1.958.069 3.468 1.6 3.74 3.507.088.613.13 2.158.16 3.276l.001.027c.01.333.017.63.025.856a.987.987 0 0 1-1.974.069c-.008-.23-.016-.539-.025-.881v-.002c-.028-1.103-.066-2.541-.142-3.065-.143-1.004-.895-1.78-1.854-1.813-2.444-.087-4.466-.13-6.064-.131-1.598 0-3.619.044-6.063.13a2.037 2.037 0 0 0-1.945 1.748c-.15 1.04-.225 2.341-.225 3.904 0 1.874.11 3.474.325 4.798.154.949.95 1.66 1.91 1.708a97.58 97.58 0 0 0 5.416.139.988.988 0 0 1 0 1.975c-2.196 0-3.61-.047-5.513-.141A4.012 4.012 0 0 1 2.197 17.7c-.236-1.446-.351-3.151-.351-5.116 0-1.64.08-3.035.245-4.184A4.013 4.013 0 0 1 5.92 4.96c.761-.027 1.483-.05 2.164-.069Zm4.436 4.707h-1.32v4.63h2.222v.848h-2.618v1.078h2.431a5.01 5.01 0 0 1 3.575-3.115V9.598h-1.276a8.59 8.59 0 0 0 .748-1.42l-1.089-.384a14.232 14.232 0 0 1-.814 1.804h-1.518l.693-.308a8.862 8.862 0 0 0-.814-1.408l-1.045.352c.297.396.572.847.825 1.364Zm-4.18 3.564.154-1.485h1.98V8.289h-3.2v.979h2.067v1.43H7.483l-.308 3.454h2.277c0 1.166-.044 1.925-.12 2.277-.078.352-.386.528-.936.528-.308 0-.616-.022-.902-.055l.297 1.067.062.004c.285.02.551.04.818.04 1.001-.066 1.562-.418 1.694-1.056.11-.638.176-1.903.176-3.795h-2.2Zm7.458.11v-.858h-1.254v.858H15.8Zm-2.376-.858v.858h-1.199v-.858h1.2Zm-1.199-.946h1.2v-.902h-1.2v.902Zm2.321 0v-.902H15.8v.902h-1.254Zm3.517 10.594a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-.002-1.502a2.5 2.5 0 0 1-2.217-3.657l3.326 3.398a2.49 2.49 0 0 1-1.109.259Zm2.5-2.5c0 .42-.103.815-.286 1.162l-3.328-3.401a2.5 2.5 0 0 1 3.614 2.239Z" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24" id="svgcontent" overflow="visible" width="24" height="24" x="597" y="286"><g class="layer" style="pointer-events:all"><title style="pointer-events:inherit">Layer 1</title><path fill-rule="evenodd" d="M11.99,4.83C11.52,4.83 11.01,4.83 10.47,4.84L8.76,2.27A1.01,1.01 0 0 0 7.09,3.39L8.08,4.89C7.4,4.91 6.68,4.93 5.92,4.96A4.01,4.01 0 0 0 2.09,8.4C1.93,9.55 1.85,10.94 1.85,12.58C1.85,14.55 1.96,16.25 2.2,17.7A4.01,4.01 0 0 0 5.96,21.06L6.86,21.11C8.07,21.17 8.67,21.2 10.47,21.2A0.99,0.99 0 0 0 10.47,19.23C8.71,19.23 8.13,19.2 6.97,19.14L6.06,19.09A2.04,2.04 0 0 1 4.15,17.38C3.93,16.06 3.82,14.46 3.82,12.58C3.82,11.02 3.9,9.72 4.05,8.68C4.19,7.7 5.01,6.97 5.99,6.93C8.43,6.85 10.46,6.8 12.05,6.8C13.65,6.8 15.67,6.85 18.12,6.93C19.08,6.97 19.83,7.74 19.97,8.75C20.05,9.27 20.09,10.71 20.11,11.81L20.11,11.81C20.12,12.16 20.13,12.46 20.14,12.69A0.99,0.99 0 1 0 22.11,12.63C22.1,12.4 22.1,12.1 22.09,11.77L22.09,11.74C22.06,10.62 22.01,9.08 21.93,8.47C21.65,6.56 20.14,5.03 18.19,4.96C17.29,4.93 16.44,4.9 15.66,4.88L16.71,3.42A1.01,1.01 0 0 0 15.07,2.24L13.21,4.83C12.83,4.83 12.46,4.83 12.12,4.83L11.99,4.83zM12.51,9.6L11.19,9.6L11.19,14.23L13.41,14.23L13.41,15.08L10.79,15.08L10.79,16.16L13.41,16.16L13.42,16.84C13.78,16.86 14.13,17 14.43,17.24L14.54,17.24L14.54,16.16L17.23,16.16L17.23,15.08L14.53,15.08L14.53,14.23L16.8,14.23L16.8,9.6L15.52,9.6A8.59,8.59 0 0 0 16.27,8.18L15.18,7.8A14.23,14.23 0 0 1 14.37,9.6L12.85,9.6L13.54,9.3A8.86,8.86 0 0 0 12.73,7.89L11.68,8.24C11.98,8.64 12.26,9.09 12.51,9.6zM8.33,13.17L8.48,11.68L10.46,11.68L10.46,8.29L7.26,8.29L7.26,9.27L9.33,9.27L9.33,10.7L7.47,10.7L7.16,14.16L9.44,14.16C9.44,15.32 9.4,16.08 9.32,16.43C9.24,16.79 8.94,16.96 8.39,16.96C8.08,16.96 7.77,16.94 7.48,16.91L7.78,17.97L7.84,17.98C8.13,18 8.39,18.02 8.66,18.02C9.66,17.95 10.22,17.6 10.35,16.96C10.46,16.32 10.53,15.06 10.53,13.17L8.33,13.17zM15.79,13.28L15.79,12.42L14.53,12.42L14.53,13.28L15.79,13.28zM13.41,12.42L13.41,13.28L12.21,13.28L12.21,12.42L13.41,12.42zM12.21,11.47L13.41,11.47L13.41,10.57L12.21,10.57L12.21,11.47zM14.53,11.47L14.53,10.57L15.79,10.57L15.79,11.47L14.53,11.47z" clip-rule="evenodd" id="svg_1"></path><path fill="#000000" fill-rule="evenodd" d="M22.85,14.63A1,1 0 0 0 21.43,14.7L16.34,20.41L14.13,18.13L14.03,18.04L14.02,18.04A1,1 0 0 0 12.7,19.53L15.66,22.57L15.76,22.66L15.76,22.66C16.17,22.98 16.76,22.93 17.12,22.54L22.93,16.03L23.01,15.93L23.01,15.92A1,1 0 0 0 22.85,14.63z" clip-rule="evenodd" id="svg_2"></path></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,27 +0,0 @@
## 1.0.22
### 功能
+ 字幕
+ 全屏时选集
+ 动态转发
+ 评论视频并转发
+ 收藏夹删除
+ 合集显示封面
+ 底部导航栏编辑、排序功能
+ 历史记录进度条展示
+ 直播画质切换
+ 排行榜功能
+ 视频详情页推荐视频开关
+ 显示联合投稿up
### 修复
+ 收藏夹个数错误
+ 封面保存权限问题
+ 合集最后1p未展示
+ up主页关注按钮触发灰屏
### 优化
+ 视频简介查看逻辑
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,14 +0,0 @@
## 1.0.23
### 功能
+ 封面下载
### 修复
+ 全屏问题
+ 视频播放器灰屏问题
+ 评论区点击区域问题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,23 +0,0 @@
## 1.0.24
### 功能
+ 私信功能
+ 回复我的、收到的赞查看
+ 新的登录方式
+ 全屏选集
+ 一键三连
+ 按分区搜索
### 优化
+ 页面跳转动画
+ 评论区跳转
### 修复
+ 音画不同步问题
+ 分集字幕未同步
+ 多语言字幕
+ 弹幕设置未生效
+
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,39 +0,0 @@
## 1.0.25
### 功能
+ 直播弹幕
+ 稍后再看、收藏夹播放全部
+ 收藏夹新建、编辑
+ 评论删除
+ 评论保存为图片
+ 动态页滑动切换up
+ up投稿筛选充电视频
+ 直播tab展示关注up
+ up主页专栏展示
### 优化
+ 视频详情页一键三连
+ 动态页标识充电视频
+ 播放器亮度、音量调整百分比展示
+ 封面预览时视频标题可复制
+ 竖屏直播布局
+ 图片预览
+ 专栏渲染优化
+ 私信图片查看
### 修复
+ 收藏夹点击异常
+ 搜索up异常
+ 系统通知已读异常
+ [赞了我的]展示错误
+ 部分up合集无法打开
+ 切换合集视频投币个数未重置
+ 搜索条件筛选面板无法滚动
+ 部分机型导航条未沉浸
+ 专栏图片渲染问题
+ 专栏浏览历史记录
+ 直播间历史记录
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,38 +0,0 @@
## 1.0.26
### 功能
+ 番剧历史记录
+ 番组追番状态
+ up主投稿播放
+ 视频tags展示
+ 视频带图评论
+ 无效视频查看
+ 支持合集订阅
+ 艾特评论展示
+ 视频合集展示
+ 视频硬解格式
+ cookie登录
+ 默认空降助手
+ 支持评论区关闭
+ 支持纯黑色背景
+ 简介默认展开
### 优化
+ 竖屏视频播放
+ 专栏文章查看
+ 播放器亮度、音量调整百分比展示
+ 竖屏直播布局
+ 图片预览
+ 专栏渲染优化
+ 私信图片查看
### 修复
+ 字幕匹配错误
+ 视频全屏异常
+ 直播间横屏异常
+ 其他一些问题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>12.0</string> <string>11.0</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,5 @@
PODS: PODS:
- app_links (0.0.2): - appscheme (1.0.4):
- Flutter - Flutter
- audio_service (0.0.1): - audio_service (0.0.1):
- Flutter - Flutter
@ -9,7 +9,7 @@ PODS:
- Flutter - Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- FlutterMacOS - ReachabilitySwift
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
@ -23,12 +23,10 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.9): - gt3_flutter_plugin (0.0.8):
- Flutter - Flutter
- GT3Captcha-iOS - GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3) - GT3Captcha-iOS (0.15.8.3)
- image_picker_ios (0.0.1):
- Flutter
- 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):
@ -40,12 +38,20 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0): - permission_handler_apple (9.1.1):
- Flutter - Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1): - saver_gallery (0.0.1):
- Flutter - Flutter
- screen_brightness_ios (0.1.0): - screen_brightness_ios (0.1.0):
- Flutter - Flutter
- Sentry/HybridSDK (8.19.0):
- SentryPrivate (= 8.19.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.19.0)
- SentryPrivate (8.19.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- sqflite (0.0.3): - sqflite (0.0.3):
@ -68,18 +74,17 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`) - appscheme (from `.symlinks/plugins/appscheme/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@ -88,6 +93,7 @@ DEPENDENCIES:
- 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`) - 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`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/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`)
@ -102,11 +108,14 @@ SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- GT3Captcha-iOS - GT3Captcha-iOS
- ReachabilitySwift
- Sentry
- SentryPrivate
- Toast - Toast
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links: appscheme:
:path: ".symlinks/plugins/app_links/ios" :path: ".symlinks/plugins/appscheme/ios"
audio_service: audio_service:
:path: ".symlinks/plugins/audio_service/ios" :path: ".symlinks/plugins/audio_service/ios"
audio_session: audio_session:
@ -114,7 +123,7 @@ EXTERNAL SOURCES:
auto_orientation: auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios" :path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
@ -127,8 +136,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin: gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios" :path: ".symlinks/plugins/gt3_flutter_plugin/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop: media_kit_native_event_loop:
@ -145,6 +152,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/saver_gallery/ios" :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"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
sqflite: sqflite:
@ -165,38 +174,41 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios" :path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345 audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
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
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
Sentry: 1ebcaef678a27c8ac515f974cb5425dd1bbdec2f
sentry_flutter: ecdfbedee55337205561cfa782ee02d31ec83e1f
SentryPrivate: 765c9b4ebe9ac1a5fcdc067c5a1cfbf3f10e1677
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44 system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
Toast: ec33c32b8688982cecc6348adeae667c1b9938da Toast: ec33c32b8688982cecc6348adeae667c1b9938da
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be

View File

@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1510; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1510" LastUpgradeVersion = "1430"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,5 @@
import UIKit import UIKit
import Flutter import Flutter
import AVFoundation
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@ -9,14 +8,6 @@ import AVFoundation
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
//
do {
try AVAudioSession.sharedInstance().setCategory(.playback, options: [.duckOthers])
} catch {
print("Failed to set audio session category: \(error)")
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }

View File

@ -49,8 +49,6 @@
<true/> <true/>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string> <string>请允许APP保存图片到相册</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string> <string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key> <key>NSAppleMusicUsageDescription</key>
@ -65,29 +63,44 @@
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>bilibili</string> <string></string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>http</string> <string>http</string>
<string>https</string> <string>https</string>
<string>bilibili</string>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
<string>pili</string>
<string>pilipala</string>
</array> </array>
<key>FlutterDeepLinkingEnabled</key> <key>CFBundleURLTypes</key>
<false/> <array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
</array>
</dict>
</array>
</dict>
<!-- 当其他应用程序或系统通过 bilibili -->
<dict>
<key>CFBundleURLName</key>
<string>bilibili</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bilibili</string>
</array>
</dict> </dict>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@ -15,4 +15,6 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端 // 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
} }

View File

@ -1,844 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:scrollview_observer/scrollview_observer.dart';
import '../models/common/video_episode_type.dart';
import 'widgets/badge.dart';
import 'widgets/stat/danmu.dart';
import 'widgets/stat/view.dart';
class EpisodeBottomSheet {
final List<dynamic> episodes;
final int currentCid;
final dynamic dataType;
final Function changeFucCall;
final int? cid;
final double? sheetHeight;
bool isFullScreen = false;
final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
EpisodeBottomSheet({
required this.episodes,
required this.currentCid,
required this.dataType,
required this.changeFucCall,
this.cid,
this.sheetHeight,
this.isFullScreen = false,
this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
});
Widget buildShowContent() {
return PagesBottomSheet(
episodes: episodes,
currentCid: currentCid,
dataType: dataType,
changeFucCall: changeFucCall,
cid: cid,
sheetHeight: sheetHeight,
isFullScreen: isFullScreen,
ugcSeason: ugcSeason,
currentEpisodeIndex: currentEpisodeIndex,
currentIndex: currentIndex,
);
}
PersistentBottomSheetController show(BuildContext context) {
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context,
builder: (BuildContext context) {
return buildShowContent();
},
);
return btmSheetCtr;
}
}
class PagesBottomSheet extends StatefulWidget {
const PagesBottomSheet({
super.key,
required this.episodes,
required this.currentCid,
required this.dataType,
required this.changeFucCall,
this.cid,
this.sheetHeight,
this.isFullScreen = false,
this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
});
final List<dynamic> episodes;
final int currentCid;
final dynamic dataType;
final Function changeFucCall;
final int? cid;
final double? sheetHeight;
final bool isFullScreen;
final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
@override
State<PagesBottomSheet> createState() => _PagesBottomSheetState();
}
class _PagesBottomSheetState extends State<PagesBottomSheet>
with TickerProviderStateMixin {
final ScrollController _listScrollController = ScrollController();
late ListObserverController _listObserverController;
late GridObserverController _gridObserverController;
final ScrollController _gridScrollController = ScrollController();
late int currentIndex;
TabController? tabController;
List<ListObserverController>? _listObserverControllerList;
List<ScrollController>? _listScrollControllerList;
final String heroTag = Get.arguments['heroTag'];
VideoDetailController? _videoDetailController;
RxInt isSubscribe = (-1).obs;
bool isVisible = false;
@override
void initState() {
super.initState();
currentIndex = widget.currentIndex ??
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
_scrollToInit();
_scrollPositionInit();
if (widget.dataType == VideoEpidoesType.videoEpisode) {
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
_getSubscribeStatus();
}
}
String prefix() {
switch (widget.dataType) {
case VideoEpidoesType.videoEpisode:
return '选集';
case VideoEpidoesType.videoPart:
return '分集';
case VideoEpidoesType.bangumiEpisode:
return '选集';
}
return '选集';
}
// 滚动器初始化
void _scrollToInit() {
/// 单个
_listObserverController =
ListObserverController(controller: _listScrollController);
if (widget.dataType == VideoEpidoesType.videoEpisode &&
widget.ugcSeason?.sections != null &&
widget.ugcSeason!.sections!.length > 1) {
tabController = TabController(
length: widget.ugcSeason!.sections!.length,
vsync: this,
initialIndex: widget.currentEpisodeIndex ?? 0,
);
/// 多tab
_listScrollControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ScrollController();
},
);
_listObserverControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ListObserverController(
controller: _listScrollControllerList![index],
);
},
);
} else {
_gridObserverController =
GridObserverController(controller: _gridScrollController);
}
}
// 滚动器位置初始化
void _scrollPositionInit() {
if (widget.dataType == VideoEpidoesType.videoEpisode) {
// 单个 多tab
if (widget.ugcSeason?.sections != null) {
if (widget.ugcSeason!.sections!.length == 1) {
_listObserverController.initialIndexModel =
ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
} else {
_listObserverControllerList![widget.currentEpisodeIndex ?? 0]
.initialIndexModel = ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
}
}
} else {
_gridObserverController.initialIndexModel = ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: false,
);
}
}
// 获取订阅状态
void _getSubscribeStatus() async {
var res =
await VideoHttp.getSubscribeStatus(bvid: _videoDetailController!.bvid);
if (res['status']) {
isSubscribe.value = res['data']['season_fav'] ? 1 : 0;
}
}
// 更改订阅状态
void _changeSubscribeStatus() async {
if (isSubscribe.value == -1) {
return;
}
dynamic result = await VideoHttp.seasonFav(
isFav: isSubscribe.value == 1,
seasonId: widget.ugcSeason!.id,
);
if (result['status']) {
SmartDialog.showToast(isSubscribe.value == 1 ? '取消订阅成功' : '订阅成功');
isSubscribe.value = isSubscribe.value == 1 ? 0 : 1;
} else {
SmartDialog.showToast(result['msg']);
}
}
// 更改展开状态
void _changeVisible() {
setState(() {
isVisible = !isVisible;
});
}
@override
void dispose() {
try {
_listObserverController.controller?.dispose();
_gridObserverController.controller?.dispose();
_listScrollController.dispose();
_gridScrollController.dispose();
for (var element in _listObserverControllerList!) {
element.controller?.dispose();
}
for (var element in _listScrollControllerList!) {
element.dispose();
}
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SizedBox(
height: widget.sheetHeight,
child: Column(
children: [
TitleBar(
title: '${prefix()}${widget.episodes.length}',
isFullScreen: widget.isFullScreen,
),
if (widget.ugcSeason != null) ...[
UgcSeasonBuild(
ugcSeason: widget.ugcSeason!,
isSubscribe: isSubscribe,
isVisible: isVisible,
changeFucCall: _changeSubscribeStatus,
changeVisible: _changeVisible,
),
],
Expanded(
child: Material(
child: widget.dataType == VideoEpidoesType.videoEpisode
? (widget.ugcSeason!.sections!.length == 1
? ListViewObserver(
controller: _listObserverController,
child: ListView.builder(
controller: _listScrollController,
itemCount: widget.episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem =
index == widget.episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: EpisodeListItem(
episode: widget.episodes[index],
index: index,
isCurrentIndex: isCurrentIndex,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
)
: buildTabBar())
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0), // 设置左右间距为12
child: GridViewObserver(
controller: _gridObserverController,
child: GridView.count(
controller: _gridScrollController,
crossAxisCount: 2,
crossAxisSpacing: StyleString.safeSpace,
childAspectRatio: 2.6,
children: List.generate(
widget.episodes.length,
(index) {
bool isCurrentIndex = currentIndex == index;
return EpisodeGridItem(
episode: widget.episodes[index],
index: index,
isCurrentIndex: isCurrentIndex,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
),
),
),
),
),
],
),
);
});
}
Widget buildTabBar() {
return Column(
children: [
// 背景色
Container(
color: Theme.of(context).colorScheme.surface,
child: TabBar(
controller: tabController,
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
tabAlignment: TabAlignment.start,
splashBorderRadius: BorderRadius.circular(4),
tabs: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
return Tab(
text: section.title,
);
}).toList()
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
final int fIndex = widget.ugcSeason!.sections!.indexOf(section);
return ListViewObserver(
controller: _listObserverControllerList![fIndex],
child: ListView.builder(
controller: _listScrollControllerList![fIndex],
itemCount: section.episodes!.length + 1,
itemBuilder: (BuildContext context, int index) {
final bool isLastItem = index == section.episodes!.length;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom + 20,
)
: EpisodeListItem(
episode: section.episodes![index], // 调整索引
index: index, // 调整索引
isCurrentIndex: widget.currentCid ==
section.episodes![index].cid,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
);
}).toList()
],
),
),
],
);
}
}
class TitleBar extends StatelessWidget {
final String title;
final bool isFullScreen;
const TitleBar({
Key? key,
required this.title,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 1,
scrolledUnderElevation: 1,
title: Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: !isFullScreen
? [
SizedBox(
width: 35,
height: 35,
child: IconButton(
icon: const Icon(Icons.close, size: 20),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(width: 8),
]
: null,
);
}
}
class EpisodeListItem extends StatelessWidget {
final dynamic episode;
final int index;
final bool isCurrentIndex;
final dynamic dataType;
final Function changeFucCall;
final bool isFullScreen;
const EpisodeListItem({
Key? key,
required this.episode,
required this.index,
required this.isCurrentIndex,
required this.dataType,
required this.changeFucCall,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface;
String title = '';
switch (dataType) {
case VideoEpidoesType.videoEpisode:
title = episode.title;
break;
case VideoEpidoesType.videoPart:
title = episode.pagePart;
break;
case VideoEpidoesType.bangumiEpisode:
title = '${episode.title}${episode.longTitle!}';
break;
}
return isFullScreen || episode?.cover == null || episode?.cover == ''
? _buildListTile(context, title, primary, onSurface)
: _buildInkWell(context, title, primary, onSurface);
}
Widget _buildListTile(
BuildContext context, String title, Color primary, Color onSurface) {
return ListTile(
onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.png',
color: primary,
height: 12,
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
),
);
}
Widget _buildInkWell(
BuildContext context, String title, Color primary, Color onSurface) {
return InkWell(
onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 6, StyleString.safeSpace, 6),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
const double width = 160;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: episode?.cover ?? '',
width: maxWidth,
height: maxHeight,
),
if (episode.duration != 0)
PBadge(
text: Utils.timeFormat(episode.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
episode.title as String,
textAlign: TextAlign.start,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
color: isCurrentIndex ? primary : onSurface,
),
),
const Spacer(),
if (dataType != VideoEpidoesType.videoPart) ...[
if (episode?.pubdate != null ||
episode.pubTime != null)
Text(
Utils.dateFormat(
episode?.pubdate ?? episode.pubTime),
style: TextStyle(
fontSize: 11,
color:
Theme.of(context).colorScheme.outline),
),
const SizedBox(height: 2),
if (episode.stat != null)
Row(
children: [
StatView(view: episode.stat.view),
const SizedBox(width: 8),
StatDanMu(danmu: episode.stat.danmaku),
const Spacer(),
],
),
const SizedBox(height: 4),
]
],
),
),
)
],
),
);
},
),
),
);
}
}
class EpisodeGridItem extends StatelessWidget {
final dynamic episode;
final int index;
final bool isCurrentIndex;
final dynamic dataType;
final Function changeFucCall;
final bool isFullScreen;
const EpisodeGridItem({
Key? key,
required this.episode,
required this.index,
required this.isCurrentIndex,
required this.dataType,
required this.changeFucCall,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextStyle textStyle = TextStyle(
color: isCurrentIndex ? colorScheme.primary : colorScheme.onSurface,
fontSize: 14,
);
return Stack(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: StyleString.safeSpace),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isCurrentIndex
? colorScheme.primaryContainer.withOpacity(0.6)
: colorScheme.onInverseSurface.withOpacity(0.6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isCurrentIndex
? colorScheme.primary.withOpacity(0.8)
: Colors.transparent,
width: 1,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「${episode.title}');
changeFucCall.call(episode, index);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text(
dataType == VideoEpidoesType.bangumiEpisode
? '${index + 1}'
: '${index + 1}p',
style: textStyle),
const SizedBox(height: 1),
Text(
episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textStyle,
),
],
),
),
),
),
if (dataType == VideoEpidoesType.bangumiEpisode &&
episode.badge != '' &&
episode.badge != null)
Positioned(
right: 8,
top: 18,
child: Text(
episode.badge,
style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)),
),
)
],
);
}
}
class UgcSeasonBuild extends StatelessWidget {
final UgcSeason ugcSeason;
final RxInt isSubscribe;
final bool isVisible;
final Function changeFucCall;
final Function changeVisible;
const UgcSeasonBuild({
Key? key,
required this.ugcSeason,
required this.isSubscribe,
required this.isVisible,
required this.changeFucCall,
required this.changeVisible,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Color outline = theme.colorScheme.outline;
final Color surface = theme.colorScheme.surface;
final Color primary = theme.colorScheme.primary;
final Color onPrimary = theme.colorScheme.onPrimary;
final Color onInverseSurface = theme.colorScheme.onInverseSurface;
final TextStyle titleMedium = theme.textTheme.titleMedium!;
final TextStyle labelMedium = theme.textTheme.labelMedium!;
final Color dividerColor = theme.dividerColor.withOpacity(0.1);
return isVisible
? Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
color: surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(height: 1, thickness: 1, color: dividerColor),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: Text(
'合集:${ugcSeason.title}',
style: titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Obx(
() => isSubscribe.value == -1
? const SizedBox(height: 32)
: SizedBox(
height: 32,
child: FilledButton.tonal(
onPressed: () => changeFucCall.call(),
style: TextButton.styleFrom(
padding:
const EdgeInsets.only(left: 8, right: 8),
foregroundColor: isSubscribe.value == 1
? outline
: onPrimary,
backgroundColor: isSubscribe.value == 1
? onInverseSurface
: primary,
),
child:
Text(isSubscribe.value == 1 ? '已订阅' : '订阅'),
),
),
),
],
),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 6),
Container(
constraints: const BoxConstraints(maxHeight: 100),
child: SingleChildScrollView(
child: Text(
ugcSeason.intro!,
style: TextStyle(color: outline, fontSize: 12),
),
),
),
],
const SizedBox(height: 4),
Text.rich(
TextSpan(
style: TextStyle(
fontSize: labelMedium.fontSize, color: outline),
children: [
TextSpan(
text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
const TextSpan(text: ' · '),
TextSpan(
text:
'${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
],
),
),
const SizedBox(height: 14),
Align(
alignment: Alignment.center,
child: Material(
color: surface,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 0),
child: Text(
'收起简介',
style: TextStyle(color: primary, fontSize: 12),
),
),
),
),
),
Divider(height: 1, thickness: 1, color: dividerColor),
],
),
)
: Align(
alignment: Alignment.center,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Text(
'展开简介',
style: TextStyle(color: primary, fontSize: 12),
),
),
),
);
}
}

View File

@ -3,9 +3,14 @@ import 'package:pilipala/common/constants.dart';
import 'skeleton.dart'; import 'skeleton.dart';
class MediaBangumiSkeleton extends StatelessWidget { class MediaBangumiSkeleton extends StatefulWidget {
const MediaBangumiSkeleton({super.key}); const MediaBangumiSkeleton({super.key});
@override
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
}
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface; Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
@ -30,25 +35,25 @@ class MediaBangumiSkeleton extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
color: bgColor, color: Theme.of(context).colorScheme.onInverseSurface,
width: 200, width: 200,
height: 20, height: 20,
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
), ),
Container( Container(
color: bgColor, color: Theme.of(context).colorScheme.onInverseSurface,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: bgColor, color: Theme.of(context).colorScheme.onInverseSurface,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: bgColor, color: Theme.of(context).colorScheme.onInverseSurface,
width: 150, width: 150,
height: 13, height: 13,
), ),
@ -59,7 +64,7 @@ class MediaBangumiSkeleton extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(20)), const BorderRadius.all(Radius.circular(20)),
color: bgColor, color: Theme.of(context).colorScheme.onInverseSurface,
), ),
), ),
], ],

View File

@ -13,8 +13,8 @@ class Skeleton extends StatelessWidget {
var shimmerGradient = LinearGradient( var shimmerGradient = LinearGradient(
colors: [ colors: [
Colors.transparent, Colors.transparent,
Theme.of(context).colorScheme.surface.withAlpha(10), Theme.of(context).colorScheme.background.withAlpha(10),
Theme.of(context).colorScheme.surface.withAlpha(10), Theme.of(context).colorScheme.background.withAlpha(10),
Colors.transparent, Colors.transparent,
], ],
stops: const [ stops: const [

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import '../constants.dart';
class UserListSkeleton extends StatelessWidget {
const UserListSkeleton({super.key});
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace, vertical: 7),
child: Row(
children: [
ClipOval(
child: Container(width: 42, height: 42, color: bgColor),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(color: bgColor, width: 60, height: 13),
const SizedBox(width: 10),
Container(color: bgColor, width: 40, height: 13),
],
),
const SizedBox(height: 6),
Container(
color: bgColor,
width: 100,
height: 13,
),
],
),
),
],
));
}
}

View File

@ -1,126 +0,0 @@
import 'package:flutter/material.dart';
import '../constants.dart';
import 'skeleton.dart';
class VideoIntroSkeleton extends StatelessWidget {
const VideoIntroSkeleton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 18),
Container(
width: double.infinity,
height: 20,
margin: const EdgeInsets.only(bottom: 6),
color: bgColor,
),
Container(
width: 220,
height: 20,
margin: const EdgeInsets.only(bottom: 12),
color: bgColor,
),
Row(
children: [
Container(
width: 45,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 45,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 45,
height: 14,
color: bgColor,
),
const Spacer(),
Container(
width: 35,
height: 14,
color: bgColor,
),
const SizedBox(width: 4),
],
),
const SizedBox(height: 30),
LayoutBuilder(builder: (context, constraints) {
// 并列5个正方形
double width = (constraints.maxWidth - 30) / 5;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(5, (index) {
return Container(
width: width - 24,
height: width - 24,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
),
);
}),
);
}),
const SizedBox(height: 20),
Container(
width: double.infinity,
height: 30,
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 20),
Row(
children: [
ClipOval(
child: Container(
width: 44,
height: 44,
color: bgColor,
),
),
const SizedBox(width: 12),
Container(
width: 50,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 35,
height: 14,
color: bgColor,
),
const Spacer(),
Container(
width: 55,
height: 30,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 2)
],
),
const SizedBox(height: 10),
],
),
),
);
}
}

View File

@ -66,7 +66,7 @@ class PBadge extends StatelessWidget {
border: Border.all(color: borderColor), border: Border.all(color: borderColor),
), ),
child: Text( child: Text(
text ?? '', text!,
style: TextStyle(fontSize: fs ?? fontSize, color: color), style: TextStyle(fontSize: fs ?? fontSize, color: color),
), ),
); );

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
Box<dynamic> setting = GStorage.setting; Box<dynamic> setting = GStrorage.setting;
class CustomToast extends StatelessWidget { class CustomToast extends StatelessWidget {
const CustomToast({super.key, required this.msg}); const CustomToast({super.key, required this.msg});

View File

@ -1,25 +0,0 @@
import 'package:flutter/material.dart';
class DragHandle extends StatelessWidget {
const DragHandle({super.key});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: Navigator.of(context).pop,
child: SizedBox(
height: 36,
child: Center(
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: BorderRadius.circular(4),
),
),
),
),
);
}
}

View File

@ -1,158 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/feed_back.dart';
class GroupPanel extends StatefulWidget {
final int? mid;
final ScrollController scrollController;
const GroupPanel({super.key, this.mid, required this.scrollController});
@override
State<GroupPanel> createState() => _GroupPanelState();
}
class _GroupPanelState extends State<GroupPanel> {
late Future _futureBuilderFuture;
late List<MemberTagItemModel> tagsList;
bool showDefault = true;
@override
void initState() {
super.initState();
_futureBuilderFuture = MemberHttp.followUpTags();
}
void onSave() async {
feedBack();
// 是否有选中的 有选中的带id没选使用默认0
final bool anyHasChecked =
tagsList.any((MemberTagItemModel e) => e.checked == true);
late String tagids;
if (anyHasChecked) {
final List<MemberTagItemModel> checkedList =
tagsList.where((MemberTagItemModel e) => e.checked == true).toList();
final List<int> tagidList =
checkedList.map<int>((e) => e.tagid!).toList();
tagids = tagidList.join(',');
} else {
tagids = '0';
}
// 保存
final res = await MemberHttp.addUsers(widget.mid, tagids);
SmartDialog.showToast(res['msg']);
if (res['status']) {
Get.back();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
AppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title: Text('设置关注分组',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold)),
),
Expanded(
child: Material(
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
tagsList = data['data'];
return ListView.builder(
controller: widget.scrollController,
itemCount: data['data'].length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
data['data'][index].checked =
!data['data'][index].checked;
showDefault =
!data['data'].any((e) => e.checked == true);
setState(() {});
},
dense: true,
leading: const Icon(Icons.group_outlined),
minLeadingWidth: 0,
title: Text(data['data'][index].name),
subtitle: data['data'][index].tip != ''
? Text(data['data'][index].tip)
: null,
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: data['data'][index].checked,
onChanged: (bool? checkValue) {
data['data'][index].checked = checkValue;
showDefault =
!data['data'].any((e) => e.checked == true);
setState(() {});
},
),
),
);
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return const Center(child: Text('请求中'));
}
},
),
),
),
Divider(
height: 1,
color: Theme.of(context).disabledColor.withOpacity(0.08),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 12,
bottom: MediaQuery.paddingOf(context).bottom + 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => onSave(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
child: Text(showDefault ? '保存至默认分组' : '保存'),
),
],
),
),
],
);
}
}

View File

@ -1,11 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; import 'network_img_layer.dart';
import 'package:pilipala/utils/highlight.dart';
import '../../utils/global_data_cache.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class HtmlRender extends StatelessWidget { class HtmlRender extends StatelessWidget {
@ -26,25 +22,9 @@ class HtmlRender extends StatelessWidget {
data: htmlContent, data: htmlContent,
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {}, onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
extensions: [ extensions: [
TagExtension(
tagsToExtend: <String>{'pre'},
builder: (ExtensionContext extensionContext) {
final Map<String, dynamic> attributes = extensionContext.attributes;
final String lang = attributes['data-lang'] as String;
final String code = attributes['codecontent'] as String;
List<String> selectedLanguages = [lang.split('@').first];
TextSpan? result = highlightExistingText(code, selectedLanguages);
if (result == null) {
return const Center(child: Text('代码块渲染失败'));
}
return SelectableText.rich(result);
},
),
TagExtension( TagExtension(
tagsToExtend: <String>{'img'}, tagsToExtend: <String>{'img'},
builder: (ExtensionContext extensionContext) { builder: (ExtensionContext extensionContext) {
int defaultImgQuality = 10;
defaultImgQuality = GlobalDataCache.imgQuality;
try { try {
final Map<String, dynamic> attributes = final Map<String, dynamic> attributes =
extensionContext.attributes; extensionContext.attributes;
@ -59,56 +39,24 @@ class HtmlRender extends StatelessWidget {
imgUrl = imgUrl.replaceAll('http://', 'https://'); imgUrl = imgUrl.replaceAll('http://', 'https://');
} }
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl; imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
// final bool isEmote = imgUrl.contains('/emote/'); final bool isEmote = imgUrl.contains('/emote/');
final bool isMall = imgUrl.contains('/mall/'); final bool isMall = imgUrl.contains('/mall/');
if (isMall) { if (isMall) {
return const SizedBox(); return const SizedBox();
} }
return InkWell( // bool inTable =
onTap: () { // extensionContext.element!.previousElementSibling == null ||
Navigator.of(context).push( // extensionContext.element!.nextElementSibling == null;
HeroDialogRoute<void>( // imgUrl = Utils().imageUrl(imgUrl!);
builder: (BuildContext context) => // return Image.network(
InteractiveviewerGallery( // imgUrl,
sources: imgList ?? [imgUrl], // width: isEmote ? 22 : null,
initIndex: imgList?.indexOf(imgUrl) ?? 0, // height: isEmote ? 22 : null,
itemBuilder: ( // );
BuildContext context, return NetworkImgLayer(
int index, width: isEmote ? 22 : Get.size.width - 24,
bool isFocus, height: isEmote ? 22 : 200,
bool enablePageView, src: imgUrl,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: imgList?[index] ?? imgUrl,
child: CachedNetworkImage(
fadeInDuration:
const Duration(milliseconds: 0),
imageUrl: imgList?[index] ?? imgUrl,
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: '$imgUrl@${defaultImgQuality}q.webp',
),
),
); );
} catch (err) { } catch (err) {
return const SizedBox(); return const SizedBox();
@ -118,7 +66,7 @@ class HtmlRender extends StatelessWidget {
], ],
style: { style: {
'html': Style( 'html': Style(
fontSize: FontSize.large, fontSize: FontSize.medium,
lineHeight: LineHeight.percent(140), lineHeight: LineHeight.percent(140),
), ),
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero), 'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
@ -130,7 +78,7 @@ class HtmlRender extends StatelessWidget {
margin: Margins.only(bottom: 10), margin: Margins.only(bottom: 10),
), ),
'span': Style( 'span': Style(
fontSize: FontSize.large, fontSize: FontSize.medium,
height: Height(1.65), height: Height(1.65),
), ),
'div': Style(height: Height.auto()), 'div': Style(height: Height.auto()),
@ -142,14 +90,6 @@ class HtmlRender extends StatelessWidget {
textAlign: TextAlign.justify, textAlign: TextAlign.justify,
), ),
'img': Style(margin: Margins.only(top: 4, bottom: 4)), 'img': Style(margin: Margins.only(top: 4, bottom: 4)),
'figcaption': Style(
textAlign: TextAlign.center,
margin: Margins.only(top: 8, bottom: 20),
color: Theme.of(context).colorScheme.secondary,
),
'figure': Style(
margin: Margins.zero,
),
}, },
); );
} }

View File

@ -2,54 +2,50 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class HttpError extends StatelessWidget { class HttpError extends StatelessWidget {
const HttpError({ const HttpError(
required this.errMsg, {required this.errMsg, required this.fn, this.btnText, super.key});
this.fn,
this.btnText,
this.isShowBtn = true,
this.isInSliver = true,
super.key,
});
final String? errMsg; final String? errMsg;
final Function()? fn; final Function()? fn;
final String? btnText; final String? btnText;
final bool isShowBtn;
final bool isInSliver;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary; return SliverToBoxAdapter(
final errorContent = SizedBox( child: SizedBox(
height: 400, height: 400,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisAlignment: MainAxisAlignment.center,
SvgPicture.asset("assets/images/error.svg", height: 200), children: [
const SizedBox(height: 30), SvgPicture.asset(
Text( "assets/images/error.svg",
errMsg ?? '请求异常', height: 200,
textAlign: TextAlign.center, ),
style: Theme.of(context).textTheme.titleSmall, const SizedBox(height: 30),
), Text(
const SizedBox(height: 20), errMsg ?? '请求异常',
if (isShowBtn) textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 20),
FilledButton.tonal( FilledButton.tonal(
onPressed: () => fn?.call(), onPressed: () {
fn!();
},
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) { backgroundColor: MaterialStateProperty.resolveWith((states) {
return primary.withAlpha(20); return Theme.of(context).colorScheme.primary.withAlpha(20);
}), }),
), ),
child: Text(btnText ?? '点击重试', style: TextStyle(color: primary)), child: Text(
btnText ?? '点击重试',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
), ),
], ],
),
), ),
); );
if (isInSliver) {
return SliverToBoxAdapter(child: errorContent);
} else {
return Align(alignment: Alignment.topCenter, child: errorContent);
}
} }
} }

View File

@ -2,11 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/extension.dart'; import 'package:pilipala/utils/extension.dart';
import 'package:pilipala/utils/global_data_cache.dart'; import 'package:pilipala/utils/global_data.dart';
import '../../utils/storage.dart'; import '../../utils/storage.dart';
import '../constants.dart'; import '../constants.dart';
Box<dynamic> setting = GStorage.setting; Box<dynamic> setting = GStrorage.setting;
class NetworkImgLayer extends StatelessWidget { class NetworkImgLayer extends StatelessWidget {
const NetworkImgLayer({ const NetworkImgLayer({
@ -20,7 +20,6 @@ class NetworkImgLayer extends StatelessWidget {
// 图片质量 默认1% // 图片质量 默认1%
this.quality, this.quality,
this.origAspectRatio, this.origAspectRatio,
this.radius,
}); });
final String? src; final String? src;
@ -31,31 +30,13 @@ class NetworkImgLayer extends StatelessWidget {
final Duration? fadeInDuration; final Duration? fadeInDuration;
final int? quality; final int? quality;
final double? origAspectRatio; final double? origAspectRatio;
final double? radius;
BorderRadius getBorderRadius(String? type, double? radius) {
return BorderRadius.circular(
radius ??
(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int defaultImgQuality = 10; final int defaultImgQuality = GlobalData().imgQuality;
try {
defaultImgQuality = GlobalDataCache.imgQuality;
} catch (_) {}
if (src == '' || src == null) {
return placeholder(context);
}
final String imageUrl = final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';
print(imageUrl);
int? memCacheWidth, memCacheHeight; int? memCacheWidth, memCacheHeight;
double aspectRatio = (width / height).toDouble(); double aspectRatio = (width / height).toDouble();
@ -85,7 +66,13 @@ class NetworkImgLayer extends StatelessWidget {
return src != '' && src != null return src != '' && src != null
? ClipRRect( ? ClipRRect(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
borderRadius: getBorderRadius(type, radius), borderRadius: BorderRadius.circular(
type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x,
),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: imageUrl, imageUrl: imageUrl,
width: width, width: width,
@ -114,7 +101,11 @@ class NetworkImgLayer extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4), color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: getBorderRadius(type, radius), borderRadius: BorderRadius.circular(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
), ),
child: type == 'bg' child: type == 'bg'
? const SizedBox() ? const SizedBox()

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import '../../utils/download.dart';
import '../constants.dart';
import 'network_img_layer.dart';
class OverlayPop extends StatelessWidget {
const OverlayPop({super.key, this.videoItem, this.closeFn});
final dynamic videoItem;
final Function? closeFn;
@override
Widget build(BuildContext context) {
final double imgWidth = MediaQuery.sizeOf(context).width - 8 * 2;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
NetworkImgLayer(
width: imgWidth,
height: imgWidth / StyleString.aspectRatio,
src: videoItem.pic! as String,
quality: 100,
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius:
const BorderRadius.all(Radius.circular(20))),
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => closeFn!(),
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: Text(
videoItem.title! as String,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
await DownloadUtils.downloadImg(
videoItem.pic != null
? videoItem.pic as String
: videoItem.cover as String,
);
// closeFn!();
},
icon: const Icon(Icons.download, size: 20),
)
],
)),
],
),
);
}
}

View File

@ -0,0 +1,130 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:math';
import 'dart:ui' as ui show Image;
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
double get maxDragOffset => 100;
double hideHeight = maxDragOffset / 2.3;
double refreshHeight = maxDragOffset / 1.5;
class PullToRefreshHeader extends StatelessWidget {
const PullToRefreshHeader(
this.info,
this.lastRefreshTime, {
this.color,
super.key,
});
final PullToRefreshScrollNotificationInfo? info;
final DateTime? lastRefreshTime;
final Color? color;
@override
Widget build(BuildContext context) {
final PullToRefreshScrollNotificationInfo? infos = info;
if (infos == null) {
return const SizedBox();
}
String text = '';
if (infos.mode == PullToRefreshIndicatorMode.armed) {
text = 'Release to refresh';
} else if (infos.mode == PullToRefreshIndicatorMode.refresh ||
infos.mode == PullToRefreshIndicatorMode.snap) {
text = 'Loading...';
} else if (infos.mode == PullToRefreshIndicatorMode.done) {
text = 'Refresh completed.';
} else if (infos.mode == PullToRefreshIndicatorMode.drag) {
text = 'Pull to refresh';
} else if (infos.mode == PullToRefreshIndicatorMode.canceled) {
text = 'Cancel refresh';
}
final TextStyle ts = const TextStyle(
color: Colors.grey,
).copyWith(fontSize: 14);
final double dragOffset = info?.dragOffset ?? 0.0;
final DateTime time = lastRefreshTime ?? DateTime.now();
final double top = -hideHeight + dragOffset;
return Container(
height: dragOffset,
color: color ?? Colors.transparent,
// padding: EdgeInsets.only(top: dragOffset / 3),
// padding: EdgeInsets.only(bottom: 5.0),
child: Stack(
children: <Widget>[
Positioned(
left: 0.0,
right: 0.0,
top: top,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(
alignment: Alignment.centerRight,
margin: const EdgeInsets.only(right: 12.0),
child: RefreshImage(top, null),
),
),
Column(
children: <Widget>[
Text(text, style: ts),
Text(
'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}',
style: ts.copyWith(fontSize: 14),
)
],
),
const Spacer(),
],
),
)
],
),
);
}
}
class RefreshImage extends StatelessWidget {
const RefreshImage(this.top, Key? key) : super(key: key);
final double top;
@override
Widget build(BuildContext context) {
const double imageSize = 30;
return ExtendedImage.asset(
'assets/flutterCandies_grey.png',
width: imageSize,
height: imageSize,
afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
final double imageHeight = image.height.toDouble();
final double imageWidth = image.width.toDouble();
final Size size = rect.size;
final double y =
(1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight;
canvas.drawImageRect(
image,
Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
size.width, (imageHeight - y) / imageHeight * size.height),
Paint()
..colorFilter =
const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
..isAntiAlias = false
..filterQuality = FilterQuality.low,
);
//canvas.restore();
},
);
}
}

View File

@ -6,7 +6,7 @@ class StatDanMu extends StatelessWidget {
final dynamic danmu; final dynamic danmu;
final String? size; final String? size;
const StatDanMu({Key? key, this.theme = 'gray', this.danmu, this.size}) const StatDanMu({Key? key, this.theme, this.danmu, this.size})
: super(key: key); : super(key: key);
@override @override
@ -14,49 +14,24 @@ class StatDanMu extends StatelessWidget {
Map<String, Color> colorObject = { Map<String, Color> colorObject = {
'white': Colors.white, 'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline, 'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
}; };
Color color = colorObject[theme]!; Color color = colorObject[theme]!;
return StatIconText(
icon: Icons.subtitles_outlined,
text: Utils.numFormat(danmu!),
color: color,
size: size,
);
}
}
class StatIconText extends StatelessWidget {
final IconData icon;
final String text;
final Color color;
final String? size;
const StatIconText({
Key? key,
required this.icon,
required this.text,
required this.color,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon( Icon(
icon, Icons.subtitles_outlined,
size: 14, size: 14,
color: color, color: color,
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
text, Utils.numFormat(danmu!),
style: TextStyle( style: TextStyle(
fontSize: size == 'medium' ? 12 : 11, fontSize: size == 'medium' ? 12 : 11,
color: color, color: color,
), ),
), )
], ],
); );
} }

View File

@ -6,56 +6,27 @@ class StatView extends StatelessWidget {
final dynamic view; final dynamic view;
final String? size; final String? size;
const StatView({ const StatView({Key? key, this.theme, this.view, this.size})
Key? key, : super(key: key);
this.theme = 'gray',
this.view,
this.size,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map<String, Color> colorObject = { Map<String, Color> colorObject = {
'white': Colors.white, 'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline, 'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
}; };
Color color = colorObject[theme]!; Color color = colorObject[theme]!;
return StatIconText(
icon: Icons.play_circle_outlined,
text: Utils.numFormat(view!),
color: color,
size: size,
);
}
}
class StatIconText extends StatelessWidget {
final IconData icon;
final String text;
final Color color;
final String? size;
const StatIconText({
Key? key,
required this.icon,
required this.text,
required this.color,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon( Icon(
icon, Icons.play_circle_outlined,
size: 13, size: 13,
color: color, color: color,
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
text, Utils.numFormat(view!),
style: TextStyle( style: TextStyle(
fontSize: size == 'medium' ? 12 : 11, fontSize: size == 'medium' ? 12 : 11,
color: color, color: color,

View File

@ -1,18 +1,12 @@
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:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/url_utils.dart';
import '../../http/search.dart'; import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
import '../../utils/utils.dart'; import '../../utils/utils.dart';
import '../constants.dart'; import '../constants.dart';
import 'badge.dart'; import 'badge.dart';
import 'drag_handle.dart';
import 'network_img_layer.dart'; import 'network_img_layer.dart';
import 'stat/danmu.dart'; import 'stat/danmu.dart';
import 'stat/view.dart'; import 'stat/view.dart';
@ -22,24 +16,23 @@ class VideoCardH extends StatelessWidget {
const VideoCardH({ const VideoCardH({
super.key, super.key,
required this.videoItem, required this.videoItem,
this.onPressedFn, this.longPress,
this.longPressEnd,
this.source = 'normal', this.source = 'normal',
this.showOwner = true, this.showOwner = true,
this.showView = true, this.showView = true,
this.showDanmaku = true, this.showDanmaku = true,
this.showPubdate = false, this.showPubdate = false,
this.showCharge = false,
}); });
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final videoItem; final videoItem;
final Function()? onPressedFn; final Function()? longPress;
// normal 推荐, later 稍后再看, search 搜索 final Function()? longPressEnd;
final String source; final String source;
final bool showOwner; final bool showOwner;
final bool showView; final bool showView;
final bool showDanmaku; final bool showDanmaku;
final bool showPubdate; final bool showPubdate;
final bool showCharge;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -50,117 +43,102 @@ class VideoCardH extends StatelessWidget {
type = videoItem.type; type = videoItem.type;
} catch (_) {} } catch (_) {}
final String heroTag = Utils.makeHeroTag(aid); final String heroTag = Utils.makeHeroTag(aid);
return InkWell( return GestureDetector(
onTap: () async { onLongPress: () {
try { if (longPress != null) {
if (type == 'ketang') { longPress!();
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
if (showCharge && videoItem?.typeid == 33) {
final String redirectUrl = await UrlUtils.parseRedirectUrl(
'${HttpString.baseUrl}/video/$bvid/');
final String lastPathSegment = redirectUrl.split('/').last;
if (lastPathSegment.contains('ss')) {
RoutePush.bangumiPush(
Utils.matchNum(lastPathSegment).first, null);
}
if (lastPathSegment.contains('ep')) {
RoutePush.bangumiPush(
null, Utils.matchNum(lastPathSegment).first);
}
return;
}
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
} catch (err) {
SmartDialog.showToast(err.toString());
} }
}, },
onLongPress: () => imageSaveDialog( // onLongPressEnd: (details) {
context, // if (longPressEnd != null) {
videoItem, // longPressEnd!();
SmartDialog.dismiss, // }
), // },
child: Padding( child: InkWell(
padding: const EdgeInsets.fromLTRB( onTap: () async {
StyleString.safeSpace, 5, StyleString.safeSpace, 5), try {
child: LayoutBuilder( if (type == 'ketang') {
builder: (BuildContext context, BoxConstraints boxConstraints) { SmartDialog.showToast('课堂视频暂不支持播放');
final double width = (boxConstraints.maxWidth - return;
StyleString.cardSpace * }
6 / final int cid =
MediaQuery.textScalerOf(context).scale(1.0)) / videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
2; Get.toNamed('/video?bvid=$bvid&cid=$cid',
return Container( arguments: {'videoItem': videoItem, 'heroTag': heroTag});
constraints: const BoxConstraints(minHeight: 88), } catch (err) {
height: width / StyleString.aspectRatio, SmartDialog.showToast(err.toString());
child: Row( }
mainAxisAlignment: MainAxisAlignment.start, },
crossAxisAlignment: CrossAxisAlignment.start, child: Padding(
children: <Widget>[ padding: const EdgeInsets.fromLTRB(
AspectRatio( StyleString.safeSpace, 5, StyleString.safeSpace, 5),
aspectRatio: StyleString.aspectRatio, child: LayoutBuilder(
child: LayoutBuilder( builder: (BuildContext context, BoxConstraints boxConstraints) {
builder: (BuildContext context, final double width = (boxConstraints.maxWidth -
BoxConstraints boxConstraints) { StyleString.cardSpace *
final double maxWidth = boxConstraints.maxWidth; 6 /
final double maxHeight = boxConstraints.maxHeight; MediaQuery.textScalerOf(context).scale(1.0)) /
return Stack( 2;
children: [ return Container(
Hero( constraints: const BoxConstraints(minHeight: 88),
tag: heroTag, height: width / StyleString.aspectRatio,
child: NetworkImgLayer( child: Row(
src: videoItem.pic as String, mainAxisAlignment: MainAxisAlignment.start,
width: maxWidth, crossAxisAlignment: CrossAxisAlignment.start,
height: maxHeight, children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic as String,
width: maxWidth,
height: maxHeight,
),
), ),
), if (videoItem.duration != 0)
if (videoItem.duration != 0) PBadge(
PBadge( text: Utils.timeFormat(videoItem.duration!),
text: Utils.timeFormat(videoItem.duration!), right: 6.0,
right: 6.0, bottom: 6.0,
bottom: 6.0, type: 'gray',
type: 'gray', ),
), if (type != 'video')
if (type != 'video') PBadge(
PBadge( text: type,
text: type, left: 6.0,
left: 6.0, bottom: 6.0,
bottom: 6.0, type: 'primary',
type: 'primary', ),
), // if (videoItem.rcmdReason != null &&
// if (videoItem.rcmdReason != null && // videoItem.rcmdReason.content != '')
// videoItem.rcmdReason.content != '') // pBadge(videoItem.rcmdReason.content, context,
// pBadge(videoItem.rcmdReason.content, context, // 6.0, 6.0, null, null),
// 6.0, 6.0, null, null), ],
if (showCharge && videoItem?.isChargingSrc) );
const PBadge( },
text: '充电专属', ),
right: 6.0,
top: 6.0,
type: 'primary',
),
],
);
},
), ),
), VideoContent(
VideoContent( videoItem: videoItem,
videoItem: videoItem, source: source,
source: source, showOwner: showOwner,
showOwner: showOwner, showView: showView,
showView: showView, showDanmaku: showDanmaku,
showDanmaku: showDanmaku, showPubdate: showPubdate,
showPubdate: showPubdate, )
onPressedFn: onPressedFn, ],
) ),
], );
), },
); ),
},
), ),
), ),
); );
@ -175,7 +153,6 @@ class VideoContent extends StatelessWidget {
final bool showView; final bool showView;
final bool showDanmaku; final bool showDanmaku;
final bool showPubdate; final bool showPubdate;
final Function()? onPressedFn;
const VideoContent({ const VideoContent({
super.key, super.key,
@ -185,7 +162,6 @@ class VideoContent extends StatelessWidget {
this.showView = true, this.showView = true,
this.showDanmaku = true, this.showDanmaku = true,
this.showPubdate = false, this.showPubdate = false,
this.onPressedFn,
}); });
@override @override
@ -196,7 +172,7 @@ class VideoContent extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (source == 'normal' || source == 'later') ...[ if (videoItem.title is String) ...[
Text( Text(
videoItem.title as String, videoItem.title as String,
textAlign: TextAlign.start, textAlign: TextAlign.start,
@ -211,7 +187,7 @@ class VideoContent extends StatelessWidget {
maxLines: 2, maxLines: 2,
text: TextSpan( text: TextSpan(
children: [ children: [
for (final i in videoItem.titleList) ...[ for (final i in videoItem.title) ...[
TextSpan( TextSpan(
text: i['text'] as String, text: i['text'] as String,
style: TextStyle( style: TextStyle(
@ -267,49 +243,128 @@ class VideoContent extends StatelessWidget {
Row( Row(
children: [ children: [
if (showView) ...[ if (showView) ...[
StatView(view: videoItem.stat.view as int), StatView(
theme: 'gray',
view: videoItem.stat.view as int,
),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
if (showDanmaku) if (showDanmaku)
StatDanMu(danmu: videoItem.stat.danmaku as int), StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku as int,
),
const Spacer(), const Spacer(),
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
if (source == 'normal') if (source == 'normal')
SizedBox( SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: IconButton( child: PopupMenuButton<String>(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(videoItem: videoItem);
},
);
},
icon: Icon( icon: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 14, size: 14,
), ),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 40,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
const PopupMenuDivider(),
PopupMenuItem<String>(
onTap: () async {
SmartDialog.show(
useSystem: true,
animationType:
SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(
'确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
SmartDialog.showToast(res['code'] == 0
? '成功'
: res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
},
value: 'pause',
height: 40,
child: Row(
children: [
const Icon(Icons.block, size: 16),
const SizedBox(width: 6),
Text('拉黑:${videoItem.owner.name}',
style: const TextStyle(fontSize: 13))
],
),
),
],
), ),
), ),
if (source == 'later') ...[
IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => onPressedFn?.call(),
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
)
],
], ],
), ),
], ],
@ -318,95 +373,3 @@ class VideoContent extends StatelessWidget {
); );
} }
} }
class MorePanel extends StatelessWidget {
final dynamic videoItem;
const MorePanel({super.key, required this.videoItem});
Future<dynamic> menuActionHandler(String type) async {
switch (type) {
case 'block':
blockUser();
break;
case 'watchLater':
var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
Get.back();
break;
default:
}
}
void blockUser() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
SmartDialog.showToast(res['msg'] ?? '成功');
},
child: const Text('确认'),
)
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandle(),
ListTile(
onTap: () async => await menuActionHandler('block'),
minLeadingWidth: 0,
leading: const Icon(Icons.block, size: 19),
title: Text(
'拉黑up主 「${videoItem.owner.name}',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
onTap: () async => await menuActionHandler('watchLater'),
minLeadingWidth: 0,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title:
Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -1,16 +1,14 @@
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:get/get.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/route_push.dart';
import '../../models/model_rec_video_item.dart'; import '../../models/model_rec_video_item.dart';
import 'drag_handle.dart';
import 'stat/danmu.dart'; import 'stat/danmu.dart';
import 'stat/view.dart'; import 'stat/view.dart';
import '../../http/dynamics.dart'; import '../../http/dynamics.dart';
import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
import '../../models/common/search_type.dart';
import '../../utils/id_utils.dart'; import '../../utils/id_utils.dart';
import '../../utils/utils.dart'; import '../../utils/utils.dart';
import '../constants.dart'; import '../constants.dart';
@ -21,13 +19,15 @@ import 'network_img_layer.dart';
class VideoCardV extends StatelessWidget { class VideoCardV extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final int crossAxisCount; final int crossAxisCount;
final Function? blockUserCb; final Function()? longPress;
final Function()? longPressEnd;
const VideoCardV({ const VideoCardV({
Key? key, Key? key,
required this.videoItem, required this.videoItem,
required this.crossAxisCount, required this.crossAxisCount,
this.blockUserCb, this.longPress,
this.longPressEnd,
}) : super(key: key); }) : super(key: key);
bool isStringNumeric(String str) { bool isStringNumeric(String str) {
@ -44,11 +44,23 @@ class VideoCardV extends StatelessWidget {
return; return;
} }
int epId = videoItem.param; int epId = videoItem.param;
RoutePush.bangumiPush( SmartDialog.showLoading(msg: '资源获取中');
null, var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
epId, if (result['status']) {
heroTag: heroTag, var bangumiDetail = result['data'];
); int cid = bangumiDetail.episodes!.first.cid;
String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
SmartDialog.dismiss().then(
(value) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: {
'pic': videoItem.pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
);
}
break; break;
case 'av': case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid); String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
@ -61,13 +73,17 @@ class VideoCardV extends StatelessWidget {
// 动态 // 动态
case 'picture': case 'picture':
try { try {
String dynamicType = 'picture';
String uri = videoItem.uri; String uri = videoItem.uri;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) { if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554 // https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+'); RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!; Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!; String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber); videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
} }
if (uri.startsWith('http')) { if (uri.startsWith('http')) {
String path = Uri.parse(uri).path; String path = Uri.parse(uri).path;
@ -85,10 +101,11 @@ class VideoCardV extends StatelessWidget {
return; return;
} }
} }
Get.toNamed('/read', parameters: { Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title, 'title': videoItem.title,
'id': videoItem.param, 'id': id,
'articleType': 'read' 'dynamicType': dynamicType
}); });
} catch (err) { } catch (err) {
SmartDialog.showToast(err.toString()); SmartDialog.showToast(err.toString());
@ -110,57 +127,64 @@ class VideoCardV extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id); String heroTag = Utils.makeHeroTag(videoItem.id);
return InkWell( return Card(
onTap: () async => onPushDetail(heroTag), elevation: 0,
onLongPress: () => imageSaveDialog( clipBehavior: Clip.hardEdge,
context, margin: EdgeInsets.zero,
videoItem, child: GestureDetector(
SmartDialog.dismiss, onLongPress: () {
), if (longPress != null) {
borderRadius: BorderRadius.circular(16), longPress!();
child: Column( }
children: [ },
AspectRatio( // onLongPressEnd: (details) {
aspectRatio: StyleString.aspectRatio, // if (longPressEnd != null) {
child: LayoutBuilder(builder: (context, boxConstraints) { // longPressEnd!();
double maxWidth = boxConstraints.maxWidth; // }
double maxHeight = boxConstraints.maxHeight; // },
return Stack( child: InkWell(
children: [ onTap: () async => onPushDetail(heroTag),
Hero( child: Column(
tag: heroTag, children: [
child: NetworkImgLayer( AspectRatio(
src: videoItem.pic, aspectRatio: StyleString.aspectRatio,
width: maxWidth, child: LayoutBuilder(builder: (context, boxConstraints) {
height: maxHeight, double maxWidth = boxConstraints.maxWidth;
), double maxHeight = boxConstraints.maxHeight;
), return Stack(
if (videoItem.duration > 0) children: [
if (crossAxisCount == 1) ...[ Hero(
PBadge( tag: heroTag,
bottom: 10, child: NetworkImgLayer(
right: 10, src: videoItem.pic,
text: Utils.timeFormat(videoItem.duration), width: maxWidth,
) height: maxHeight,
] else ...[ ),
PBadge( ),
bottom: 6, if (videoItem.duration > 0)
right: 7, if (crossAxisCount == 1) ...[
size: 'small', PBadge(
type: 'gray', bottom: 10,
text: Utils.timeFormat(videoItem.duration), right: 10,
) text: Utils.timeFormat(videoItem.duration),
)
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: Utils.timeFormat(videoItem.duration),
)
],
], ],
], );
); }),
}), ),
VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
],
), ),
VideoContent( ),
videoItem: videoItem,
crossAxisCount: crossAxisCount,
blockUserCb: blockUserCb,
)
],
), ),
); );
} }
@ -169,101 +193,125 @@ class VideoCardV extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final int crossAxisCount; final int crossAxisCount;
final Function? blockUserCb; const VideoContent(
{Key? key, required this.videoItem, required this.crossAxisCount})
const VideoContent({ : super(key: key);
Key? key,
required this.videoItem,
required this.crossAxisCount,
this.blockUserCb,
}) : super(key: key);
Widget _buildBadge(String text, String type, [double fs = 12]) {
return PBadge(
text: text,
stack: 'normal',
size: 'small',
type: type,
fs: fs,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Expanded(
padding: crossAxisCount == 1 flex: crossAxisCount == 1 ? 0 : 1,
? const EdgeInsets.fromLTRB(9, 9, 9, 4) child: Padding(
: const EdgeInsets.fromLTRB(5, 8, 5, 4), padding: crossAxisCount == 1
child: Column( ? const EdgeInsets.fromLTRB(9, 9, 9, 4)
crossAxisAlignment: CrossAxisAlignment.start, : const EdgeInsets.fromLTRB(5, 8, 5, 4),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
videoItem.title, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
maxLines: 2, children: [
overflow: TextOverflow.ellipsis, Row(
), children: [
if (crossAxisCount > 1) ...[ Expanded(
const SizedBox(height: 2), child: Text(
VideoStat(videoItem: videoItem, crossAxisCount: crossAxisCount), videoItem.title,
], maxLines: 2,
if (crossAxisCount == 1) const SizedBox(height: 4), overflow: TextOverflow.ellipsis,
Row(
children: [
if (videoItem.goto == 'bangumi')
_buildBadge(videoItem.bangumiBadge, 'line', 9),
if (videoItem.rcmdReason != null)
_buildBadge(videoItem.rcmdReason, 'color'),
if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),
if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
), ),
), ),
), if (videoItem.goto == 'av' && crossAxisCount == 1) ...[
if (crossAxisCount == 1) ...[ const SizedBox(width: 10),
const SizedBox(width: 10), VideoPopupMenu(
VideoStat( size: 32,
videoItem: videoItem, iconSize: 18,
crossAxisCount: crossAxisCount, videoItem: videoItem,
), ),
const Spacer(), ],
], ],
if (videoItem.goto == 'av') ),
SizedBox( if (crossAxisCount > 1) ...[
width: 24, const SizedBox(height: 2),
height: 24, VideoStat(
child: IconButton( videoItem: videoItem,
padding: EdgeInsets.zero, crossAxisCount: crossAxisCount,
onPressed: () { ),
feedBack(); ],
showModalBottomSheet( if (crossAxisCount == 1) const SizedBox(height: 4),
context: context, Row(
useRootNavigator: true, children: [
isScrollControlled: true, if (videoItem.goto == 'bangumi') ...[
builder: (context) { PBadge(
return MorePanel( text: videoItem.bangumiBadge,
videoItem: videoItem, stack: 'normal',
blockUserCb: blockUserCb, size: 'small',
); type: 'line',
}, fs: 9,
); )
}, ],
icon: Icon( if (videoItem.rcmdReason != null &&
Icons.more_vert_outlined, videoItem.rcmdReason.content != '') ...[
PBadge(
text: videoItem.rcmdReason.content,
stack: 'normal',
size: 'small',
type: 'color',
)
],
if (videoItem.goto == 'picture') ...[
const PBadge(
text: '动态',
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
if (videoItem.isFollowed == 1) ...[
const PBadge(
text: '已关注',
stack: 'normal',
size: 'small',
type: 'color',
)
],
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 14,
), ),
), ),
) ),
], if (crossAxisCount == 1) ...[
), Text(
], '',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
const Spacer(),
],
if (videoItem.goto == 'av' && crossAxisCount != 1) ...[
VideoPopupMenu(
size: 24,
iconSize: 14,
videoItem: videoItem,
),
] else ...[
const SizedBox(height: 24)
]
],
),
],
),
), ),
); );
} }
@ -283,10 +331,15 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
if (videoItem.stat.view != null) StatView(view: videoItem.stat.view), StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8), const SizedBox(width: 8),
if (videoItem.stat.danmu != null) StatDanMu(
StatDanMu(danmu: videoItem.stat.danmu), theme: 'gray',
danmu: videoItem.stat.danmu,
),
if (videoItem is RecVideoItemModel) ...<Widget>[ if (videoItem is RecVideoItemModel) ...<Widget>[
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText( RichText(
@ -305,101 +358,99 @@ class VideoStat extends StatelessWidget {
} }
} }
class MorePanel extends StatelessWidget { class VideoPopupMenu extends StatelessWidget {
final double? size;
final double? iconSize;
final dynamic videoItem; final dynamic videoItem;
final Function? blockUserCb;
const MorePanel({ const VideoPopupMenu({
super.key, Key? key,
required this.size,
required this.iconSize,
required this.videoItem, required this.videoItem,
this.blockUserCb, }) : super(key: key);
});
Future<dynamic> menuActionHandler(String type) async {
switch (type) {
case 'block':
Get.back();
blockUser();
break;
case 'watchLater':
var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
Get.back();
break;
default:
}
}
void blockUser() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
blockUserCb?.call(videoItem.owner.mid);
}
SmartDialog.showToast(res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return SizedBox(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), width: size,
child: Column( height: size,
mainAxisSize: MainAxisSize.min, child: PopupMenuButton<String>(
children: [ padding: EdgeInsets.zero,
const DragHandle(), icon: Icon(
ListTile( Icons.more_vert_outlined,
onTap: () async => await menuActionHandler('block'), color: Theme.of(context).colorScheme.outline,
minLeadingWidth: 0, size: iconSize,
leading: const Icon(Icons.block, size: 19), ),
title: Text( position: PopupMenuPosition.under,
'拉黑up主 「${videoItem.owner.name}', // constraints: const BoxConstraints(maxHeight: 35),
style: Theme.of(context).textTheme.titleSmall, onSelected: (String type) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res =
await UserHttp.toViewLater(bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 40,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
), ),
), ),
ListTile( const PopupMenuDivider(),
onTap: () async => await menuActionHandler('watchLater'), PopupMenuItem<String>(
minLeadingWidth: 0, onTap: () async {
leading: const Icon(Icons.watch_later_outlined, size: 19), SmartDialog.show(
title: useSystem: true,
Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall), animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(
'确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
SmartDialog.showToast(res['msg'] ?? '成功');
},
child: const Text('确认'),
)
],
);
},
);
},
value: 'pause',
height: 40,
child: Row(
children: [
const Icon(Icons.block, size: 16),
const SizedBox(width: 6),
Text('拉黑:${videoItem.owner.name}',
style: const TextStyle(fontSize: 13))
],
),
), ),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
], ],
), ),
); );

View File

@ -104,7 +104,7 @@ class Api {
// 评论列表 // 评论列表
// 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/main'; static const String replyList = '/x/v2/reply';
// 楼中楼 // 楼中楼
static const String replyReplyList = '/x/v2/reply/reply'; static const String replyReplyList = '/x/v2/reply/reply';
@ -175,7 +175,7 @@ class Api {
static const String delHistory = '/x/v2/history/delete'; static const String delHistory = '/x/v2/history/delete';
// 搜索历史记录 // 搜索历史记录
static const String searchHistory = '/x/web-interface/history/search'; static const String searchHistory = '/x/web-goblin/history/search';
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
@ -189,7 +189,7 @@ class Api {
'https://s.search.bilibili.com/main/suggest'; 'https://s.search.bilibili.com/main/suggest';
// 分类搜索 // 分类搜索
static const String searchByType = '/x/web-interface/wbi/search/type'; static const String searchByType = '/x/web-interface/search/type';
// 记录视频播放进度 // 记录视频播放进度
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
@ -260,8 +260,7 @@ class Api {
static const String memberArchive = '/x/space/wbi/arc/search'; static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索 // 用户动态搜索
static const String memberDynamicSearch = static const String memberDynamicSearch = '/x/space/dynamic/search';
'/x/polymer/web-dynamic/v1/feed/space/search';
// 用户动态 // 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space'; static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -302,6 +301,10 @@ class Api {
static const String bangumiList = static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的订阅
static const String bangumiFollow =
'/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
@ -397,24 +400,12 @@ class Api {
'${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web'; '${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web';
// web端短信验证码 // web端短信验证码
static const String webSmsCode = static const String smsCode =
'${HttpString.passBaseUrl}/x/passport-login/web/sms/send'; '${HttpString.passBaseUrl}/x/passport-login/web/sms/send';
// web端验证码登录 // web端验证码登录
static const String webSmsLogin =
'${HttpString.passBaseUrl}/x/passport-login/web/login/sms';
// web端密码登录 // web端密码登录
static const String loginInByWebPwd =
'${HttpString.passBaseUrl}/x/passport-login/web/login';
// web端二维码
static const String qrCodeApi =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/generate';
// 扫码登录
static const String loginInByQrcode =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/poll';
// app端短信验证码 // app端短信验证码
static const String appSmsCode = static const String appSmsCode =
@ -484,8 +475,6 @@ class Api {
static const getSeasonDetailApi = static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list'; '/x/polymer/web-space/seasons_archives_list';
static const getSeriesDetailApi = '/x/series/archives';
/// 获取未读动态数 /// 获取未读动态数
static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';
@ -495,17 +484,11 @@ class Api {
/// 激活buvid3 /// 激活buvid3
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
/// 获取字幕配置
static const getSubtitleConfig = '/x/player/wbi/v2';
/// 我的订阅 /// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list'; static const userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情 type 21 /// 我的订阅详情
static const userSeasonList = '/x/space/fav/season/list'; static const userSubFolderDetail = '/x/space/fav/season/list';
/// 我的订阅详情 type 11
static const userResourceList = '/x/v3/fav/resource/list';
/// 表情 /// 表情
static const emojiList = '/x/emote/user/panel/web'; static const emojiList = '/x/emote/user/panel/web';
@ -520,114 +503,4 @@ class Api {
/// 排行榜 /// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2"; static const String getRankApi = "/x/web-interface/ranking/v2";
/// 取消订阅
static const String cancelSub = '/x/v3/fav/season/unfav';
/// 动态转发
static const String dynamicForwardUrl = '/x/dynamic/feed/create/submit_check';
/// 创建动态
static const String dynamicCreate = '/x/dynamic/feed/create/dyn';
/// 删除收藏夹
static const String delFavFolder = '/x/v3/fav/folder/del';
/// 搜索结果计数
static const String searchCount = '/x/web-interface/wbi/search/all/v2';
/// 关闭会话
static const String removeSession =
'${HttpString.tUrl}/session_svr/v1/session_svr/remove_session';
/// 消息未读数
static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread';
/// 回复我的
static const String messageReplyAPi = '/x/msgfeed/reply';
/// 收到的赞
static const String messageLikeAPi = '/x/msgfeed/like';
/// 系统通知
static const String messageSystemAPi =
'${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify';
/// 系统通知 个人
static const String userMessageSystemAPi =
'${HttpString.messageBaseUrl}/x/sys-msg/query_user_notify';
/// 系统通知标记已读
static const String systemMarkRead =
'${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';
/// 编辑收藏夹
static const String editFavFolder = '/x/v3/fav/folder/edit';
/// 新建收藏夹
static const String addFavFolder = '/x/v3/fav/folder/add';
/// 直播间弹幕信息
static const String getDanmuInfo =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';
/// 直播间发送弹幕
static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
/// 我的关注 - 正在直播
static const String getFollowingLive =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
/// 稍后再看&收藏夹视频列表
static const String mediaList = '/x/v2/medialist/resource/list';
/// 用户专栏
static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';
///
static const String getViewInfo = '/x/article/viewinfo';
/// 直播间记录
static const String liveRoomEntry =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';
/// 用户信息
static const String accountInfo = '/x/member/web/account';
/// 更新用户信息
static const String updateAccountInfo = '/x/member/web/update';
/// 删除评论
static const String replyDel = '/x/v2/reply/del';
/// 图片上传
static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs';
/// 更新追番状态
static const String updateBangumiStatus = '/pgc/web/follow/status/update';
/// 番剧点赞投币收藏状态
static const String bangumiActionStatus = '/pgc/season/episode/community';
/// @我的
static const String messageAtAPi = '/x/msgfeed/at?';
/// 订阅
static const String confirmSub = '/x/v3/fav/season/fav';
/// 订阅状态
static const String videoRelation = '/x/web-interface/archive/relation';
/// 获取空降区间
static const String getSkipSegments =
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments';
/// 视频标签
static const String videoTag = '/x/tag/archive/tags';
/// 修复标题和海报
// /api/view?id=${aid} /all/video/av${aid} /video/av${aid}/
static const String fixTitleAndPic = '${HttpString.biliplusBaseUrl}/api/view';
/// 专栏详情
static const String opusDetail = '/x/polymer/web-dynamic/v1/opus/detail';
} }

View File

@ -1,8 +1,5 @@
import 'dart:convert';
import '../models/bangumi/list.dart'; import '../models/bangumi/list.dart';
import 'index.dart'; import 'index.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:html/dom.dart' as html_dom;
class BangumiHttp { class BangumiHttp {
static Future bangumiList({int? page}) async { static Future bangumiList({int? page}) async {
@ -21,19 +18,8 @@ class BangumiHttp {
} }
} }
static Future getRecentBangumi({ static Future bangumiFollow({int? mid}) async {
int? mid, var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
int type = 1,
int pn = 1,
int ps = 20,
}) async {
var res = await Request().get(Api.getRecentBangumiApi, data: {
'vmid': mid,
'type': type,
'follow_status': 0,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
@ -47,62 +33,4 @@ class BangumiHttp {
}; };
} }
} }
// 获取追番状态
static Future bangumiStatus({required int seasonId}) async {
var res = await Request()
.get('https://www.bilibili.com/bangumi/play/ss$seasonId');
html_dom.Document document = html_parser.parse(res.data);
// 查找 id 为 __NEXT_DATA__ 的 script 元素
html_dom.Element? scriptElement =
document.querySelector('script#\\__NEXT_DATA__');
if (scriptElement != null) {
// 提取 script 元素的内容
String scriptContent = scriptElement.text;
final dynamic scriptContentJson = jsonDecode(scriptContent);
Map followState = scriptContentJson['props']['pageProps']['followState'];
return {
'status': true,
'data': {
'isFollowed': followState['isFollowed'],
'followStatus': followState['followStatus']
}
};
} else {
print('Script element with id "__NEXT_DATA__" not found.');
}
}
// 更新追番状态
static Future updateBangumiStatus({
required int seasonId,
required int status,
}) async {
var res = await Request().post(Api.updateBangumiStatus, data: {
'season_id': seasonId,
'status': status,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取番剧点赞投币收藏状态
static Future bangumiActionStatus({required int epId}) async {
var res = await Request().get(
Api.bangumiActionStatus,
data: {'ep_id': epId},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -28,7 +28,7 @@ class BlackHttp {
static Future removeBlack({required int fid}) async { static Future removeBlack({required int fid}) async {
var res = await Request().post( var res = await Request().post(
Api.removeBlack, Api.removeBlack,
data: { queryParameters: {
'act': 6, 'act': 6,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
'fid': fid, 'fid': fid,

View File

@ -1,15 +1,6 @@
import 'package:pilipala/models/common/invalid_video.dart';
import 'dart:convert';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/models/sponsor_block/segment.dart';
import 'index.dart'; import 'index.dart';
class CommonHttp { class CommonHttp {
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
static Future unReadDynamic() async { static Future unReadDynamic() async {
var res = await Request().get(Api.getUnreadDynamic, var res = await Request().get(Api.getUnreadDynamic,
data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0}); data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0});
@ -23,95 +14,4 @@ class CommonHttp {
}; };
} }
} }
static Future querySkipSegments({required String bvid}) async {
var res = await Request().getWithoutCookie(Api.getSkipSegments, data: {
'videoID': bvid,
});
if (res.data is List && res.data.isNotEmpty) {
try {
return {
'status': true,
'data': res.data
.map<SegmentDataModel>((e) => SegmentDataModel.fromJson(e))
.toList(),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': 'sponsorBlock数据解析失败: $err',
};
}
} else {
return {
'status': false,
'data': [],
};
}
}
static Future fixVideoPicAndTitle({required int aid}) async {
var res = await Request().getWithoutCookie(Api.fixTitleAndPic, data: {
'id': aid,
});
if (res != null) {
if (res.data['code'] == -404) {
return {
'status': false,
'data': null,
'msg': '没有相关信息',
};
} else {
return {
'status': true,
'data': InvalidVideoModel.fromJson(res.data),
};
}
} else {
return {
'status': false,
'data': null,
'msg': '没有相关信息',
};
}
}
static Future buvidActivate() async {
try {
// 获取 HTML 数据
var html = await Request().get(Api.dynamicSpmPrefix);
// 提取 spmPrefix
String spmPrefix = spmPrefixExp.firstMatch(html.data)?.group(1) ?? '';
// 生成随机 PNG 结束部分
Random rand = Random();
String randPngEnd = base64.encode(
List<int>.generate(32, (_) => rand.nextInt(256))
..addAll(List<int>.filled(4, 0))
..addAll([73, 69, 78, 68])
..addAll(List<int>.generate(4, (_) => rand.nextInt(256))),
);
// 构建 JSON 数据
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
// 发送 POST 请求
await Request().post(
Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: 'application/json'),
);
} catch (err) {
debugPrint('buvidActivate error: $err');
}
}
} }

View File

@ -5,11 +5,6 @@ class HttpString {
static const String appBaseUrl = 'https://app.bilibili.com'; static const String appBaseUrl = 'https://app.bilibili.com';
static const String liveBaseUrl = 'https://api.live.bilibili.com'; static const String liveBaseUrl = 'https://api.live.bilibili.com';
static const String passBaseUrl = 'https://passport.bilibili.com'; static const String passBaseUrl = 'https://passport.bilibili.com';
static const String messageBaseUrl = 'https://message.bilibili.com';
static const String bangumiBaseUrl = 'https://bili.meark.me';
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
static const String biliplusBaseUrl = 'https://www.biliplus.com';
static const List<int> validateStatusCodes = [ static const List<int> validateStatusCodes = [
302, 302,
304, 304,

View File

@ -17,9 +17,7 @@ class DanmakaHttp {
var response = await Request().get( var response = await Request().get(
Api.webDanmaku, Api.webDanmaku,
data: params, data: params,
options: Options( extra: {'resType': ResponseType.bytes},
responseType: ResponseType.bytes,
),
); );
return DmSegMobileReply.fromBuffer(response.data); return DmSegMobileReply.fromBuffer(response.data);
} }
@ -69,6 +67,9 @@ class DanmakaHttp {
var response = await Request().post( var response = await Request().post(
Api.shootDanmaku, Api.shootDanmaku,
data: params, data: params,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
return { return {

View File

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:dio/dio.dart';
import '../models/dynamics/result.dart'; import '../models/dynamics/result.dart';
import '../models/dynamics/up.dart'; import '../models/dynamics/up.dart';
import 'index.dart'; import 'index.dart';
@ -42,7 +40,6 @@ class DynamicsHttp {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'], 'msg': res.data['message'],
'code': res.data['code'],
}; };
} }
} }
@ -70,7 +67,7 @@ class DynamicsHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.likeDynamic, Api.likeDynamic,
data: { queryParameters: {
'dynamic_id': dynamicId, 'dynamic_id': dynamicId,
'up': up, 'up': up,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -92,7 +89,7 @@ class DynamicsHttp {
// //
static Future dynamicDetail({ static Future dynamicDetail({
required String id, String? id,
}) async { }) async {
var res = await Request().get(Api.dynamicDetail, data: { var res = await Request().get(Api.dynamicDetail, data: {
'timezone_offset': -480, 'timezone_offset': -480,
@ -120,120 +117,4 @@ class DynamicsHttp {
}; };
} }
} }
static Future dynamicForward() async {
var res = await Request().post(
Api.dynamicForwardUrl,
queryParameters: {
'csrf': await Request.getCsrf(),
'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},
'x-bili-web-req-json': {'spm_id': '333.999'},
},
data: {
'attach_card': null,
'scene': 4,
'content': {
'conetents': [
{'raw_text': "2", 'type': 1, 'biz_id': ""}
]
}
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
static Future dynamicCreate({
required int mid,
required int scene,
int? oid,
String? dynIdStr,
String? rawText,
}) async {
DateTime now = DateTime.now();
int timestamp = now.millisecondsSinceEpoch ~/ 1000;
Random random = Random();
int randomNumber = random.nextInt(9000) + 1000;
String uploadId = '${mid}_${timestamp}_$randomNumber';
Map<String, dynamic> webRepostSrc = {
'dyn_id_str': dynIdStr ?? '',
};
/// 投稿转发
if (scene == 5) {
webRepostSrc = {
'revs_id': {'dyn_type': 8, 'rid': oid}
};
}
var res = await Request().post(
Api.dynamicCreate,
queryParameters: {
'platform': 'web',
'csrf': await Request.getCsrf(),
'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},
'x-bili-web-req-json': {'spm_id': '333.999'},
},
data: {
'dyn_req': {
'content': {
'contents': [
{'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}
]
},
'scene': scene,
'attach_card': null,
'upload_id': uploadId,
'meta': {
'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}
}
},
'web_repost_src': webRepostSrc
},
options: Options(contentType: 'application/json'),
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
static Future opusDetail({
required int opusId,
}) async {
var res = await Request().get(
Api.opusDetail,
data: {'id': opusId},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -1,67 +0,0 @@
import 'index.dart';
class FavHttp {
/// 编辑收藏夹
static Future editFolder({
required String title,
required String intro,
required String mediaId,
String? cover,
int? privacy,
}) async {
var res = await Request().post(
Api.editFavFolder,
data: {
'title': title,
'intro': intro,
'media_id': mediaId,
'cover': cover ?? '',
'privacy': privacy ?? 0,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
/// 新建收藏夹
static Future addFolder({
required String title,
required String intro,
String? cover,
int? privacy,
}) async {
var res = await Request().post(
Api.addFavFolder,
data: {
'title': title,
'intro': intro,
'cover': cover ?? '',
'privacy': privacy ?? 0,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -21,6 +21,7 @@ class HtmlHttp {
} }
try { try {
Document rootTree = parse(response.data); Document rootTree = parse(response.data);
// log(response.data.body.toString());
Element body = rootTree.body!; Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!; Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!; Element authorHeader = appDom.querySelector('.fixed-author-header')!;
@ -51,6 +52,7 @@ class HtmlHttp {
.className .className
.split(' ')[1] .split(' ')[1]
.split('-')[2]; .split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return { return {
'status': true, 'status': true,
'avatar': avatar, 'avatar': avatar,
@ -74,10 +76,20 @@ class HtmlHttp {
Element body = rootTree.body!; Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!; Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!; 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(); String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情 // 动态详情
Element opusDetail = appDom.querySelector('.article-content')!; Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间 // 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent = String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml; opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+'); RegExp digitRegExp = RegExp(r'\d+');

View File

@ -1,14 +1,19 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' show Random;
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/utils.dart'; import '../utils/utils.dart';
import 'api.dart';
import 'constants.dart'; import 'constants.dart';
import 'interceptor.dart'; import 'interceptor.dart';
@ -17,17 +22,16 @@ 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 = GStorage.setting; Box setting = GStrorage.setting;
static Box localCache = GStorage.localCache; static Box localCache = GStrorage.localCache;
late bool enableSystemProxy; late bool enableSystemProxy;
late String systemProxyHost; late String systemProxyHost;
late String systemProxyPort; late String systemProxyPort;
static String? buvid; static final RegExp spmPrefixExp = RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
/// 设置cookie /// 设置cookie
static setCookie() async { static setCookie() async {
Box userInfoCache = GStorage.userInfo; Box userInfoCache = GStrorage.userInfo;
Box setting = GStorage.setting;
final String cookiePath = await Utils.getCookiePath(); final String cookiePath = await Utils.getCookiePath();
final PersistCookieJar cookieJar = PersistCookieJar( final PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true, ignoreExpires: true,
@ -38,56 +42,40 @@ class Request {
final List<Cookie> cookie = await cookieManager.cookieJar final List<Cookie> cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)); .loadForRequest(Uri.parse(HttpString.baseUrl));
final userInfo = userInfoCache.get('userInfoCache'); final userInfo = userInfoCache.get('userInfoCache');
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null); if (userInfo != null && userInfo.mid != null) {
String baseUrlType = 'default'; final List<Cookie> cookie2 = await cookieManager.cookieJar
if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) { .loadForRequest(Uri.parse(HttpString.tUrl));
baseUrlType = 'bangumi'; if (cookie2.isEmpty) {
try {
await Request().get(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
}
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
try {
await buvidActivate();
} catch (e) {
log("setCookie, ${e.toString()}");
} }
setBaseUrl(type: baseUrlType);
final String cookieString = cookie final String cookieString = cookie
.map((Cookie cookie) => '${cookie.name}=${cookie.value}') .map((Cookie cookie) => '${cookie.name}=${cookie.value}')
.join('; '); .join('; ');
dio.options.headers['cookie'] = cookieString; dio.options.headers['cookie'] = cookieString;
} }
// 从cookie中获取 csrf token // 从cookie中获取 csrf token
static Future<String> getCsrf() async { static Future<String> getCsrf() async {
List<Cookie> cookies = await cookieManager.cookieJar List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
String token = cookies
.firstWhere((e) => e.name == 'bili_jct',
orElse: () => Cookie('bili_jct', ''))
.value;
return token;
}
static Future<String> getBuvid() async {
if (buvid != null) {
return buvid!;
}
final List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.apiBaseUrl)); .loadForRequest(Uri.parse(HttpString.apiBaseUrl));
buvid = cookies String token = '';
.firstWhere((cookie) => cookie.name == 'buvid3', if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
orElse: () => Cookie('buvid3', '')) token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
.value;
if (buvid == null || buvid!.isEmpty) {
try {
var result = await Request().get(
"${HttpString.apiBaseUrl}/x/frontend/finger/spi",
);
buvid = result["data"]["b_3"].toString();
} catch (e) {
// 处理请求错误
buvid = '';
print("Error fetching buvid: $e");
}
} }
return token;
return buvid!;
} }
static setOptionsHeaders(userInfo, bool status) { static setOptionsHeaders(userInfo, bool status) {
@ -102,6 +90,33 @@ class Request {
dio.options.headers['referer'] = 'https://www.bilibili.com/'; dio.options.headers['referer'] = 'https://www.bilibili.com/';
} }
static Future buvidActivate() async {
var html = await Request().get(Api.dynamicSpmPrefix);
String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
Random rand = Random();
String rand_png_end = base64.encode(
List<int>.generate(32, (_) => rand.nextInt(256)) +
List<int>.filled(4, 0) +
[73, 69, 78, 68] +
List<int>.generate(4, (_) => rand.nextInt(256))
);
String jsonData = json.encode({
'3064': 1,
'39c8': '${spmPrefix}.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': rand_png_end.substring(rand_png_end.length - 50),
},
});
await Request().post(
Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: 'application/json')
);
}
/* /*
* config it and create * config it and create
*/ */
@ -127,6 +142,15 @@ class Request {
dio = Dio(options); dio = Dio(options);
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
// ..httpClientAdapter = Http2Adapter(
// ConnectionManager(
// idleTimeout: const Duration(milliseconds: 10000),
// onClientCreate: (_, ClientSetting config) =>
// config.onBadCertificate = (_) => true,
// ),
// );
/// 设置代理 /// 设置代理
if (enableSystemProxy) { if (enableSystemProxy) {
dio.httpClientAdapter = IOHttpClientAdapter( dio.httpClientAdapter = IOHttpClientAdapter(
@ -164,15 +188,18 @@ class Request {
/* /*
* get请求 * get请求
*/ */
get(url, {data, Options? options, cancelToken, extra}) async { get(url, {data, options, cancelToken, extra}) async {
Response response; Response response;
final Options options = Options();
ResponseType resType = ResponseType.json;
if (extra != null) { if (extra != null) {
resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) { if (extra['ua'] != null) {
options ??= Options(); options.headers = {'user-agent': headerUa(type: extra['ua'])};
options.headers ??= <String, dynamic>{};
options.headers?['user-agent'] = headerUa(type: extra['ua']);
} }
} }
options.responseType = resType;
try { try {
response = await dio.get( response = await dio.get(
url, url,
@ -182,44 +209,32 @@ class Request {
); );
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
return Response( Response errResponse = Response(
data: {'message': await ApiInterceptor.dioError(e)}, data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200, statusCode: 200,
requestOptions: RequestOptions(), requestOptions: RequestOptions(),
); );
return errResponse;
} }
} }
/*
* get请求
*/
getWithoutCookie(url, {data}) {
return get(
url,
data: data,
options: Options(
headers: {
'cookie': 'buvid3= ; b_nut= ; sid= ',
'user-agent': headerUa(type: 'pc'),
},
),
);
}
/* /*
* post请求 * post请求
*/ */
post(url, {data, queryParameters, options, cancelToken, extra}) async { post(url, {data, queryParameters, options, cancelToken, extra}) async {
// print('post-data: $data');
Response response; Response response;
try { try {
response = await dio.post( response = await dio.post(
url, url,
data: data, data: data,
queryParameters: queryParameters, queryParameters: queryParameters,
options: options: options,
options ?? Options(contentType: Headers.formUrlEncodedContentType),
cancelToken: cancelToken, cancelToken: cancelToken,
); );
// print('post success: ${response.data}');
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
Response errResponse = Response( Response errResponse = Response(
@ -275,21 +290,8 @@ class Request {
} }
} else { } else {
headerUa = headerUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.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;
} }
static setBaseUrl({String type = 'default'}) {
switch (type) {
case 'default':
dio.options.baseUrl = HttpString.apiBaseUrl;
break;
case 'bangumi':
dio.options.baseUrl = HttpString.bangumiBaseUrl;
break;
default:
dio.options.baseUrl = HttpString.apiBaseUrl;
}
}
} }

View File

@ -3,7 +3,8 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
// import 'package:pilipala/utils/login.dart'; import 'package:hive/hive.dart';
import '../utils/storage.dart';
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@ -18,10 +19,21 @@ class ApiInterceptor extends Interceptor {
@override @override
void onResponse(Response response, ResponseInterceptorHandler handler) { void onResponse(Response response, ResponseInterceptorHandler handler) {
try { try {
// 在响应之后处理数据 if (response.statusCode == 302) {
// if (response.data is Map && response.data['code'] == -101) { final List<String> locations = response.headers['location']!;
// LoginUtils.loginOut(); if (locations.isNotEmpty) {
// } if (locations.first.startsWith('https://www.mcbbs.net')) {
final Uri uri = Uri.parse(locations.first);
final String? accessKey = uri.queryParameters['access_key'];
final String? mid = uri.queryParameters['mid'];
try {
Box localCache = GStrorage.localCache;
localCache.put(LocalCacheKey.accessKey,
<String, String?>{'mid': mid, 'value': accessKey});
} catch (_) {}
}
}
}
} catch (err) { } catch (err) {
print('ApiInterceptor: $err'); print('ApiInterceptor: $err');
} }
@ -34,13 +46,11 @@ class ApiInterceptor extends Interceptor {
// 处理网络请求错误 // 处理网络请求错误
// handler.next(err); // handler.next(err);
String url = err.requestOptions.uri.toString(); String url = err.requestOptions.uri.toString();
final excludedPatterns = RegExp(r'heartbeat|seg\.so|online/total'); print('🌹🌹ApiInterceptor: $url');
if (!excludedPatterns.hasMatch(url)) { if (!url.contains('heartBeat')) {
SmartDialog.showToast( SmartDialog.showToast(
await dioError(err), await dioError(err),
displayType: SmartToastType.onlyRefresh, displayType: SmartToastType.onlyRefresh,
displayTime: const Duration(seconds: 1),
debounce: true,
); );
} }
super.onError(err, handler); super.onError(err, handler);
@ -64,29 +74,28 @@ class ApiInterceptor extends Interceptor {
return '发送请求超时,请检查网络设置'; return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown: case DioExceptionType.unknown:
final String res = await checkConnect(); final String res = await checkConnect();
return '$res ${error.error}'; return '$res,网络异常!';
} }
} }
static Future<String> checkConnect() async { static Future<String> checkConnect() async {
final List<ConnectivityResult> connectivityResult = final ConnectivityResult connectivityResult =
await Connectivity().checkConnectivity(); await Connectivity().checkConnectivity();
if (connectivityResult.contains(ConnectivityResult.mobile)) { switch (connectivityResult) {
return '正在使用移动流量'; case ConnectivityResult.mobile:
} else if (connectivityResult.contains(ConnectivityResult.wifi)) { return '正在使用移动流量';
return '正在使用wifi'; case ConnectivityResult.wifi:
} else if (connectivityResult.contains(ConnectivityResult.ethernet)) { return '正在使用wifi';
return '正在使用局域网'; case ConnectivityResult.ethernet:
} else if (connectivityResult.contains(ConnectivityResult.vpn)) { return '正在使用局域网';
return '正在使用代理网络'; case ConnectivityResult.vpn:
} else if (connectivityResult.contains(ConnectivityResult.bluetooth)) { return '正在使用代理网络';
return '正在使用蓝牙网络'; case ConnectivityResult.other:
} else if (connectivityResult.contains(ConnectivityResult.other)) { return '正在使用其他网络';
return '正在使用其他网络'; case ConnectivityResult.none:
} else if (connectivityResult.contains(ConnectivityResult.none)) { return '未连接到任何网络';
return '未连接到任何网络'; default:
} else { return '';
return '';
} }
} }
} }

View File

@ -1,5 +1,3 @@
import 'package:pilipala/models/live/follow.dart';
import '../models/live/item.dart'; import '../models/live/item.dart';
import '../models/live/room_info.dart'; import '../models/live/room_info.dart';
import '../models/live/room_info_h5.dart'; import '../models/live/room_info_h5.dart';
@ -67,96 +65,4 @@ class LiveHttp {
}; };
} }
} }
// 获取弹幕信息
static Future liveDanmakuInfo({roomId}) async {
var res = await Request().get(Api.getDanmuInfo, data: {
'id': roomId,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 发送弹幕
static Future sendDanmaku({roomId, msg}) async {
var res = await Request().post(
Api.sendLiveMsg,
data: {
'bubble': 0,
'msg': msg,
'color': 16777215, // 颜色
'mode': 1, // 模式
'room_type': 0,
'jumpfrom': 71001, // 直播间来源
'reply_mid': 0,
'reply_attr': 0,
'replay_dmid': '',
'statistics': {"appId": 100, "platform": 5},
'fontsize': 25, // 字体大小
'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳
'roomid': roomId,
'csrf': await Request.getCsrf(),
'csrf_token': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 我的关注 正在直播
static Future liveFollowing({int? pn, int? ps}) async {
var res = await Request().get(Api.getFollowingLive, data: {
'page': pn,
'page_size': ps,
'platform': 'web',
'ignoreRecord': 1,
'hit_ab': true,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': LiveFollowingModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 直播历史记录
static Future liveRoomEntry({required int roomId}) async {
await Request().post(
Api.liveRoomEntry,
data: {
'room_id': roomId,
'platform': 'pc',
'csrf_token': await Request.getCsrf(),
'csrf': await Request.getCsrf(),
'visit_id': '',
},
);
}
} }

View File

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:pilipala/http/constants.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/login/index.dart'; import '../models/login/index.dart';
import '../utils/login.dart'; import '../utils/login.dart';
@ -22,32 +21,32 @@ class LoginHttp {
} }
} }
// static Future sendSmsCode({ static Future sendSmsCode({
// int? cid, int? cid,
// required int tel, required int tel,
// required String token, required String token,
// required String challenge, required String challenge,
// required String validate, required String validate,
// required String seccode, required String seccode,
// }) async { }) async {
// var res = await Request().post( var res = await Request().post(
// Api.appSmsCode, Api.appSmsCode,
// data: { data: {
// 'cid': cid, 'cid': cid,
// 'tel': tel, 'tel': tel,
// "source": "main_web", "source": "main_web",
// 'token': token, 'token': token,
// 'challenge': challenge, 'challenge': challenge,
// 'validate': validate, 'validate': validate,
// 'seccode': seccode, 'seccode': seccode,
// }, },
// options: Options( options: Options(
// contentType: Headers.formUrlEncodedContentType, contentType: Headers.formUrlEncodedContentType,
// // headers: {'user-agent': ApiConstants.userAgent} // headers: {'user-agent': ApiConstants.userAgent}
// ), ),
// ); );
// print(res); print(res);
// } }
// web端验证码 // web端验证码
static Future sendWebSmsCode({ static Future sendWebSmsCode({
@ -61,7 +60,6 @@ class LoginHttp {
Map data = { Map data = {
'cid': cid, 'cid': cid,
'tel': tel, 'tel': tel,
"source": "main_web",
'token': token, 'token': token,
'challenge': challenge, 'challenge': challenge,
'validate': validate, 'validate': validate,
@ -69,50 +67,17 @@ class LoginHttp {
}; };
FormData formData = FormData.fromMap({...data}); FormData formData = FormData.fromMap({...data});
var res = await Request().post( var res = await Request().post(
Api.webSmsCode, Api.smsCode,
data: formData, data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (res.data['code'] == 0) { print(res);
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
} }
// web端验证码登录 // web端验证码登录
static Future loginInByWebSmsCode({ static Future loginInByWebSmsCode() async {}
int? cid,
required int tel,
required int code,
required String captchaKey,
}) async {
// webSmsLogin
Map data = {
"cid": cid,
"tel": tel,
"code": code,
"source": "main_mini",
"keep": 0,
"captcha_key": captchaKey,
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.webSmsLogin,
data: formData,
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端密码登录 // web端密码登录
static Future liginInByWebPwd() async {} static Future liginInByWebPwd() async {}
@ -149,6 +114,9 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.appSmsCode, Api.appSmsCode,
data: data, data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
print(res); print(res);
} }
@ -199,82 +167,10 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.loginInByPwdApi, Api.loginInByPwdApi,
data: data, data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
print(res); print(res);
} }
// web端密码登录
static Future loginInByWebPwd({
required int username,
required String password,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map data = {
'username': username,
'password': password,
'keep': 0,
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
'source': 'main-fe-header',
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.loginInByWebPwd,
data: formData,
);
if (res.data['code'] == 0) {
if (res.data['data']['status'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'code': 1,
'data': res.data['data'],
'msg': res.data['data']['message'],
};
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// web端登录二维码
static Future getWebQrcode() async {
var res = await Request().get(Api.qrCodeApi);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端二维码轮询登录状态
static Future queryWebQrcodeStatus(String qrcodeKey) async {
var res = await Request()
.get(Api.loginInByQrcode, data: {'qrcode_key': qrcodeKey});
if (res.data['data']['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -1,11 +1,5 @@
import 'dart:convert';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/member/article.dart';
import 'package:pilipala/models/member/like.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import '../common/constants.dart'; import '../common/constants.dart';
import '../models/dynamics/result.dart'; import '../models/dynamics/result.dart';
import '../models/follow/result.dart'; import '../models/follow/result.dart';
@ -21,20 +15,14 @@ import 'index.dart';
class MemberHttp { class MemberHttp {
static Future memberInfo({ static Future memberInfo({
required int mid, int? mid,
String token = '', String token = '',
}) async { }) async {
String? wWebid;
if ((await getWWebid(mid: mid))['status']) {
wWebid = GlobalDataCache.wWebid;
}
Map params = await WbiSign().makSign({ Map params = await WbiSign().makSign({
'mid': mid, 'mid': mid,
'token': token, 'token': token,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 1550101,
...wWebid != null ? {'w_webid': wWebid} : {},
}); });
var res = await Request().get( var res = await Request().get(
Api.memberInfo, Api.memberInfo,
@ -91,11 +79,6 @@ class MemberHttp {
String order = 'pubdate', String order = 'pubdate',
bool orderAvoided = true, bool orderAvoided = true,
}) async { }) async {
String? wWebid;
if ((await getWWebid(mid: mid!))['status']) {
wWebid = GlobalDataCache.wWebid;
}
String dmImgStr = Utils.base64EncodeRandomString(16, 64); String dmImgStr = Utils.base64EncodeRandomString(16, 64);
String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128); String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
Map params = await WbiSign().makSign({ Map params = await WbiSign().makSign({
@ -112,21 +95,12 @@ class MemberHttp {
'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2), 'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2),
'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2), 'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2),
'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}', 'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}',
...order == 'charge'
? {
'order': 'pubdate',
'special_type': 'charging',
}
: {},
...wWebid != null ? {'w_webid': wWebid} : {},
}); });
var res = await Request().get( var res = await Request().get(
Api.memberArchive, Api.memberArchive,
data: params, data: params,
extra: {'ua': 'pc'}, extra: {'ua': 'pc'},
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
@ -170,17 +144,13 @@ class MemberHttp {
} }
// 搜索用户动态 // 搜索用户动态
static Future memberDynamicSearch({ static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
int? pn, var res = await Request().get(Api.memberDynamic, data: {
int? mid, 'keyword': '海拔',
String? offset, 'mid': mid,
required String keyword, 'pn': pn,
}) async { 'ps': ps,
var res = await Request().get(Api.memberDynamicSearch, data: { 'platform': 'web'
'host_mid': mid,
'page': pn,
'offset': offset,
'keyword': keyword,
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -217,15 +187,13 @@ class MemberHttp {
// 设置分组 // 设置分组
static Future addUsers(int? fids, String? tagids) async { static Future addUsers(int? fids, String? tagids) async {
var res = await Request().post( var res = await Request().post(Api.addUsers, queryParameters: {
Api.addUsers, 'fids': fids,
data: { 'tagids': tagids ?? '0',
'fids': fids, 'csrf': await Request.getCsrf(),
'tagids': tagids ?? '0', }, data: {
'csrf': await Request.getCsrf(), 'cross_domain': true
}, });
queryParameters: {'cross_domain': true},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'}; return {'status': true, 'data': [], 'msg': '操作成功'};
} else { } else {
@ -360,9 +328,7 @@ class MemberHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': res.data['data']['list'] 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
.map<MemberLikeDataModel>((e) => MemberLikeDataModel.fromJson(e))
.toList(),
}; };
} else { } else {
return { return {
@ -443,14 +409,11 @@ class MemberHttp {
static Future cookieToKey() async { static Future cookieToKey() async {
var authCodeRes = await getTVCode(); var authCodeRes = await getTVCode();
if (authCodeRes['status']) { if (authCodeRes['status']) {
var res = await Request().post( var res = await Request().post(Api.cookieToKey, queryParameters: {
Api.cookieToKey, 'auth_code': authCodeRes['data'],
data: { 'build': 708200,
'auth_code': authCodeRes['data'], 'csrf': await Request.getCsrf(),
'build': 708200, });
'csrf': await Request.getCsrf(),
},
);
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
await qrcodePoll(authCodeRes['data']); await qrcodePoll(authCodeRes['data']);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@ -482,11 +445,11 @@ class MemberHttp {
SmartDialog.dismiss(); SmartDialog.dismiss();
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token']; String accessKey = res.data['data']['access_token'];
Box localCache = GStorage.localCache; Box localCache = GStrorage.localCache;
Box userInfoCache = GStorage.userInfo; Box userInfoCache = GStrorage.userInfo;
final UserInfoData? userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
localCache.put( localCache.put(
LocalCacheKey.accessKey, {'mid': userInfo!.mid, 'value': accessKey}); LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});
return {'status': true, 'data': [], 'msg': '操作成功'}; return {'status': true, 'data': [], 'msg': '操作成功'};
} else { } else {
return { return {
@ -547,96 +510,4 @@ class MemberHttp {
}; };
} }
} }
static Future getSeriesDetail({
required int mid,
required int currentMid,
required int seriesId,
required int pn,
}) async {
var res = await Request().get(
Api.getSeriesDetailApi,
data: {
'mid': mid,
'series_id': seriesId,
'only_normal': true,
'sort': 'desc',
'pn': pn,
'ps': 30,
'current_mid': currentMid,
},
);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data'])
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
static Future getWWebid({required int mid}) async {
String? wWebid = GlobalDataCache.wWebid;
if (wWebid != null) {
return {'status': true, 'data': wWebid};
}
var res = await Request().get('https://space.bilibili.com/$mid/article');
String? headContent = parse(res.data).head?.outerHtml;
final regex = RegExp(
r'<script id="__RENDER_DATA__" type="application/json">(.*?)</script>');
if (headContent != null) {
final match = regex.firstMatch(headContent);
if (match != null && match.groupCount >= 1) {
final content = match.group(1);
String decodedString = Uri.decodeComponent(content!);
Map<String, dynamic> map = jsonDecode(decodedString);
GlobalDataCache.wWebid = map['access_id'];
return {'status': true, 'data': map['access_id']};
} else {
return {'status': false, 'data': '请检查登录状态'};
}
}
return {'status': false, 'data': '请检查登录状态'};
}
// 获取用户专栏
static Future getMemberArticle({
required int mid,
required int pn,
String? offset,
}) async {
String? wWebid;
if ((await getWWebid(mid: mid))['status']) {
wWebid = GlobalDataCache.wWebid;
}
Map params = await WbiSign().makSign({
'host_mid': mid,
'page': pn,
'offset': offset,
'web_location': 333.999,
...wWebid != null ? {'w_webid': wWebid} : {},
});
var res = await Request().get(Api.opusList, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberArticleDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'] ?? '请求异常',
};
}
}
} }

View File

@ -1,10 +1,4 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart';
import 'package:pilipala/models/msg/at.dart';
import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/models/msg/system.dart';
import '../models/msg/account.dart'; import '../models/msg/account.dart';
import '../models/msg/session.dart'; import '../models/msg/session.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
@ -65,7 +59,7 @@ class MsgHttp {
.toList(), .toList(),
}; };
} catch (err) { } catch (err) {
debugPrint('err: $err'); print('err🔟: $err');
} }
} else { } else {
return { return {
@ -128,45 +122,68 @@ class MsgHttp {
'data': res.data['data'], 'data': res.data['data'],
}; };
} else { } else {
return {'status': false, 'date': [], 'msg': res.data['message']}; return {
'status': false,
'date': [],
'msg': "message: ${res.data['message']},"
" msg: ${res.data['msg']},"
" code: ${res.data['code']}",
};
} }
} }
// 发送私信 // 发送私信
static Future sendMsg({ static Future sendMsg({
required int senderUid, int? senderUid,
required int receiverId, int? receiverId,
int? receiverType, int? receiverType,
int? msgType, int? msgType,
dynamic content, dynamic content,
}) async { }) async {
String csrf = await Request.getCsrf(); String csrf = await Request.getCsrf();
var res = await Request().post( Map<String, dynamic> params = await WbiSign().makSign({
Api.sendMsg, 'msg[sender_uid]': senderUid,
data: { 'msg[receiver_id]': receiverId,
'msg[sender_uid]': senderUid, 'msg[receiver_type]': receiverType ?? 1,
'msg[receiver_id]': receiverId, 'msg[msg_type]': msgType ?? 1,
'msg[receiver_type]': 1, 'msg[msg_status]': 0,
'msg[msg_type]': 1, 'msg[dev_id]': getDevId(),
'msg[msg_status]': 0, 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'msg[content]': jsonEncode(content), 'msg[new_face_version]': 0,
'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'msg[content]': content,
'msg[new_face_version]': 1, 'from_firework': 0,
'msg[dev_id]': getDevId(), 'build': 0,
'from_firework': 0, 'mobi_app': 'web',
'build': 0, 'csrf_token': csrf,
'mobi_app': 'web', 'csrf': csrf,
'csrf_token': csrf, });
'csrf': csrf, var res =
}, await Request().post(Api.sendMsg, queryParameters: <String, dynamic>{
); ...params,
'csrf_token': csrf,
'csrf': csrf,
}, data: {
'w_sender_uid': params['msg[sender_uid]'],
'w_receiver_id': params['msg[receiver_id]'],
'w_dev_id': params['msg[dev_id]'],
'w_rid': params['w_rid'],
'wts': params['wts'],
'csrf_token': csrf,
'csrf': csrf,
});
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': res.data['data'], 'data': res.data['data'],
}; };
} else { } else {
return {'status': false, 'date': [], 'msg': res.data['message']}; return {
'status': false,
'date': [],
'msg': "message: ${res.data['message']},"
" msg: ${res.data['msg']},"
" code: ${res.data['code']}",
};
} }
} }
@ -203,176 +220,4 @@ class MsgHttp {
} }
return s.join(); return s.join();
} }
static Future removeSession({
int? talkerId,
}) async {
String csrf = await Request.getCsrf();
Map params = await WbiSign().makSign({
'talker_id': talkerId,
'session_type': 1,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf
});
var res = await Request().get(Api.removeSession, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
static Future unread() async {
var res = await Request().get(Api.unread);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 回复我的
static Future messageReply({
int? id,
int? replyTime,
}) async {
var params = {
if (id != null) 'id': id,
if (replyTime != null) 'reply_time': replyTime,
};
var res = await Request().get(Api.messageReplyAPi, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageReplyModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 收到的赞
static Future messageLike({
int? id,
int? likeTime,
}) async {
var params = {
if (id != null) 'id': id,
if (likeTime != null) 'like_time': likeTime,
};
var res = await Request().get(Api.messageLikeAPi, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageLikeModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
static Future messageSystem() async {
var res = await Request().get(Api.messageSystemAPi, data: {
'csrf': await Request.getCsrf(),
'page_size': 20,
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': res.data['data']['system_notify_list']
?.map<MessageSystemModel>(
(e) => MessageSystemModel.fromJson(e))
.toList() ??
[],
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 系统消息标记已读
static Future systemMarkRead(int cursor) async {
String csrf = await Request.getCsrf();
var res = await Request().get(Api.systemMarkRead, data: {
'csrf': csrf,
'cursor': cursor,
});
if (res.data['code'] == 0) {
return {
'status': true,
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
static Future messageSystemAccount() async {
var res = await Request().get(Api.userMessageSystemAPi, data: {
'csrf': await Request.getCsrf(),
'page_size': 20,
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': res.data['data']['system_notify_list']
?.map<MessageSystemModel>(
(e) => MessageSystemModel.fromJson(e))
.toList() ??
[],
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// @我的
static Future messageAt() async {
var res = await Request().get(Api.messageAtAPi, data: {
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageAtModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -1,122 +0,0 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/read/opus.dart';
import 'package:pilipala/models/read/read.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import 'index.dart';
class ReadHttp {
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 解析专栏 opus格式
static Future parseArticleOpus({required String id}) async {
var res = await Request().get('https://www.bilibili.com/opus/$id', extra: {
'ua': 'pc',
});
String? headContent = parse(res.data).head?.outerHtml;
var document = parse(headContent);
var linkTags = document.getElementsByTagName('link');
bool isCv = false;
String cvId = '';
for (var linkTag in linkTags) {
var attributes = linkTag.attributes;
if (attributes.containsKey('rel') &&
attributes['rel'] == 'canonical' &&
attributes.containsKey('data-vue-meta') &&
attributes['data-vue-meta'] == 'true') {
final String cvHref = linkTag.attributes['href']!;
RegExp regex = RegExp(r'cv(\d+)');
RegExpMatch? match = regex.firstMatch(cvHref);
if (match != null) {
cvId = match.group(1)!;
} else {
print('No match found.');
}
isCv = true;
break;
}
}
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': OpusDataModel.fromJson(jsonData),
'isCv': isCv,
'cvId': cvId,
};
}
// 解析专栏 cv格式
static Future parseArticleCv({required String id}) async {
var res = await Request().get(
'https://www.bilibili.com/read/cv$id',
extra: {'ua': 'pc'},
options: Options(
headers: {
'cookie': 'opus-goback=1',
},
),
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': ReadDataModel.fromJson(jsonData),
};
}
//
static Future getViewInfo({required String id}) async {
Map params = await WbiSign().makSign({
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
});
var res = await Request().get(
Api.getViewInfo,
data: {
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -1,8 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
import '../models/video/reply/data.dart'; import '../models/video/reply/data.dart';
import '../models/video/reply/emote.dart'; import '../models/video/reply/emote.dart';
import 'api.dart'; import 'api.dart';
@ -11,29 +6,35 @@ import 'init.dart';
class ReplyHttp { class ReplyHttp {
static Future replyList({ static Future replyList({
required int oid, required int oid,
required String nextOffset, required int pageNum,
required int type, required int type,
int? ps, int? ps,
int sort = 1, int sort = 1,
}) async { }) async {
var res = await Request().get(Api.replyList, data: { var res = await Request().get(Api.replyList, data: {
'oid': oid, 'oid': oid,
'pn': pageNum,
'type': type, 'type': type,
'pagination_str': jsonEncode({'offset': nextOffset}), 'sort': sort,
'mode': sort + 2, 'ps': ps ?? 20
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': ReplyData.fromJson(res.data['data']), 'data': ReplyData.fromJson(res.data['data']),
'code': 200,
}; };
} else { } else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区',
};
return { return {
'status': false, 'status': false,
'date': [], 'date': [],
'code': res.data['code'], 'msg': errMap[res.data['code']] ?? res.data['message'],
'msg': res.data['message'],
}; };
} }
} }
@ -56,13 +57,19 @@ class ReplyHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': ReplyReplyData.fromJson(res.data['data']), 'data': ReplyData.fromJson(res.data['data']),
}; };
} else { } else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '评论区已关闭',
12009: '评论主体的type不合法',
};
return { return {
'status': false, 'status': false,
'date': [], 'date': [],
'msg': res.data['message'], 'msg': errMap[res.data['code']] ?? '请求异常',
}; };
} }
} }
@ -76,7 +83,7 @@ class ReplyHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.likeReply, Api.likeReply,
data: { queryParameters: {
'type': type, 'type': type,
'oid': oid, 'oid': oid,
'rpid': rpid, 'rpid': rpid,
@ -113,65 +120,4 @@ class ReplyHttp {
}; };
} }
} }
static Future replyDel({
required int type, //replyType
required int oid,
required int rpid,
}) async {
var res = await Request().post(
Api.replyDel,
queryParameters: {
'type': type, //type.index
'oid': oid,
'rpid': rpid,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '删除成功'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 图片上传
static Future uploadImage(
{required XFile xFile, String type = 'new_dyn'}) async {
var formData = FormData.fromMap({
'file_up': await xFileToMultipartFile(xFile),
'biz': type,
'csrf': await Request.getCsrf(),
'category': 'daily',
});
var res = await Request().post(
Api.uploadImage,
data: formData,
);
if (res.data['code'] == 0) {
var data = res.data['data'];
data['img_src'] = data['image_url'];
data['img_width'] = data['image_width'];
data['img_height'] = data['image_height'];
data.remove('image_url');
data.remove('image_width');
data.remove('image_height');
return {
'status': true,
'data': data,
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future<MultipartFile> xFileToMultipartFile(XFile xFile) async {
var file = File(xFile.path);
var bytes = await file.readAsBytes();
return MultipartFile.fromBytes(bytes, filename: xFile.name);
}
} }

View File

@ -1,7 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/search/all.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import '../models/bangumi/info.dart'; import '../models/bangumi/info.dart';
import '../models/common/search_type.dart'; import '../models/common/search_type.dart';
import '../models/search/hot.dart'; import '../models/search/hot.dart';
@ -11,7 +9,7 @@ import '../utils/storage.dart';
import 'index.dart'; import 'index.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStorage.setting; 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 is String) { if (res.data is String) {
@ -75,7 +73,6 @@ class SearchHttp {
required page, required page,
String? order, String? order,
int? duration, int? duration,
int? tids,
}) async { }) async {
var reqData = { var reqData = {
'search_type': searchType.type, 'search_type': searchType.type,
@ -85,14 +82,9 @@ class SearchHttp {
'page': page, 'page': page,
if (order != null) 'order': order, if (order != null) 'order': order,
if (duration != null) 'duration': duration, if (duration != null) 'duration': duration,
if (tids != null && tids != -1) 'tids': tids,
}; };
var res = await Request().get(Api.searchByType, data: reqData); var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0) { if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
if (res.data['data']['numPages'] == 0) {
// 我想返回数据使得可以通过data.list 取值,结果为[]
return {'status': true, 'data': Data()};
}
Object data; Object data;
try { try {
switch (searchType) { switch (searchType) {
@ -129,7 +121,9 @@ class SearchHttp {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'], 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
? '没有相关数据'
: res.data['message'],
}; };
} }
} }
@ -143,11 +137,7 @@ class SearchHttp {
} }
final dynamic res = final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data}); await Request().get(Api.ab2c, data: <String, dynamic>{...data});
if (res.data['code'] == 0) { return res.data['data'].first['cid'];
return res.data['data'].first['cid'];
} else {
return -1;
}
} }
static Future<Map<String, dynamic>> bangumiInfo( static Future<Map<String, dynamic>> bangumiInfo(
@ -173,48 +163,4 @@ class SearchHttp {
}; };
} }
} }
static Future<Map<String, dynamic>> ab2cWithPic(
{int? aid, String? bvid}) async {
Map<String, dynamic> data = {};
if (aid != null) {
data['aid'] = aid;
} else if (bvid != null) {
data['bvid'] = bvid;
}
final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
return {
'cid': res.data['data'].first['cid'],
'pic': res.data['data'].first['first_frame'],
};
}
static Future<Map<String, dynamic>> searchCount(
{required String keyword}) async {
Map<String, dynamic> data = {
'keyword': keyword,
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
final dynamic res = await Request().get(Api.searchCount, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SearchAllModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
}
}
class Data {
List<dynamic> list;
Data({this.list = const []});
} }

View File

@ -1,8 +1,5 @@
import 'dart:convert'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../common/constants.dart';
import 'package:dio/dio.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/video/later.dart';
import '../models/model_hot_video_item.dart'; import '../models/model_hot_video_item.dart';
import '../models/user/fav_detail.dart'; import '../models/user/fav_detail.dart';
import '../models/user/fav_folder.dart'; import '../models/user/fav_folder.dart';
@ -60,15 +57,12 @@ class UserHttp {
if (res.data['data'] != null) { if (res.data['data'] != null) {
data = FavFolderData.fromJson(res.data['data']); data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data}; return {'status': true, 'data': data};
} else {
return {'status': false, 'msg': '收藏夹为空'};
} }
} else { } else {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'], 'msg': res.data['message'] ?? '账号未登录'
'code': res.data['code'],
}; };
} }
} }
@ -117,12 +111,7 @@ class UserHttp {
'data': {'list': list, 'count': res.data['data']['count']} 'data': {'list': list, 'count': res.data['data']['count']}
}; };
} else { } else {
return { return {'status': false, 'data': [], 'msg': res.data['message']};
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -137,12 +126,7 @@ class UserHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])}; return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else { } else {
return { return {'status': false, 'data': [], 'msg': res.data['message']};
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -151,7 +135,7 @@ class UserHttp {
// 暂停switchStatus传true 否则false // 暂停switchStatus传true 否则false
var res = await Request().post( var res = await Request().post(
Api.pauseHistory, Api.pauseHistory,
data: { queryParameters: {
'switch': switchStatus, 'switch': switchStatus,
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -170,7 +154,7 @@ class UserHttp {
static Future clearHistory() async { static Future clearHistory() async {
var res = await Request().post( var res = await Request().post(
Api.clearHistory, Api.clearHistory,
data: { queryParameters: {
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -188,7 +172,7 @@ class UserHttp {
} }
var res = await Request().post( var res = await Request().post(
Api.toViewLater, Api.toViewLater,
data: data, queryParameters: data,
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah稍后再看'}; return {'status': true, 'msg': 'yeah稍后再看'};
@ -207,7 +191,7 @@ class UserHttp {
params[aid != null ? 'aid' : 'viewed'] = aid ?? true; params[aid != null ? 'aid' : 'viewed'] = aid ?? true;
var res = await Request().post( var res = await Request().post(
Api.toViewDel, Api.toViewDel,
data: params, queryParameters: params,
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah成功移除'}; return {'status': true, 'msg': 'yeah成功移除'};
@ -216,11 +200,30 @@ class UserHttp {
} }
} }
// 获取用户凭证 失效
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',
data: {
'appkey': Constants.appKey,
'api': Constants.thirdApi,
'sign': Constants.thirdSign,
},
);
try {
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
} catch (err) {
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
}
}
// 清空稍后再看 // 清空稍后再看
static Future toViewClear() async { static Future toViewClear() async {
var res = await Request().post( var res = await Request().post(
Api.toViewClear, Api.toViewClear,
data: { queryParameters: {
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -236,7 +239,7 @@ class UserHttp {
static Future delHistory(kid) async { static Future delHistory(kid) async {
var res = await Request().post( var res = await Request().post(
Api.delHistory, Api.delHistory,
data: { queryParameters: {
'kid': kid, 'kid': kid,
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -262,6 +265,30 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; 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( static Future searchHistory(
@ -299,21 +326,16 @@ class UserHttp {
'data': SubFolderModelData.fromJson(res.data['data']) 'data': SubFolderModelData.fromJson(res.data['data'])
}; };
} else { } else {
return { return {'status': false, 'msg': res.data['message']};
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
static Future userSeasonList({ static Future userSubFolderDetail({
required int seasonId, required int seasonId,
required int pn, required int pn,
required int ps, required int ps,
}) async { }) async {
var res = await Request().get(Api.userSeasonList, data: { var res = await Request().get(Api.userSubFolderDetail, data: {
'season_id': seasonId, 'season_id': seasonId,
'ps': ps, 'ps': ps,
'pn': pn, 'pn': pn,
@ -327,223 +349,4 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
static Future userResourceList({
required int seasonId,
required int pn,
required int ps,
}) async {
var res = await Request().get(Api.userResourceList, data: {
'media_id': seasonId,
'ps': ps,
'pn': pn,
'keyword': '',
'order': 'mtime',
'type': 0,
'tid': 0,
'platform': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': SubDetailModelData.fromJson(res.data['data'])
};
} catch (err) {
return {'status': false, 'msg': err};
}
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 取消订阅
static Future cancelSub({required int seasonId}) async {
var res = await Request().post(
Api.cancelSub,
data: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 删除文件夹
static Future delFavFolder({required int mediaIds}) async {
var res = await Request().post(
Api.delFavFolder,
data: {
'media_ids': mediaIds,
'platform': 'web',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 稍后再看列表
static Future getMediaList({
required int type,
required int bizId,
required int ps,
int? oid,
}) async {
var res = await Request().get(
Api.mediaList,
data: {
'mobi_app': 'web',
'type': type,
'biz_id': bizId,
'oid': oid ?? '',
'otype': 2,
'ps': ps,
'direction': false,
'desc': true,
'sort_field': 1,
'tid': 0,
'with_current': false,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['media_list'] != null
? res.data['data']['media_list']
.map<MediaVideoItemModel>(
(e) => MediaVideoItemModel.fromJson(e))
.toList()
: []
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 解析收藏夹视频
static Future parseFavVideo({
required int mediaId,
required int oid,
required String bvid,
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/ml$mediaId',
data: {
'oid': mediaId,
'bvid': bvid,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
static Future getAccountInfo() async {
var res = await Request().get(
Api.accountInfo,
data: {'web_location': 333.33},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': {},
'mag': res.data['message'],
};
}
}
static Future updateAccountInfo({
required String uname,
required String sign,
required String sex,
required String birthday,
}) async {
var res = await Request().post(
Api.updateAccountInfo,
data: {
'uname': uname,
'usersign': sign,
'sex': sex,
'birthday': birthday,
'csrf': await Request.getCsrf(),
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {
'status': true,
'msg': '更新成功',
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
// 解析up投稿
static Future parseUpArchiveVideo({
required int mid,
required int oid,
required String bvid,
String sortField = 'pubtime',
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/$mid',
data: {
'oid': oid,
'bvid': bvid,
'sort_field': sortField,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
} }

View File

@ -1,9 +1,5 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/models/video/tags.dart';
import '../common/constants.dart'; import '../common/constants.dart';
import '../models/common/reply_type.dart'; import '../models/common/reply_type.dart';
import '../models/home/rcmd/result.dart'; import '../models/home/rcmd/result.dart';
@ -12,11 +8,9 @@ import '../models/model_rec_video_item.dart';
import '../models/user/fav_folder.dart'; import '../models/user/fav_folder.dart';
import '../models/video/ai.dart'; import '../models/video/ai.dart';
import '../models/video/play/url.dart'; import '../models/video/play/url.dart';
import '../models/video/subTitile/result.dart';
import '../models/video_detail_res.dart'; import '../models/video_detail_res.dart';
import '../utils/recommend_filter.dart'; import '../utils/recommend_filter.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/subtitle.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
import 'api.dart'; import 'api.dart';
import 'init.dart'; import 'init.dart';
@ -26,11 +20,11 @@ import 'init.dart';
/// 返回{'status': bool, 'data': List} /// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑 /// view层根据 status 判断渲染逻辑
class VideoHttp { class VideoHttp {
static Box localCache = GStorage.localCache; static Box localCache = GStrorage.localCache;
static Box setting = GStorage.setting; static Box setting = GStrorage.setting;
static bool enableRcmdDynamic = static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true); setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
static Box userInfoCache = GStorage.userInfo; 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 {
@ -39,7 +33,7 @@ class VideoHttp {
Api.recommendListWeb, Api.recommendListWeb,
data: { data: {
'version': 1, 'version': 1,
'feed_version': 'V3', 'feed_version': 'V8',
'homepage_ver': 1, 'homepage_ver': 1,
'ps': ps, 'ps': ps,
'fresh_idx': freshIdx, 'fresh_idx': freshIdx,
@ -74,45 +68,47 @@ class VideoHttp {
// 添加额外的loginState变量模拟未登录状态 // 添加额外的loginState变量模拟未登录状态
static Future rcmdVideoListApp( static Future rcmdVideoListApp(
{bool loginStatus = true, required int freshIdx}) async { {bool loginStatus = true, required int freshIdx}) async {
var res = await Request().get( try {
Api.recommendListApp, var res = await Request().get(
data: { Api.recommendListApp,
'idx': freshIdx, data: {
'flush': '5', 'idx': freshIdx,
'column': '4', 'flush': '5',
'device': 'pad', 'column': '4',
'device_type': 0, 'device': 'pad',
'device_name': 'vivo', 'device_type': 0,
'pull': freshIdx == 0 ? 'true' : 'false', 'device_name': 'vivo',
'appkey': Constants.appKey, 'pull': freshIdx == 0 ? 'true' : 'false',
'access_key': loginStatus 'appkey': Constants.appKey,
? (localCache 'access_key': loginStatus
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ?? ? (localCache.get(LocalCacheKey.accessKey,
'') defaultValue: {})['value'] ??
: '' '')
}, : ''
); },
if (res.data['code'] == 0) { );
List<RecVideoItemAppModel> list = []; if (res.data['code'] == 0) {
List<int> blackMidsList = List<RecVideoItemAppModel> list = [];
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); List<int> blackMidsList =
for (var i in res.data['data']['items']) { setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
// 屏蔽推广和拉黑用户 for (var i in res.data['data']['items']) {
if (i['card_goto'] != 'ad_av' && // 屏蔽推广和拉黑用户
i['card_goto'] != 'ad_web_s' && if (i['card_goto'] != 'ad_av' &&
i['card_goto'] != 'ad_web' && (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && (i['args'] != null &&
(i['args'] != null && !blackMidsList.contains(i['args']['up_mid']))) {
!blackMidsList.contains(i['args']['up_mid']))) { RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i); if (!RecommendFilter.filter(videoItem)) {
if (!RecommendFilter.filter(videoItem)) { list.add(videoItem);
list.add(videoItem); }
} }
} }
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
} }
return {'status': true, 'data': list}; } catch (err) {
} else { return {'status': false, 'data': [], 'msg': err.toString()};
return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} }
@ -194,15 +190,22 @@ class VideoHttp {
// 视频信息 标题、简介 // 视频信息 标题、简介
static Future videoIntro({required String bvid}) async { static Future videoIntro({required String bvid}) async {
var res = await Request().get(Api.videoIntro, data: {'bvid': bvid}); var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
if (res.data['code'] == 0) { VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); if (result.code == 0) {
return {'status': true, 'data': result.data!}; return {'status': true, 'data': result.data!};
} else { } else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return { return {
'status': false, 'status': false,
'data': null, 'data': null,
'code': res.data['code'], 'code': result.code,
'msg': res.data['message'], 'msg': errMap[result.code] ?? '请求异常',
}; };
} }
} }
@ -249,7 +252,7 @@ class VideoHttp {
static Future coinVideo({required String bvid, required int multiply}) async { static Future coinVideo({required String bvid, required int multiply}) async {
var res = await Request().post( var res = await Request().post(
Api.coinVideo, Api.coinVideo,
data: { queryParameters: {
'bvid': bvid, 'bvid': bvid,
'multiply': multiply, 'multiply': multiply,
'select_like': 0, 'select_like': 0,
@ -277,7 +280,7 @@ class VideoHttp {
static Future oneThree({required String bvid}) async { static Future oneThree({required String bvid}) async {
var res = await Request().post( var res = await Request().post(
Api.oneThree, Api.oneThree,
data: { queryParameters: {
'bvid': bvid, 'bvid': bvid,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -293,7 +296,7 @@ class VideoHttp {
static Future likeVideo({required String bvid, required bool type}) async { static Future likeVideo({required String bvid, required bool type}) async {
var res = await Request().post( var res = await Request().post(
Api.likeVideo, Api.likeVideo,
data: { queryParameters: {
'bvid': bvid, 'bvid': bvid,
'like': type ? 1 : 2, 'like': type ? 1 : 2,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -309,16 +312,13 @@ class VideoHttp {
// (取消)收藏 // (取消)收藏
static Future favVideo( static Future favVideo(
{required int aid, String? addIds, String? delIds}) async { {required int aid, String? addIds, String? delIds}) async {
var res = await Request().post( var res = await Request().post(Api.favVideo, queryParameters: {
Api.favVideo, 'rid': aid,
data: { 'type': 2,
'rid': aid, 'add_media_ids': addIds ?? '',
'type': 2, 'del_media_ids': delIds ?? '',
'add_media_ids': addIds ?? '', 'csrf': await Request.getCsrf(),
'del_media_ids': delIds ?? '', });
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
@ -352,37 +352,20 @@ class VideoHttp {
required String message, required String message,
int? root, int? root,
int? parent, int? parent,
List<Map<dynamic, dynamic>>? pictures,
}) async { }) async {
if (message == '') { if (message == '') {
return {'status': false, 'data': [], 'msg': '请输入评论内容'}; return {'status': false, 'data': [], 'msg': '请输入评论内容'};
} }
var params = <String, dynamic>{ var res = await Request().post(Api.replyAdd, queryParameters: {
'plat': 1,
'oid': oid,
'type': type.index, 'type': type.index,
'oid': oid,
'root': root == null || root == 0 ? '' : root, 'root': root == null || root == 0 ? '' : root,
'parent': parent == null || parent == 0 ? '' : parent, 'parent': parent == null || parent == 0 ? '' : parent,
'message': message, 'message': message,
'at_name_to_mid': {},
if (pictures != null) 'pictures': jsonEncode(pictures),
'gaia_source': 'main_web',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}; });
Map sign = await WbiSign().makSign(params); log(res.toString());
params.remove('wts');
params.remove('w_rid');
FormData formData = FormData.fromMap({...params});
var res = await Request().post(
Api.replyAdd,
queryParameters: {
'w_rid': sign['w_rid'],
'wts': sign['wts'],
},
data: formData,
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
log(res.toString());
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {'status': false, 'data': [], 'msg': res.data['message']};
@ -402,57 +385,42 @@ class VideoHttp {
// 操作用户关系 // 操作用户关系
static Future relationMod( static Future relationMod(
{required int mid, required int act, required int reSrc}) async { {required int mid, required int act, required int reSrc}) async {
var res = await Request().post( var res = await Request().post(Api.relationMod, queryParameters: {
Api.relationMod, 'fid': mid,
data: { 'act': act,
'fid': mid, 're_src': reSrc,
'act': act, 'csrf': await Request.getCsrf(),
're_src': reSrc, });
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
if (act == 5) { return {'status': true, 'data': res.data['data']};
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
blackMidsList.add(mid);
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
}
return {'status': true, 'data': res.data['data'], 'msg': '成功'};
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {'status': false, 'data': []};
} }
} }
// 视频播放进度 // 视频播放进度
static Future heartBeat({bvid, cid, progress, realtime}) async { static Future heartBeat({bvid, cid, progress, realtime}) async {
await Request().post( await Request().post(Api.heartBeat, queryParameters: {
Api.heartBeat, // 'aid': aid,
data: { 'bvid': bvid,
// 'aid': aid, 'cid': cid,
'bvid': bvid, // 'epid': '',
'cid': cid, // 'sid': '',
// 'epid': '', // 'mid': '',
// 'sid': '', 'played_time': progress,
// 'mid': '', // 'realtime': realtime,
'played_time': progress, // 'type': '',
// 'realtime': realtime, // 'sub_type': '',
// 'type': '', 'csrf': await Request.getCsrf(),
// 'sub_type': '', });
'csrf': await Request.getCsrf(),
},
);
} }
// 添加追番 // 添加追番
static Future bangumiAdd({int? seasonId}) async { static Future bangumiAdd({int? seasonId}) async {
var res = await Request().post( var res = await Request().post(Api.bangumiAdd, queryParameters: {
Api.bangumiAdd, 'season_id': seasonId,
data: { 'csrf': await Request.getCsrf(),
'season_id': seasonId, });
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': res.data['result']['toast']}; return {'status': true, 'msg': res.data['result']['toast']};
} else { } else {
@ -462,13 +430,10 @@ class VideoHttp {
// 取消追番 // 取消追番
static Future bangumiDel({int? seasonId}) async { static Future bangumiDel({int? seasonId}) async {
var res = await Request().post( var res = await Request().post(Api.bangumiDel, queryParameters: {
Api.bangumiDel, 'season_id': seasonId,
data: { 'csrf': await Request.getCsrf(),
'season_id': seasonId, });
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': res.data['result']['toast']}; return {'status': true, 'msg': res.data['result']['toast']};
} else { } else {
@ -511,26 +476,6 @@ class VideoHttp {
} }
} }
static Future getSubtitle({int? cid, String? bvid, String? aid}) async {
var res = await Request().get(Api.getSubtitleConfig, data: {
'cid': cid,
if (bvid != null) 'bvid': bvid,
if (aid != null) 'aid': aid,
});
try {
if (res.data['code'] == 0) {
return {
'status': true,
'data': SubTitlteModel.fromJson(res.data['data']),
};
} else {
return {'status': false, 'data': [], 'msg': res.data['msg']};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': res.data['msg']};
}
}
// 视频排行 // 视频排行
static Future getRankVideoList(int rid) async { static Future getRankVideoList(int rid) async {
try { try {
@ -553,74 +498,4 @@ class VideoHttp {
return {'status': false, 'data': [], 'msg': err}; return {'status': false, 'data': [], 'msg': err};
} }
} }
// 获取字幕内容
static Future<Map<String, dynamic>> getSubtitleContent(url) async {
var res = await Request().get('https:$url');
final String content =
await SubTitleUtils.convertToWebVTT(res.data['body']);
final List body = res.data['body'];
return {'content': content, 'body': body};
}
static Future<Map<String, dynamic>> getSubscribeStatus(
{required dynamic bvid}) async {
var res = await Request().get(
Api.videoRelation,
data: {
'aid': IdUtils.bv2av(bvid),
'bvid': bvid,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
static Future seasonFav({
required bool isFav,
required dynamic seasonId,
}) async {
var res = await Request().post(
isFav ? Api.cancelSub : Api.confirmSub,
data: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
// 获取视频标签
static Future getVideoTag({required String bvid}) async {
var res = await Request().get(Api.videoTag, data: {'bvid': bvid});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'].map<VideoTagItem>((e) {
return VideoTagItem.fromJson(e);
}).toList()
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -10,7 +9,6 @@ import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart'; import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/common.dart';
import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/color_type.dart'; import 'package:pilipala/models/common/color_type.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
@ -18,56 +16,89 @@ 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/disable_battery_opt.dart';
import 'package:pilipala/services/service_locator.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/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:pilipala/utils/recommend_filter.dart'; import 'package:pilipala/utils/recommend_filter.dart';
import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart'; import './services/loggeer.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'services/sentry.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
await SystemChrome.setPreferredOrientations( SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
await GStorage.init(); .then((_) async {
clearLogs(); await GStrorage.init();
Request(); await setupServiceLocator();
await Request.setCookie(); Request();
await Request.setCookie();
RecommendFilter();
// 异常捕获 logo记录 // 异常捕获 logo记录
final Catcher2Options releaseConfig = Catcher2Options( final Catcher2Options debugConfig = Catcher2Options(
SilentReportMode(), SilentReportMode(),
[FileHandler(await getLogsPath())], [
); FileHandler(await getLogsPath()),
ConsoleHandler(
enableDeviceParameters: false,
enableApplicationParameters: false,
)
],
);
Catcher2( final Catcher2Options releaseConfig = Catcher2Options(
releaseConfig: releaseConfig, SilentReportMode(),
runAppFunction: () { [FileHandler(await getLogsPath())],
runApp(const MyApp()); );
},
);
// 小白条、导航栏沉浸 // Catcher2(
if (Platform.isAndroid) { // debugConfig: debugConfig,
final androidInfo = await DeviceInfoPlugin().androidInfo; // releaseConfig: releaseConfig,
if (androidInfo.version.sdkInt >= 29) { // runAppFunction: () {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // runApp(const MyApp());
} // },
// );
await SentryService.sentryInit(
() => runApp(
SentryScreenshotWidget(
child: SentryUserInteractionWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(),
child: const MyApp(),
),
),
),
),
);
// try {
// int? test;
// test! + 3;
// } catch (exception, stackTrace) {
// debugPrint('111');
// await Sentry.captureException(exception, stackTrace: '$stackTrace');
// debugPrint('222');
// }
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: false,
)); ));
} Data.init();
PiliSchame.init();
PiliSchame.init(); DisableBatteryOpt();
await GlobalDataCache.initialize(); });
CommonHttp.buvidActivate();
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -75,7 +106,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Box setting = GStorage.setting; Box setting = GStrorage.setting;
// 主题色 // 主题色
Color defaultColor = Color defaultColor =
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)] colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
@ -108,39 +139,6 @@ class MyApp extends StatelessWidget {
} catch (_) {} } catch (_) {}
} }
if (Platform.isAndroid) {
return AndroidApp(
brandColor: brandColor,
isDynamicColor: isDynamicColor,
currentThemeValue: currentThemeValue,
textScale: textScale,
);
} else {
return OtherApp(
brandColor: brandColor,
currentThemeValue: currentThemeValue,
textScale: textScale,
);
}
}
}
class AndroidApp extends StatelessWidget {
const AndroidApp({
super.key,
required this.brandColor,
required this.isDynamicColor,
required this.currentThemeValue,
required this.textScale,
});
final Color brandColor;
final bool isDynamicColor;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) { builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme; ColorScheme? lightColorScheme;
@ -160,153 +158,72 @@ class AndroidApp extends StatelessWidget {
brightness: Brightness.dark, brightness: Brightness.dark,
); );
} }
return BuildMainApp( // 图片缓存
lightColorScheme: lightColorScheme, // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
darkColorScheme: darkColorScheme, return GetMaterialApp(
currentThemeValue: currentThemeValue, title: 'PiLiPaLa',
textScale: textScale, theme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData(
actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer,
closeIconColor: lightColorScheme.secondary,
contentTextStyle: TextStyle(color: lightColorScheme.secondary),
elevation: 20,
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
),
darkTheme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme
: darkColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData(
actionTextColor: darkColorScheme.primary,
backgroundColor: darkColorScheme.secondaryContainer,
closeIconColor: darkColorScheme.secondary,
contentTextStyle: TextStyle(color: darkColorScheme.secondary),
elevation: 20,
),
),
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
locale: const Locale("zh", "CN"),
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,
SentryNavigatorObserver(),
],
); );
}), }),
); );
} }
} }
class OtherApp extends StatelessWidget {
const OtherApp({
super.key,
required this.brandColor,
required this.currentThemeValue,
required this.textScale,
});
final Color brandColor;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
return BuildMainApp(
lightColorScheme: ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.light,
),
darkColorScheme: ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.dark,
),
currentThemeValue: currentThemeValue,
textScale: textScale,
);
}
}
class BuildMainApp extends StatelessWidget {
const BuildMainApp({
super.key,
required this.lightColorScheme,
required this.darkColorScheme,
required this.currentThemeValue,
required this.textScale,
});
final ColorScheme lightColorScheme;
final ColorScheme darkColorScheme;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
Box setting = GStorage.setting;
/// 纯黑模式主题配置
ColorScheme? pureDarkColorScheme;
final bool enablePureBlack =
setting.get(SettingBoxKey.enablePureBlack, defaultValue: false);
if (enablePureBlack) {
pureDarkColorScheme = darkColorScheme.copyWith(
background: Colors.black,
surface: Colors.black,
onPrimary: Colors.black,
onSecondary: Colors.black,
);
}
final SnackBarThemeData snackBarTheme = SnackBarThemeData(
actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer,
closeIconColor: lightColorScheme.secondary,
contentTextStyle: TextStyle(color: lightColorScheme.secondary),
elevation: 20,
);
AppBarTheme appBarTheme(ColorScheme colorScheme) {
return AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
titleSpacing: 0,
scrolledUnderElevation: 0,
// titleTextStyle: TextStyle(
// fontSize: Theme.of(context).textTheme.titleLarge!.fontSize),
);
}
ThemeData buildThemeData(ColorScheme colorScheme) {
return ThemeData(
colorScheme: colorScheme,
snackBarTheme: snackBarTheme,
appBarTheme: appBarTheme(colorScheme),
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
);
}
return GetMaterialApp(
title: 'PiliPala',
theme: buildThemeData(
currentThemeValue == ThemeType.dark
? pureDarkColorScheme ?? darkColorScheme
: lightColorScheme,
),
darkTheme: buildThemeData(
currentThemeValue == ThemeType.light
? lightColorScheme
: pureDarkColorScheme ?? darkColorScheme,
),
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
locale: const Locale("zh", "CN"),
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,
],
onReady: () async {
RecommendFilter();
Data.init();
setupServiceLocator();
},
);
}
}

View File

@ -144,7 +144,6 @@ class EpisodeItem {
this.link, this.link,
this.longTitle, this.longTitle,
this.pubTime, this.pubTime,
this.pubdate,
this.pv, this.pv,
this.releaseDate, this.releaseDate,
this.rights, this.rights,
@ -156,7 +155,6 @@ class EpisodeItem {
this.subtitle, this.subtitle,
this.title, this.title,
this.vid, this.vid,
this.stat,
}); });
int? aid; int? aid;
@ -175,7 +173,6 @@ class EpisodeItem {
String? link; String? link;
String? longTitle; String? longTitle;
int? pubTime; int? pubTime;
int? pubdate;
int? pv; int? pv;
String? releaseDate; String? releaseDate;
Map? rights; Map? rights;
@ -187,7 +184,6 @@ class EpisodeItem {
String? subtitle; String? subtitle;
String? title; String? title;
String? vid; String? vid;
String? stat;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
aid = json['aid']; aid = json['aid'];
@ -206,7 +202,6 @@ class EpisodeItem {
link = json['link']; link = json['link'];
longTitle = json['long_title']; longTitle = json['long_title'];
pubTime = json['pub_time']; pubTime = json['pub_time'];
pubdate = json['pub_time'];
pv = json['pv']; pv = json['pv'];
releaseDate = json['release_date']; releaseDate = json['release_date'];
rights = json['rights']; rights = json['rights'];
@ -216,7 +211,7 @@ class EpisodeItem {
skip = json['skip']; skip = json['skip'];
status = json['status']; status = json['status'];
subtitle = json['subtitle']; subtitle = json['subtitle'];
title = json['long_title']; title = json['title'];
vid = json['vid']; vid = json['vid'];
} }
} }

View File

@ -30,7 +30,6 @@ class BangumiListItemModel {
BangumiListItemModel({ BangumiListItemModel({
this.badge, this.badge,
this.badgeType, this.badgeType,
this.pic,
this.cover, this.cover,
// this.firstEp, // this.firstEp,
this.indexShow, this.indexShow,
@ -47,12 +46,10 @@ class BangumiListItemModel {
this.title, this.title,
this.titleIcon, this.titleIcon,
this.progress, this.progress,
this.progressIndex,
}); });
String? badge; String? badge;
int? badgeType; int? badgeType;
String? pic;
String? cover; String? cover;
String? indexShow; String? indexShow;
int? isFinish; int? isFinish;
@ -67,13 +64,12 @@ class BangumiListItemModel {
String? subTitle; String? subTitle;
String? title; String? title;
String? titleIcon; String? titleIcon;
String? progress; String? progress;
int? progressIndex;
BangumiListItemModel.fromJson(Map<String, dynamic> json) { BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge']; badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type']; badgeType = json['badge_type'];
pic = json['cover'];
cover = json['cover']; cover = json['cover'];
indexShow = json['index_show']; indexShow = json['index_show'];
isFinish = json['is_finish']; isFinish = json['is_finish'];
@ -88,9 +84,7 @@ class BangumiListItemModel {
subTitle = json['sub_title']; subTitle = json['sub_title'];
title = json['title']; title = json['title'];
titleIcon = json['title_icon']; titleIcon = json['title_icon'];
progress = json['progress']; progress = json['progress'];
progressIndex = int.parse(
RegExp(r'第(\d+)话').firstMatch(json['progress'] ?? '第1话')?.group(1) ??
'0');
} }
} }

View File

@ -1,93 +0,0 @@
// 操作类型的枚举值:点赞 不喜欢 收藏 投币 稍后再看 下载封面 后台播放 听视频 分享 下载视频
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum ActionType {
like,
coin,
collect,
watchLater,
share,
dislike,
downloadCover,
copyLink,
// backgroundPlay,
// listenVideo,
// downloadVideo,
}
extension ActionTypeExtension on ActionType {
String get value => [
'like',
'coin',
'collect',
'watchLater',
'share',
'dislike',
'downloadCover',
'copyLink',
// 'backgroundPlay',
// 'listenVideo',
// 'downloadVideo',
][index];
String get label => [
'点赞视频',
'投币',
'收藏视频',
'稍后再看',
'视频分享',
'不喜欢',
'下载封面',
'复制链接',
// '后台播放',
// '听视频',
// '下载视频',
][index];
}
List<Map> actionMenuConfig = [
{
'icon': const Icon(Icons.thumb_up_alt_outlined),
'label': '点赞视频',
'value': ActionType.like,
},
{
'icon': Image.asset(
'assets/images/coin.png',
width: 26,
color: IconTheme.of(Get.context!).color!.withOpacity(0.65),
),
'label': '投币',
'value': ActionType.coin,
},
{
'icon': const Icon(Icons.star_border),
'label': '收藏视频',
'value': ActionType.collect,
},
{
'icon': const Icon(Icons.watch_later_outlined),
'label': '稍后再看',
'value': ActionType.watchLater,
},
{
'icon': const Icon(Icons.share),
'label': '视频分享',
'value': ActionType.share,
},
{
'icon': const Icon(Icons.thumb_down_alt_outlined),
'label': '不喜欢',
'value': ActionType.dislike,
},
{
'icon': const Icon(Icons.image_outlined),
'label': '下载封面',
'value': ActionType.downloadCover,
},
{
'icon': const Icon(Icons.link_outlined),
'label': '复制链接',
'value': ActionType.copyLink,
},
];

View File

@ -1,18 +0,0 @@
enum CommentRangeType {
video,
bangumi,
// dynamic,
}
extension ActionTypeExtension on CommentRangeType {
String get value => [
'video',
'bangumi',
// 'dynamic',
][index];
String get label => [
'视频',
'番剧',
// '动态',
][index];
}

View File

@ -4,12 +4,9 @@ enum FullScreenGestureMode {
/// 从下滑到上 /// 从下滑到上
fromBottomtoTop, fromBottomtoTop,
/// 关闭手势
none,
} }
extension FullScreenGestureModeExtension on FullScreenGestureMode { extension FullScreenGestureModeExtension on FullScreenGestureMode {
String get values => ['fromToptoBottom', 'fromBottomtoTop', 'none'][index]; String get values => ['fromToptoBottom', 'fromBottomtoTop'][index];
String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏', '关闭手势'][index]; String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏'][index];
} }

View File

@ -1,73 +0,0 @@
class InvalidVideoModel {
final int? id;
final int? ver;
final int? aid;
final String? lastupdate;
final int? lastupdatets;
final String? title;
final String? description;
final String? pic;
final int? tid;
final String? typename;
final int? created;
final String? createdAt;
final String? author;
final int? mid;
final String? play;
final String? coins;
final String? review;
final String? videoReview;
final String? favorites;
final String? tag;
final List<String>? tagList;
InvalidVideoModel({
this.id,
this.ver,
this.aid,
this.lastupdate,
this.lastupdatets,
this.title,
this.description,
this.pic,
this.tid,
this.typename,
this.created,
this.createdAt,
this.author,
this.mid,
this.play,
this.coins,
this.review,
this.videoReview,
this.favorites,
this.tag,
this.tagList,
});
factory InvalidVideoModel.fromJson(Map<String, dynamic> json) {
return InvalidVideoModel(
id: json['id'],
ver: json['ver'],
aid: json['aid'],
lastupdate: json['lastupdate'],
lastupdatets: json['lastupdatets'],
title: json['title'],
description: json['description'],
pic: json['pic'],
tid: json['tid'],
typename: json['typename'],
created: json['created'],
createdAt: json['created_at'],
author: json['author'],
mid: json['mid'],
play: json['play'],
coins: json['coins'],
review: json['review'],
videoReview: json['video_review'],
favorites: json['favorites'],
tag: json['tag'],
tagList: json['tag'].toString().split(',').toList(),
);
}
}

View File

@ -1,8 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/pages/mine/index.dart';
import '../../pages/dynamics/index.dart';
import '../../pages/home/index.dart';
import '../../pages/rank/index.dart';
List defaultNavigationBars = [ List defaultNavigationBars = [
{ {
@ -17,7 +13,6 @@ List defaultNavigationBars = [
), ),
'label': "首页", 'label': "首页",
'count': 0, 'count': 0,
'page': const HomePage(),
}, },
{ {
'id': 1, 'id': 1,
@ -31,7 +26,6 @@ List defaultNavigationBars = [
), ),
'label': "排行榜", 'label': "排行榜",
'count': 0, 'count': 0,
'page': const RankPage(),
}, },
{ {
'id': 2, 'id': 2,
@ -45,20 +39,18 @@ List defaultNavigationBars = [
), ),
'label': "动态", 'label': "动态",
'count': 0, 'count': 0,
'page': const DynamicsPage(),
}, },
{ {
'id': 3, 'id': 3,
'icon': const Icon( 'icon': const Icon(
Icons.person_outline, Icons.video_collection_outlined,
size: 20, size: 20,
), ),
'selectIcon': const Icon( 'selectIcon': const Icon(
Icons.person, Icons.video_collection,
size: 21, size: 21,
), ),
'label': "我的", 'label': "媒体库",
'count': 0, 'count': 0,
'page': const MinePage(), }
},
]; ];

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/rank/zone/index.dart'; import 'package:pilipala/pages/rank/zone/index.dart';
enum RandType { enum RandType {
@ -73,6 +74,7 @@ List tabsConfig = [
), ),
'label': '全站', 'label': '全站',
'type': RandType.all, 'type': RandType.all,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 0), 'page': const ZonePage(rid: 0),
}, },
{ {
@ -82,6 +84,7 @@ List tabsConfig = [
), ),
'label': '国创相关', 'label': '国创相关',
'type': RandType.creation, 'type': RandType.creation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 168), 'page': const ZonePage(rid: 168),
}, },
{ {
@ -91,6 +94,7 @@ List tabsConfig = [
), ),
'label': '动画', 'label': '动画',
'type': RandType.animation, 'type': RandType.animation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 1), 'page': const ZonePage(rid: 1),
}, },
{ {
@ -100,6 +104,7 @@ List tabsConfig = [
), ),
'label': '音乐', 'label': '音乐',
'type': RandType.music, 'type': RandType.music,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 3), 'page': const ZonePage(rid: 3),
}, },
{ {
@ -109,6 +114,7 @@ List tabsConfig = [
), ),
'label': '舞蹈', 'label': '舞蹈',
'type': RandType.dance, 'type': RandType.dance,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 129), 'page': const ZonePage(rid: 129),
}, },
{ {
@ -118,6 +124,7 @@ List tabsConfig = [
), ),
'label': '游戏', 'label': '游戏',
'type': RandType.game, 'type': RandType.game,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 4), 'page': const ZonePage(rid: 4),
}, },
{ {
@ -127,6 +134,7 @@ List tabsConfig = [
), ),
'label': '知识', 'label': '知识',
'type': RandType.knowledge, 'type': RandType.knowledge,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 36), 'page': const ZonePage(rid: 36),
}, },
{ {
@ -136,6 +144,7 @@ List tabsConfig = [
), ),
'label': '科技', 'label': '科技',
'type': RandType.technology, 'type': RandType.technology,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 188), 'page': const ZonePage(rid: 188),
}, },
{ {
@ -145,6 +154,7 @@ List tabsConfig = [
), ),
'label': '运动', 'label': '运动',
'type': RandType.sport, 'type': RandType.sport,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 234), 'page': const ZonePage(rid: 234),
}, },
{ {
@ -154,6 +164,7 @@ List tabsConfig = [
), ),
'label': '汽车', 'label': '汽车',
'type': RandType.car, 'type': RandType.car,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 223), 'page': const ZonePage(rid: 223),
}, },
{ {
@ -163,6 +174,7 @@ List tabsConfig = [
), ),
'label': '生活', 'label': '生活',
'type': RandType.life, 'type': RandType.life,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 160), 'page': const ZonePage(rid: 160),
}, },
{ {
@ -172,6 +184,7 @@ List tabsConfig = [
), ),
'label': '美食', 'label': '美食',
'type': RandType.food, 'type': RandType.food,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 211), 'page': const ZonePage(rid: 211),
}, },
{ {
@ -181,6 +194,7 @@ List tabsConfig = [
), ),
'label': '动物圈', 'label': '动物圈',
'type': RandType.animal, 'type': RandType.animal,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 217), 'page': const ZonePage(rid: 217),
}, },
{ {
@ -190,6 +204,7 @@ List tabsConfig = [
), ),
'label': '鬼畜', 'label': '鬼畜',
'type': RandType.madness, 'type': RandType.madness,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 119), 'page': const ZonePage(rid: 119),
}, },
{ {
@ -199,6 +214,7 @@ List tabsConfig = [
), ),
'label': '时尚', 'label': '时尚',
'type': RandType.fashion, 'type': RandType.fashion,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 155), 'page': const ZonePage(rid: 155),
}, },
{ {
@ -208,6 +224,7 @@ List tabsConfig = [
), ),
'label': '娱乐', 'label': '娱乐',
'type': RandType.entertainment, 'type': RandType.entertainment,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 5), 'page': const ZonePage(rid: 5),
}, },
{ {
@ -217,6 +234,7 @@ List tabsConfig = [
), ),
'label': '影视', 'label': '影视',
'type': RandType.film, 'type': RandType.film,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 181), 'page': const ZonePage(rid: 181),
} }
]; ];

View File

@ -28,7 +28,7 @@ extension SearchTypeExtension on SearchType {
String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index]; String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
} }
// 搜索类型为视频时 // 搜索类型为视频、专栏及相簿
enum ArchiveFilterType { enum ArchiveFilterType {
totalrank, totalrank,
click, click,
@ -44,21 +44,3 @@ extension ArchiveFilterTypeExtension on ArchiveFilterType {
String get description => String get description =>
['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index]; ['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];
} }
// 搜索类型为专栏时
enum ArticleFilterType {
// 综合排序
totalrank,
// 最新发布
pubdate,
// 最多点击
click,
// 最多喜欢
attention,
// 最多评论
scores,
}
extension ArticleFilterTypeExtension on ArticleFilterType {
String get description => ['综合排序', '最新发布', '最多点击', '最多喜欢', '最多评论'][index];
}

View File

@ -1,5 +0,0 @@
enum VideoEpidoesType {
videoEpisode,
videoPart,
bangumiEpisode,
}

View File

@ -5,12 +5,10 @@ class DynamicsDataModel {
this.hasMore, this.hasMore,
this.items, this.items,
this.offset, this.offset,
this.total,
}); });
bool? hasMore; bool? hasMore;
List<DynamicItemModel>? items; List<DynamicItemModel>? items;
String? offset; String? offset;
int? total;
DynamicsDataModel.fromJson(Map<String, dynamic> json) { DynamicsDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more']; hasMore = json['has_more'];
@ -18,7 +16,6 @@ class DynamicsDataModel {
.map<DynamicItemModel>((e) => DynamicItemModel.fromJson(e)) .map<DynamicItemModel>((e) => DynamicItemModel.fromJson(e))
.toList(); .toList();
offset = json['offset']; offset = json['offset'];
total = json['total'];
} }
} }
@ -417,8 +414,6 @@ class DynamicMajorModel {
this.none, this.none,
this.type, this.type,
this.courses, this.courses,
this.common,
this.music,
}); });
DynamicArchiveModel? archive; DynamicArchiveModel? archive;
@ -434,8 +429,6 @@ class DynamicMajorModel {
// MAJOR_TYPE_OPUS 图文/文章 // MAJOR_TYPE_OPUS 图文/文章
String? type; String? type;
Map? courses; Map? courses;
Map? common;
Map? music;
DynamicMajorModel.fromJson(Map<String, dynamic> json) { DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null archive = json['archive'] != null
@ -459,8 +452,6 @@ class DynamicMajorModel {
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null; json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
type = json['type']; type = json['type'];
courses = json['courses'] ?? {}; courses = json['courses'] ?? {};
common = json['common'] ?? {};
music = json['music'] ?? {};
} }
} }

View File

@ -34,7 +34,7 @@ class RecVideoItemAppModel {
String? title; String? title;
int? isFollowed; int? isFollowed;
RcmdOwner? owner; RcmdOwner? owner;
String? rcmdReason; RcmdReason? rcmdReason;
String? goto; String? goto;
int? param; int? param;
String? uri; String? uri;
@ -62,19 +62,24 @@ class RecVideoItemAppModel {
duration = duration =
json['player_args'] != null ? json['player_args']['duration'] : -1; json['player_args'] != null ? json['player_args']['duration'] : -1;
//duration = json['cover_right_text']; //duration = json['cover_right_text'];
title = json['title'] ?? '获取标题失败'; title = json['title'];
owner = RcmdOwner.fromJson(json); owner = RcmdOwner.fromJson(json);
rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason']; rcmdReason = json['rcmd_reason_style'] != null
? RcmdReason.fromJson(json['rcmd_reason_style'])
: null;
// 由于app端api并不会直接返回与owner的关注状态 // 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效 // 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
RegExp regex = RegExp(r'已关注|新关注'); isFollowed = rcmdReason != null &&
isFollowed = regex.hasMatch(rcmdReason ?? '') ? 1 : 0; rcmdReason!.content != null &&
rcmdReason!.content!.contains('关注')
? 1
: 0;
// 如果是就无需再显示推荐原因交由view统一处理即可 // 如果是就无需再显示推荐原因交由view统一处理即可
if (isFollowed == 1) { if (isFollowed == 1) {
rcmdReason = null; rcmdReason = null;
} }
goto = json['goto']; goto = json['goto'];
param = int.parse(json['param'] ?? '-1'); param = int.parse(json['param']);
uri = json['uri']; uri = json['uri'];
talkBack = json['talk_back']; talkBack = json['talk_back'];

View File

@ -1,127 +0,0 @@
class LiveFollowingModel {
int? count;
List<LiveFollowingItemModel>? list;
int? liveCount;
int? neverLivedCount;
List? neverLivedFaces;
int? pageSize;
String? title;
int? totalPage;
LiveFollowingModel({
this.count,
this.list,
this.liveCount,
this.neverLivedCount,
this.neverLivedFaces,
this.pageSize,
this.title,
this.totalPage,
});
LiveFollowingModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
if (json['list'] != null) {
list = <LiveFollowingItemModel>[];
json['list'].forEach((v) {
list!.add(LiveFollowingItemModel.fromJson(v));
});
}
liveCount = json['live_count'];
neverLivedCount = json['never_lived_count'];
if (json['never_lived_faces'] != null) {
neverLivedFaces = <dynamic>[];
json['never_lived_faces'].forEach((v) {
neverLivedFaces!.add(v);
});
}
pageSize = json['pageSize'];
title = json['title'];
totalPage = json['totalPage'];
}
}
class LiveFollowingItemModel {
int? roomId;
int? uid;
String? uname;
String? title;
String? face;
int? liveStatus;
int? recordNum;
String? recentRecordId;
int? isAttention;
int? clipNum;
int? fansNum;
String? areaName;
String? areaValue;
String? tags;
String? recentRecordIdV2;
int? recordNumV2;
int? recordLiveTime;
String? areaNameV2;
String? roomNews;
String? watchIcon;
String? textSmall;
String? cover;
String? pic;
int? parentAreaId;
int? areaId;
LiveFollowingItemModel({
this.roomId,
this.uid,
this.uname,
this.title,
this.face,
this.liveStatus,
this.recordNum,
this.recentRecordId,
this.isAttention,
this.clipNum,
this.fansNum,
this.areaName,
this.areaValue,
this.tags,
this.recentRecordIdV2,
this.recordNumV2,
this.recordLiveTime,
this.areaNameV2,
this.roomNews,
this.watchIcon,
this.textSmall,
this.cover,
this.pic,
this.parentAreaId,
this.areaId,
});
LiveFollowingItemModel.fromJson(Map<String, dynamic> json) {
roomId = json['roomid'];
uid = json['uid'];
uname = json['uname'];
title = json['title'];
face = json['face'];
liveStatus = json['live_status'];
recordNum = json['record_num'];
recentRecordId = json['recent_record_id'];
isAttention = json['is_attention'];
clipNum = json['clipnum'];
fansNum = json['fans_num'];
areaName =
json['area_name'] == '' ? json['area_name_v2'] : json['area_name'];
areaValue = json['area_value'];
tags = json['tags'];
recentRecordIdV2 = json['recent_record_id_v2'];
recordNumV2 = json['record_num_v2'];
recordLiveTime = json['record_live_time'];
areaNameV2 = json['area_name_v2'];
roomNews = json['room_news'];
watchIcon = json['watch_icon'];
textSmall = json['text_small'];
cover = json['room_cover'];
pic = json['room_cover'];
parentAreaId = json['parent_area_id'];
areaId = json['area_id'];
}
}

View File

@ -1,101 +0,0 @@
class LiveMessageModel {
// 消息类型
final LiveMessageType type;
// 用户名
final String userName;
// 信息
final String? message;
// 数据
final dynamic data;
final String? face;
final int? uid;
final Map<String, dynamic>? emots;
// 颜色
final LiveMessageColor color;
LiveMessageModel({
required this.type,
required this.userName,
required this.message,
required this.color,
this.data,
this.face,
this.uid,
this.emots,
});
}
class LiveSuperChatMessage {
final String backgroundBottomColor;
final String backgroundColor;
final DateTime endTime;
final String face;
final String message;
final String price;
final DateTime startTime;
final String userName;
LiveSuperChatMessage({
required this.backgroundBottomColor,
required this.backgroundColor,
required this.endTime,
required this.face,
required this.message,
required this.price,
required this.startTime,
required this.userName,
});
}
enum LiveMessageType {
// 普通留言
chat,
// 醒目留言
superChat,
//
online,
// 加入
join,
// 关注
follow,
}
class LiveMessageColor {
final int r, g, b;
LiveMessageColor(this.r, this.g, this.b);
static LiveMessageColor get white => LiveMessageColor(255, 255, 255);
static LiveMessageColor numberToColor(int intColor) {
var obj = intColor.toRadixString(16);
LiveMessageColor color = LiveMessageColor.white;
if (obj.length == 4) {
obj = "00$obj";
}
if (obj.length == 6) {
var R = int.parse(obj.substring(0, 2), radix: 16);
var G = int.parse(obj.substring(2, 4), radix: 16);
var B = int.parse(obj.substring(4, 6), radix: 16);
color = LiveMessageColor(R, G, B);
}
if (obj.length == 8) {
var R = int.parse(obj.substring(2, 4), radix: 16);
var G = int.parse(obj.substring(4, 6), radix: 16);
var B = int.parse(obj.substring(6, 8), radix: 16);
//var A = int.parse(obj.substring(0, 2), radix: 16);
color = LiveMessageColor(R, G, B);
}
return color;
}
@override
String toString() {
return "#${r.toRadixString(16).padLeft(2, '0')}${g.toRadixString(16).padLeft(2, '0')}${b.toRadixString(16).padLeft(2, '0')}";
}
}

View File

@ -1,13 +1,11 @@
class RoomInfoModel { class RoomInfoModel {
RoomInfoModel({ RoomInfoModel({
this.roomId, this.roomId,
this.isPortrait,
this.liveStatus, this.liveStatus,
this.liveTime, this.liveTime,
this.playurlInfo, this.playurlInfo,
}); });
int? roomId; int? roomId;
bool? isPortrait;
int? liveStatus; int? liveStatus;
int? liveTime; int? liveTime;
PlayurlInfo? playurlInfo; PlayurlInfo? playurlInfo;
@ -15,7 +13,6 @@ class RoomInfoModel {
RoomInfoModel.fromJson(Map<String, dynamic> json) { RoomInfoModel.fromJson(Map<String, dynamic> json) {
roomId = json['room_id']; roomId = json['room_id'];
liveStatus = json['live_status']; liveStatus = json['live_status'];
isPortrait = json['is_portrait'];
liveTime = json['live_time']; liveTime = json['live_time'];
playurlInfo = PlayurlInfo.fromJson(json['playurl_info']); playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);
} }

View File

@ -1,46 +0,0 @@
class MemberArticleDataModel {
MemberArticleDataModel({
this.hasMore,
this.items,
this.offset,
this.updateNum,
});
bool? hasMore;
List<MemberArticleItemModel>? items;
String? offset;
int? updateNum;
MemberArticleDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more'];
items = json['items']
.map<MemberArticleItemModel>((e) => MemberArticleItemModel.fromJson(e))
.toList();
offset = json['offset'];
updateNum = json['update_num'];
}
}
class MemberArticleItemModel {
MemberArticleItemModel({
this.content,
this.cover,
this.jumpUrl,
this.opusId,
this.stat,
});
String? content;
Map? cover;
String? jumpUrl;
String? opusId;
Map? stat;
MemberArticleItemModel.fromJson(Map<String, dynamic> json) {
content = json['content'];
cover = json['cover'];
jumpUrl = json['jump_url'];
opusId = json['opus_id'];
stat = json['stat'];
}
}

View File

@ -8,7 +8,6 @@ class MemberInfoModel {
this.level, this.level,
this.isFollowed, this.isFollowed,
this.topPhoto, this.topPhoto,
this.silence,
this.official, this.official,
this.vip, this.vip,
this.liveRoom, this.liveRoom,
@ -22,7 +21,6 @@ class MemberInfoModel {
int? level; int? level;
bool? isFollowed; bool? isFollowed;
String? topPhoto; String? topPhoto;
int? silence;
Map? official; Map? official;
Vip? vip; Vip? vip;
LiveRoom? liveRoom; LiveRoom? liveRoom;
@ -36,7 +34,6 @@ class MemberInfoModel {
level = json['level']; level = json['level'];
isFollowed = json['is_followed']; isFollowed = json['is_followed'];
topPhoto = json['top_photo']; topPhoto = json['top_photo'];
silence = json['silence'] ?? 0;
official = json['official']; official = json['official'];
vip = Vip.fromJson(json['vip']); vip = Vip.fromJson(json['vip']);
liveRoom = liveRoom =
@ -50,23 +47,18 @@ class Vip {
this.status, this.status,
this.dueDate, this.dueDate,
this.label, this.label,
this.nicknameColor,
}); });
int? type; int? type;
int? status; int? status;
int? dueDate; int? dueDate;
Map? label; Map? label;
int? nicknameColor;
Vip.fromJson(Map<String, dynamic> json) { Vip.fromJson(Map<String, dynamic> json) {
type = json['type']; type = json['type'];
status = json['status']; status = json['status'];
dueDate = json['due_date']; dueDate = json['due_date'];
label = json['label']; label = json['label'];
nicknameColor = json['nickname_color'] == ''
? null
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
} }
} }

View File

@ -1,210 +0,0 @@
class MemberLikeDataModel {
MemberLikeDataModel({
this.aid,
this.videos,
this.tid,
this.tname,
this.pic,
this.title,
this.pubdate,
this.ctime,
this.desc,
this.state,
this.duration,
this.redirectUrl,
this.rights,
this.owner,
this.stat,
this.dimension,
this.cover43,
this.bvid,
this.interVideo,
this.resourceType,
this.subtitle,
this.enableVt,
});
final int? aid;
final int? videos;
final int? tid;
final String? tname;
final String? pic;
final String? title;
final int? pubdate;
final int? ctime;
final String? desc;
final int? state;
final int? duration;
final String? redirectUrl;
final Rights? rights;
final Owner? owner;
final Stat? stat;
final Dimension? dimension;
final String? cover43;
final String? bvid;
final bool? interVideo;
final String? resourceType;
final String? subtitle;
final int? enableVt;
factory MemberLikeDataModel.fromJson(Map<String, dynamic> json) =>
MemberLikeDataModel(
aid: json["aid"],
videos: json["videos"],
tid: json["tid"],
tname: json["tname"],
pic: json["pic"],
title: json["title"],
pubdate: json["pubdate"],
ctime: json["ctime"],
desc: json["desc"],
state: json["state"],
duration: json["duration"],
redirectUrl: json["redirect_url"],
rights: Rights.fromJson(json["rights"]),
owner: Owner.fromJson(json["owner"]),
stat: Stat.fromJson(json["stat"]),
dimension: Dimension.fromJson(json["dimension"]),
cover43: json["cover43"],
bvid: json["bvid"],
interVideo: json["inter_video"],
resourceType: json["resource_type"],
subtitle: json["subtitle"],
enableVt: json["enable_vt"],
);
}
class Dimension {
Dimension({
required this.width,
required this.height,
required this.rotate,
});
final int width;
final int height;
final int rotate;
factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(
width: json["width"],
height: json["height"],
rotate: json["rotate"],
);
}
class Owner {
Owner({
required this.mid,
required this.name,
required this.face,
});
final int mid;
final String name;
final String face;
factory Owner.fromJson(Map<String, dynamic> json) => Owner(
mid: json["mid"],
name: json["name"],
face: json["face"],
);
}
class Rights {
Rights({
required this.bp,
required this.elec,
required this.download,
required this.movie,
required this.pay,
required this.hd5,
required this.noReprint,
required this.autoplay,
required this.ugcPay,
required this.isCooperation,
required this.ugcPayPreview,
required this.noBackground,
required this.arcPay,
required this.payFreeWatch,
});
final int bp;
final int elec;
final int download;
final int movie;
final int pay;
final int hd5;
final int noReprint;
final int autoplay;
final int ugcPay;
final int isCooperation;
final int ugcPayPreview;
final int noBackground;
final int arcPay;
final int payFreeWatch;
factory Rights.fromJson(Map<String, dynamic> json) => Rights(
bp: json["bp"],
elec: json["elec"],
download: json["download"],
movie: json["movie"],
pay: json["pay"],
hd5: json["hd5"],
noReprint: json["no_reprint"],
autoplay: json["autoplay"],
ugcPay: json["ugc_pay"],
isCooperation: json["is_cooperation"],
ugcPayPreview: json["ugc_pay_preview"],
noBackground: json["no_background"],
arcPay: json["arc_pay"],
payFreeWatch: json["pay_free_watch"],
);
}
class Stat {
Stat({
required this.aid,
required this.view,
required this.danmaku,
required this.reply,
required this.favorite,
required this.coin,
required this.share,
required this.nowRank,
required this.hisRank,
required this.like,
required this.dislike,
required this.vt,
required this.vv,
});
final int aid;
final int view;
final int danmaku;
final int reply;
final int favorite;
final int coin;
final int share;
final int nowRank;
final int hisRank;
final int like;
final int dislike;
final int vt;
final int vv;
factory Stat.fromJson(Map<String, dynamic> json) => Stat(
aid: json["aid"],
view: json["view"],
danmaku: json["danmaku"],
reply: json["reply"],
favorite: json["favorite"],
coin: json["coin"],
share: json["share"],
nowRank: json["now_rank"],
hisRank: json["his_rank"],
like: json["like"],
dislike: json["dislike"],
vt: json["vt"],
vv: json["vv"],
);
}

View File

@ -2,32 +2,18 @@ class MemberSeasonsDataModel {
MemberSeasonsDataModel({ MemberSeasonsDataModel({
this.page, this.page,
this.seasonsList, this.seasonsList,
this.seriesList,
}); });
Map? page; Map? page;
List<MemberSeasonsList>? seasonsList; List<MemberSeasonsList>? seasonsList;
List<MemberArchiveItem>? seriesList;
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) { MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
page = json['page']; page = json['page'];
var tempList1 = json['seasons_list'] != null seasonsList = json['seasons_list'] != null
? json['seasons_list'] ? json['seasons_list']
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e)) .map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
.toList() .toList()
: []; : [];
var tempList2 = json['series_list'] != null
? json['series_list']
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
.toList()
: [];
seriesList = json['archives'] != null
? json['archives']
.map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))
.toList()
: [];
seasonsList = [...tempList1, ...tempList2];
} }
} }
@ -100,8 +86,6 @@ class MamberMeta {
this.ptime, this.ptime,
this.seasonId, this.seasonId,
this.total, this.total,
this.seriesId,
this.category,
}); });
String? cover; String? cover;
@ -111,8 +95,6 @@ class MamberMeta {
int? ptime; int? ptime;
int? seasonId; int? seasonId;
int? total; int? total;
int? seriesId;
int? category;
MamberMeta.fromJson(Map<String, dynamic> json) { MamberMeta.fromJson(Map<String, dynamic> json) {
cover = json['cover']; cover = json['cover'];
@ -122,7 +104,5 @@ class MamberMeta {
ptime = json['ptime']; ptime = json['ptime'];
seasonId = json['season_id']; seasonId = json['season_id'];
total = json['total']; total = json['total'];
seriesId = json['series_id'];
category = json['category'];
} }
} }

View File

@ -23,7 +23,6 @@ class HotVideoItemModel {
this.dimension, this.dimension,
this.shortLinkV2, this.shortLinkV2,
this.firstFrame, this.firstFrame,
this.cover,
this.pubLocation, this.pubLocation,
this.seasontype, this.seasontype,
this.isOgv, this.isOgv,
@ -51,7 +50,6 @@ class HotVideoItemModel {
Dimension? dimension; Dimension? dimension;
String? shortLinkV2; String? shortLinkV2;
String? firstFrame; String? firstFrame;
String? cover;
String? pubLocation; String? pubLocation;
int? seasontype; int? seasontype;
bool? isOgv; bool? isOgv;
@ -79,7 +77,6 @@ class HotVideoItemModel {
dimension = Dimension.fromMap(json['dimension']); dimension = Dimension.fromMap(json['dimension']);
shortLinkV2 = json["short_link_v2"]; shortLinkV2 = json["short_link_v2"];
firstFrame = json["first_frame"]; firstFrame = json["first_frame"];
cover = json["first_frame"];
pubLocation = json["pub_location"]; pubLocation = json["pub_location"];
seasontype = json["seasontype"]; seasontype = json["seasontype"];
isOgv = json["isOgv"]; isOgv = json["isOgv"];

View File

@ -1,12 +1,19 @@
import 'package:hive/hive.dart';
part 'model_owner.g.dart';
@HiveType(typeId: 3)
class Owner { class Owner {
Owner({ Owner({
this.mid, this.mid,
this.name, this.name,
this.face, this.face,
}); });
@HiveField(0)
int? mid; int? mid;
@HiveField(1)
String? name; String? name;
@HiveField(2)
String? face; String? face;
Owner.fromJson(Map<String, dynamic> json) { Owner.fromJson(Map<String, dynamic> json) {

View File

@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model_owner.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OwnerAdapter extends TypeAdapter<Owner> {
@override
final int typeId = 3;
@override
Owner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Owner(
mid: fields[0] as int?,
name: fields[1] as String?,
face: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, Owner obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.mid)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.face);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,5 +1,9 @@
import './model_owner.dart'; import './model_owner.dart';
import 'package:hive/hive.dart';
part 'model_rec_video_item.g.dart';
@HiveType(typeId: 0)
class RecVideoItemModel { class RecVideoItemModel {
RecVideoItemModel({ RecVideoItemModel({
this.id, this.id,
@ -17,19 +21,32 @@ class RecVideoItemModel {
this.rcmdReason, this.rcmdReason,
}); });
@HiveField(0)
int? id = -1; int? id = -1;
@HiveField(1)
String? bvid = ''; String? bvid = '';
@HiveField(2)
int? cid = -1; int? cid = -1;
@HiveField(3)
String? goto = ''; String? goto = '';
@HiveField(4)
String? uri = ''; String? uri = '';
@HiveField(5)
String? pic = ''; String? pic = '';
@HiveField(6)
String? title = ''; String? title = '';
@HiveField(7)
int? duration = -1; int? duration = -1;
@HiveField(8)
int? pubdate = -1; int? pubdate = -1;
@HiveField(9)
Owner? owner; Owner? owner;
@HiveField(10)
Stat? stat; Stat? stat;
@HiveField(11)
int? isFollowed; int? isFollowed;
String? rcmdReason; @HiveField(12)
RcmdReason? rcmdReason;
RecVideoItemModel.fromJson(Map<String, dynamic> json) { RecVideoItemModel.fromJson(Map<String, dynamic> json) {
id = json["id"]; id = json["id"];
@ -44,20 +61,26 @@ class RecVideoItemModel {
owner = Owner.fromJson(json["owner"]); owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]); stat = Stat.fromJson(json["stat"]);
isFollowed = json["is_followed"] ?? 0; isFollowed = json["is_followed"] ?? 0;
rcmdReason = json["rcmd_reason"]?['content']; rcmdReason = json["rcmd_reason"] != null
? RcmdReason.fromJson(json["rcmd_reason"])
: RcmdReason(content: '');
} }
} }
@HiveType(typeId: 1)
class Stat { class Stat {
Stat({ Stat({
this.view, this.view,
this.like, this.like,
this.danmu, this.danmu,
}); });
@HiveField(0)
int? view; int? view;
@HiveField(1)
int? like; int? like;
@HiveField(2)
int? danmu; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
// 无需在model中转换以保留原始数据在view层处理即可 // 无需在model中转换以保留原始数据在view层处理即可
view = json["view"]; view = json["view"];
@ -65,3 +88,20 @@ class Stat {
danmu = json['danmaku']; danmu = json['danmaku'];
} }
} }
@HiveType(typeId: 2)
class RcmdReason {
RcmdReason({
this.reasonType,
this.content,
});
@HiveField(0)
int? reasonType;
@HiveField(1)
String? content = '';
RcmdReason.fromJson(Map<String, dynamic> json) {
reasonType = json["reason_type"];
content = json["content"] ?? '';
}
}

View File

@ -0,0 +1,154 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model_rec_video_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
@override
final int typeId = 0;
@override
RecVideoItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemModel(
id: fields[0] as int?,
bvid: fields[1] as String?,
cid: fields[2] as int?,
goto: fields[3] as String?,
uri: fields[4] as String?,
pic: fields[5] as String?,
title: fields[6] as String?,
duration: fields[7] as int?,
pubdate: fields[8] as int?,
owner: fields[9] as Owner?,
stat: fields[10] as Stat?,
isFollowed: fields[11] as int?,
rcmdReason: fields[12] as RcmdReason?,
);
}
@override
void write(BinaryWriter writer, RecVideoItemModel obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.bvid)
..writeByte(2)
..write(obj.cid)
..writeByte(3)
..write(obj.goto)
..writeByte(4)
..write(obj.uri)
..writeByte(5)
..write(obj.pic)
..writeByte(6)
..write(obj.title)
..writeByte(7)
..write(obj.duration)
..writeByte(8)
..write(obj.pubdate)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.stat)
..writeByte(11)
..write(obj.isFollowed)
..writeByte(12)
..write(obj.rcmdReason);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class StatAdapter extends TypeAdapter<Stat> {
@override
final int typeId = 1;
@override
Stat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Stat(
view: fields[0] as int?,
like: fields[1] as int?,
danmu: fields[2] as int?,
);
}
@override
void write(BinaryWriter writer, Stat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 2;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
reasonType: fields[0] as int?,
content: fields[1] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.reasonType)
..writeByte(1)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,140 +0,0 @@
class MessageAtModel {
Cursor? cursor;
List<MessageAtItems>? items;
MessageAtModel({this.cursor, this.items});
MessageAtModel.fromJson(Map<String, dynamic> json) {
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
if (json['items'] != null) {
items = <MessageAtItems>[];
json['items'].forEach((v) {
items!.add(MessageAtItems.fromJson(v));
});
}
}
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
Cursor.fromJson(Map<String, dynamic> json) {
id = json['id'];
isEnd = json['isEnd'];
time = json['time'];
}
}
class MessageAtItems {
int? id;
int? atTime;
User? user;
MessageAtItem? item;
MessageAtItems({this.id, this.atTime, this.user, this.item});
MessageAtItems.fromJson(Map<String, dynamic> json) {
id = json['id'];
atTime = json['at_time'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
item = json['item'] != null ? MessageAtItem.fromJson(json['item']) : null;
}
}
class MessageAtItem {
String? type;
String? business;
int? businessId;
String? title;
String? image;
String? uri;
int? subjectId;
int? rootId;
int? targetId;
int? sourceId;
String? sourceContent;
String? nativeUri;
List<User>? atDetails;
List<dynamic>? topicDetails;
bool? hideReplyButton;
MessageAtItem({
this.type,
this.business,
this.businessId,
this.title,
this.image,
this.uri,
this.subjectId,
this.rootId,
this.targetId,
this.sourceId,
this.sourceContent,
this.nativeUri,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
});
MessageAtItem.fromJson(Map<String, dynamic> json) {
type = json['type'];
business = json['business'];
businessId = json['business_id'];
title = json['title'];
image = json['image'];
uri = json['uri'];
subjectId = json['subject_id'];
rootId = json['root_id'];
targetId = json['target_id'];
sourceId = json['source_id'];
sourceContent = json['source_content'];
nativeUri = json['native_uri'];
if (json['at_details'] != null) {
atDetails = <User>[];
json['at_details'].forEach((v) {
atDetails!.add(User.fromJson(v));
});
}
if (json['topic_details'] != null) {
topicDetails = <dynamic>[];
json['topic_details'].forEach((v) {
topicDetails!.add(v);
});
}
hideReplyButton = json['hide_reply_button'];
}
}
class User {
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
User(
{this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
User.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -1,183 +0,0 @@
class MessageLikeModel {
MessageLikeModel({
this.latest,
this.total,
});
Latest? latest;
Total? total;
factory MessageLikeModel.fromJson(Map<String, dynamic> json) =>
MessageLikeModel(
latest: json["latest"] == null ? null : Latest.fromJson(json["latest"]),
total: json["total"] == null ? null : Total.fromJson(json["total"]),
);
}
class Latest {
Latest({
this.items,
this.lastViewAt,
});
List? items;
int? lastViewAt;
factory Latest.fromJson(Map<String, dynamic> json) => Latest(
items: json["items"],
lastViewAt: json["last_view_at"],
);
}
class Total {
Total({
this.cursor,
this.items,
});
Cursor? cursor;
List<MessageLikeItem>? items;
factory Total.fromJson(Map<String, dynamic> json) => Total(
cursor: json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null,
items: json["items"] == null
? []
: json["items"].map<MessageLikeItem>((e) {
return MessageLikeItem.fromJson(e);
}).toList(),
);
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
factory Cursor.fromJson(Map<String, dynamic> json) => Cursor(
id: json['id'],
isEnd: json['is_end'],
time: json['time'],
);
}
class MessageLikeItem {
MessageLikeItem({
this.id,
this.users,
this.item,
this.counts,
this.likeTime,
this.noticeState,
this.isExpand = false,
});
int? id;
List<MessageLikeUser>? users;
MessageLikeItemItem? item;
int? counts;
int? likeTime;
int? noticeState;
bool isExpand;
factory MessageLikeItem.fromJson(Map<String, dynamic> json) =>
MessageLikeItem(
id: json["id"],
users: json["users"] == null
? []
: json["users"].map<MessageLikeUser>((e) {
return MessageLikeUser.fromJson(e);
}).toList(),
item: json["item"] == null
? null
: MessageLikeItemItem.fromJson(json["item"]),
counts: json["counts"],
likeTime: json["like_time"],
noticeState: json["notice_state"],
);
}
class MessageLikeUser {
MessageLikeUser({
this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow,
});
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
factory MessageLikeUser.fromJson(Map<String, dynamic> json) =>
MessageLikeUser(
mid: json["mid"],
fans: json["fans"],
nickname: json["nickname"],
avatar: json["avatar"],
midLink: json["mid_link"],
follow: json["follow"],
);
}
class MessageLikeItemItem {
MessageLikeItemItem({
this.itemId,
this.pid,
this.type,
this.business,
this.businessId,
this.replyBusinessId,
this.likeBusinessId,
this.title,
this.desc,
this.image,
this.uri,
this.detailName,
this.nativeUri,
this.ctime,
});
int? itemId;
int? pid;
String? type;
String? business;
int? businessId;
int? replyBusinessId;
int? likeBusinessId;
String? title;
String? desc;
String? image;
String? uri;
String? detailName;
String? nativeUri;
int? ctime;
factory MessageLikeItemItem.fromJson(Map<String, dynamic> json) =>
MessageLikeItemItem(
itemId: json["item_id"],
pid: json["pid"],
type: json["type"],
business: json["business"],
businessId: json["business_id"],
replyBusinessId: json["reply_business_id"],
likeBusinessId: json["like_business_id"],
title: json["title"],
desc: json["desc"],
image: json["image"],
uri: json["uri"],
detailName: json["detail_name"],
nativeUri: json["native_uri"],
ctime: json["ctime"],
);
}

View File

@ -1,168 +0,0 @@
class MessageReplyModel {
MessageReplyModel({
this.cursor,
this.items,
});
Cursor? cursor;
List<MessageReplyItem>? items;
MessageReplyModel.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']);
items = json["items"] != null
? json["items"].map<MessageReplyItem>((e) {
return MessageReplyItem.fromJson(e);
}).toList()
: [];
}
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
Cursor.fromJson(Map<String, dynamic> json) {
id = json['id'];
isEnd = json['is_end'];
time = json['time'];
}
}
class MessageReplyItem {
MessageReplyItem({
this.count,
this.id,
this.isMulti,
this.item,
this.replyTime,
this.user,
});
int? count;
int? id;
int? isMulti;
ReplyContentItem? item;
int? replyTime;
ReplyUser? user;
MessageReplyItem.fromJson(Map<String, dynamic> json) {
count = json['count'];
id = json['id'];
isMulti = json['is_multi'];
item = ReplyContentItem.fromJson(json["item"]);
replyTime = json['reply_time'];
user = ReplyUser.fromJson(json['user']);
}
}
class ReplyContentItem {
ReplyContentItem({
this.subjectId,
this.rootId,
this.sourceId,
this.targetId,
this.type,
this.businessId,
this.business,
this.title,
this.desc,
this.image,
this.uri,
this.nativeUri,
this.detailTitle,
this.rootReplyContent,
this.sourceContent,
this.targetReplyContent,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
this.hideLikeButton,
this.likeState,
this.danmu,
this.message,
});
int? subjectId;
int? rootId;
int? sourceId;
int? targetId;
String? type;
int? businessId;
String? business;
String? title;
String? desc;
String? image;
String? uri;
String? nativeUri;
String? detailTitle;
String? rootReplyContent;
String? sourceContent;
String? targetReplyContent;
List? atDetails;
List? topicDetails;
bool? hideReplyButton;
bool? hideLikeButton;
int? likeState;
String? danmu;
String? message;
ReplyContentItem.fromJson(Map<String, dynamic> json) {
subjectId = json['subject_id'];
rootId = json['root_id'];
sourceId = json['source_id'];
targetId = json['target_id'];
type = json['type'];
businessId = json['business_id'];
business = json['business'];
title = json['title'];
desc = json['desc'];
image = json['image'];
uri = json['uri'];
nativeUri = json['native_uri'];
detailTitle = json['detail_title'];
rootReplyContent = json['root_reply_content'];
sourceContent = json['source_content'];
targetReplyContent = json['target_reply_content'];
atDetails = json['at_details'];
topicDetails = json['topic_details'];
hideReplyButton = json['hide_reply_button'];
hideLikeButton = json['hide_like_button'];
likeState = json['like_state'];
danmu = json['danmu'];
message = json['message'];
}
}
class ReplyUser {
ReplyUser({
this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow,
});
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
ReplyUser.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -13,9 +13,8 @@ class SessionDataModel {
SessionDataModel.fromJson(Map<String, dynamic> json) { SessionDataModel.fromJson(Map<String, dynamic> json) {
sessionList = json['session_list'] sessionList = json['session_list']
?.map<SessionList>((e) => SessionList.fromJson(e)) ?.map<SessionList>((e) => SessionList.fromJson(e))
.toList() ?? .toList();
[];
hasMore = json['has_more']; hasMore = json['has_more'];
} }
} }
@ -111,7 +110,7 @@ class SessionList {
class LastMsg { class LastMsg {
LastMsg({ LastMsg({
this.senderUid, this.senderIid,
this.receiverType, this.receiverType,
this.receiverId, this.receiverId,
this.msgType, this.msgType,
@ -125,7 +124,7 @@ class LastMsg {
// this.newFaceVersion, // this.newFaceVersion,
}); });
int? senderUid; int? senderIid;
int? receiverType; int? receiverType;
int? receiverId; int? receiverId;
int? msgType; int? msgType;
@ -139,7 +138,7 @@ class LastMsg {
// int? newFaceVersion; // int? newFaceVersion;
LastMsg.fromJson(Map<String, dynamic> json) { LastMsg.fromJson(Map<String, dynamic> json) {
senderUid = json['sender_uid']; senderIid = json['sender_uid'];
receiverType = json['receiver_type']; receiverType = json['receiver_type'];
receiverId = json['receiver_id']; receiverId = json['receiver_id'];
msgType = json['msg_type']; msgType = json['msg_type'];

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