diff --git a/.github/ISSUE_TEMPLATE/bug-反馈.md b/.github/ISSUE_TEMPLATE/bug-反馈.md
new file mode 100644
index 00000000..0c2c4826
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-反馈.md
@@ -0,0 +1,23 @@
+---
+name: Bug 反馈
+about: 描述你所遇到的bug
+title: ''
+labels: 问题反馈
+assignees: guozhigq
+
+---
+
+### 问题描述
+请提供一个清晰而简明的问题描述。
+
+### 复现步骤
+请提供复现该问题所需的具体步骤。
+
+### 预期行为
+请描述你期望的正确行为或结果。
+
+### 系统信息
+请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
+
+### 相关截图或日志
+如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。
diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/功能请求.md
new file mode 100644
index 00000000..7e57d1c1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/功能请求.md
@@ -0,0 +1,20 @@
+---
+name: 功能请求
+about: 对于功能的一些建议
+title: ''
+labels: 功能
+assignees: guozhigq
+
+---
+
+### 功能描述
+请提供对所请求功能的清晰描述。
+
+### 目标
+请描述你希望通过这个功能实现的目标。
+
+### 解决方案
+如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
+
+### 其他
+请提供已实现该功能或类似功能的应用
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..249f932c
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,84 @@
+name: build_apk
+
+# action事件触发
+on:
+ push:
+ # push tag时触发
+ tags:
+ - 'v*.*.*'
+
+# 可以有多个jobs
+jobs:
+ build_apk:
+ # 运行环境 ubuntu-latest window-latest mac-latest
+ runs-on: ubuntu-latest
+
+ # 每个jobs中可以有多个steps
+ steps:
+ - name: 代码迁出
+ uses: actions/checkout@v3
+
+ - name: 构建Java环境
+ uses: actions/setup-java@v3
+ with:
+ distribution: "zulu"
+ java-version: "17"
+ token: ${{secrets.GIT_TOKEN}}
+
+ - name: 检查缓存
+ uses: actions/cache@v2
+ id: cache-flutter
+ with:
+ path: /root/flutter-sdk # Flutter SDK 的路径
+ key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
+
+ - name: 安装Flutter
+ if: steps.cache-flutter.outputs.cache-hit != 'true'
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: 3.10.6
+ channel: any
+
+ - name: 下载项目依赖
+ run: flutter pub get
+
+ - name: 解码生成 jks
+ run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks
+ env:
+ KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
+
+ - name: flutter build apk
+ # 对应 android/app/build.gradle signingConfigs中的配置项
+ run: flutter build apk --release --split-per-abi
+ env:
+ KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
+ KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
+
+ - name: 获取版本号
+ id: version
+ run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
+
+ # - name: 获取当前日期
+ # id: date
+ # run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
+
+ - name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk
+ run: |
+ # DATE=${{ steps.date.outputs.date }}
+ for file in build/app/outputs/flutter-apk/app-*-release.apk; do
+ if [[ $file =~ app-(.*)-release.apk ]]; then
+ new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk"
+ mv "$file" "$new_file_name"
+ fi
+ done
+
+ - name: 构建和发布release
+ uses: ncipollo/release-action@v1
+ with:
+ # release title
+ name: v${{ steps.version.outputs.version }}
+ artifacts: "build/app/outputs/flutter-apk/Pili-*.apk"
+ bodyFile: "change_log/${{steps.version.outputs.version}}.md"
+ token: ${{ secrets.GIT_TOKEN }}
+ allowUpdates: true
diff --git a/README.md b/README.md
index 5e69024f..56c748ec 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,128 @@
-# pilipala
+
+

+
-A new Flutter project.
-## Getting Started
+
+
PiliPala
+
使用Flutter开发的BiliBili第三方客户端
+
+

+

+

+
+
+
-This project is a starting point for a Flutter application.
+### 开发环境
+Xcode 13.4 不支持**auto_orientation**,请注释相关代码
-A few resources to get you started if this is your first Flutter project:
+```bash
+[✓] Flutter (Channel stable, 3.10.6, on macOS 12.1 21C52 darwin-arm64, locale
+ zh-Hans-CN)
+[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
+[✓] Xcode - develop for iOS and macOS (Xcode 13.4)
+[✓] Chrome - develop for the web
+[✓] Android Studio (version 2022.2)
+[✓] VS Code (version 1.77.3)
-- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+```
-For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+
+
+### 功能
+
+目前着重移动端(Android、iOS),暂时没有适配桌面端、Pad端、手表端等
+
+
+
+现有功能及[开发计划](https://github.com/users/guozhigq/projects/5)
+
+
+- [x] 推荐视频列表(app端)
+- [x] 最热视频列表
+- [x] 热门直播
+- [x] 番剧列表
+- [x] 屏蔽黑名单内用户视频
+
+- [x] 用户相关
+ - [x] 粉丝、关注用户、拉黑用户查看
+ - [x] 用户主页查看
+ - [x] 关注/取关用户
+ - [ ] 离线缓存
+ - [x] 稍后再看
+ - [x] 观看记录
+ - [x] 我的收藏
+
+- [x] 动态相关
+ - [x] 全部、投稿、番剧分类查看
+ - [x] 动态评论查看
+ - [x] 动态评论回复功能
+
+- [x] 视频播放相关
+ - [x] 双击快进/快退
+ - [x] 双击播放/暂停
+ - [x] 垂直方向调节亮度/音量
+ - [x] 垂直方向上滑全屏、下滑退出全屏
+ - [x] 水平方向手势快进/快退
+ - [x] 全屏方向设置
+ - [x] 倍速选择/长按2倍速
+ - [x] 硬件加速(视机型而定)
+ - [x] 画质选择(高清画质未解锁)
+ - [x] 音质选择(视视频而定)
+ - [x] 解码格式选择(视视频而定)
+ - [ ] 弹幕
+ - [ ] 字幕
+ - [x] 记忆播放
+
+- [x] 搜索相关
+ - [x] 热搜
+ - [x] 搜索历史
+ - [x] 默认搜索词
+ - [x] 投稿、番剧、直播间、用户搜索
+
+- [x] 视频详情页相关
+ - [x] 视频选集(分p)切换
+ - [x] 点赞、投币、收藏/取消收藏
+ - [x] 相关视频查看
+ - [x] 评论用户身份标识
+ - [x] 评论(排序)查看、二楼评论查看
+ - [x] 主楼、二楼评论回复功能
+ - [x] 评论点赞
+ - [x] 评论笔记图片查看、保存
+
+- [x] 设置相关
+ - [x] 画质、音质、解码方式预设
+ - [x] 图片质量设定
+ - [x] 主题模式:亮色/暗色/跟随系统
+ - [x] 震动反馈(可选)
+- [ ] 等等
+
+
+
+### 下载
+
+可以通过右侧release进行下载或拉取代码到本地进行编译
+
+
+
+### 声明
+
+此项目(PiliPala)是个人为了兴趣而开发, 仅用于学习和测试。
+所用API皆从官方网站收集, 不提供任何破解内容。
+
+感谢使用
+
+
+
+### 致谢
+
+- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
+- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)
+- [media-kit](https://github.com/media-kit/media-kit)
+- [dio](https://pub.dev/packages/dio)
+- 等等
+
+
+
+
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 12caecb2..06845daf 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -25,6 +25,17 @@ 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 keystoreProperties = new Properties()
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+}
+
+def _storeFile = file(System.getenv("KEYSTORE") ?: keystoreProperties["storeFile"] ?: "vvex.jks")
+def _storePassword = System.getenv("KEYSTORE_PASSWORD") ?: keystoreProperties["storePassword"]
+def _keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties["keyAlias"]
+def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"]
+
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
@@ -54,11 +65,24 @@ android {
minSdkVersion 19
}
+ signingConfigs {
+ // 添加签名配置
+ release {
+ // 配置密钥库文件的位置、别名、密码等信息
+ storeFile _storeFile
+ storePassword _storePassword
+ keyAlias _keyAlias
+ keyPassword _keyPassword
+ v1SigningEnabled true
+ v2SigningEnabled true
+ }
+ }
+
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig signingConfigs.debug
+ signingConfig signingConfigs.release
}
}
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0faef731..94db6539 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -21,7 +21,7 @@
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
index df13b128..4f468fbe 100644
Binary files a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
index 8cdf4851..432b3425 100644
Binary files a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
index 1bdb8bb3..c37d4b82 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
index 140baca6..2c03aecb 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
index b1b58395..ffc6bc92 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index f606c4d8..5f349f7f 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,5 +2,4 @@
-
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 1cb4b0c4..1c01ae34 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 9eaf9392..740b2e82 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 6bf71b8f..efd04701 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 9c4d6c7a..64583ebb 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 59f64433..fc95b79a 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/ReachabilitySwift.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/ReachabilitySwift.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/ReachabilitySwift.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/connectivity_plus.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/connectivity_plus.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/connectivity_plus.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/device_info_plus.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/device_info_plus.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/device_info_plus.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/image_gallery_saver.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/image_gallery_saver.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/image_gallery_saver.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_libs_ios_video.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_libs_ios_video.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_libs_ios_video.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_native_event_loop.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_native_event_loop.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_native_event_loop.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_video.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_video.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/media_kit_video.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/package_info_plus.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/package_info_plus.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/package_info_plus.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/permission_handler_apple.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/permission_handler_apple.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/permission_handler_apple.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/screen_brightness_ios.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/screen_brightness_ios.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/screen_brightness_ios.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/share_plus.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/share_plus.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/share_plus.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/volume_controller.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/volume_controller.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/volume_controller.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/wakelock_plus.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/wakelock_plus.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/wakelock_plus.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/webview_cookie_manager.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/webview_cookie_manager.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/webview_cookie_manager.build/dgph differ
diff --git a/android/build/ios/Pods.build/Release-iphonesimulator/webview_flutter_wkwebview.build/dgph b/android/build/ios/Pods.build/Release-iphonesimulator/webview_flutter_wkwebview.build/dgph
new file mode 100644
index 00000000..c431c0f7
Binary files /dev/null and b/android/build/ios/Pods.build/Release-iphonesimulator/webview_flutter_wkwebview.build/dgph differ
diff --git a/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf b/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf
deleted file mode 100755
index aff150a1..00000000
Binary files a/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf and /dev/null differ
diff --git a/assets/images/logo/logo_android.png b/assets/images/logo/logo_android.png
index 5e220fb4..db737743 100644
Binary files a/assets/images/logo/logo_android.png and b/assets/images/logo/logo_android.png differ
diff --git a/assets/images/logo/logo_android_2.png b/assets/images/logo/logo_android_2.png
new file mode 100644
index 00000000..9e018982
Binary files /dev/null and b/assets/images/logo/logo_android_2.png differ
diff --git a/assets/images/logo/logo_big.png b/assets/images/logo/logo_big.png
deleted file mode 100644
index 62370832..00000000
Binary files a/assets/images/logo/logo_big.png and /dev/null differ
diff --git a/assets/images/logo/logo_ios.png b/assets/images/logo/logo_ios.png
index a9992b4a..f1c73b96 100644
Binary files a/assets/images/logo/logo_ios.png and b/assets/images/logo/logo_ios.png differ
diff --git a/assets/sreenshot/174shots_so.png b/assets/sreenshot/174shots_so.png
new file mode 100644
index 00000000..bf16b34f
Binary files /dev/null and b/assets/sreenshot/174shots_so.png differ
diff --git a/assets/sreenshot/510shots_so.png b/assets/sreenshot/510shots_so.png
new file mode 100644
index 00000000..ae00cf9f
Binary files /dev/null and b/assets/sreenshot/510shots_so.png differ
diff --git a/assets/sreenshot/850shots_so.png b/assets/sreenshot/850shots_so.png
new file mode 100644
index 00000000..fbdfb88c
Binary files /dev/null and b/assets/sreenshot/850shots_so.png differ
diff --git a/assets/sreenshot/bangumi.png b/assets/sreenshot/bangumi.png
new file mode 100644
index 00000000..8f03ed82
Binary files /dev/null and b/assets/sreenshot/bangumi.png differ
diff --git a/assets/sreenshot/bangumi_detail.png b/assets/sreenshot/bangumi_detail.png
new file mode 100644
index 00000000..b20b8a76
Binary files /dev/null and b/assets/sreenshot/bangumi_detail.png differ
diff --git a/assets/sreenshot/dynamic.png b/assets/sreenshot/dynamic.png
new file mode 100644
index 00000000..0f43e9a2
Binary files /dev/null and b/assets/sreenshot/dynamic.png differ
diff --git a/assets/sreenshot/home.png b/assets/sreenshot/home.png
new file mode 100644
index 00000000..42573f6b
Binary files /dev/null and b/assets/sreenshot/home.png differ
diff --git a/assets/sreenshot/media.png b/assets/sreenshot/media.png
new file mode 100644
index 00000000..72bfb211
Binary files /dev/null and b/assets/sreenshot/media.png differ
diff --git a/assets/sreenshot/member.png b/assets/sreenshot/member.png
new file mode 100644
index 00000000..370e7fc2
Binary files /dev/null and b/assets/sreenshot/member.png differ
diff --git a/assets/sreenshot/search.png b/assets/sreenshot/search.png
new file mode 100644
index 00000000..fa73dcbd
Binary files /dev/null and b/assets/sreenshot/search.png differ
diff --git a/assets/sreenshot/set.png b/assets/sreenshot/set.png
new file mode 100644
index 00000000..0ce47e35
Binary files /dev/null and b/assets/sreenshot/set.png differ
diff --git a/change_log/1.0.0.0817.md b/change_log/1.0.0.0817.md
new file mode 100644
index 00000000..1750f4ae
--- /dev/null
+++ b/change_log/1.0.0.0817.md
@@ -0,0 +1,11 @@
+## 1.0.0
+
+### 初始版本
++ 直播、推荐、动态功能
++ 投稿、番剧播放功能
++ 播放器手势支持
++ 画质、音质、解码格式支持
++ 点赞、投币、收藏功能
++ 关注/取关、用户主页功能
++ 评论功能
++ 历史记录、稍后再看功能
\ No newline at end of file
diff --git a/change_log/1.0.1.0817.md b/change_log/1.0.1.0817.md
new file mode 100644
index 00000000..ebc1e155
--- /dev/null
+++ b/change_log/1.0.1.0817.md
@@ -0,0 +1,7 @@
+## 1.0.1
+
+### 修复
++ 升级播放器依赖
++ android平台 AV1格式视频支持
++ 视频全屏功能
+
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index aad4a9b0..5100bf76 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -5,6 +5,8 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
+ - flutter_volume_controller (0.0.1):
+ - Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
@@ -31,6 +33,8 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
+ - url_launcher_ios (0.0.1):
+ - Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@@ -44,6 +48,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
+ - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/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`)
@@ -54,6 +59,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
@@ -71,6 +77,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
+ flutter_volume_controller:
+ :path: ".symlinks/plugins/flutter_volume_controller/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
media_kit_libs_ios_video:
@@ -91,6 +99,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
@@ -104,6 +114,7 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+ flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
@@ -116,6 +127,7 @@ SPEC CHECKSUMS:
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
+ url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 7f19eb8f..54537b4d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 4b8fadf7..3c8fa957 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index bf7de64d..5068d682 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 59305c75..adc46282 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index b22a8706..8b881c37 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index 4eb2bd8d..b319822c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 8cfd292f..6681e796 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index bf7de64d..5068d682 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index fd0c7eab..5503374f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index c2d7c252..d5d59d30 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
index e1f6fde2..b3755d61 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
index 48ce80ad..f0cf6a54 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
index 0b3f7b66..d406f4eb 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
index ebc40996..2399d21a 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index c2d7c252..d5d59d30 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index 41f9638c..6b2cd229 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
index 5ba23fa6..d966761c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
index ccfaddf5..732524f2 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 59436734..11f3a515 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 371d4763..496e0107 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 04d36300..bdb5f801 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 86182b8e..863a8ef8 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Pilipala
+ PiliPala
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/lib/common/constants.dart b/lib/common/constants.dart
index d321b903..08a54805 100644
--- a/lib/common/constants.dart
+++ b/lib/common/constants.dart
@@ -7,3 +7,10 @@ class StyleString {
static const Radius imgRadius = Radius.circular(10);
static const double aspectRatio = 16 / 10;
}
+
+class Constants {
+ static const String appKey = '27eb53fc9058f8c3';
+ static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
+ static const String thirdApi =
+ 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
+}
diff --git a/lib/common/skeleton/video_card_h.dart b/lib/common/skeleton/video_card_h.dart
index 547f836c..efd86ff9 100644
--- a/lib/common/skeleton/video_card_h.dart
+++ b/lib/common/skeleton/video_card_h.dart
@@ -14,7 +14,7 @@ class VideoCardHSkeleton extends StatelessWidget {
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
- (boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
+ (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(
diff --git a/lib/common/widgets/animated_dialog.dart b/lib/common/widgets/animated_dialog.dart
index 4d35e3a0..7c7c4395 100644
--- a/lib/common/widgets/animated_dialog.dart
+++ b/lib/common/widgets/animated_dialog.dart
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget {
- const AnimatedDialog({Key? key, required this.child}) : super(key: key);
+ const AnimatedDialog({Key? key, required this.child, this.closeFn})
+ : super(key: key);
final Widget child;
+ final Function? closeFn;
@override
State createState() => AnimatedDialogState();
@@ -39,12 +41,16 @@ class AnimatedDialogState extends State
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(opacityAnimation!.value),
- child: Center(
- child: FadeTransition(
- opacity: scaleAnimation!,
- child: ScaleTransition(
- scale: scaleAnimation!,
- child: widget.child,
+ child: InkWell(
+ splashColor: Colors.transparent,
+ onTap: () => widget.closeFn!(),
+ child: Center(
+ child: FadeTransition(
+ opacity: scaleAnimation!,
+ child: ScaleTransition(
+ scale: scaleAnimation!,
+ child: widget.child,
+ ),
),
),
),
diff --git a/lib/common/widgets/app_bar_ani.dart b/lib/common/widgets/app_bar_ani.dart
deleted file mode 100644
index 4fea635c..00000000
--- a/lib/common/widgets/app_bar_ani.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-import 'package:flutter/material.dart';
-
-class AppBarAni extends StatelessWidget implements PreferredSizeWidget {
- const AppBarAni({
- required this.child,
- required this.controller,
- required this.visible,
- this.position,
- Key? key,
- }) : super(key: key);
-
- final PreferredSizeWidget child;
- final AnimationController controller;
- final bool visible;
- final String? position;
-
- @override
- Size get preferredSize => child.preferredSize;
-
- @override
- Widget build(BuildContext context) {
- visible ? controller.reverse() : controller.forward();
- return SlideTransition(
- position: Tween(
- begin: Offset.zero,
- end: Offset(0, position! == 'top' ? -1 : 1),
- ).animate(CurvedAnimation(
- parent: controller,
- curve: Curves.easeInOut,
- )),
- child: Container(
- decoration: BoxDecoration(
- gradient: position! == 'top'
- ? const LinearGradient(
- begin: Alignment.bottomCenter,
- end: Alignment.topCenter,
- colors: [
- Colors.transparent,
- Colors.black45,
- ],
- tileMode: TileMode.clamp,
- )
- : const LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.transparent,
- Colors.black45,
- ],
- tileMode: TileMode.mirror,
- ),
- ),
- child: child,
- ),
- );
- }
-}
diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart
index 035be598..baa2bff2 100644
--- a/lib/common/widgets/badge.dart
+++ b/lib/common/widgets/badge.dart
@@ -1,33 +1,120 @@
import 'package:flutter/material.dart';
-Widget pBadge(
- text,
- context,
- double? top,
- double? right,
- double? bottom,
- double? left, {
- type = 'primary',
-}) {
- Color bgColor = Theme.of(context).colorScheme.primary;
- Color color = Theme.of(context).colorScheme.onPrimary;
- if (type == 'gray') {
- bgColor = Colors.black54.withOpacity(0.4);
- color = Colors.white;
- }
- return Positioned(
- top: top,
- left: left,
- right: right,
- bottom: bottom,
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
- decoration:
- BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
- child: Text(
- text,
- style: TextStyle(fontSize: 11, color: color),
+// Widget pBadge(
+// text,
+// context,
+// double? top,
+// double? right,
+// double? bottom,
+// double? left, {
+// type = 'primary',
+// }) {
+// Color bgColor = Theme.of(context).colorScheme.primary;
+// Color color = Theme.of(context).colorScheme.onPrimary;
+// if (type == 'gray') {
+// bgColor = Colors.black54.withOpacity(0.4);
+// color = Colors.white;
+// }
+// return Positioned(
+// top: top,
+// left: left,
+// right: right,
+// bottom: bottom,
+// child: Container(
+// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
+// decoration:
+// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
+// child: Text(
+// text,
+// style: TextStyle(fontSize: 11, color: color),
+// ),
+// ),
+// );
+// }
+
+class PBadge extends StatelessWidget {
+ final String? text;
+ final double? top;
+ final double? right;
+ final double? bottom;
+ final double? left;
+ final String? type;
+ final String? size;
+ final String? stack;
+ final double? fs;
+
+ const PBadge({
+ super.key,
+ this.text,
+ this.top,
+ this.right,
+ this.bottom,
+ this.left,
+ this.type = 'primary',
+ this.size = 'medium',
+ this.stack = 'position',
+ this.fs = 11,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ ColorScheme t = Theme.of(context).colorScheme;
+ // 背景色
+ Color bgColor = t.primary;
+ // 前景色
+ Color color = t.onPrimary;
+ // 边框色
+ Color borderColor = Colors.transparent;
+ if (type == 'gray') {
+ bgColor = Colors.black54.withOpacity(0.4);
+ color = Colors.white;
+ }
+ if (type == 'color') {
+ bgColor = t.primaryContainer.withOpacity(0.6);
+ color = t.primary;
+ }
+ if (type == 'line') {
+ bgColor = Colors.transparent;
+ color = t.primary;
+ borderColor = t.primary;
+ }
+
+ EdgeInsets paddingStyle =
+ const EdgeInsets.symmetric(vertical: 1, horizontal: 6);
+ double fontSize = 11;
+ BorderRadius br = BorderRadius.circular(4);
+
+ if (size == 'small') {
+ paddingStyle = const EdgeInsets.symmetric(vertical: 0, horizontal: 3);
+ fontSize = 11;
+ br = BorderRadius.circular(3);
+ }
+
+ Widget content = Container(
+ padding: paddingStyle,
+ decoration: BoxDecoration(
+ borderRadius: br,
+ color: bgColor,
+ border: Border.all(color: borderColor),
),
- ),
- );
+ child: Text(
+ text!,
+ style: TextStyle(fontSize: fs ?? fontSize, color: color),
+ ),
+ );
+ if (stack == 'position') {
+ return Positioned(
+ top: top,
+ left: left,
+ right: right,
+ bottom: bottom,
+ child: content,
+ );
+ } else {
+ return Padding(
+ padding: const EdgeInsets.only(right: 5),
+ child: content,
+ );
+ }
+ }
}
diff --git a/lib/common/widgets/live_card.dart b/lib/common/widgets/live_card.dart
index 8c19b3db..01d0bf32 100644
--- a/lib/common/widgets/live_card.dart
+++ b/lib/common/widgets/live_card.dart
@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
-import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
-import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/utils.dart';
class LiveCard extends StatelessWidget {
@@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
- maxLines: Get.find().crossAxisCount,
+ maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(
diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart
index b07144e3..46683e96 100644
--- a/lib/common/widgets/network_img_layer.dart
+++ b/lib/common/widgets/network_img_layer.dart
@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
+import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/utils/storage.dart';
+
+Box setting = GStrorage.setting;
class NetworkImgLayer extends StatelessWidget {
final String? src;
@@ -24,12 +28,14 @@ class NetworkImgLayer extends StatelessWidget {
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
- this.quality = 1,
+ this.quality,
}) : super(key: key);
@override
Widget build(BuildContext context) {
double pr = MediaQuery.of(context).devicePixelRatio;
+ int picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
+
// double pr = 2;
return src != ''
? ClipRRect(
@@ -41,7 +47,7 @@ class NetworkImgLayer extends StatelessWidget {
: StyleString.imgRadius.x),
child: CachedNetworkImage(
imageUrl:
- '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality}q.webp',
+ '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp',
width: width ?? double.infinity,
height: height ?? double.infinity,
alignment: Alignment.center,
diff --git a/lib/common/widgets/overlay_pop.dart b/lib/common/widgets/overlay_pop.dart
index a3511402..53d4c9a1 100644
--- a/lib/common/widgets/overlay_pop.dart
+++ b/lib/common/widgets/overlay_pop.dart
@@ -1,42 +1,82 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/utils/download.dart';
class OverlayPop extends StatelessWidget {
final dynamic videoItem;
- const OverlayPop({super.key, this.videoItem});
+ final Function? closeFn;
+ const OverlayPop({super.key, this.videoItem, this.closeFn});
@override
Widget build(BuildContext context) {
+ double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
return Container(
- margin: const EdgeInsets.symmetric(horizontal: 8.0),
+ margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(6.0),
+ borderRadius: BorderRadius.circular(10.0),
),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(6.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- NetworkImgLayer(
- width: (MediaQuery.of(context).size.width - 16),
- height: (MediaQuery.of(context).size.width - 16) /
- StyleString.aspectRatio,
- src: videoItem.pic!,
- ),
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
- child: Text(
- videoItem.title!,
- // maxLines: 1,
- // overflow: TextOverflow.ellipsis,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Stack(
+ children: [
+ NetworkImgLayer(
+ width: imgWidth,
+ height: imgWidth / StyleString.aspectRatio,
+ src: videoItem.pic!,
+ 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!,
+ ),
+ ),
+ const SizedBox(width: 4),
+ IconButton(
+ tooltip: '保存封面图',
+ onPressed: () async {
+ await DownloadUtils.downloadImg(
+ videoItem.pic ?? videoItem.cover);
+ // closeFn!();
+ },
+ icon: const Icon(Icons.download, size: 20),
+ )
+ ],
+ )),
+ ],
),
);
}
diff --git a/lib/common/widgets/stat/up.dart b/lib/common/widgets/stat/up.dart
deleted file mode 100644
index d217e2c4..00000000
--- a/lib/common/widgets/stat/up.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'package:flutter/material.dart';
-
-class UpTag extends StatelessWidget {
- const UpTag({super.key});
-
- @override
- Widget build(BuildContext context) {
- return Container(
- width: 14,
- height: 10,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(2),
- border: Border.all(color: Theme.of(context).colorScheme.outline)),
- margin: const EdgeInsets.only(right: 4),
- child: Center(
- child: Text(
- 'UP',
- style: TextStyle(
- fontSize: 6, color: Theme.of(context).colorScheme.outline),
- ),
- ),
- );
- }
-}
diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart
index 02ec260f..4cfce1ef 100644
--- a/lib/common/widgets/video_card_h.dart
+++ b/lib/common/widgets/video_card_h.dart
@@ -16,12 +16,14 @@ class VideoCardH extends StatelessWidget {
final videoItem;
final Function()? longPress;
final Function()? longPressEnd;
+ final String source;
const VideoCardH({
Key? key,
required this.videoItem,
this.longPress,
this.longPressEnd,
+ this.source = 'normal',
}) : super(key: key);
@override
@@ -35,11 +37,11 @@ class VideoCardH extends StatelessWidget {
longPress!();
}
},
- onLongPressEnd: (details) {
- if (longPressEnd != null) {
- longPressEnd!();
- }
- },
+ // onLongPressEnd: (details) {
+ // if (longPressEnd != null) {
+ // longPressEnd!();
+ // }
+ // },
child: InkWell(
onTap: () async {
try {
@@ -57,8 +59,9 @@ class VideoCardH extends StatelessWidget {
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
- (boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
- return SizedBox(
+ (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
+ return Container(
+ constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
@@ -80,19 +83,24 @@ class VideoCardH extends StatelessWidget {
height: maxHeight,
),
),
- pBadge(Utils.timeFormat(videoItem.duration!),
- context, null, 6.0, 6.0, null,
- type: 'gray'),
- if (videoItem.rcmdReason != null &&
- videoItem.rcmdReason.content != '')
- pBadge(videoItem.rcmdReason.content, context,
- 6.0, 6.0, null, null),
+ PBadge(
+ text: Utils.timeFormat(videoItem.duration!),
+ top: null,
+ right: 6.0,
+ bottom: 6.0,
+ left: null,
+ type: 'gray',
+ ),
+ // if (videoItem.rcmdReason != null &&
+ // videoItem.rcmdReason.content != '')
+ // pBadge(videoItem.rcmdReason.content, context,
+ // 6.0, 6.0, null, null),
],
);
},
),
),
- VideoContent(videoItem: videoItem)
+ VideoContent(videoItem: videoItem, source: source)
],
),
);
@@ -107,7 +115,9 @@ class VideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
- const VideoContent({super.key, required this.videoItem});
+ final String source;
+ const VideoContent(
+ {super.key, required this.videoItem, this.source = 'normal'});
@override
Widget build(BuildContext context) {
@@ -124,7 +134,6 @@ class VideoContent extends StatelessWidget {
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
- letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -198,26 +207,62 @@ class VideoContent extends StatelessWidget {
// color: Theme.of(context).colorScheme.outline),
// )
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,
+ // 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')
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: PopupMenuButton(
+ padding: EdgeInsets.zero,
+ tooltip: '稍后再看',
+ icon: Icon(
+ Icons.more_vert_outlined,
+ color: Theme.of(context).colorScheme.outline,
+ size: 14,
+ ),
+ position: PopupMenuPosition.under,
+ // constraints: const BoxConstraints(maxHeight: 35),
+ onSelected: (String type) {},
+ itemBuilder: (BuildContext context) =>
+ >[
+ PopupMenuItem(
+ onTap: () async {
+ var res = await UserHttp.toViewLater(
+ bvid: videoItem.bvid);
+ SmartDialog.showToast(res['msg']);
+ },
+ value: 'pause',
+ height: 35,
+ child: const Row(
+ children: [
+ Icon(Icons.watch_later_outlined, size: 16),
+ SizedBox(width: 6),
+ Text('稍后再看', style: TextStyle(fontSize: 13))
+ ],
+ ),
+ ),
+ ],
),
),
- ),
],
),
],
diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart
index 97e1077c..53300d3e 100644
--- a/lib/common/widgets/video_card_v.dart
+++ b/lib/common/widgets/video_card_v.dart
@@ -2,18 +2,19 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
+import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
-import 'package:pilipala/pages/rcmd/index.dart';
+import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
- // ignore: prefer_typing_uninitialized_variables
- final videoItem;
+ final dynamic videoItem;
final Function()? longPress;
final Function()? longPressEnd;
@@ -24,6 +25,54 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd,
}) : super(key: key);
+ void onPushDetail(heroTag) async {
+ String goto = videoItem.goto;
+ switch (goto) {
+ case 'bangumi':
+ if (videoItem.bangumiBadge == '电影') {
+ SmartDialog.showToast('暂不支持电影观看');
+ return;
+ }
+ int epId = videoItem.param;
+ SmartDialog.showLoading(msg: '资源获取中');
+ var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
+ if (result['status']) {
+ 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;
+ case 'av':
+ String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
+ Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}', arguments: {
+ // 'videoItem': videoItem,
+ 'pic': videoItem.pic,
+ 'heroTag': heroTag,
+ });
+ break;
+ default:
+ SmartDialog.showToast(videoItem.goto);
+ Get.toNamed(
+ '/webview',
+ parameters: {
+ 'url': videoItem.uri,
+ 'type': 'url',
+ 'pageTitle': videoItem.title,
+ },
+ );
+ }
+ }
+
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
@@ -40,61 +89,29 @@ class VideoCardV extends StatelessWidget {
longPress!();
}
},
- onLongPressEnd: (details) {
- if (longPressEnd != null) {
- longPressEnd!();
- }
- },
+ // onLongPressEnd: (details) {
+ // if (longPressEnd != null) {
+ // longPressEnd!();
+ // }
+ // },
child: InkWell(
- onTap: () async {
- String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
- Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}',
- arguments: {'videoItem': videoItem, 'heroTag': heroTag});
- },
+ onTap: () async => onPushDetail(heroTag),
child: Column(
children: [
- ClipRRect(
- borderRadius: const BorderRadius.only(
- topLeft: StyleString.imgRadius,
- topRight: StyleString.imgRadius,
- bottomLeft: StyleString.imgRadius,
- bottomRight: StyleString.imgRadius,
- ),
- child: AspectRatio(
- aspectRatio: StyleString.aspectRatio,
- child: LayoutBuilder(builder: (context, boxConstraints) {
- double maxWidth = boxConstraints.maxWidth;
- double maxHeight = boxConstraints.maxHeight;
- return Stack(
- children: [
- Hero(
- tag: heroTag,
- child: NetworkImgLayer(
- src: videoItem.pic,
- width: maxWidth,
- height: maxHeight,
- ),
- ),
- // if (videoItem.stat.view is int &&
- // videoItem.stat.danmaku is int)
- // Positioned(
- // left: 0,
- // right: 0,
- // bottom: 0,
- // child: AnimatedOpacity(
- // opacity: 1,
- // duration: const Duration(milliseconds: 200),
- // child: VideoStat(
- // view: videoItem.stat.view,
- // danmaku: videoItem.stat.danmaku,
- // duration: videoItem.duration,
- // ),
- // ),
- // ),
- ],
- );
- }),
- ),
+ AspectRatio(
+ aspectRatio: StyleString.aspectRatio,
+ child: LayoutBuilder(builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: videoItem.pic,
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ );
+ }),
),
VideoContent(videoItem: videoItem)
],
@@ -106,113 +123,151 @@ class VideoCardV extends StatelessWidget {
}
class VideoContent extends StatelessWidget {
- // ignore: prefer_typing_uninitialized_variables
- final videoItem;
+ final dynamic videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
- // 多列
- padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
- // 单列
- // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
+ padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
- textAlign: TextAlign.start,
- style: const TextStyle(
- fontSize: 13,
- fontWeight: FontWeight.w500,
- letterSpacing: 0.3,
- ),
- maxLines: Get.find().crossAxisCount,
+ style: const TextStyle(fontSize: 13),
+ maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
+ if (videoItem.goto == 'bangumi') ...[
+ PBadge(
+ text: videoItem.bangumiBadge,
+ stack: 'normal',
+ size: 'small',
+ type: 'line',
+ fs: 9,
+ )
+ ],
if (videoItem.rcmdReason != null &&
- videoItem.rcmdReason.content != '' ||
- videoItem.isFollowed == 1) ...[
- Container(
- padding: const EdgeInsets.fromLTRB(3, 0, 3, 0),
- decoration: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .primaryContainer
- .withOpacity(0.6),
- borderRadius: BorderRadius.circular(3)),
- child: Center(
- child: Text(
- videoItem.rcmdReason != null &&
- videoItem.rcmdReason.content != ''
- ? videoItem.rcmdReason.content
- : '已关注',
- style: TextStyle(
- fontSize: Theme.of(context)
- .textTheme
- .labelSmall!
- .fontSize,
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- )),
- const SizedBox(width: 4)
+ 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,
+ )
],
Expanded(
- child: LayoutBuilder(builder:
- (BuildContext context, BoxConstraints constraints) {
- return SizedBox(
- width: constraints.maxWidth,
- child: Text(
- videoItem.owner.name,
- maxLines: 1,
- style: TextStyle(
- fontSize:
- Theme.of(context).textTheme.labelMedium!.fontSize,
- color: Theme.of(context).colorScheme.outline,
- ),
- ),
- );
- }),
- ),
- 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,
+ child: Text(
+ videoItem.owner.name,
+ maxLines: 1,
+ style: TextStyle(
+ fontSize:
+ Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
- size: 14,
),
),
),
+ if (videoItem.goto == 'av')
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: PopupMenuButton(
+ padding: EdgeInsets.zero,
+ tooltip: '稍后再看',
+ icon: Icon(
+ Icons.more_vert_outlined,
+ color: Theme.of(context).colorScheme.outline,
+ size: 14,
+ ),
+ position: PopupMenuPosition.under,
+ // constraints: const BoxConstraints(maxHeight: 35),
+ onSelected: (String type) {},
+ itemBuilder: (BuildContext context) =>
+ >[
+ PopupMenuItem(
+ onTap: () async {
+ int aid = videoItem.param;
+ var res = await UserHttp.toViewLater(
+ bvid: IdUtils.av2bv(aid));
+ SmartDialog.showToast(res['msg']);
+ },
+ value: 'pause',
+ height: 35,
+ child: const Row(
+ children: [
+ Icon(Icons.watch_later_outlined, size: 16),
+ SizedBox(width: 6),
+ Text('稍后再看', style: TextStyle(fontSize: 13))
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
],
),
// Row(
// children: [
+ // const SizedBox(width: 1),
// StatView(
- // theme: 'black',
+ // theme: 'gray',
// view: videoItem.stat.view,
// ),
- // const SizedBox(width: 6),
+ // const SizedBox(width: 10),
// StatDanMu(
- // theme: 'black',
+ // theme: 'gray',
// danmu: videoItem.stat.danmaku,
// ),
+ // const Spacer(),
+ // SizedBox(
+ // width: 24,
+ // height: 24,
+ // child: PopupMenuButton(
+ // padding: EdgeInsets.zero,
+ // tooltip: '稍后再看',
+ // icon: Icon(
+ // Icons.more_vert_outlined,
+ // color: Theme.of(context).colorScheme.outline,
+ // size: 14,
+ // ),
+ // position: PopupMenuPosition.under,
+ // // constraints: const BoxConstraints(maxHeight: 35),
+ // onSelected: (String type) {},
+ // itemBuilder: (BuildContext context) =>
+ // >[
+ // PopupMenuItem(
+ // onTap: () async {
+ // var res =
+ // await UserHttp.toViewLater(bvid: videoItem.bvid);
+ // SmartDialog.showToast(res['msg']);
+ // },
+ // value: 'pause',
+ // height: 35,
+ // child: const Row(
+ // children: [
+ // Icon(Icons.watch_later_outlined, size: 16),
+ // SizedBox(width: 6),
+ // Text('稍后再看', style: TextStyle(fontSize: 13))
+ // ],
+ // ),
+ // ),
+ // ],
+ // ),
+ // ),
// ],
// ),
],
@@ -237,7 +292,7 @@ class VideoStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- height: 45,
+ height: 48,
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
decoration: const BoxDecoration(
gradient: LinearGradient(
diff --git a/lib/http/api.dart b/lib/http/api.dart
index d1dc350a..5074ccfe 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -248,9 +248,44 @@ class Api {
// 移除已观看
static const String toViewDel = '/x/v2/history/toview/del';
+ // 清空稍后再看
+ static const String toViewClear = '/x/v2/history/toview/clear';
+
// 追番
static const String bangumiAdd = '/pgc/web/follow/add';
// 取消追番
static const String bangumiDel = '/pgc/web/follow/del';
+
+ // 番剧列表
+ // https://api.bilibili.com/pgc/season/index/result?
+ // st=1&
+ // order=3
+ // season_version=-1 全部-1 正片1 电影2 其他3
+ // spoken_language_type=-1 全部-1 原生1 中文配音2
+ // area=-1&
+ // is_finish=-1&
+ // copyright=-1&
+ // season_status=-1&
+ // season_month=-1&
+ // year=-1&
+ // style_id=-1&
+ // sort=0&
+ // page=1&
+ // season_type=1&
+ // pagesize=20&
+ // type=1
+ static const String bangumiList =
+ '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-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';
+
+ // github 获取最新版
+ static const String latestApp =
+ 'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
}
diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart
new file mode 100644
index 00000000..bd20366c
--- /dev/null
+++ b/lib/http/bangumi.dart
@@ -0,0 +1,36 @@
+import 'package:pilipala/http/index.dart';
+import 'package:pilipala/models/bangumi/list.dart';
+
+class BangumiHttp {
+ static Future bangumiList({int? page}) async {
+ var res = await Request().get(Api.bangumiList, data: {'page': page});
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': BangumiListDataModel.fromJson(res.data['data'])
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+
+ static Future bangumiFollow({int? mid}) async {
+ var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': BangumiListDataModel.fromJson(res.data['data'])
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+}
diff --git a/lib/http/black.dart b/lib/http/black.dart
new file mode 100644
index 00000000..599b088b
--- /dev/null
+++ b/lib/http/black.dart
@@ -0,0 +1,26 @@
+import 'package:pilipala/http/index.dart';
+import 'package:pilipala/models/user/black.dart';
+
+class BlackHttp {
+ static Future blackList({required int pn, int? ps}) async {
+ var res = await Request().get(Api.blackLst, data: {
+ 'pn': pn,
+ 'ps': ps ?? 50,
+ 're_version': 0,
+ 'jsonp': 'jsonp',
+ 'csrf': await Request.getCsrf(),
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': BlackListDataModel.fromJson(res.data['data'])
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+}
diff --git a/lib/http/init.dart b/lib/http/init.dart
index 08be1514..05bf5e28 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -15,20 +15,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
-
+ static late final Dio dio;
factory Request() => _instance;
- static Dio dio = Dio()
- ..httpClientAdapter = Http2Adapter(
- ConnectionManager(
- idleTimeout: const Duration(milliseconds: 10000),
- // Ignore bad certificate
- onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
- ),
- );
-
/// 设置cookie
static setCookie() async {
+ Box user = GStrorage.user;
var cookiePath = await Utils.getCookiePath();
var cookieJar = PersistCookieJar(
ignoreExpires: true,
@@ -38,8 +30,18 @@ class Request {
dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
- var cookie2 = await cookieManager.cookieJar
- .loadForRequest(Uri.parse(HttpString.tUrl));
+ if (user.get(UserBoxKey.userMid) != null) {
+ var cookie2 = await cookieManager.cookieJar
+ .loadForRequest(Uri.parse(HttpString.tUrl));
+ if (cookie2.isEmpty) {
+ try {
+ await Request().get(HttpString.tUrl);
+ } catch (e) {
+ log("setCookie, ${e.toString()}");
+ }
+ }
+ }
+
if (cookie.isEmpty) {
try {
await Request().get(HttpString.baseUrl);
@@ -47,23 +49,9 @@ class Request {
log("setCookie, ${e.toString()}");
}
}
- if (cookie2.isEmpty) {
- try {
- await Request().get(HttpString.tUrl);
- } catch (e) {
- log("setCookie, ${e.toString()}");
- }
- }
- }
-
- // 移除cookie
- static removeCookie() async {
- await cookieManager.cookieJar
- .saveFromResponse(Uri.parse(HttpString.baseUrl), []);
- await cookieManager.cookieJar
- .saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
- cookieManager.cookieJar.deleteAll();
- dio.interceptors.add(cookieManager);
+ var cookieString =
+ cookie.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
+ dio.options.headers['cookie'] = cookieString;
}
// 从cookie中获取 csrf token
@@ -95,28 +83,38 @@ class Request {
//Http请求头.
headers: {
// 'cookie': '',
- "env": 'prod',
- "app-key": 'android',
- "x-bili-aurora-eid": 'UlMFQVcABlAH',
- "x-bili-aurora-zone": 'sh001',
- 'referer': 'https://www.bilibili.com/',
},
);
Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) != null) {
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString();
+ options.headers['env'] = 'prod';
+ options.headers['app-key'] = 'android64';
+ options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
+ options.headers['x-bili-aurora-zone'] = 'sh001';
+ options.headers['referer'] = 'https://www.bilibili.com/';
}
- dio.options = options;
+
+ dio = Dio(options)
+ ..httpClientAdapter = Http2Adapter(
+ ConnectionManager(
+ idleTimeout: const Duration(milliseconds: 10000),
+ // Ignore bad certificate
+ onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
+ ),
+ );
+
//添加拦截器
- dio.interceptors
- ..add(ApiInterceptor())
- // 日志拦截器 输出请求、响应内容
- ..add(LogInterceptor(
- request: false,
- requestHeader: false,
- responseHeader: false,
- ));
+ dio.interceptors.add(ApiInterceptor());
+
+ // 日志拦截器 输出请求、响应内容
+ dio.interceptors.add(LogInterceptor(
+ request: false,
+ requestHeader: false,
+ responseHeader: false,
+ ));
+
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || status == 304 || status == 302;
@@ -161,7 +159,7 @@ class Request {
* post请求
*/
post(url, {data, queryParameters, options, cancelToken, extra}) async {
- print('post-data: $data');
+ // print('post-data: $data');
Response response;
try {
response = await dio.post(
@@ -171,7 +169,7 @@ class Request {
options: options,
cancelToken: cancelToken,
);
- print('post success: ${response.data}');
+ // print('post success: ${response.data}');
return response;
} on DioException catch (e) {
print('post error: $e');
diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart
index 87b97810..2521d02d 100644
--- a/lib/http/interceptor.dart
+++ b/lib/http/interceptor.dart
@@ -1,6 +1,10 @@
+// ignore_for_file: avoid_print
+
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/utils/storage.dart';
// import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor {
@@ -13,8 +17,26 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
+ Box user = GStrorage.user;
+
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
+ try {
+ if (response.statusCode == 302) {
+ List locations = response.headers['location']!;
+ if (locations.isNotEmpty) {
+ if (locations.first.startsWith('https://www.mcbbs.net')) {
+ final uri = Uri.parse(locations.first);
+ final accessKey = uri.queryParameters['access_key'];
+ final mid = uri.queryParameters['mid'];
+ user.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey});
+ }
+ }
+ }
+ } catch (err) {
+ print('ApiInterceptor: $err');
+ }
+
handler.next(response);
}
diff --git a/lib/http/user.dart b/lib/http/user.dart
index 8cefb209..050470fb 100644
--- a/lib/http/user.dart
+++ b/lib/http/user.dart
@@ -1,3 +1,4 @@
+import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
@@ -84,6 +85,12 @@ class UserHttp {
static Future seeYouLater() async {
var res = await Request().get(Api.seeYouLater);
if (res.data['code'] == 0) {
+ if (res.data['data']['count'] == 0) {
+ return {
+ 'status': true,
+ 'data': {'list': [], 'count': 0}
+ };
+ }
List list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
@@ -179,4 +186,35 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
+
+ // 获取用户凭证
+ 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,
+ },
+ );
+ if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
+ Request().get(res.data['data']['confirm_uri']);
+ }
+ }
+
+ // 清空稍后再看
+ static Future toViewClear() async {
+ var res = await Request().post(
+ Api.toViewClear,
+ queryParameters: {
+ 'jsonp': 'jsonp',
+ 'csrf': await Request.getCsrf(),
+ },
+ );
+ if (res.data['code'] == 0) {
+ return {'status': true, 'msg': '操作完成'};
+ } else {
+ return {'status': false, 'msg': res.data['message']};
+ }
+ }
}
diff --git a/lib/http/video.dart b/lib/http/video.dart
index 24c0648a..18048377 100644
--- a/lib/http/video.dart
+++ b/lib/http/video.dart
@@ -1,5 +1,7 @@
import 'dart:developer';
+import 'package:hive/hive.dart';
+import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/reply_type.dart';
@@ -9,12 +11,16 @@ import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart';
+import 'package:pilipala/utils/storage.dart';
/// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果
/// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑
class VideoHttp {
+ static Box user = GStrorage.user;
+ static Box setting = GStrorage.setting;
+
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
try {
@@ -42,8 +48,7 @@ class VideoHttp {
}
}
- static Future rcmdVideoListApp(
- {required int ps, required int freshIdx}) async {
+ static Future rcmdVideoListApp({int? ps, required int freshIdx}) async {
try {
var res = await Request().get(
Api.recommendListApp,
@@ -55,12 +60,22 @@ class VideoHttp {
'device_type': 0,
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
+ 'appkey': Constants.appKey,
+ 'access_key':
+ user.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
},
);
if (res.data['code'] == 0) {
List list = [];
+ List blackMidsList =
+ setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['items']) {
- list.add(RecVideoItemAppModel.fromJson(i));
+ // 屏蔽推广和拉黑用户
+ if (i['card_goto'] != 'ad_av' &&
+ (i['args'] != null &&
+ !blackMidsList.contains(i['args']['up_mid']))) {
+ list.add(RecVideoItemAppModel.fromJson(i));
+ }
}
return {'status': true, 'data': list};
} else {
@@ -80,8 +95,12 @@ class VideoHttp {
);
if (res.data['code'] == 0) {
List list = [];
+ List blackMidsList =
+ setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['list']) {
- list.add(HotVideoItemModel.fromJson(i));
+ if (!blackMidsList.contains(i['owner']['mid'])) {
+ list.add(HotVideoItemModel.fromJson(i));
+ }
}
return {'status': true, 'data': list};
} else {
diff --git a/lib/main.dart b/lib/main.dart
index 50d686c2..d35d8399 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -4,8 +4,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
+import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.dart';
+import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
@@ -33,19 +35,38 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ Color brandColor = const Color.fromARGB(255, 92, 182, 123);
+ Box setting = GStrorage.setting;
+ ThemeType currentThemeValue = ThemeType.values[setting
+ .get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
return DynamicColorBuilder(
- builder: ((lightDynamic, darkDynamic) {
+ builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
+ ColorScheme? lightColorScheme;
+ ColorScheme? darkColorScheme;
+ if (lightDynamic != null && darkDynamic != null) {
+ // dynamic取色成功
+ lightColorScheme = lightDynamic.harmonized();
+ darkColorScheme = darkDynamic.harmonized();
+ } else {
+ // dynamic取色失败,采用品牌色
+ lightColorScheme = ColorScheme.fromSeed(
+ seedColor: brandColor,
+ brightness: Brightness.light,
+ );
+ darkColorScheme = ColorScheme.fromSeed(
+ seedColor: brandColor,
+ brightness: Brightness.dark,
+ );
+ }
// 图片缓存
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp(
title: 'PiLiPaLa',
theme: ThemeData(
- fontFamily: 'HarmonyOS',
- colorScheme: lightDynamic ??
- ColorScheme.fromSeed(
- seedColor: Colors.green,
- brightness: Brightness.light,
- ),
+ // fontFamily: 'HarmonyOS',
+ colorScheme: currentThemeValue == ThemeType.dark
+ ? darkColorScheme
+ : lightColorScheme,
useMaterial3: true,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
@@ -56,12 +77,10 @@ class MyApp extends StatelessWidget {
),
),
darkTheme: ThemeData(
- fontFamily: 'HarmonyOS',
- colorScheme: darkDynamic ??
- ColorScheme.fromSeed(
- seedColor: Colors.green,
- brightness: Brightness.dark,
- ),
+ // fontFamily: 'HarmonyOS',
+ colorScheme: currentThemeValue == ThemeType.light
+ ? lightColorScheme
+ : darkColorScheme,
useMaterial3: true,
),
localizationsDelegates: const [
diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart
new file mode 100644
index 00000000..c15014d0
--- /dev/null
+++ b/lib/models/bangumi/list.dart
@@ -0,0 +1,90 @@
+class BangumiListDataModel {
+ BangumiListDataModel({
+ this.hasNext,
+ this.list,
+ this.num,
+ this.size,
+ this.total,
+ });
+
+ int? hasNext;
+ List? list;
+ int? num;
+ int? size;
+ int? total;
+
+ BangumiListDataModel.fromJson(Map json) {
+ hasNext = json['has_next'];
+ list = json['list'] != null
+ ? json['list']
+ .map((e) => BangumiListItemModel.fromJson(e))
+ .toList()
+ : [];
+ num = json['num'];
+ size = json['size'];
+ total = json['total'];
+ }
+}
+
+class BangumiListItemModel {
+ BangumiListItemModel({
+ this.badge,
+ this.badgeType,
+ this.cover,
+ // this.firstEp,
+ this.indexShow,
+ this.isFinish,
+ this.link,
+ this.mediaId,
+ this.order,
+ this.orderType,
+ this.score,
+ this.seasonId,
+ this.seaconStatus,
+ this.seasonType,
+ this.subTitle,
+ this.title,
+ this.titleIcon,
+ this.progress,
+ });
+
+ String? badge;
+ int? badgeType;
+ String? cover;
+ String? indexShow;
+ int? isFinish;
+ String? link;
+ int? mediaId;
+ String? order;
+ String? orderType;
+ String? score;
+ int? seasonId;
+ int? seaconStatus;
+ int? seasonType;
+ String? subTitle;
+ String? title;
+ String? titleIcon;
+
+ String? progress;
+
+ BangumiListItemModel.fromJson(Map json) {
+ badge = json['badge'] == '' ? null : json['badge'];
+ badgeType = json['badge_type'];
+ cover = json['cover'];
+ indexShow = json['index_show'];
+ isFinish = json['is_finish'];
+ link = json['link'];
+ mediaId = json['media_id'];
+ order = json['order'];
+ orderType = json['order_type'];
+ score = json['score'];
+ seasonId = json['season_id'];
+ seaconStatus = json['seacon_status'];
+ seasonType = json['season_type'];
+ subTitle = json['sub_title'];
+ title = json['title'];
+ titleIcon = json['title_icon'];
+
+ progress = json['progress'];
+ }
+}
diff --git a/lib/models/common/tab_type.dart b/lib/models/common/tab_type.dart
new file mode 100644
index 00000000..90d19029
--- /dev/null
+++ b/lib/models/common/tab_type.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/pages/bangumi/index.dart';
+import 'package:pilipala/pages/hot/index.dart';
+import 'package:pilipala/pages/live/index.dart';
+import 'package:pilipala/pages/rcmd/index.dart';
+
+enum TabType { live, rcmd, hot, bangumi }
+
+extension TabTypeDesc on TabType {
+ String get description => ['直播', '推荐', '热门', '番剧'][index];
+}
+
+List tabsConfig = [
+ {
+ 'icon': const Icon(
+ Icons.live_tv_outlined,
+ size: 15,
+ ),
+ 'label': '直播',
+ 'type': TabType.live,
+ 'ctr': Get.find,
+ 'page': const LivePage(),
+ },
+ {
+ 'icon': const Icon(
+ Icons.thumb_up_off_alt_outlined,
+ size: 15,
+ ),
+ 'label': '推荐',
+ 'type': TabType.rcmd,
+ 'ctr': Get.find,
+ 'page': const RcmdPage(),
+ },
+ {
+ 'icon': const Icon(
+ Icons.whatshot_outlined,
+ size: 15,
+ ),
+ 'label': '热门',
+ 'type': TabType.hot,
+ 'ctr': Get.find,
+ 'page': const HotPage(),
+ },
+ {
+ 'icon': const Icon(
+ Icons.play_circle_outlined,
+ size: 15,
+ ),
+ 'label': '番剧',
+ 'type': TabType.bangumi,
+ 'ctr': Get.find,
+ 'page': const BangumiPage(),
+ },
+];
diff --git a/lib/models/common/theme_type.dart b/lib/models/common/theme_type.dart
new file mode 100644
index 00000000..d2dac752
--- /dev/null
+++ b/lib/models/common/theme_type.dart
@@ -0,0 +1,13 @@
+enum ThemeType {
+ light,
+ dark,
+ system,
+}
+
+extension ThemeTypeDesc on ThemeType {
+ String get description => ['浅色', '深色', '跟随系统'][index];
+}
+
+extension ThemeTypeCode on ThemeType {
+ int get code => [0, 1, 2][index];
+}
diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart
index 53cee9d5..78991418 100644
--- a/lib/models/dynamics/result.dart
+++ b/lib/models/dynamics/result.dart
@@ -478,6 +478,8 @@ class DynamicArchiveModel {
this.stat,
this.title,
this.type,
+ this.epid,
+ this.seasonId,
});
int? aid;
@@ -491,6 +493,8 @@ class DynamicArchiveModel {
Stat? stat;
String? title;
int? type;
+ int? epid;
+ int? seasonId;
DynamicArchiveModel.fromJson(Map json) {
aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];
@@ -503,6 +507,8 @@ class DynamicArchiveModel {
stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;
title = json['title'];
type = json['type'];
+ epid = json['epid'];
+ seasonId = json['season_id'];
}
}
diff --git a/lib/models/github/latest.dart b/lib/models/github/latest.dart
new file mode 100644
index 00000000..1b2d0706
--- /dev/null
+++ b/lib/models/github/latest.dart
@@ -0,0 +1,45 @@
+class LatestDataModel {
+ LatestDataModel({
+ this.url,
+ this.tagName,
+ this.createdAt,
+ this.assets,
+ });
+
+ String? url;
+ String? tagName;
+ String? createdAt;
+ List? assets;
+
+ LatestDataModel.fromJson(Map json) {
+ url = json['url'];
+ tagName = json['tag_name'];
+ createdAt = json['created_at'];
+ assets =
+ json['assets'].map((e) => AssetItem.fromJson(e)).toList();
+ }
+}
+
+class AssetItem {
+ AssetItem({
+ this.url,
+ this.name,
+ this.size,
+ this.downloadCount,
+ this.downloadUrl,
+ });
+
+ String? url;
+ String? name;
+ int? size;
+ int? downloadCount;
+ String? downloadUrl;
+
+ AssetItem.fromJson(Map json) {
+ url = json['url'];
+ name = json['name'];
+ size = json['size'];
+ downloadCount = json['download_count'];
+ downloadUrl = json['browser_download_url'];
+ }
+}
diff --git a/lib/models/home/rcmd/result.dart b/lib/models/home/rcmd/result.dart
index 592d7edb..94ed0727 100644
--- a/lib/models/home/rcmd/result.dart
+++ b/lib/models/home/rcmd/result.dart
@@ -1,3 +1,8 @@
+import 'package:hive/hive.dart';
+
+part 'result.g.dart';
+
+@HiveType(typeId: 0)
class RecVideoItemAppModel {
RecVideoItemAppModel({
this.id,
@@ -11,55 +16,141 @@ class RecVideoItemAppModel {
this.isFollowed,
this.owner,
this.rcmdReason,
+ this.goto,
+ this.param,
+ this.uri,
+ this.talkBack,
+ this.bangumiView,
+ this.bangumiFollow,
+ this.bangumiBadge,
+ this.cardType,
+ this.adInfo,
});
+ @HiveField(0)
int? id;
+ @HiveField(1)
int? aid;
- int? bvid;
+ @HiveField(2)
+ String? bvid;
+ @HiveField(3)
int? cid;
+ @HiveField(4)
String? pic;
- Stat? stat;
- int? duration;
+ @HiveField(5)
+ RcmdStat? stat;
+ @HiveField(6)
+ String? duration;
+ @HiveField(7)
String? title;
+ @HiveField(8)
int? isFollowed;
- Owner? owner;
- String? rcmdReason;
+ @HiveField(9)
+ RcmdOwner? owner;
+ @HiveField(10)
+ RcmdReason? rcmdReason;
+ @HiveField(11)
+ String? goto;
+ @HiveField(12)
+ int? param;
+ @HiveField(13)
+ String? uri;
+ @HiveField(14)
+ String? talkBack;
+ // 番剧
+ @HiveField(15)
+ String? bangumiView;
+ @HiveField(16)
+ String? bangumiFollow;
+ @HiveField(17)
+ String? bangumiBadge;
+
+ @HiveField(18)
+ String? cardType;
+ @HiveField(19)
+ Map? adInfo;
RecVideoItemAppModel.fromJson(Map json) {
- id = json['player_args']['aid'];
- aid = json['player_args']['aid'];
- cid = json['player_args']['cid'];
+ id = json['player_args'] != null
+ ? json['player_args']['aid']
+ : int.parse(json['param'] ?? '-1');
+ aid = json['player_args'] != null ? json['player_args']['aid'] : -1;
+ bvid = null;
+ cid = json['player_args'] != null ? json['player_args']['cid'] : -1;
pic = json['cover'];
- stat = Stat.fromJson(json);
- duration = json['player_args']['duration'];
+ stat = RcmdStat.fromJson(json);
+ duration = json['cover_right_text'];
title = json['title'];
isFollowed = 0;
- owner = Owner.fromJson(json);
+ owner = RcmdOwner.fromJson(json);
+ rcmdReason = json['rcmd_reason_style'] != null
+ ? RcmdReason.fromJson(json['rcmd_reason_style'])
+ : null;
+ goto = json['goto'];
+ param = int.parse(json['param']);
+ uri = json['uri'];
+ talkBack = json['talk_back'];
+
+ if (json['goto'] == 'bangumi') {
+ bangumiView = json['cover_left_text_1'];
+ bangumiFollow = json['cover_left_text_2'];
+ bangumiBadge = json['badge'];
+ }
+
+ cardType = json['card_type'];
+ adInfo = json['ad_info'];
}
}
-class Stat {
- Stat({
+@HiveType(typeId: 1)
+class RcmdStat {
+ RcmdStat({
this.view,
this.like,
- this.danmaku,
+ this.danmu,
});
+ @HiveField(0)
String? view;
+ @HiveField(1)
String? like;
- String? danmaku;
+ @HiveField(2)
+ String? danmu;
- Stat.fromJson(Map json) {
+ RcmdStat.fromJson(Map json) {
view = json["cover_left_text_1"];
- danmaku = json['cover_left_text_2'];
+ danmu = json['cover_left_text_2'];
}
}
-class Owner {
- Owner({this.name});
+@HiveType(typeId: 2)
+class RcmdOwner {
+ RcmdOwner({this.name, this.mid});
+ @HiveField(0)
String? name;
+ @HiveField(1)
+ int? mid;
- Owner.fromJson(Map json) {
- name = json['args']['up_name'];
+ RcmdOwner.fromJson(Map json) {
+ name = json['goto'] == 'av'
+ ? json['args']['up_name']
+ : json['desc_button'] != null
+ ? json['desc_button']['text']
+ : '';
+ mid = json['args']['up_id'] ?? -1;
+ }
+}
+
+@HiveType(typeId: 8)
+class RcmdReason {
+ RcmdReason({
+ this.content,
+ });
+
+ @HiveField(0)
+ String? content;
+
+ RcmdReason.fromJson(Map json) {
+ content = json["text"] ?? '';
}
}
diff --git a/lib/models/home/rcmd/result.g.dart b/lib/models/home/rcmd/result.g.dart
new file mode 100644
index 00000000..43bf4bcf
--- /dev/null
+++ b/lib/models/home/rcmd/result.g.dart
@@ -0,0 +1,209 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'result.dart';
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class RecVideoItemAppModelAdapter extends TypeAdapter {
+ @override
+ final int typeId = 0;
+
+ @override
+ RecVideoItemAppModel read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return RecVideoItemAppModel(
+ id: fields[0] as int?,
+ aid: fields[1] as int?,
+ bvid: fields[2] as String?,
+ cid: fields[3] as int?,
+ pic: fields[4] as String?,
+ stat: fields[5] as RcmdStat?,
+ duration: fields[6] as String?,
+ title: fields[7] as String?,
+ isFollowed: fields[8] as int?,
+ owner: fields[9] as RcmdOwner?,
+ rcmdReason: fields[10] as RcmdReason?,
+ goto: fields[11] as String?,
+ param: fields[12] as int?,
+ uri: fields[13] as String?,
+ talkBack: fields[14] as String?,
+ bangumiView: fields[15] as String?,
+ bangumiFollow: fields[16] as String?,
+ bangumiBadge: fields[17] as String?,
+ cardType: fields[18] as String?,
+ adInfo: (fields[19] as Map?)?.cast(),
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, RecVideoItemAppModel obj) {
+ writer
+ ..writeByte(20)
+ ..writeByte(0)
+ ..write(obj.id)
+ ..writeByte(1)
+ ..write(obj.aid)
+ ..writeByte(2)
+ ..write(obj.bvid)
+ ..writeByte(3)
+ ..write(obj.cid)
+ ..writeByte(4)
+ ..write(obj.pic)
+ ..writeByte(5)
+ ..write(obj.stat)
+ ..writeByte(6)
+ ..write(obj.duration)
+ ..writeByte(7)
+ ..write(obj.title)
+ ..writeByte(8)
+ ..write(obj.isFollowed)
+ ..writeByte(9)
+ ..write(obj.owner)
+ ..writeByte(10)
+ ..write(obj.rcmdReason)
+ ..writeByte(11)
+ ..write(obj.goto)
+ ..writeByte(12)
+ ..write(obj.param)
+ ..writeByte(13)
+ ..write(obj.uri)
+ ..writeByte(14)
+ ..write(obj.talkBack)
+ ..writeByte(15)
+ ..write(obj.bangumiView)
+ ..writeByte(16)
+ ..write(obj.bangumiFollow)
+ ..writeByte(17)
+ ..write(obj.bangumiBadge)
+ ..writeByte(18)
+ ..write(obj.cardType)
+ ..writeByte(19)
+ ..write(obj.adInfo);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is RecVideoItemAppModelAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
+
+class RcmdStatAdapter extends TypeAdapter {
+ @override
+ final int typeId = 1;
+
+ @override
+ RcmdStat read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return RcmdStat(
+ view: fields[0] as String?,
+ like: fields[1] as String?,
+ danmu: fields[2] as String?,
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, RcmdStat obj) {
+ writer
+ ..writeByte(3)
+ ..writeByte(0)
+ ..write(obj.view)
+ ..writeByte(1)
+ ..write(obj.like)
+ ..writeByte(2)
+ ..write(obj.danmu);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is RcmdStatAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
+
+class RcmdOwnerAdapter extends TypeAdapter {
+ @override
+ final int typeId = 2;
+
+ @override
+ RcmdOwner read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return RcmdOwner(
+ name: fields[0] as String?,
+ mid: fields[1] as int?,
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, RcmdOwner obj) {
+ writer
+ ..writeByte(2)
+ ..writeByte(0)
+ ..write(obj.name)
+ ..writeByte(1)
+ ..write(obj.mid);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is RcmdOwnerAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
+
+class RcmdReasonAdapter extends TypeAdapter {
+ @override
+ final int typeId = 8;
+
+ @override
+ RcmdReason read(BinaryReader reader) {
+ final numOfFields = reader.readByte();
+ final fields = {
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+ };
+ return RcmdReason(
+ content: fields[0] as String?,
+ );
+ }
+
+ @override
+ void write(BinaryWriter writer, RcmdReason obj) {
+ writer
+ ..writeByte(1)
+ ..writeByte(0)
+ ..write(obj.content);
+ }
+
+ @override
+ int get hashCode => typeId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is RcmdReasonAdapter &&
+ runtimeType == other.runtimeType &&
+ typeId == other.typeId;
+}
diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart
index af5e2cfb..91070215 100644
--- a/lib/models/search/result.dart
+++ b/lib/models/search/result.dart
@@ -224,10 +224,12 @@ class SearchLiveItemModel {
this.liveTime,
this.uname,
this.uface,
+ this.face,
this.userCover,
this.type,
this.title,
this.cover,
+ this.pic,
this.online,
this.rankIndex,
this.rankScore,
@@ -242,16 +244,19 @@ class SearchLiveItemModel {
String? liveTime;
String? uname;
String? uface;
+ String? face;
String? userCover;
String? type;
List? title;
String? cover;
+ String? pic;
int? online;
int? rankIndex;
int? rankScore;
int? roomid;
int? attentions;
String? cateName;
+ Map? watchedShow;
SearchLiveItemModel.fromJson(Map json) {
rankOffset = json['rank_offset'];
@@ -260,10 +265,12 @@ class SearchLiveItemModel {
liveTime = json['live_time'];
uname = json['uname'];
uface = json['uface'];
+ face = json['uface'];
userCover = json['user_cover'];
type = json['type'];
title = Em.regTitle(json['title']);
cover = json['cover'];
+ pic = json['cover'];
online = json['online'];
rankIndex = json['rank_index'];
rankScore = json['rank_score'];
diff --git a/lib/models/user/black.dart b/lib/models/user/black.dart
new file mode 100644
index 00000000..9833967b
--- /dev/null
+++ b/lib/models/user/black.dart
@@ -0,0 +1,37 @@
+class BlackListDataModel {
+ BlackListDataModel({
+ this.list,
+ this.total,
+ });
+
+ List? list;
+ int? total;
+
+ BlackListDataModel.fromJson(Map json) {
+ list = json['list']
+ .map((e) => BlackListItem.fromJson(e))
+ .toList();
+ total = json['total'];
+ }
+}
+
+class BlackListItem {
+ BlackListItem({
+ this.face,
+ this.mid,
+ this.mtime,
+ this.uname,
+ });
+
+ String? face;
+ int? mid;
+ int? mtime;
+ String? uname;
+
+ BlackListItem.fromJson(Map json) {
+ face = json['face'];
+ mid = json['mid'];
+ mtime = json['mtime'];
+ uname = json['uname'];
+ }
+}
diff --git a/lib/models/video/play/quality.dart b/lib/models/video/play/quality.dart
index 7536b971..4d9d7d6e 100644
--- a/lib/models/video/play/quality.dart
+++ b/lib/models/video/play/quality.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: constant_identifier_names
+
enum VideoQuality {
speed240,
flunt360,
@@ -89,3 +91,46 @@ extension AudioQualityDesc on AudioQuality {
];
get description => _descList[index];
}
+
+enum VideoDecodeFormats {
+ AV1,
+ HEVC,
+ AVC,
+}
+
+extension VideoDecodeFormatsDesc on VideoDecodeFormats {
+ static final List _descList = [
+ 'AV1',
+ 'HEVC',
+ 'AVC',
+ ];
+ get description => _descList[index];
+}
+
+extension VideoDecodeFormatsCode on VideoDecodeFormats {
+ static final List _codeList = [
+ 'av01',
+ 'hev1',
+ 'avc1',
+ ];
+ get code => _codeList[index];
+
+ static VideoDecodeFormats? fromCode(String code) {
+ final index = _codeList.indexOf(code);
+ if (index != -1) {
+ return VideoDecodeFormats.values[index];
+ }
+ return null;
+ }
+
+ static VideoDecodeFormats? fromString(String val) {
+ var result = VideoDecodeFormats.values.first;
+ for (var i in _codeList) {
+ if (val.startsWith(i)) {
+ result = VideoDecodeFormats.values[_codeList.indexOf(i)];
+ break;
+ }
+ }
+ return result;
+ }
+}
diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart
index 07dd684c..c3109467 100644
--- a/lib/models/video/play/url.dart
+++ b/lib/models/video/play/url.dart
@@ -29,7 +29,7 @@ class PlayUrlModel {
int? timeLength;
String? acceptFormat;
List? acceptDesc;
- List? acceptQuality;
+ List? acceptQuality;
int? videoCodecid;
String? seekParam;
String? seekType;
@@ -48,7 +48,7 @@ class PlayUrlModel {
timeLength = json['timelength'];
acceptFormat = json['accept_format'];
acceptDesc = json['accept_description'];
- acceptQuality = json['accept_quality'];
+ acceptQuality = json['accept_quality'].map((e) => e as int).toList();
videoCodecid = json['video_codecid'];
seekParam = json['seek_param'];
seekType = json['seek_type'];
diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart
index 277d1e30..38e0b877 100644
--- a/lib/models/video_detail_res.dart
+++ b/lib/models/video_detail_res.dart
@@ -580,9 +580,11 @@ class UgcSeason {
intro = json['intro'];
signState = json['sign_state'];
attribute = json['attribute'];
- sections = json['sections']
- .map((e) => SectionItem.fromJson(e))
- .toList();
+ sections = json['sections'] != null
+ ? json['sections']
+ .map((e) => SectionItem.fromJson(e))
+ .toList()
+ : [];
stat = Stat.fromJson(json['stat']);
epCount = json['ep_count'];
seasonType = json['season_type'];
diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart
new file mode 100644
index 00000000..66c66abb
--- /dev/null
+++ b/lib/pages/about/index.dart
@@ -0,0 +1,246 @@
+import 'dart:io';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:pilipala/http/index.dart';
+import 'package:pilipala/models/github/latest.dart';
+import 'package:pilipala/utils/utils.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class AboutPage extends StatefulWidget {
+ const AboutPage({super.key});
+
+ @override
+ State createState() => _AboutPageState();
+}
+
+class _AboutPageState extends State {
+ final AboutController _aboutController = Get.put(AboutController());
+
+ @override
+ Widget build(BuildContext context) {
+ Color outline = Theme.of(context).colorScheme.outline;
+ TextStyle subTitleStyle =
+ TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('关于', style: Theme.of(context).textTheme.titleMedium),
+ ),
+ body: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Divider(
+ thickness: 8,
+ height: 10,
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ ),
+ Image.asset(
+ 'assets/images/logo/logo_android_2.png',
+ width: 150,
+ ),
+ Text(
+ 'PiliPala',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 20),
+ Obx(
+ () => ListTile(
+ title: const Text("当前版本"),
+ trailing: Text(_aboutController.currentVersion.value,
+ style: subTitleStyle),
+ ),
+ ),
+ Obx(
+ () => ListTile(
+ onTap: () => _aboutController.onUpdate(),
+ title: const Text('最新版本'),
+ trailing: Text(
+ _aboutController.isLoading.value
+ ? '正在获取'
+ : _aboutController.isUpdate.value
+ ? '有新版本 ❤️${_aboutController.remoteVersion.value}'
+ : '当前已是最新版',
+ style: subTitleStyle,
+ ),
+ ),
+ ),
+ // ListTile(
+ // onTap: () {},
+ // title: const Text('更新日志'),
+ // trailing: const Icon(
+ // Icons.arrow_forward_ios,
+ // size: 16,
+ // ),
+ // ),
+ Divider(
+ thickness: 8,
+ height: 30,
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ ),
+ ListTile(
+ onTap: () {},
+ title: const Text('作者'),
+ trailing: Text('guozhigq', style: subTitleStyle),
+ ),
+ ListTile(
+ onTap: () {},
+ title: const Text('酷安'),
+ trailing: Text('影若风', style: subTitleStyle),
+ ),
+ ListTile(
+ onTap: () => _aboutController.githubUrl(),
+ title: const Text('Github'),
+ trailing: Text(
+ 'github.com/guozhigq/pilipala',
+ style: subTitleStyle,
+ ),
+ ),
+ ListTile(
+ onTap: () => _aboutController.feedback(),
+ title: const Text('问题反馈'),
+ trailing: Icon(
+ Icons.arrow_forward_ios,
+ size: 16,
+ color: outline,
+ ),
+ ),
+ ListTile(
+ onTap: () => _aboutController.qqChanel(),
+ title: const Text('QQ频道'),
+ trailing: Icon(
+ Icons.arrow_forward_ios,
+ size: 16,
+ color: outline,
+ ),
+ ),
+ ListTile(
+ onTap: () => _aboutController.tgChanel(),
+ title: const Text('TG频道'),
+ trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
+ ),
+ Divider(
+ thickness: 8,
+ height: 30,
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class AboutController extends GetxController {
+ RxString currentVersion = ''.obs;
+ RxString remoteVersion = ''.obs;
+ late LatestDataModel remoteAppInfo;
+ RxBool isUpdate = true.obs;
+ RxBool isLoading = true.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ init();
+ // 获取当前版本
+ getCurrentApp();
+ // 获取最新的版本
+ getRemoteApp();
+ }
+
+ // 获取设备信息
+ Future init() async {
+ DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
+ if (Platform.isAndroid) {
+ AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
+ print(androidInfo.supportedAbis);
+ } else if (Platform.isIOS) {
+ IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
+ print(iosInfo);
+ }
+ }
+
+ // 获取啊当前版本
+ Future getCurrentApp() async {
+ var result = await PackageInfo.fromPlatform();
+ currentVersion.value = result.version;
+ }
+
+ // 获取远程版本
+ Future getRemoteApp() async {
+ var result = await Request().get(Api.latestApp);
+ LatestDataModel data = LatestDataModel.fromJson(result.data);
+ remoteAppInfo = data;
+ remoteVersion.value = data.tagName!;
+ isUpdate.value =
+ Utils.needUpdate(currentVersion.value, remoteVersion.value);
+ isLoading.value = false;
+ }
+
+ // 跳转下载/本地更新
+ Future onUpdate() async {
+ // final dir = await getApplicationSupportDirectory();
+ // final path = '${dir.path}/pilipala.apk';
+ // var result = await Request()
+ // .downloadFile(remoteAppInfo.assets!.first.downloadUrl, path);
+ // print(result);
+ launchUrl(
+ Uri.parse('https://github.com/guozhigq/pilipala/releases'),
+ mode: LaunchMode.externalApplication,
+ );
+ }
+
+ // 跳转github
+ githubUrl() {
+ launchUrl(
+ Uri.parse('https://github.com/guozhigq/pilipala'),
+ mode: LaunchMode.externalApplication,
+ );
+ }
+
+ // 问题反馈
+ feedback() {
+ launchUrl(
+ Uri.parse('https://github.com/guozhigq/pilipala/issues'),
+ // 系统自带浏览器打开
+ mode: LaunchMode.externalApplication,
+ );
+ }
+
+ // qq频道
+ qqChanel() {
+ Clipboard.setData(
+ const ClipboardData(text: 'https://pd.qq.com/s/css9rdwga'),
+ );
+ SmartDialog.showToast(
+ '已复制,即将在浏览器打开',
+ displayTime: const Duration(milliseconds: 500),
+ ).then(
+ (value) => launchUrl(
+ Uri.parse('https://pd.qq.com/s/css9rdwga'),
+ mode: LaunchMode.externalApplication,
+ ),
+ );
+ }
+
+ // tg频道
+ tgChanel() {
+ Clipboard.setData(
+ const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'),
+ );
+ SmartDialog.showToast(
+ '已复制,即将在浏览器打开',
+ displayTime: const Duration(milliseconds: 500),
+ ).then(
+ (value) => launchUrl(
+ Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'),
+ mode: LaunchMode.externalApplication,
+ ),
+ );
+ }
+}
diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart
index cb02a3f7..7f9f6a61 100644
--- a/lib/pages/bangumi/controller.dart
+++ b/lib/pages/bangumi/controller.dart
@@ -1,3 +1,68 @@
+import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/http/bangumi.dart';
+import 'package:pilipala/models/bangumi/list.dart';
+import 'package:pilipala/utils/storage.dart';
-class BangumiController extends GetxController {}
+class BangumiController extends GetxController {
+ final ScrollController scrollController = ScrollController();
+ RxList bangumiList = [BangumiListItemModel()].obs;
+ RxList bangumiFollowList = [BangumiListItemModel()].obs;
+ int _currentPage = 1;
+ bool isLoadingMore = true;
+ Box user = GStrorage.user;
+ RxBool userLogin = false.obs;
+ late int mid;
+
+ @override
+ void onInit() {
+ super.onInit();
+ if (user.get(UserBoxKey.userMid) != null) {
+ mid = int.parse(user.get(UserBoxKey.userMid).toString());
+ }
+ userLogin.value = user.get(UserBoxKey.userLogin) != null;
+ }
+
+ Future queryBangumiListFeed({type = 'init'}) async {
+ if (type == 'init') {
+ _currentPage = 1;
+ }
+ var result = await BangumiHttp.bangumiList(page: _currentPage);
+ if (result['status']) {
+ if (type == 'init') {
+ bangumiList.value = result['data'].list;
+ } else {
+ bangumiList.addAll(result['data'].list);
+ }
+ _currentPage += 1;
+ } else {}
+ isLoadingMore = false;
+ return result;
+ }
+
+ // 上拉加载
+ Future onLoad() async {
+ queryBangumiListFeed(type: 'onLoad');
+ }
+
+ // 我的订阅
+ Future queryBangumiFollow() async {
+ var result = await BangumiHttp.bangumiFollow(mid: 17340771);
+ if (result['status']) {
+ bangumiFollowList.value = result['data'].list;
+ } else {}
+ return result;
+ }
+
+ // 返回顶部并刷新
+ void animateToTop() async {
+ if (scrollController.offset >=
+ MediaQuery.of(Get.context!).size.height * 5) {
+ scrollController.jumpTo(0);
+ } else {
+ await scrollController.animateTo(0,
+ duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
+ }
+ }
+}
diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart
index 1b1599cb..b0ce9452 100644
--- a/lib/pages/bangumi/introduction/controller.dart
+++ b/lib/pages/bangumi/introduction/controller.dart
@@ -7,7 +7,6 @@ import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart';
-import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
@@ -36,7 +35,6 @@ class BangumiIntroController extends GetxController {
RxBool isLoading = false.obs;
// 视频详情 请求返回
- Rx videoDetail = VideoDetailData().obs;
Rx bangumiDetail = BangumiInfoModel().obs;
// 请求返回的信息
@@ -89,11 +87,6 @@ class BangumiIntroController extends GetxController {
// 获取番剧简介&选集
Future queryBangumiIntro() async {
- var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
- if (result['status']) {
- bangumiDetail.value = result['data'];
- epId = bangumiDetail.value.episodes!.first.id;
- }
if (userLogin) {
// 获取点赞状态
queryHasLikeVideo();
@@ -102,6 +95,11 @@ class BangumiIntroController extends GetxController {
// 获取收藏状态
queryHasFavVideo();
}
+ var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
+ if (result['status']) {
+ bangumiDetail.value = result['data'];
+ epId = bangumiDetail.value.episodes!.first.id;
+ }
return result;
}
@@ -132,15 +130,10 @@ class BangumiIntroController extends GetxController {
Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) {
- if (!hasLike.value) {
- SmartDialog.showToast('点赞成功 👍');
- hasLike.value = true;
- videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;
- } else if (hasLike.value) {
- SmartDialog.showToast('取消赞');
- hasLike.value = false;
- videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1;
- }
+ SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞');
+ hasLike.value = !hasLike.value;
+ bangumiDetail.value.stat!['likes'] =
+ bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);
hasLike.refresh();
} else {
SmartDialog.showToast(result['msg']);
@@ -193,8 +186,8 @@ class BangumiIntroController extends GetxController {
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
- videoDetail.value.stat!.coin =
- videoDetail.value.stat!.coin! + _tempThemeValue;
+ bangumiDetail.value.stat!['coins'] =
+ bangumiDetail.value.stat!['coins'] + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
@@ -287,4 +280,13 @@ class BangumiIntroController extends GetxController {
await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId);
SmartDialog.showToast(result['msg']);
}
+
+ Future queryVideoInFolder() async {
+ var result = await VideoHttp.videoInFolder(
+ mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
+ if (result['status']) {
+ favFolderData.value = result['data'];
+ }
+ return result;
+ }
}
diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart
index a8503152..456f532c 100644
--- a/lib/pages/bangumi/introduction/view.dart
+++ b/lib/pages/bangumi/introduction/view.dart
@@ -22,7 +22,11 @@ import 'controller.dart';
import 'widgets/intro_detail.dart';
class BangumiIntroPanel extends StatefulWidget {
- const BangumiIntroPanel({super.key});
+ final int? cid;
+ const BangumiIntroPanel({
+ Key? key,
+ this.cid,
+ }) : super(key: key);
@override
State createState() => _BangumiIntroPanelState();
@@ -33,6 +37,7 @@ class _BangumiIntroPanelState extends State
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
BangumiInfoModel? bangumiDetail;
+ late Future _futureBuilderFuture;
// 添加页面缓存
@override
@@ -44,13 +49,14 @@ class _BangumiIntroPanelState extends State
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
+ _futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
- future: bangumiIntroController.queryBangumiIntro(),
+ future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@@ -67,7 +73,11 @@ class _BangumiIntroPanelState extends State
);
}
} else {
- return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
+ return BangumiInfo(
+ loadingStatus: true,
+ bangumiDetail: bangumiDetail,
+ cid: widget.cid,
+ );
}
},
);
@@ -77,11 +87,13 @@ class _BangumiIntroPanelState extends State
class BangumiInfo extends StatefulWidget {
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail;
+ final int? cid;
const BangumiInfo({
Key? key,
this.loadingStatus = false,
this.bangumiDetail,
+ this.cid,
}) : super(key: key);
@override
@@ -89,21 +101,22 @@ class BangumiInfo extends StatefulWidget {
}
class _BangumiInfoState extends State {
- late BangumiInfoModel? bangumiItem;
- final BangumiIntroController bangumiIntroController =
- Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
-
- late VideoDetailController? videoDetailCtr;
+ String heroTag = Get.arguments['heroTag'];
+ late final BangumiIntroController bangumiIntroController;
+ late final VideoDetailController videoDetailCtr;
Box localCache = GStrorage.localCache;
+ late final BangumiInfoModel? bangumiItem;
late double sheetHeight;
+ int? cid;
@override
void initState() {
super.initState();
+ bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
+ videoDetailCtr = Get.find(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
- videoDetailCtr =
- Get.find(tag: Get.arguments['heroTag']);
sheetHeight = localCache.get('sheetHeight');
+ cid = widget.cid!;
}
// 收藏
@@ -160,13 +173,14 @@ class _BangumiInfoState extends State {
),
if (bangumiItem != null &&
bangumiItem!.rating != null)
- pBadge(
- '评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}',
- context,
- null,
- 6,
- 6,
- null),
+ PBadge(
+ text:
+ '评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}',
+ top: null,
+ right: 6,
+ bottom: 6,
+ left: null,
+ ),
],
),
const SizedBox(width: 10),
@@ -318,9 +332,10 @@ class _BangumiInfoState extends State {
pages: bangumiItem != null
? bangumiItem!.episodes!
: widget.bangumiDetail!.episodes!,
- cid: bangumiItem != null
- ? bangumiItem!.episodes!.first.cid
- : widget.bangumiDetail!.episodes!.first.cid,
+ cid: cid ??
+ (bangumiItem != null
+ ? bangumiItem!.episodes!.first.cid
+ : widget.bangumiDetail!.episodes!.first.cid),
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid),
@@ -357,10 +372,10 @@ class _BangumiInfoState extends State {
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => bangumiIntroController.actionLikeVideo(),
selectStatus: bangumiIntroController.hasLike.value,
- loadingStatus: widget.loadingStatus,
+ loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
- : '-'),
+ : bangumiItem!.stat!['likes']!.toString()),
),
Obx(
() => ActionItem(
@@ -368,10 +383,10 @@ class _BangumiInfoState extends State {
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => bangumiIntroController.actionCoinVideo(),
selectStatus: bangumiIntroController.hasCoin.value,
- loadingStatus: widget.loadingStatus,
+ loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
- : '-'),
+ : bangumiItem!.stat!['coins']!.toString()),
),
Obx(
() => ActionItem(
@@ -379,29 +394,29 @@ class _BangumiInfoState extends State {
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
- loadingStatus: widget.loadingStatus,
+ loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
- : '-'),
+ : bangumiItem!.stat!['favorite']!.toString()),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),
selectIcon: const Icon(FontAwesomeIcons.reply),
- onTap: () => videoDetailCtr!.tabCtr!.animateTo(1),
+ onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false,
- loadingStatus: widget.loadingStatus,
+ loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
- : '-',
+ : bangumiItem!.stat!['reply']!.toString(),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false,
- loadingStatus: widget.loadingStatus,
+ loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString()
- : '-'),
+ : bangumiItem!.stat!['share']!.toString()),
],
),
),
@@ -465,9 +480,6 @@ class _BangumiInfoState extends State {
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
- // text: !widget.loadingStatus
- // ? widget.videoDetail!.stat!.share!.toString()
- // : '-',
text: '转发'),
]);
}
diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart
index cb1b1ddb..6c25e508 100644
--- a/lib/pages/bangumi/view.dart
+++ b/lib/pages/bangumi/view.dart
@@ -1,4 +1,15 @@
+import 'dart:async';
+
import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/pages/main/index.dart';
+import 'package:pilipala/pages/rcmd/view.dart';
+
+import 'controller.dart';
+import 'widgets/bangumu_card_v.dart';
class BangumiPage extends StatefulWidget {
const BangumiPage({super.key});
@@ -7,12 +18,181 @@ class BangumiPage extends StatefulWidget {
State createState() => _BangumiPageState();
}
-class _BangumiPageState extends State {
+class _BangumiPageState extends State
+ with AutomaticKeepAliveClientMixin {
+ final BangumiController _bangumidController = Get.put(BangumiController());
+ late Future? _futureBuilderFuture;
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ ScrollController scrollController = _bangumidController.scrollController;
+ StreamController mainStream =
+ Get.find().bottomBarStream;
+ _futureBuilderFuture = _bangumidController.queryBangumiListFeed();
+ scrollController.addListener(
+ () async {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 200) {
+ if (!_bangumidController.isLoadingMore) {
+ _bangumidController.isLoadingMore = true;
+ await _bangumidController.onLoad();
+ }
+ }
+
+ final ScrollDirection direction =
+ scrollController.position.userScrollDirection;
+ if (direction == ScrollDirection.forward) {
+ mainStream.add(true);
+ } else if (direction == ScrollDirection.reverse) {
+ mainStream.add(false);
+ }
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
- return const Scaffold(
- body: Center(
- child: Text('还在开发中'),
+ super.build(context);
+ return RefreshIndicator(
+ onRefresh: () async {
+ await _bangumidController.queryBangumiListFeed(type: 'init');
+ return _bangumidController.queryBangumiFollow();
+ },
+ child: CustomScrollView(
+ controller: _bangumidController.scrollController,
+ slivers: [
+ SliverToBoxAdapter(
+ child: Obx(
+ () => Visibility(
+ visible: _bangumidController.userLogin.value,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: StyleString.safeSpace, bottom: 10, left: 16),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '最近追番',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ],
+ ),
+ ),
+ SizedBox(
+ height: 258,
+ child: FutureBuilder(
+ future: _bangumidController.queryBangumiFollow(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState ==
+ ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return Obx(
+ () => ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: _bangumidController
+ .bangumiFollowList.length,
+ itemBuilder: (context, index) {
+ return Container(
+ width: Get.size.width / 3,
+ height: 254,
+ margin: EdgeInsets.only(
+ left: StyleString.safeSpace,
+ right: index ==
+ _bangumidController
+ .bangumiFollowList
+ .length -
+ 1
+ ? StyleString.safeSpace
+ : 0),
+ child: BangumiCardV(
+ bangumiItem: _bangumidController
+ .bangumiFollowList[index],
+ ),
+ );
+ },
+ ),
+ );
+ } else {
+ return const SizedBox();
+ }
+ } else {
+ return const SizedBox();
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 10, bottom: 10, left: 16),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '推荐',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ],
+ ),
+ ),
+ ),
+ SliverPadding(
+ padding: const EdgeInsets.fromLTRB(
+ StyleString.safeSpace, 0, StyleString.safeSpace, 0),
+ sliver: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return Obx(() => contentGrid(
+ _bangumidController, _bangumidController.bangumiList));
+ } else {
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () => {},
+ );
+ }
+ } else {
+ return contentGrid(_bangumidController, []);
+ }
+ },
+ ),
+ ),
+ const LoadingMore()
+ ],
+ ),
+ );
+ }
+
+ Widget contentGrid(ctr, bangumiList) {
+ return SliverGrid(
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ // 行间距
+ mainAxisSpacing: StyleString.cardSpace - 2,
+ // 列间距
+ crossAxisSpacing: StyleString.cardSpace,
+ // 列数
+ crossAxisCount: 3,
+ mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
+ ),
+ delegate: SliverChildBuilderDelegate(
+ (BuildContext context, int index) {
+ return bangumiList!.isNotEmpty
+ ? BangumiCardV(bangumiItem: bangumiList[index])
+ : const SizedBox();
+ },
+ childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),
);
}
diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart
index 85e70d19..aca5f086 100644
--- a/lib/pages/bangumi/widgets/bangumi_panel.dart
+++ b/lib/pages/bangumi/widgets/bangumi_panel.dart
@@ -22,73 +22,106 @@ class BangumiPanel extends StatefulWidget {
class _BangumiPanelState extends State {
late int currentIndex;
+ final ScrollController listViewScrollCtr = ScrollController();
+ final ScrollController listViewScrollCtr_2 = ScrollController();
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
+ scrollToIndex();
+ }
+
+ @override
+ void dispose() {
+ listViewScrollCtr.dispose();
+ listViewScrollCtr_2.dispose();
+ super.dispose();
}
void showBangumiPanel() {
showBottomSheet(
context: context,
- builder: (_) => Container(
- height: widget.sheetHeight,
- color: Theme.of(context).colorScheme.background,
- child: Column(
- children: [
- Container(
- height: 45,
- padding: const EdgeInsets.only(left: 14, right: 14),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ await Future.delayed(const Duration(milliseconds: 200));
+ listViewScrollCtr_2.animateTo(currentIndex * 56,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeInOut);
+ });
+ // 在这里使用 setState 更新状态
+ return Container(
+ height: widget.sheetHeight,
+ color: Theme.of(context).colorScheme.background,
+ child: Column(
children: [
- Text(
- '合集(${widget.pages.length})',
- style: Theme.of(context).textTheme.titleMedium,
+ AppBar(
+ toolbarHeight: 45,
+ automaticallyImplyLeading: false,
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '合集(${widget.pages.length})',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ ),
+ titleSpacing: 10,
),
- IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => Navigator.pop(context),
+ Expanded(
+ child: Material(
+ child: ListView.builder(
+ controller: listViewScrollCtr_2,
+ itemCount: widget.pages.length,
+ itemBuilder: (context, index) {
+ return ListTile(
+ onTap: () {
+ setState(() {
+ changeFucCall(widget.pages[index], index);
+ });
+ },
+ dense: false,
+ leading: index == currentIndex
+ ? Image.asset(
+ 'assets/images/live.gif',
+ color:
+ Theme.of(context).colorScheme.primary,
+ height: 12,
+ )
+ : null,
+ title: Text(
+ '第${index + 1}话 ${widget.pages[index].longTitle!}',
+ style: TextStyle(
+ fontSize: 14,
+ color: index == currentIndex
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.onSurface,
+ ),
+ ),
+ trailing: widget.pages[index].badge != null
+ ? Image.asset(
+ 'assets/images/big-vip.png',
+ height: 20,
+ )
+ : const SizedBox(),
+ );
+ },
+ ),
+ ),
),
],
),
- ),
- Divider(
- height: 1,
- color: Theme.of(context).dividerColor.withOpacity(0.1),
- ),
- Expanded(
- child: Material(
- child: ListView.builder(
- itemCount: widget.pages.length,
- itemBuilder: (context, index) {
- return ListTile(
- onTap: () => changeFucCall(widget.pages[index], index),
- dense: false,
- title: Text(
- widget.pages[index].longTitle!,
- style: TextStyle(
- fontSize: 14,
- color: index == currentIndex
- ? Theme.of(context).colorScheme.primary
- : Theme.of(context).colorScheme.onSurface,
- ),
- ),
- trailing: widget.pages[index].badge != null
- ? Image.asset(
- 'assets/images/big-vip.png',
- height: 20,
- )
- : const SizedBox(),
- );
- },
- ),
- ),
- ),
- ],
- ),
- ),
+ );
+ },
+ );
+ },
);
}
@@ -104,6 +137,15 @@ class _BangumiPanelState extends State {
);
currentIndex = i;
setState(() {});
+ scrollToIndex();
+ }
+
+ void scrollToIndex() {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ // 在回调函数中获取更新后的状态
+ listViewScrollCtr.animateTo(currentIndex * 150,
+ duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
+ });
}
@override
@@ -146,8 +188,10 @@ class _BangumiPanelState extends State {
SizedBox(
height: 60,
child: ListView.builder(
+ controller: listViewScrollCtr,
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
+ itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,
@@ -217,87 +261,6 @@ class _BangumiPanelState extends State {
);
})),
)
- // SingleChildScrollView(
- // padding: const EdgeInsets.only(top: 7, bottom: 7),
- // scrollDirection: Axis.horizontal,
- // child: ConstrainedBox(
- // constraints: BoxConstraints(
- // minWidth: MediaQuery.of(context).size.width,
- // ),
- // child: Row(
- // children: [
- // for (int i = 0; i < widget.pages.length; i++) ...[
- // Container(
- // width: 150,
- // margin: const EdgeInsets.only(right: 10),
- // child: Material(
- // color: Theme.of(context).colorScheme.onInverseSurface,
- // borderRadius: BorderRadius.circular(6),
- // clipBehavior: Clip.hardEdge,
- // child: InkWell(
- // onTap: () => changeFucCall(widget.pages[i], i),
- // child: Padding(
- // padding: const EdgeInsets.symmetric(
- // vertical: 8, horizontal: 10),
- // child: Column(
- // crossAxisAlignment: CrossAxisAlignment.start,
- // children: [
- // Row(
- // children: [
- // if (i == currentIndex) ...[
- // Image.asset(
- // 'assets/images/live.gif',
- // color:
- // Theme.of(context).colorScheme.primary,
- // height: 12,
- // ),
- // const SizedBox(width: 6)
- // ],
- // Text(
- // '第${i + 1}话',
- // style: TextStyle(
- // fontSize: 13,
- // color: i == currentIndex
- // ? Theme.of(context)
- // .colorScheme
- // .primary
- // : Theme.of(context)
- // .colorScheme
- // .onSurface),
- // ),
- // const SizedBox(width: 2),
- // if (widget.pages[i].badge != null) ...[
- // Image.asset(
- // 'assets/images/big-vip.png',
- // height: 16,
- // ),
- // ],
- // ],
- // ),
- // const SizedBox(height: 3),
- // Text(
- // widget.pages[i].longTitle!,
- // maxLines: 1,
- // style: TextStyle(
- // fontSize: 13,
- // color: i == currentIndex
- // ? Theme.of(context).colorScheme.primary
- // : Theme.of(context)
- // .colorScheme
- // .onSurface),
- // overflow: TextOverflow.ellipsis,
- // )
- // ],
- // ),
- // ),
- // ),
- // ),
- // ),
- // ]
- // ],
- // ),
- // ),
- // )
],
);
}
diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart
new file mode 100644
index 00000000..47629331
--- /dev/null
+++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart
@@ -0,0 +1,184 @@
+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/badge.dart';
+import 'package:pilipala/http/search.dart';
+import 'package:pilipala/models/bangumi/info.dart';
+import 'package:pilipala/models/common/search_type.dart';
+import 'package:pilipala/utils/utils.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+
+// 视频卡片 - 垂直布局
+class BangumiCardV extends StatelessWidget {
+ // ignore: prefer_typing_uninitialized_variables
+ final bangumiItem;
+ final Function()? longPress;
+ final Function()? longPressEnd;
+
+ const BangumiCardV({
+ Key? key,
+ required this.bangumiItem,
+ this.longPress,
+ this.longPressEnd,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
+ return Card(
+ elevation: 0,
+ clipBehavior: Clip.hardEdge,
+ shape: RoundedRectangleBorder(
+ borderRadius: StyleString.mdRadius,
+ ),
+ margin: EdgeInsets.zero,
+ child: GestureDetector(
+ // onLongPress: () {
+ // if (longPress != null) {
+ // longPress!();
+ // }
+ // },
+ // onLongPressEnd: (details) {
+ // if (longPressEnd != null) {
+ // longPressEnd!();
+ // }
+ // },
+ child: InkWell(
+ onTap: () async {
+ int seasonId = bangumiItem.seasonId;
+ SmartDialog.showLoading(msg: '获取中...');
+ var res = await SearchHttp.bangumiInfo(seasonId: seasonId);
+ SmartDialog.dismiss().then((value) {
+ if (res['status']) {
+ if (res['data'].episodes.isEmpty) {
+ SmartDialog.showToast('资源加载失败');
+ return;
+ }
+ EpisodeItem episode = res['data'].episodes.first;
+ String bvid = episode.bvid!;
+ int cid = episode.cid!;
+ String pic = episode.cover!;
+ String heroTag = Utils.makeHeroTag(cid);
+ Get.toNamed(
+ '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
+ arguments: {
+ 'pic': pic,
+ 'heroTag': heroTag,
+ 'videoType': SearchType.media_bangumi,
+ 'bangumiItem': res['data'],
+ },
+ );
+ }
+ });
+ },
+ child: Column(
+ children: [
+ ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: StyleString.imgRadius,
+ topRight: StyleString.imgRadius,
+ bottomLeft: StyleString.imgRadius,
+ bottomRight: StyleString.imgRadius,
+ ),
+ child: AspectRatio(
+ aspectRatio: 0.65,
+ child: LayoutBuilder(builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Stack(
+ children: [
+ Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: bangumiItem.cover,
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ ),
+ if (bangumiItem.badge != null)
+ PBadge(
+ text: bangumiItem.badge,
+ top: 6,
+ right: 6,
+ bottom: null,
+ left: null),
+ if (bangumiItem.order != null)
+ PBadge(
+ text: bangumiItem.order,
+ top: null,
+ right: null,
+ bottom: 6,
+ left: 6,
+ type: 'gray',
+ ),
+ ],
+ );
+ }),
+ ),
+ ),
+ BangumiContent(bangumiItem: bangumiItem)
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class BangumiContent extends StatelessWidget {
+ // ignore: prefer_typing_uninitialized_variables
+ final bangumiItem;
+ const BangumiContent({Key? key, required this.bangumiItem}) : super(key: key);
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: Padding(
+ // 多列
+ padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
+ // 单列
+ // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ // mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ bangumiItem.title,
+ textAlign: TextAlign.start,
+ style: const TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ letterSpacing: 0.3,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ )),
+ ],
+ ),
+ if (bangumiItem.indexShow != null)
+ Text(
+ bangumiItem.indexShow,
+ maxLines: 1,
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ if (bangumiItem.progress != null)
+ Text(
+ bangumiItem.progress,
+ maxLines: 1,
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/blacklist/index.dart b/lib/pages/blacklist/index.dart
new file mode 100644
index 00000000..27aa770f
--- /dev/null
+++ b/lib/pages/blacklist/index.dart
@@ -0,0 +1,156 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/http/black.dart';
+import 'package:pilipala/models/user/black.dart';
+import 'package:pilipala/utils/storage.dart';
+import 'package:pilipala/utils/utils.dart';
+
+class BlackListPage extends StatefulWidget {
+ const BlackListPage({super.key});
+
+ @override
+ State createState() => _BlackListPageState();
+}
+
+class _BlackListPageState extends State {
+ final BlackListController _blackListController =
+ Get.put(BlackListController());
+ final ScrollController scrollController = ScrollController();
+ Future? _futureBuilderFuture;
+ bool _isLoadingMore = false;
+ Box setting = GStrorage.setting;
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = _blackListController.queryBlacklist();
+ scrollController.addListener(
+ () async {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 200) {
+ if (!_isLoadingMore) {
+ _isLoadingMore = true;
+ await _blackListController.queryBlacklist(type: 'onLoad');
+ _isLoadingMore = false;
+ }
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ List blackMidsList =
+ _blackListController.blackList.map((e) => e.mid!).toList();
+ setting.put(SettingBoxKey.blackMidsList, blackMidsList);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ elevation: 0,
+ scrolledUnderElevation: 0,
+ titleSpacing: 0,
+ centerTitle: false,
+ title: Obx(
+ () => Text(
+ '黑名单管理 (${_blackListController.blackList.length} / 5000)',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ ),
+ body: RefreshIndicator(
+ onRefresh: () async => await _blackListController.queryBlacklist(),
+ child: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var data = snapshot.data;
+ if (data['status']) {
+ List list = _blackListController.blackList;
+ return Obx(
+ () => list.length == 1
+ ? const SizedBox()
+ : ListView.builder(
+ controller: scrollController,
+ itemCount: list.length,
+ itemBuilder: (BuildContext context, int index) {
+ return ListTile(
+ onTap: () {},
+ leading: NetworkImgLayer(
+ width: 45,
+ height: 45,
+ type: 'avatar',
+ src: list[index].face,
+ ),
+ title: Text(
+ list[index].uname!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(fontSize: 14),
+ ),
+ subtitle: Text(
+ Utils.dateFormat(list[index].mtime),
+ maxLines: 1,
+ style: TextStyle(
+ color:
+ Theme.of(context).colorScheme.outline),
+ overflow: TextOverflow.ellipsis,
+ ),
+ dense: true,
+ // trailing: TextButton(
+ // onPressed: () {},
+ // child: const Text('移除'),
+ // ),
+ );
+ },
+ ),
+ );
+ } else {
+ return CustomScrollView(
+ slivers: [
+ HttpError(
+ errMsg: data['msg'],
+ fn: () => _blackListController.queryBlacklist(),
+ )
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return const SizedBox();
+ }
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class BlackListController extends GetxController {
+ int currentPage = 1;
+ int pageSize = 50;
+ RxList blackList = [BlackListItem()].obs;
+
+ Future queryBlacklist({type = 'init'}) async {
+ if (type == 'init') {
+ currentPage = 1;
+ }
+ var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);
+ if (result['status']) {
+ if (type == 'init') {
+ blackList.value = result['data'].list;
+ } else {
+ blackList.addAll(result['data'].list);
+ }
+
+ currentPage += 1;
+ }
+ return result;
+ }
+}
diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart
index 42647f3f..5bef4794 100644
--- a/lib/pages/dynamics/controller.dart
+++ b/lib/pages/dynamics/controller.dart
@@ -3,13 +3,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
+import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
+import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
+import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart';
+import 'package:pilipala/utils/storage.dart';
+import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@@ -46,8 +51,19 @@ class DynamicsController extends GetxController {
];
bool flag = false;
RxInt initialValue = 1.obs;
+ Box user = GStrorage.user;
+ RxBool userLogin = false.obs;
+
+ @override
+ void onInit() {
+ userLogin.value = user.get(UserBoxKey.userLogin, defaultValue: false);
+ super.onInit();
+ }
Future queryFollowDynamic({type = 'init'}) async {
+ if (!userLogin.value) {
+ return {'status': false, 'msg': '未登录'};
+ }
if (type == 'init') {
dynamicsList.clear();
}
@@ -142,10 +158,39 @@ class DynamicsController extends GetxController {
/// TODO
case 'DYNAMIC_TYPE_UGC_SEASON':
print('合集');
+ break;
+ case 'DYNAMIC_TYPE_PGC_UNION':
+ print('DYNAMIC_TYPE_PGC_UNION 番剧');
+ DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
+ if (pgc.epid != null) {
+ SmartDialog.showLoading(msg: '获取中...');
+ var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
+ SmartDialog.dismiss();
+ if (res['status']) {
+ EpisodeItem episode = res['data'].episodes.first;
+ String bvid = episode.bvid!;
+ int cid = episode.cid!;
+ String pic = episode.cover!;
+ String heroTag = Utils.makeHeroTag(cid);
+ Get.toNamed(
+ '/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
+ arguments: {
+ 'pic': pic,
+ 'heroTag': heroTag,
+ 'videoType': SearchType.media_bangumi,
+ 'bangumiItem': res['data'],
+ },
+ );
+ }
+ }
+ break;
}
}
- Future queryFollowUp() async {
+ Future queryFollowUp({type = 'init'}) async {
+ if (type == 'init') {
+ upData = FollowUpModel().obs;
+ }
var res = await DynamicsHttp.followUp();
if (res['status']) {
upData.value = res['data'];
diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart
index 2e01537b..4f6b6a40 100644
--- a/lib/pages/dynamics/view.dart
+++ b/lib/pages/dynamics/view.dart
@@ -1,17 +1,15 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
-import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
-import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
-import 'package:pilipala/pages/mine/index.dart';
+import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@@ -29,9 +27,11 @@ class DynamicsPage extends StatefulWidget {
class _DynamicsPageState extends State
with AutomaticKeepAliveClientMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController());
- Future? _futureBuilderFuture;
+ late Future _futureBuilderFuture;
+ late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box user = GStrorage.user;
+ EventBus eventBus = EventBus();
@override
bool get wantKeepAlive => true;
@@ -40,6 +40,7 @@ class _DynamicsPageState extends State
void initState() {
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
+ _futureBuilderFutureUp = _dynamicsController.queryFollowUp();
ScrollController scrollController = _dynamicsController.scrollController;
StreamController mainStream =
Get.find().bottomBarStream;
@@ -63,6 +64,14 @@ class _DynamicsPageState extends State
}
},
);
+
+ eventBus.on(EventName.loginEvent, (args) {
+ _dynamicsController.userLogin.value = args['status'];
+ setState(() {
+ _futureBuilderFuture = _dynamicsController.queryFollowDynamic();
+ _futureBuilderFutureUp = _dynamicsController.queryFollowUp();
+ });
+ });
}
@override
@@ -108,115 +117,82 @@ class _DynamicsPageState extends State
return const SizedBox();
}
}),
- Obx(() => Visibility(
- visible: _dynamicsController.mid.value == -1,
- child: CustomSlidingSegmentedControl(
- initialValue: _dynamicsController.initialValue.value,
- children: {
- 1: Text(
- '全部',
- style: TextStyle(
- fontSize: Theme.of(context)
- .textTheme
- .labelMedium!
- .fontSize),
+ Obx(
+ () => _dynamicsController.userLogin.value
+ ? Visibility(
+ visible: _dynamicsController.mid.value == -1,
+ child: CustomSlidingSegmentedControl(
+ initialValue:
+ _dynamicsController.initialValue.value,
+ children: {
+ 1: Text(
+ '全部',
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .fontSize),
+ ),
+ 2: Text('投稿',
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .fontSize)),
+ 3: Text('番剧',
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .fontSize)),
+ // 4: Text(
+ // '专栏',
+ // style: TextStyle(
+ // fontSize: Theme.of(context)
+ // .textTheme
+ // .labelMedium!
+ // .fontSize),
+ // ),
+ },
+ padding: 13.0,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .colorScheme
+ .surfaceVariant
+ .withOpacity(0.7),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ thumbDecoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ onValueChanged: (v) {
+ feedBack();
+ _dynamicsController.onSelectType(v);
+ },
),
- 2: Text('投稿',
- style: TextStyle(
- fontSize: Theme.of(context)
- .textTheme
- .labelMedium!
- .fontSize)),
- 3: Text('番剧',
- style: TextStyle(
- fontSize: Theme.of(context)
- .textTheme
- .labelMedium!
- .fontSize)),
- // 4: Text(
- // '专栏',
- // style: TextStyle(
- // fontSize: Theme.of(context)
- // .textTheme
- // .labelMedium!
- // .fontSize),
- // ),
- },
- padding: 13.0,
- decoration: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .surfaceVariant
- .withOpacity(0.7),
- borderRadius: BorderRadius.circular(20),
- ),
- thumbDecoration: BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(20),
- ),
- duration: const Duration(milliseconds: 300),
- curve: Curves.easeInOut,
- onValueChanged: (v) {
- feedBack();
- _dynamicsController.onSelectType(v);
- },
- ),
- ))
+ )
+ : Text('动态',
+ style: Theme.of(context).textTheme.titleMedium),
+ )
],
),
- Positioned(
- right: 4,
- top: 0,
- bottom: 0,
- child: IconButton(
- padding: EdgeInsets.zero,
- onPressed: () =>
- {feedBack(), _dynamicsController.resetSearch()},
- icon: const Icon(Icons.history, size: 21),
- ),
- ),
- Positioned(
- left: 10,
- top: 0,
- bottom: 0,
- child: Align(
- alignment: Alignment.center,
- child: user.get(UserBoxKey.userLogin) ?? false
- ? GestureDetector(
- onTap: () {
- feedBack();
- showModalBottomSheet(
- context: context,
- builder: (_) => const SizedBox(
- height: 450,
- child: MinePage(),
- ),
- clipBehavior: Clip.hardEdge,
- isScrollControlled: true,
- );
- },
- child: NetworkImgLayer(
- type: 'avatar',
- width: 30,
- height: 30,
- src: user.get(UserBoxKey.userFace),
- ),
- )
- : IconButton(
- onPressed: () {
- feedBack();
- showModalBottomSheet(
- context: context,
- builder: (_) => const SizedBox(
- height: 450,
- child: MinePage(),
- ),
- clipBehavior: Clip.hardEdge,
- isScrollControlled: true,
- );
- },
- icon: const Icon(CupertinoIcons.person, size: 22),
- ),
+ Obx(
+ () => Visibility(
+ visible: _dynamicsController.userLogin.value,
+ child: Positioned(
+ right: 4,
+ top: 0,
+ bottom: 0,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ onPressed: () =>
+ {feedBack(), _dynamicsController.resetSearch()},
+ icon: const Icon(Icons.history, size: 21),
+ ),
+ ),
),
),
],
@@ -229,7 +205,7 @@ class _DynamicsPageState extends State
controller: _dynamicsController.scrollController,
slivers: [
FutureBuilder(
- future: _dynamicsController.queryFollowUp(),
+ future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
@@ -269,7 +245,14 @@ class _DynamicsPageState extends State
} else {
return HttpError(
errMsg: data['msg'],
- fn: () => _dynamicsController.onRefresh(),
+ fn: () {
+ setState(() {
+ _futureBuilderFuture =
+ _dynamicsController.queryFollowDynamic();
+ _futureBuilderFutureUp =
+ _dynamicsController.queryFollowUp();
+ });
+ },
);
}
} else {
diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart
index d95766e6..67a21371 100644
--- a/lib/pages/dynamics/widgets/author_panel.dart
+++ b/lib/pages/dynamics/widgets/author_panel.dart
@@ -36,7 +36,8 @@ Widget author(item, context) {
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
- color: item.modules.moduleAuthor!.vip!['status'] > 0
+ color: item.modules.moduleAuthor!.vip != null &&
+ item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
diff --git a/lib/pages/dynamics/widgets/live_rcmd_panel.dart b/lib/pages/dynamics/widgets/live_rcmd_panel.dart
index 2b4dedac..9589f919 100644
--- a/lib/pages/dynamics/widgets/live_rcmd_panel.dart
+++ b/lib/pages/dynamics/widgets/live_rcmd_panel.dart
@@ -77,10 +77,21 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
src: item.modules.moduleDynamic.major.liveRcmd.cover,
),
),
- pBadge(watchedShow['text_large'], context, 6, 56, null, null,
- type: 'gray'),
- pBadge(
- liveStatus == 1 ? '直播中' : '直播结束', context, 6, 6, null, null),
+ PBadge(
+ text: watchedShow['text_large'],
+ top: 6,
+ right: 56,
+ bottom: null,
+ left: null,
+ type: 'gray',
+ ),
+ PBadge(
+ text: liveStatus == 1 ? '直播中' : '直播结束',
+ top: 6,
+ right: 6,
+ bottom: null,
+ left: null,
+ ),
Positioned(
left: 0,
right: 0,
diff --git a/lib/pages/dynamics/widgets/pic_panel.dart b/lib/pages/dynamics/widgets/pic_panel.dart
index ef4691cb..a56ac8f7 100644
--- a/lib/pages/dynamics/widgets/pic_panel.dart
+++ b/lib/pages/dynamics/widgets/pic_panel.dart
@@ -85,7 +85,14 @@ Widget picWidget(item, context) {
children: list,
),
if (len == 1 && origAspectRatio < 0.4)
- pBadge('长图', context, null, null, 6.0, 6.0, type: 'gray')
+ const PBadge(
+ text: '长图',
+ top: null,
+ right: null,
+ bottom: 6.0,
+ left: 6.0,
+ type: 'gray',
+ )
],
),
);
diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart
index e1054ba0..2c0a63f7 100644
--- a/lib/pages/dynamics/widgets/up_panel.dart
+++ b/lib/pages/dynamics/widgets/up_panel.dart
@@ -40,7 +40,7 @@ class _UpPanelState extends State {
1,
UpItem(
face: user.get(UserBoxKey.userFace),
- uname: '我的',
+ uname: '我',
mid: user.get(UserBoxKey.userMid),
),
);
diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart
index 0b65a071..04c1ae19 100644
--- a/lib/pages/dynamics/widgets/video_panel.dart
+++ b/lib/pages/dynamics/widgets/video_panel.dart
@@ -89,7 +89,13 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
),
),
if (content.badge != null && type == 'pgc')
- pBadge(content.badge['text'], context, 8.0, 10.0, null, null),
+ PBadge(
+ text: content.badge['text'],
+ top: 8.0,
+ right: 10.0,
+ bottom: null,
+ left: null,
+ ),
Positioned(
left: 0,
right: 0,
diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart
index f190bc85..8c242862 100644
--- a/lib/pages/fav/view.dart
+++ b/lib/pages/fav/view.dart
@@ -13,6 +13,13 @@ class FavPage extends StatefulWidget {
class _FavPageState extends State {
final FavController _favController = Get.put(FavController());
+ late Future _futureBuilderFuture;
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = _favController.queryFavFolder();
+ }
@override
Widget build(BuildContext context) {
@@ -26,7 +33,7 @@ class _FavPageState extends State {
),
),
body: FutureBuilder(
- future: _favController.queryFavFolder(),
+ future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart
index 2d47726b..7dc8e3a5 100644
--- a/lib/pages/history/controller.dart
+++ b/lib/pages/history/controller.dart
@@ -70,6 +70,7 @@ class HistoryController extends GetxController {
SmartDialog.showToast(
!pauseStatus.value ? '暂停观看历史' : '恢复观看历史');
pauseStatus.value = !pauseStatus.value;
+ localCache.put(LocalCacheKey.historyPause, pauseStatus.value);
}
SmartDialog.dismiss();
},
@@ -85,7 +86,7 @@ class HistoryController extends GetxController {
Future historyStatus() async {
var res = await UserHttp.historyStatus();
pauseStatus.value = res.data['data'];
- localCache.put(LocalCacheKey.historyStatus, res.data['data']);
+ localCache.put(LocalCacheKey.historyPause, res.data['data']);
}
// 清空观看历史
diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart
index 2bf06c3b..b3dd7f0f 100644
--- a/lib/pages/history/widgets/item.dart
+++ b/lib/pages/history/widgets/item.dart
@@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
+import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
@@ -147,23 +148,26 @@ class HistoryItem extends StatelessWidget {
if (!BusinessType
.hiddenDurationType.hiddenDurationType
.contains(videoItem.history.business))
- pBadge(
- videoItem.progress == -1
- ? '已看完'
- : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
- context,
- null,
- 6.0,
- 6.0,
- null,
- type: 'gray'),
+ PBadge(
+ text: videoItem.progress == -1
+ ? '已看完'
+ : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
+ right: 6.0,
+ bottom: 6.0,
+ type: 'gray',
+ ),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
- pBadge(videoItem.badge, context, 6.0, 6.0,
- null, null),
+ PBadge(
+ text: videoItem.badge,
+ top: 6.0,
+ right: 6.0,
+ bottom: null,
+ left: null,
+ ),
],
);
},
@@ -229,6 +233,7 @@ class VideoContent extends StatelessWidget {
],
),
Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
Utils.dateFormat(videoItem.viewAt!),
@@ -236,7 +241,46 @@ class VideoContent extends StatelessWidget {
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
- )
+ ),
+ if (videoItem.badge != '番剧' &&
+ !videoItem.tagName.contains('动画') &&
+ videoItem.history.business != 'live' &&
+ !videoItem.history.business.contains('article'))
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: PopupMenuButton(
+ padding: EdgeInsets.zero,
+ tooltip: '稍后再看',
+ icon: Icon(
+ Icons.more_vert_outlined,
+ color: Theme.of(context).colorScheme.outline,
+ size: 14,
+ ),
+ position: PopupMenuPosition.under,
+ // constraints: const BoxConstraints(maxHeight: 35),
+ onSelected: (String type) {},
+ itemBuilder: (BuildContext context) =>
+ >[
+ PopupMenuItem(
+ onTap: () async {
+ var res = await UserHttp.toViewLater(
+ bvid: videoItem.history.bvid);
+ SmartDialog.showToast(res['msg']);
+ },
+ value: 'pause',
+ height: 35,
+ child: const Row(
+ children: [
+ Icon(Icons.watch_later_outlined, size: 16),
+ SizedBox(width: 6),
+ Text('稍后再看', style: TextStyle(fontSize: 13))
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
],
),
],
diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart
index fe185738..980d7381 100644
--- a/lib/pages/home/controller.dart
+++ b/lib/pages/home/controller.dart
@@ -1,84 +1,54 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:pilipala/http/index.dart';
-import 'package:pilipala/pages/bangumi/index.dart';
-import 'package:pilipala/pages/hot/index.dart';
-import 'package:pilipala/pages/live/index.dart';
-import 'package:pilipala/pages/rcmd/index.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/models/common/tab_type.dart';
+import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
- List tabs = [
- {
- 'icon': const Icon(
- Icons.live_tv_outlined,
- size: 15,
- ),
- 'label': '直播',
- 'type': 'live'
- },
- {
- 'icon': const Icon(
- Icons.thumb_up_off_alt_outlined,
- size: 15,
- ),
- 'label': '推荐',
- 'type': 'rcm'
- },
- {
- 'icon': const Icon(
- Icons.whatshot_outlined,
- size: 15,
- ),
- 'label': '热门',
- 'type': 'hot'
- },
- {
- 'icon': const Icon(
- Icons.play_circle_outlined,
- size: 15,
- ),
- 'label': '番剧',
- 'type': 'bangumi'
- },
- ];
+ late List tabs;
int initialIndex = 1;
late TabController tabController;
- List ctrList = [
- Get.find,
- Get.find,
- Get.find,
- Get.find,
- ];
- RxString defaultSearch = '输入关键词搜索'.obs;
+ late List tabsCtrList;
+ late List tabsPageList;
+ Box user = GStrorage.user;
+ RxBool userLogin = false.obs;
+ RxString userFace = ''.obs;
@override
void onInit() {
super.onInit();
+
+ userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
+ userFace.value = user.get(UserBoxKey.userFace) ?? '';
+
+ // 进行tabs配置
+ tabs = tabsConfig;
+ tabsCtrList = tabsConfig.map((e) => e['ctr']).toList();
+ tabsPageList = tabsConfig.map((e) => e['page']).toList();
+
tabController = TabController(
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
- searchDefault();
}
void onRefresh() {
int index = tabController.index;
- var ctr = ctrList[index];
+ var ctr = tabsCtrList[index];
ctr().onRefresh();
}
void animateToTop() {
int index = tabController.index;
- var ctr = ctrList[index];
+ var ctr = tabsCtrList[index];
ctr().animateToTop();
}
- void searchDefault() async {
- var res = await Request().get(Api.searchDefault);
- if (res.data['code'] == 0) {
- defaultSearch.value = res.data['data']['name'];
- }
+ // 更新登录状态
+ void updateLoginStatus(val) {
+ userLogin.value = val ?? false;
+ userFace.value = user.get(UserBoxKey.userFace) ?? '';
}
}
diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart
index 6a71d664..9ad8a25f 100644
--- a/lib/pages/home/view.dart
+++ b/lib/pages/home/view.dart
@@ -1,16 +1,10 @@
-import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
-import 'package:pilipala/pages/bangumi/index.dart';
-import 'package:pilipala/pages/hot/index.dart';
-import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
-import 'package:pilipala/pages/rcmd/index.dart';
+import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
-import 'package:pilipala/utils/storage.dart';
import './controller.dart';
class HomePage extends StatefulWidget {
@@ -29,6 +23,19 @@ class _HomePageState extends State
@override
bool get wantKeepAlive => true;
+ showUserBottonSheet() {
+ feedBack();
+ showModalBottomSheet(
+ context: context,
+ builder: (_) => const SizedBox(
+ height: 450,
+ child: MinePage(),
+ ),
+ clipBehavior: Clip.hardEdge,
+ isScrollControlled: true,
+ );
+ }
+
@override
Widget build(BuildContext context) {
super.build(context);
@@ -38,73 +45,40 @@ class _HomePageState extends State
appBar: AppBar(toolbarHeight: 0, elevation: 0),
body: Column(
children: [
- CustomAppBar(stream: stream, ctr: _homeController),
- Container(
- padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
- child: Stack(
- children: [
- Align(
- alignment: Alignment.center,
- child: Theme(
- data: ThemeData(
- splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
- highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
- ),
- child: Padding(
- padding: const EdgeInsets.only(top: 2),
- child: TabBar(
- controller: _homeController.tabController,
- tabs: [
- for (var i in _homeController.tabs)
- // Tab(text: i['label'])
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 0, vertical: 11),
- child: Row(
- children: [
- i['icon'],
- const SizedBox(width: 4),
- Text(i['label'])
- ],
- ),
- ),
- ],
- isScrollable: true,
- indicatorWeight: 0,
- indicatorPadding: const EdgeInsets.symmetric(
- horizontal: 4, vertical: 5),
- indicator: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .primaryContainer
- .withOpacity(0.8),
- borderRadius:
- const BorderRadius.all(Radius.circular(20)),
- ),
- indicatorSize: TabBarIndicatorSize.tab,
- labelColor: Theme.of(context).colorScheme.primary,
- labelStyle: const TextStyle(fontSize: 13),
- dividerColor: Colors.transparent,
- unselectedLabelColor:
- Theme.of(context).colorScheme.outline,
- onTap: (value) =>
- {feedBack(), _homeController.initialIndex = value},
- ),
- ),
- ),
- ),
- ],
+ CustomAppBar(
+ stream: stream,
+ ctr: _homeController,
+ callback: showUserBottonSheet,
+ ),
+ const SizedBox(height: 8),
+ SizedBox(
+ width: double.infinity,
+ height: 42,
+ child: Align(
+ alignment: Alignment.center,
+ child: TabBar(
+ controller: _homeController.tabController,
+ tabs: [
+ for (var i in _homeController.tabs) Tab(text: i['label'])
+ ],
+ isScrollable: true,
+ dividerColor: Colors.transparent,
+ enableFeedback: true,
+ splashBorderRadius: BorderRadius.circular(10),
+ onTap: (value) {
+ feedBack();
+ if (_homeController.initialIndex == value) {
+ _homeController.tabsCtrList[value]().animateToTop();
+ }
+ _homeController.initialIndex = value;
+ },
+ ),
),
),
Expanded(
child: TabBarView(
controller: _homeController.tabController,
- children: const [
- LivePage(),
- RcmdPage(),
- HotPage(),
- BangumiPage(),
- ],
+ children: _homeController.tabsPageList,
),
),
],
@@ -116,13 +90,15 @@ class _HomePageState extends State
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final Stream? stream;
- final ctr;
+ final HomeController? ctr;
+ final Function? callback;
const CustomAppBar({
super.key,
this.height = kToolbarHeight,
this.stream,
this.ctr,
+ this.callback,
});
@override
@@ -130,125 +106,81 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
- Box user = GStrorage.user;
-
return StreamBuilder(
stream: stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
- return ClipRect(
- clipBehavior: Clip.hardEdge,
- child: AnimatedOpacity(
- opacity: snapshot.data ? 1 : 0,
- duration: const Duration(milliseconds: 300),
- child: AnimatedContainer(
- curve: Curves.linear,
- duration: const Duration(milliseconds: 300),
- height: snapshot.data ? 94 : MediaQuery.of(context).padding.top,
- child: Container(
- padding: EdgeInsets.only(
- left: 12,
- right: 12,
- bottom: 4,
- top: MediaQuery.of(context).padding.top,
- ),
- child: Row(
- children: [
- const Text(
- 'PLPL',
- style: TextStyle(
- height: 2.8,
- fontSize: 17,
- fontWeight: FontWeight.bold,
- fontFamily: 'Jura-Bold',
- ),
- ),
- const SizedBox(width: 10),
- Expanded(
- child: GestureDetector(
- onTap: () {
- Get.toNamed('/search', parameters: {
- 'hintText': ctr.defaultSearch.value
- });
- },
- child: Container(
- width: 250,
- height: 45,
- clipBehavior: Clip.hardEdge,
- padding: const EdgeInsets.only(left: 12, right: 22),
- decoration: BoxDecoration(
- borderRadius:
- const BorderRadius.all(Radius.circular(25)),
- color:
- Theme.of(context).colorScheme.onInverseSurface,
- ),
- child: Row(
+ return AnimatedOpacity(
+ opacity: snapshot.data ? 1 : 0,
+ duration: const Duration(milliseconds: 300),
+ child: AnimatedContainer(
+ curve: Curves.easeInOutCubicEmphasized,
+ duration: const Duration(milliseconds: 500),
+ height: snapshot.data
+ ? MediaQuery.of(context).padding.top + 52
+ : MediaQuery.of(context).padding.top,
+ child: Container(
+ padding: EdgeInsets.only(
+ left: 20,
+ right: 20,
+ bottom: 0,
+ top: MediaQuery.of(context).padding.top + 4,
+ ),
+ child: Row(
+ children: [
+ const Expanded(child: SearchPage()),
+ const SizedBox(width: 10),
+ Obx(
+ () => ctr!.userLogin.value
+ ? Stack(
children: [
- Icon(
- Icons.search_outlined,
- size: 23,
- color: Theme.of(context).colorScheme.outline,
+ NetworkImgLayer(
+ type: 'avatar',
+ width: 34,
+ height: 34,
+ src: ctr!.userFace.value,
),
- const SizedBox(width: 7),
- Expanded(
- child: Obx(
- () => Text(
- ctr.defaultSearch.value,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- color: Theme.of(context)
- .colorScheme
- .outline),
+ Positioned.fill(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => callback!(),
+ splashColor: Theme.of(context)
+ .colorScheme
+ .primaryContainer
+ .withOpacity(0.3),
+ borderRadius: const BorderRadius.all(
+ Radius.circular(50),
+ ),
),
),
- ),
+ )
],
+ )
+ : SizedBox(
+ width: 38,
+ height: 38,
+ child: IconButton(
+ style: ButtonStyle(
+ padding:
+ MaterialStateProperty.all(EdgeInsets.zero),
+ backgroundColor:
+ MaterialStateProperty.resolveWith((states) {
+ return Theme.of(context)
+ .colorScheme
+ .onInverseSurface;
+ }),
+ ),
+ onPressed: () => callback!(),
+ icon: Icon(
+ Icons.person_rounded,
+ size: 22,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
),
- ),
- ),
- ),
- const SizedBox(width: 12),
- if (user.get(UserBoxKey.userLogin) ?? false) ...[
- GestureDetector(
- onTap: () {
- feedBack();
- showModalBottomSheet(
- context: context,
- builder: (_) => const SizedBox(
- height: 450,
- child: MinePage(),
- ),
- clipBehavior: Clip.hardEdge,
- isScrollControlled: true,
- );
- },
- child: NetworkImgLayer(
- type: 'avatar',
- width: 34,
- height: 34,
- src: user.get(UserBoxKey.userFace),
- ),
- )
- ] else ...[
- IconButton(
- onPressed: () {
- feedBack();
- showModalBottomSheet(
- context: context,
- builder: (_) => const SizedBox(
- height: 450,
- child: MinePage(),
- ),
- clipBehavior: Clip.hardEdge,
- isScrollControlled: true,
- );
- },
- icon: const Icon(CupertinoIcons.person, size: 22),
- )
- ],
- ],
- ),
+ ),
+ ],
),
),
),
diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart
index aa990326..191ebc0e 100644
--- a/lib/pages/hot/view.dart
+++ b/lib/pages/hot/view.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
+import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
@@ -59,52 +60,57 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin {
super.build(context);
return Scaffold(
body: RefreshIndicator(
- displacement: kToolbarHeight + MediaQuery.of(context).padding.top,
onRefresh: () async {
return await _hotController.onRefresh();
},
child: CustomScrollView(
controller: _hotController.scrollController,
slivers: [
- FutureBuilder(
- future: _futureBuilderFuture,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- Map data = snapshot.data as Map;
- if (data['status']) {
- return Obx(
- () => SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return VideoCardH(
- videoItem: _hotController.videoList[index],
- longPress: () {
- _hotController.popupDialog = _createPopupDialog(
- _hotController.videoList[index]);
- Overlay.of(context)
- .insert(_hotController.popupDialog!);
- },
- longPressEnd: () {
- _hotController.popupDialog?.remove();
- },
- );
- }, childCount: _hotController.videoList.length),
- ),
- );
+ SliverPadding(
+ // 单列布局 EdgeInsets.zero
+ padding:
+ const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0),
+ sliver: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return Obx(
+ () => SliverList(
+ delegate:
+ SliverChildBuilderDelegate((context, index) {
+ return VideoCardH(
+ videoItem: _hotController.videoList[index],
+ longPress: () {
+ _hotController.popupDialog = _createPopupDialog(
+ _hotController.videoList[index]);
+ Overlay.of(context)
+ .insert(_hotController.popupDialog!);
+ },
+ longPressEnd: () {
+ _hotController.popupDialog?.remove();
+ },
+ );
+ }, childCount: _hotController.videoList.length),
+ ),
+ );
+ } else {
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () => setState(() {}),
+ );
+ }
} else {
- return HttpError(
- errMsg: data['msg'],
- fn: () => setState(() {}),
+ // 骨架屏
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ return const VideoCardHSkeleton();
+ }, childCount: 10),
);
}
- } else {
- // 骨架屏
- return SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return const VideoCardHSkeleton();
- }, childCount: 10),
- );
- }
- },
+ },
+ ),
),
SliverToBoxAdapter(
child: SizedBox(
@@ -120,7 +126,9 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin {
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
- child: OverlayPop(videoItem: videoItem),
+ closeFn: _hotController.popupDialog?.remove,
+ child: OverlayPop(
+ videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}
diff --git a/lib/pages/later/controller.dart b/lib/pages/later/controller.dart
index 960e7891..6de9254c 100644
--- a/lib/pages/later/controller.dart
+++ b/lib/pages/later/controller.dart
@@ -6,15 +6,20 @@ import 'package:pilipala/models/model_hot_video_item.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
- RxList laterList = [HotVideoItemModel()].obs;
+ RxList laterList = [].obs;
int count = 0;
+ RxBool isLoading = false.obs;
Future queryLaterList() async {
+ isLoading.value = true;
var res = await UserHttp.seeYouLater();
if (res['status']) {
- laterList.value = res['data']['list'];
count = res['data']['count'];
+ if (count > 0) {
+ laterList.value = res['data']['list'];
+ }
}
+ isLoading.value = false;
return res;
}
@@ -47,4 +52,34 @@ class LaterController extends GetxController {
},
);
}
+
+ // 一键清空
+ Future toViewClear() async {
+ SmartDialog.show(
+ useSystem: true,
+ animationType: SmartAnimationType.centerFade_otherSlide,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('清空确认'),
+ content: const Text('确定要清空你的稍后再看列表吗?'),
+ actions: [
+ TextButton(
+ onPressed: () => SmartDialog.dismiss(),
+ child: const Text('取消')),
+ TextButton(
+ onPressed: () async {
+ var res = await UserHttp.toViewClear();
+ if (res['status']) {
+ laterList.clear();
+ }
+ SmartDialog.dismiss();
+ SmartDialog.showToast(res['msg']);
+ },
+ child: const Text('确认'),
+ )
+ ],
+ );
+ },
+ );
+ }
}
diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart
index a3389334..fa524157 100644
--- a/lib/pages/later/view.dart
+++ b/lib/pages/later/view.dart
@@ -29,25 +29,38 @@ class _LaterPageState extends State {
titleSpacing: 0,
centerTitle: false,
title: Obx(
- () => Text(
- '稍后再看 (${_laterController.laterList.length}/100)',
- style: Theme.of(context).textTheme.titleMedium,
- ),
+ () => _laterController.laterList.isNotEmpty
+ ? Text(
+ '稍后再看 (${_laterController.laterList.length}/100)',
+ style: Theme.of(context).textTheme.titleMedium,
+ )
+ : Text(
+ '稍后再看',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
),
actions: [
- TextButton(
- onPressed: () => _laterController.toViewDel(),
- child: const Text('移除已看'),
+ Obx(
+ () => _laterController.laterList.isNotEmpty
+ ? TextButton(
+ onPressed: () => _laterController.toViewDel(),
+ child: const Text('移除已看'),
+ )
+ : const SizedBox(),
+ ),
+ Obx(
+ () => _laterController.laterList.isNotEmpty
+ ? IconButton(
+ tooltip: '一键清空',
+ onPressed: () => _laterController.toViewClear(),
+ icon: Icon(
+ Icons.clear_all_outlined,
+ size: 21,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ )
+ : const SizedBox(),
),
- // IconButton(
- // tooltip: '一键清空',
- // onPressed: () {},
- // icon: Icon(
- // Icons.clear_all_outlined,
- // size: 21,
- // color: Theme.of(context).colorScheme.primary,
- // ),
- // ),
const SizedBox(width: 8),
],
),
@@ -61,18 +74,31 @@ class _LaterPageState extends State {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
- () => SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return VideoCardH(
- videoItem: _laterController.laterList[index],
- );
- }, childCount: _laterController.laterList.length),
- ),
+ () => _laterController.laterList.isNotEmpty &&
+ !_laterController.isLoading.value
+ ? SliverList(
+ delegate:
+ SliverChildBuilderDelegate((context, index) {
+ return VideoCardH(
+ videoItem: _laterController.laterList[index],
+ source: 'later',
+ );
+ }, childCount: _laterController.laterList.length),
+ )
+ : SliverToBoxAdapter(
+ child: Center(
+ child: Text(_laterController.isLoading.value
+ ? '加载中'
+ : '没有数据'),
+ ),
+ ),
);
} else {
return HttpError(
errMsg: data['msg'],
- fn: () => setState(() {}),
+ fn: () => setState(() {
+ _futureBuilderFuture = _laterController.queryLaterList();
+ }),
);
}
} else {
@@ -80,7 +106,7 @@ class _LaterPageState extends State {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
- }, childCount: 5),
+ }, childCount: 10),
);
}
},
diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart
index 3e26bafc..e07950ae 100644
--- a/lib/pages/live/view.dart
+++ b/lib/pages/live/view.dart
@@ -22,10 +22,12 @@ class LivePage extends StatefulWidget {
class _LivePageState extends State {
final LiveController _liveController = Get.put(LiveController());
+ late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
+ _futureBuilderFuture = _liveController.queryLiveList('init');
ScrollController scrollController = _liveController.scrollController;
StreamController mainStream =
Get.find().bottomBarStream;
@@ -52,47 +54,58 @@ class _LivePageState extends State {
@override
Widget build(BuildContext context) {
- return RefreshIndicator(
- onRefresh: () async {
- return await _liveController.onRefresh();
- },
- child: CustomScrollView(
- controller: _liveController.scrollController,
- slivers: [
- SliverPadding(
- // 单列布局 EdgeInsets.zero
- padding: const EdgeInsets.fromLTRB(
- StyleString.safeSpace, 0, StyleString.safeSpace, 0),
- sliver: FutureBuilder(
- future: _liveController.queryLiveList('init'),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- Map data = snapshot.data as Map;
- if (data['status']) {
- return Obx(() =>
- contentGrid(_liveController, _liveController.liveList));
+ return Container(
+ clipBehavior: Clip.hardEdge,
+ margin: const EdgeInsets.only(
+ left: StyleString.safeSpace, right: StyleString.safeSpace),
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(StyleString.imgRadius),
+ ),
+ child: RefreshIndicator(
+ onRefresh: () async {
+ return await _liveController.onRefresh();
+ },
+ child: CustomScrollView(
+ controller: _liveController.scrollController,
+ slivers: [
+ SliverPadding(
+ // 单列布局 EdgeInsets.zero
+ padding:
+ const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
+ sliver: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return SliverLayoutBuilder(
+ builder: (context, boxConstraints) {
+ return Obx(() => contentGrid(
+ _liveController, _liveController.liveList));
+ });
+ } else {
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () => {},
+ );
+ }
} else {
- return HttpError(
- errMsg: data['msg'],
- fn: () => {},
- );
+ // 缓存数据
+ if (_liveController.liveList.length > 1) {
+ return contentGrid(
+ _liveController, _liveController.liveList);
+ }
+ // 骨架屏
+ else {
+ return contentGrid(_liveController, []);
+ }
}
- } else {
- // 缓存数据
- if (_liveController.liveList.length > 1) {
- return contentGrid(
- _liveController, _liveController.liveList);
- }
- // 骨架屏
- else {
- return contentGrid(_liveController, []);
- }
- }
- },
+ },
+ ),
),
- ),
- const LoadingMore()
- ],
+ const LoadingMore()
+ ],
+ ),
),
);
}
@@ -100,22 +113,32 @@ class _LivePageState extends State {
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
- child: OverlayPop(videoItem: liveItem),
+ closeFn: _liveController.popupDialog?.remove,
+ child: OverlayPop(
+ videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, liveList) {
+ double maxWidth = Get.size.width;
+ int baseWidth = 500;
+ int step = 300;
+ int crossAxisCount =
+ maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
+ if (maxWidth < 300) {
+ crossAxisCount = 1;
+ }
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
- mainAxisSpacing: StyleString.cardSpace + 2,
+ mainAxisSpacing: StyleString.cardSpace + 4,
// 列间距
- crossAxisSpacing: StyleString.cardSpace + 3,
+ crossAxisSpacing: StyleString.cardSpace + 4,
// 列数
- crossAxisCount: ctr.crossAxisCount,
+ crossAxisCount: crossAxisCount,
mainAxisExtent:
- Get.size.width / ctr.crossAxisCount / StyleString.aspectRatio + 60,
+ Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart
index 692aa039..f676a877 100644
--- a/lib/pages/live/widgets/live_item.dart
+++ b/lib/pages/live/widgets/live_item.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/models/live/item.dart';
-import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@@ -35,11 +35,11 @@ class LiveCardV extends StatelessWidget {
longPress!();
}
},
- onLongPressEnd: (details) {
- if (longPressEnd != null) {
- longPressEnd!();
- }
- },
+ // onLongPressEnd: (details) {
+ // if (longPressEnd != null) {
+ // longPressEnd!();
+ // }
+ // },
child: InkWell(
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
@@ -103,7 +103,7 @@ class LiveContent extends StatelessWidget {
return Expanded(
child: Padding(
// 多列
- padding: const EdgeInsets.fromLTRB(4, 5, 6, 6),
+ padding: const EdgeInsets.fromLTRB(4, 8, 0, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -121,7 +121,12 @@ class LiveContent extends StatelessWidget {
),
Row(
children: [
- const UpTag(),
+ const PBadge(
+ text: 'UP',
+ size: 'small',
+ stack: 'normal',
+ fs: 9,
+ ),
Expanded(
child: Text(
liveItem.uname,
@@ -154,7 +159,7 @@ class VideoStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- height: 45,
+ height: 50,
padding: const EdgeInsets.only(top: 22, left: 10, right: 10),
decoration: const BoxDecoration(
gradient: LinearGradient(
diff --git a/lib/pages/liveRoom/controller.dart b/lib/pages/liveRoom/controller.dart
index 51ffd7a2..18b39756 100644
--- a/lib/pages/liveRoom/controller.dart
+++ b/lib/pages/liveRoom/controller.dart
@@ -1,4 +1,3 @@
-import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart';
@@ -14,7 +13,7 @@ class LiveRoomController extends GetxController {
// 静音状态
RxBool volumeOff = false.obs;
PlPlayerController plPlayerController =
- PlPlayerController(controlsEnabled: false);
+ PlPlayerController.getInstance(videoType: 'live');
// MeeduPlayerController meeduPlayerController = MeeduPlayerController(
// colorTheme: Theme.of(Get.context!).colorScheme.primary,
@@ -31,6 +30,9 @@ class LiveRoomController extends GetxController {
liveItem = Get.arguments['liveItem'];
heroTag = Get.arguments['heroTag'] ?? '';
if (liveItem.pic != null && liveItem.pic != '') {
+ cover = liveItem.pic;
+ }
+ if (liveItem.cover != null && liveItem.cover != '') {
cover = liveItem.cover;
}
}
@@ -72,12 +74,10 @@ class LiveRoomController extends GetxController {
if (value == 0) {
// 设置音量
volumeOff.value = false;
- // meeduPlayerController.setVolume(volume);
} else {
// 取消音量
volume = value;
volumeOff.value = true;
- // meeduPlayerController.setVolume(0);
}
}
}
diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart
index 1fe31c9e..0c650126 100644
--- a/lib/pages/liveRoom/view.dart
+++ b/lib/pages/liveRoom/view.dart
@@ -74,13 +74,13 @@ class _LiveRoomPageState extends State {
),
],
),
- actions: [
- SizedBox(
- height: 34,
- child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
- ),
- const SizedBox(width: 12),
- ],
+ // actions: [
+ // SizedBox(
+ // height: 34,
+ // child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
+ // ),
+ // const SizedBox(width: 12),
+ // ],
),
body: Column(
children: [
@@ -112,68 +112,67 @@ class _LiveRoomPageState extends State {
],
),
),
- if (_liveRoomController.liveItem.watchedShow != null)
- Container(
- height: 45,
- padding: const EdgeInsets.only(left: 12, right: 12),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- border: Border(
- bottom: BorderSide(
- color: Theme.of(context).dividerColor.withOpacity(0.1)),
- ),
- ),
- child: Row(children: [
- SizedBox(
- width: 38,
- height: 38,
- child: IconButton(
- onPressed: () {},
- icon: const Icon(
- Icons.subtitles_outlined,
- size: 21,
- ),
- ),
- ),
- const Spacer(),
- SizedBox(
- width: 38,
- height: 38,
- child: IconButton(
- onPressed: () {},
- icon: const Icon(
- Icons.hd_outlined,
- size: 20,
- ),
- ),
- ),
- SizedBox(
- width: 38,
- height: 38,
- child: IconButton(
- onPressed: () => _liveRoomController
- .setVolumn(plPlayerController!.volume.value),
- icon: Obx(() => Icon(
- _liveRoomController.volumeOff.value
- ? Icons.volume_off_outlined
- : Icons.volume_up_outlined,
- size: 21,
- )),
- ),
- ),
- SizedBox(
- width: 38,
- height: 38,
- child: IconButton(
- onPressed: () => {},
- // plPlayerController!.goToFullscreen(context),
- icon: const Icon(
- Icons.fullscreen,
- ),
- ),
- ),
- ]),
- ),
+ // Container(
+ // height: 45,
+ // padding: const EdgeInsets.only(left: 12, right: 12),
+ // decoration: BoxDecoration(
+ // color: Theme.of(context).colorScheme.background,
+ // border: Border(
+ // bottom: BorderSide(
+ // color: Theme.of(context).dividerColor.withOpacity(0.1)),
+ // ),
+ // ),
+ // child: Row(children: [
+ // SizedBox(
+ // width: 38,
+ // height: 38,
+ // child: IconButton(
+ // onPressed: () {},
+ // icon: const Icon(
+ // Icons.subtitles_outlined,
+ // size: 21,
+ // ),
+ // ),
+ // ),
+ // const Spacer(),
+ // SizedBox(
+ // width: 38,
+ // height: 38,
+ // child: IconButton(
+ // onPressed: () {},
+ // icon: const Icon(
+ // Icons.hd_outlined,
+ // size: 20,
+ // ),
+ // ),
+ // ),
+ // SizedBox(
+ // width: 38,
+ // height: 38,
+ // child: IconButton(
+ // onPressed: () => _liveRoomController
+ // .setVolumn(plPlayerController!.volume.value),
+ // icon: Obx(() => Icon(
+ // _liveRoomController.volumeOff.value
+ // ? Icons.volume_off_outlined
+ // : Icons.volume_up_outlined,
+ // size: 21,
+ // )),
+ // ),
+ // ),
+ // SizedBox(
+ // width: 38,
+ // height: 38,
+ // child: IconButton(
+ // onPressed: () => {},
+ // // plPlayerController!.goToFullscreen(context),
+ // icon: const Icon(
+ // Icons.fullscreen,
+ // ),
+ // ),
+ // ),
+ // ]),
+ // ),
],
),
);
diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart
index 122edb87..1d2385e0 100644
--- a/lib/pages/main/controller.dart
+++ b/lib/pages/main/controller.dart
@@ -15,23 +15,35 @@ class MainController extends GetxController {
RxList navigationBars = [
{
'icon': const Icon(
- Icons.motion_photos_on_outlined,
+ Icons.favorite_outline,
size: 21,
),
- 'label': "推荐",
+ 'selectIcon': const Icon(
+ Icons.favorite,
+ size: 21,
+ ),
+ 'label': "首页",
},
{
'icon': const Icon(
- Icons.bolt,
+ Icons.motion_photos_on_outlined,
+ size: 21,
+ ),
+ 'selectIcon': const Icon(
+ Icons.motion_photos_on,
size: 21,
),
'label': "动态",
},
{
'icon': const Icon(
- Icons.folder_open_outlined,
+ Icons.folder_outlined,
size: 20,
),
+ 'selectIcon': const Icon(
+ Icons.folder,
+ size: 21,
+ ),
'label': "媒体库",
}
].obs;
diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart
index 67d49c82..b771ab0f 100644
--- a/lib/pages/main/view.dart
+++ b/lib/pages/main/view.dart
@@ -1,11 +1,10 @@
-import 'dart:async';
-
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
+import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@@ -95,6 +94,13 @@ class _MainAppState extends State with SingleTickerProviderStateMixin {
}
}
+ @override
+ void dispose() async {
+ await GStrorage.close();
+ EventBus().off(EventName.loginEvent);
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
Box localCache = GStrorage.localCache;
@@ -135,21 +141,17 @@ class _MainAppState extends State with SingleTickerProviderStateMixin {
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
- curve: Curves.linear,
- duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOutCubicEmphasized,
+ duration: const Duration(milliseconds: 1000),
offset: Offset(0, snapshot.data ? 0 : 1),
- child: BottomNavigationBar(
- currentIndex: selectedIndex,
- // type: BottomNavigationBarType.shifting,
- selectedItemColor: Theme.of(context).colorScheme.primary,
- unselectedItemColor:
- Theme.of(context).colorScheme.onSurfaceVariant,
- selectedFontSize: 12.4,
- onTap: (value) => setIndex(value),
- items: [
+ child: NavigationBar(
+ onDestinationSelected: (value) => setIndex(value),
+ selectedIndex: selectedIndex,
+ destinations: [
..._mainController.navigationBars.map((e) {
- return BottomNavigationBarItem(
+ return NavigationDestination(
icon: e['icon'],
+ selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart
index e1794d97..13ab30bf 100644
--- a/lib/pages/media/view.dart
+++ b/lib/pages/media/view.dart
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart';
+import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget {
@@ -14,13 +15,29 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State
with AutomaticKeepAliveClientMixin {
+ late MediaController mediaController;
+ late Future _futureBuilderFuture;
+ EventBus eventBus = EventBus();
+
@override
bool get wantKeepAlive => true;
+ @override
+ void initState() {
+ super.initState();
+ mediaController = Get.put(MediaController());
+ _futureBuilderFuture = mediaController.queryFavFolder();
+ eventBus.on(EventName.loginEvent, (args) {
+ mediaController.userLogin.value = args['status'];
+ setState(() {
+ _futureBuilderFuture = mediaController.queryFavFolder();
+ });
+ });
+ }
+
@override
Widget build(BuildContext context) {
super.build(context);
- final MediaController mediaController = Get.put(MediaController());
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
@@ -59,7 +76,7 @@ class _MediaPageState extends State
),
),
],
- Obx(() => mediaController.userLogin.value == true
+ Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
],
@@ -107,7 +124,11 @@ class _MediaPageState extends State
),
),
trailing: IconButton(
- onPressed: () => mediaController.queryFavFolder(),
+ onPressed: () {
+ setState(() {
+ _futureBuilderFuture = mediaController.queryFavFolder();
+ });
+ },
icon: const Icon(
Icons.refresh,
size: 20,
@@ -119,7 +140,7 @@ class _MediaPageState extends State
width: double.infinity,
height: 170,
child: FutureBuilder(
- future: mediaController.queryFavFolder(),
+ future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
diff --git a/lib/pages/member/archive/view.dart b/lib/pages/member/archive/view.dart
index ba223ab1..48416774 100644
--- a/lib/pages/member/archive/view.dart
+++ b/lib/pages/member/archive/view.dart
@@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
-import 'package:pilipala/common/widgets/pull_to_refresh_header.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/pages/member/archive/index.dart';
@@ -152,7 +151,6 @@ class LoadMoreListSource extends LoadingMoreBase {
if (res['status']) {
addAll(res['data'].list.vlist);
}
- print(length);
if (length < res['data'].page['count']) {
isSuccess = true;
} else {
diff --git a/lib/pages/member/dynamic/view.dart b/lib/pages/member/dynamic/view.dart
index a1d6e8d9..867970ea 100644
--- a/lib/pages/member/dynamic/view.dart
+++ b/lib/pages/member/dynamic/view.dart
@@ -120,7 +120,7 @@ class _MemberDynamicPanelState extends State
class LoadMoreListSource extends LoadingMoreBase {
final _dynamicController = Get.put(MemberDynamicPanelController());
- // @override
+ @override
Future loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await _dynamicController.getMemberDynamic();
diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart
index 6d515243..e4118154 100644
--- a/lib/pages/mine/controller.dart
+++ b/lib/pages/mine/controller.dart
@@ -1,6 +1,8 @@
+import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
+import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/storage.dart';
@@ -10,9 +12,11 @@ class MineController extends GetxController {
Rx userInfo = UserInfoData().obs;
// 用户状态 动态、关注、粉丝
Rx userStat = UserStat().obs;
- Box user = GStrorage.user;
RxBool userLogin = false.obs;
+ Box user = GStrorage.user;
+ Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
+ Rx themeType = ThemeType.system.obs;
@override
onInit() {
@@ -21,13 +25,13 @@ class MineController extends GetxController {
if (userInfoCache.get('userInfoCache') != null) {
userInfo.value = userInfoCache.get('userInfoCache');
}
+
+ themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
+ defaultValue: ThemeType.system.code)];
}
onLogin() async {
if (!userLogin.value) {
- /// TODO
- Get.back();
- await Future.delayed(const Duration(milliseconds: 150));
Get.toNamed(
'/webview',
parameters: {
@@ -90,4 +94,31 @@ class MineController extends GetxController {
userLogin.value = false;
// Get.find().resetLast();
}
+
+ onChangeTheme() {
+ Brightness currentBrightness =
+ MediaQuery.of(Get.context!).platformBrightness;
+ ThemeType currentTheme = themeType.value;
+ switch (currentTheme) {
+ case ThemeType.dark:
+ setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
+ themeType.value = ThemeType.light;
+ break;
+ case ThemeType.light:
+ setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
+ themeType.value = ThemeType.dark;
+ break;
+ case ThemeType.system:
+ // 判断当前的颜色模式
+ if (currentBrightness == Brightness.light) {
+ setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
+ themeType.value = ThemeType.dark;
+ } else {
+ setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
+ themeType.value = ThemeType.light;
+ }
+ break;
+ }
+ Get.forceAppUpdate();
+ }
}
diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart
index 3cf560dc..e084d96b 100644
--- a/lib/pages/mine/view.dart
+++ b/lib/pages/mine/view.dart
@@ -5,15 +5,38 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/models/common/theme_type.dart';
+import 'package:pilipala/utils/event_bus.dart';
import 'controller.dart';
-class MinePage extends StatelessWidget {
+class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
- Widget build(BuildContext context) {
- final MineController mineController = Get.put(MineController());
+ State createState() => _MinePageState();
+}
+class _MinePageState extends State {
+ final MineController mineController = Get.put(MineController());
+ late Future _futureBuilderFuture;
+ EventBus eventBus = EventBus();
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = mineController.queryUserInfo();
+ eventBus.on(EventName.loginEvent, (args) {
+ mineController.userLogin.value = args['status'];
+ if (mounted) {
+ setState(() {
+ _futureBuilderFuture = mineController.queryUserInfo();
+ });
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
@@ -21,16 +44,23 @@ class MinePage extends StatelessWidget {
elevation: 0,
toolbarHeight: kTextTabBarHeight + 20,
backgroundColor: Colors.transparent,
- title: null,
+ centerTitle: false,
+ title: const Text(
+ 'PLPL',
+ style: TextStyle(
+ height: 2.8,
+ fontSize: 17,
+ fontWeight: FontWeight.bold,
+ fontFamily: 'Jura-Bold',
+ ),
+ ),
actions: [
IconButton(
- onPressed: () {
- Get.changeThemeMode(ThemeMode.dark);
- },
+ onPressed: () => mineController.onChangeTheme(),
icon: Icon(
- Get.theme == ThemeData.light()
- ? CupertinoIcons.moon
- : CupertinoIcons.sun_max,
+ mineController.themeType.value == ThemeType.dark
+ ? CupertinoIcons.sun_max
+ : CupertinoIcons.moon,
size: 22,
),
),
@@ -53,7 +83,7 @@ class MinePage extends StatelessWidget {
children: [
const SizedBox(height: 10),
FutureBuilder(
- future: mineController.queryUserInfo(),
+ future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@@ -93,7 +123,7 @@ class MinePage extends StatelessWidget {
src: _mineController.userInfo.value.face,
width: 85,
height: 85)
- : Image.asset('assets/images/loading.png'),
+ : Image.asset('assets/images/noface.jpeg'),
),
),
),
diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart
index 16fb0def..610a3ae2 100644
--- a/lib/pages/preview/view.dart
+++ b/lib/pages/preview/view.dart
@@ -132,6 +132,7 @@ class _ImagePreviewState extends State
_doubleClickAnimationController.forward();
},
+ // ignore: body_might_complete_normally_nullable
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart
index f8461070..df7aa2e7 100644
--- a/lib/pages/rcmd/controller.dart
+++ b/lib/pages/rcmd/controller.dart
@@ -2,17 +2,14 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/video.dart';
-import 'package:pilipala/models/model_rec_video_item.dart';
+import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController();
- int count = 12;
- int _currentPage = 1;
- int crossAxisCount = 2;
- RxList videoList = [RecVideoItemModel()].obs;
- bool isLoadingMore = false;
- bool flag = false;
+ int _currentPage = 0;
+ RxList videoList = [].obs;
+ bool isLoadingMore = true;
OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
@@ -21,7 +18,7 @@ class RcmdController extends GetxController {
super.onInit();
if (recVideo.get('cacheList') != null &&
recVideo.get('cacheList').isNotEmpty) {
- List list = [];
+ List list = [];
for (var i in recVideo.get('cacheList')) {
list.add(i);
}
@@ -31,13 +28,18 @@ class RcmdController extends GetxController {
// 获取推荐
Future queryRcmdFeed(type) async {
- var res = await VideoHttp.rcmdVideoList(
- ps: count,
+ if (isLoadingMore == false) {
+ return;
+ }
+ if (type == 'onRefresh') {
+ _currentPage = 0;
+ }
+ var res = await VideoHttp.rcmdVideoListApp(
freshIdx: _currentPage,
);
if (res['status']) {
if (type == 'init') {
- if (videoList.length > 1) {
+ if (videoList.isNotEmpty) {
videoList.addAll(res['data']);
} else {
videoList.value = res['data'];
@@ -56,6 +58,7 @@ class RcmdController extends GetxController {
// 下拉刷新
Future onRefresh() async {
+ isLoadingMore = true;
queryRcmdFeed('onRefresh');
}
diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart
index 1b22a2db..6cead2df 100644
--- a/lib/pages/rcmd/view.dart
+++ b/lib/pages/rcmd/view.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -23,6 +24,7 @@ class RcmdPage extends StatefulWidget {
class _RcmdPageState extends State
with AutomaticKeepAliveClientMixin {
final RcmdController _rcmdController = Get.put(RcmdController());
+ late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@@ -30,6 +32,7 @@ class _RcmdPageState extends State
@override
void initState() {
super.initState();
+ _futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController;
StreamController mainStream =
Get.find().bottomBarStream;
@@ -39,7 +42,9 @@ class _RcmdPageState extends State
scrollController.position.maxScrollExtent - 200) {
if (!_rcmdController.isLoadingMore) {
_rcmdController.isLoadingMore = true;
- _rcmdController.onLoad();
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ _rcmdController.onLoad();
+ });
}
}
@@ -57,78 +62,103 @@ class _RcmdPageState extends State
@override
Widget build(BuildContext context) {
super.build(context);
- return RefreshIndicator(
- onRefresh: () async {
- return await _rcmdController.onRefresh();
- },
- child: CustomScrollView(
- controller: _rcmdController.scrollController,
- slivers: [
- SliverPadding(
- // 单列布局 EdgeInsets.zero
- padding: _rcmdController.crossAxisCount == 1
- ? EdgeInsets.zero
- : const EdgeInsets.fromLTRB(
- StyleString.safeSpace, 0, StyleString.safeSpace, 0),
- sliver: FutureBuilder(
- future: _rcmdController.queryRcmdFeed('init'),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- Map data = snapshot.data as Map;
- if (data['status']) {
- return Obx(() => contentGrid(
- _rcmdController, _rcmdController.videoList));
+ return Container(
+ clipBehavior: Clip.hardEdge,
+ margin: const EdgeInsets.only(
+ left: StyleString.safeSpace, right: StyleString.safeSpace),
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(StyleString.imgRadius),
+ ),
+ child: RefreshIndicator(
+ onRefresh: () async {
+ return await _rcmdController.onRefresh();
+ },
+ child: CustomScrollView(
+ controller: _rcmdController.scrollController,
+ slivers: [
+ SliverPadding(
+ padding:
+ const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
+ sliver: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return Platform.isAndroid || Platform.isIOS
+ ? Obx(() => contentGrid(
+ _rcmdController, _rcmdController.videoList))
+ : SliverLayoutBuilder(
+ builder: (context, boxConstraints) {
+ return Obx(() => contentGrid(
+ _rcmdController, _rcmdController.videoList));
+ });
+ } else {
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () {
+ setState(() {
+ _futureBuilderFuture =
+ _rcmdController.queryRcmdFeed('init');
+ });
+ },
+ );
+ }
} else {
- return HttpError(
- errMsg: data['msg'],
- fn: () => {},
- );
+ // 缓存数据
+ if (_rcmdController.videoList.isNotEmpty) {
+ return contentGrid(
+ _rcmdController, _rcmdController.videoList);
+ }
+ // 骨架屏
+ else {
+ return contentGrid(_rcmdController, []);
+ }
}
- } else {
- // 缓存数据
- if (_rcmdController.videoList.length > 1) {
- return contentGrid(
- _rcmdController, _rcmdController.videoList);
- }
- // 骨架屏
- else {
- return contentGrid(_rcmdController, []);
- }
- }
- },
+ },
+ ),
),
- ),
- const LoadingMore()
- ],
+ const LoadingMore()
+ ],
+ ),
),
);
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
- builder: (context) => AnimatedDialog(
- child: OverlayPop(videoItem: videoItem),
- ));
+ builder: (context) => AnimatedDialog(
+ closeFn: _rcmdController.popupDialog?.remove,
+ child: OverlayPop(
+ videoItem: videoItem, closeFn: _rcmdController.popupDialog?.remove),
+ ),
+ );
}
Widget contentGrid(ctr, videoList) {
+ double maxWidth = Get.size.width;
+ int baseWidth = 500;
+ int step = 300;
+ int crossAxisCount =
+ maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
+ if (maxWidth < 300) {
+ crossAxisCount = 1;
+ }
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
- mainAxisSpacing: StyleString.cardSpace + 2,
+ mainAxisSpacing: StyleString.cardSpace + 4,
// 列间距
- crossAxisSpacing: StyleString.cardSpace + 3,
+ crossAxisSpacing: StyleString.cardSpace + 4,
// 列数
- crossAxisCount: ctr.crossAxisCount,
+ crossAxisCount: crossAxisCount,
mainAxisExtent:
- Get.size.width / ctr.crossAxisCount / StyleString.aspectRatio + 60,
+ Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return videoList!.isNotEmpty
- ?
- // VideoCardV(videoItem: videoList![index])
- VideoCardV(
+ ? VideoCardV(
videoItem: videoList[index],
longPress: () {
_rcmdController.popupDialog =
diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart
index 5183c635..9b3f7dad 100644
--- a/lib/pages/search/controller.dart
+++ b/lib/pages/search/controller.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
+import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
@@ -20,10 +21,12 @@ class SSearchController extends GetxController {
final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
String hintText = '搜索';
+ RxString defaultSearch = '输入关键词搜索'.obs;
@override
void onInit() {
super.onInit();
+ searchDefault();
if (hotKeyword.get('cacheList') != null &&
hotKeyword.get('cacheList').isNotEmpty) {
List list = [];
@@ -56,7 +59,7 @@ class SSearchController extends GetxController {
}
void onClear() {
- if (searchKeyWord.value.isNotEmpty) {
+ if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
@@ -121,4 +124,12 @@ class SSearchController extends GetxController {
historyList.refresh();
histiryWord.put('cacheList', []);
}
+
+ void searchDefault() async {
+ var res = await Request().get(Api.searchDefault);
+ if (res.data['code'] == 0) {
+ searchKeyWord.value =
+ hintText = defaultSearch.value = res.data['data']['name'];
+ }
+ }
}
diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart
index 4a1f809d..d4190e47 100644
--- a/lib/pages/search/view.dart
+++ b/lib/pages/search/view.dart
@@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:animations/animations.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'controller.dart';
@@ -41,61 +42,117 @@ class _SearchPageState extends State with RouteAware {
@override
Widget build(BuildContext context) {
- return Scaffold(
- resizeToAvoidBottomInset: false,
- appBar: AppBar(
- shape: Border(
- bottom: BorderSide(
- color: Theme.of(context).dividerColor.withOpacity(0.08),
- width: 1,
+ return OpenContainer(
+ closedElevation: 0,
+ openElevation: 0,
+ openColor: Theme.of(context).colorScheme.background,
+ middleColor: Theme.of(context).colorScheme.background,
+ closedColor: Theme.of(context).colorScheme.background,
+ closedShape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(30.0))),
+ openShape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(30.0))),
+ closedBuilder: (BuildContext context, VoidCallback openContainer) {
+ return Container(
+ width: 250,
+ height: 44,
+ clipBehavior: Clip.hardEdge,
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(25)),
),
- ),
- titleSpacing: 0,
- actions: [
- Hero(
- tag: 'searchTag',
- child: IconButton(
- onPressed: () => _searchController.submit(),
- icon: const Icon(CupertinoIcons.search, size: 22)),
- ),
- const SizedBox(width: 10)
- ],
- title: Obx(
- () => TextField(
- autofocus: true,
- focusNode: _searchController.searchFocusNode,
- controller: _searchController.controller.value,
- textInputAction: TextInputAction.search,
- onChanged: (value) => _searchController.onChange(value),
- decoration: InputDecoration(
- hintText: _searchController.hintText,
- border: InputBorder.none,
- suffixIcon: IconButton(
- icon: Icon(
- Icons.clear,
- size: 22,
- color: Theme.of(context).colorScheme.outline,
- ),
- onPressed: () => _searchController.onClear(),
+ child: Material(
+ color:
+ Theme.of(context).colorScheme.secondaryContainer.withAlpha(115),
+ child: InkWell(
+ splashColor: Theme.of(context)
+ .colorScheme
+ .primaryContainer
+ .withOpacity(0.3),
+ onTap: openContainer,
+ child: Row(
+ children: [
+ const SizedBox(width: 14),
+ Icon(
+ Icons.search_outlined,
+ color: Theme.of(context).colorScheme.onSecondaryContainer,
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Obx(
+ () => Text(
+ _searchController.defaultSearch.value,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ),
+ ),
+ ],
),
),
- onSubmitted: (String value) => _searchController.submit(),
),
- ),
- ),
- body: SingleChildScrollView(
- child: Column(
- children: [
- const SizedBox(height: 12),
- // 搜索建议
- _searchSuggest(),
- // 热搜
- hotSearch(),
- // 搜索历史
- _history()
- ],
- ),
- ),
+ );
+ },
+ openBuilder: (BuildContext context, VoidCallback _) {
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ appBar: AppBar(
+ shape: Border(
+ bottom: BorderSide(
+ color: Theme.of(context).dividerColor.withOpacity(0.08),
+ width: 1,
+ ),
+ ),
+ titleSpacing: 0,
+ actions: [
+ Hero(
+ tag: 'searchTag',
+ child: IconButton(
+ onPressed: () => _searchController.submit(),
+ icon: const Icon(CupertinoIcons.search, size: 22)),
+ ),
+ const SizedBox(width: 10)
+ ],
+ title: Obx(
+ () => TextField(
+ autofocus: true,
+ focusNode: _searchController.searchFocusNode,
+ controller: _searchController.controller.value,
+ textInputAction: TextInputAction.search,
+ onChanged: (value) => _searchController.onChange(value),
+ decoration: InputDecoration(
+ hintText: _searchController.hintText,
+ border: InputBorder.none,
+ suffixIcon: IconButton(
+ icon: Icon(
+ Icons.clear,
+ size: 22,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ onPressed: () => _searchController.onClear(),
+ ),
+ ),
+ onSubmitted: (String value) => _searchController.submit(),
+ ),
+ ),
+ ),
+ body: SingleChildScrollView(
+ child: Column(
+ children: [
+ const SizedBox(height: 12),
+ // 搜索建议
+ _searchSuggest(),
+ // 热搜
+ hotSearch(),
+ // 搜索历史
+ _history()
+ ],
+ ),
+ ),
+ );
+ },
);
}
@@ -211,13 +268,13 @@ class _SearchPageState extends State with RouteAware {
return Obx(
() => Container(
width: double.infinity,
- padding: const EdgeInsets.fromLTRB(10, 25, 4, 0),
+ padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_searchController.historyList.isNotEmpty)
Padding(
- padding: const EdgeInsets.fromLTRB(6, 0, 1, 2),
+ padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/lib/pages/searchPanel/controller.dart b/lib/pages/searchPanel/controller.dart
index 640ae246..b8e4a166 100644
--- a/lib/pages/searchPanel/controller.dart
+++ b/lib/pages/searchPanel/controller.dart
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
+import 'package:pilipala/utils/id_utils.dart';
+import 'package:pilipala/utils/utils.dart';
class SearchPanelController extends GetxController {
SearchPanelController({this.keyword, this.searchType});
@@ -21,6 +23,7 @@ class SearchPanelController extends GetxController {
} else if (type == 'onRefresh') {
resultList.value = result['data'].list;
}
+ onPushDetail(keyword, resultList);
}
return result;
}
@@ -40,4 +43,24 @@ class SearchPanelController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
+
+ void onPushDetail(keyword, resultList) async {
+ // 匹配输入内容,如果是AV、BV号且有结果 直接跳转详情页
+ Map matchRes = IdUtils.matchAvorBv(input: keyword);
+ List matchKeys = matchRes.keys.toList();
+ if (matchKeys.isNotEmpty && searchType == SearchType.video) {
+ String bvid = resultList.first.bvid;
+ int aid = resultList.first.aid;
+ String heroTag = Utils.makeHeroTag(bvid);
+
+ int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);
+ if (matchKeys.first == 'BV' && matchRes[matchKeys.first] == bvid ||
+ matchKeys.first == 'AV' && matchRes[matchKeys.first] == aid) {
+ Get.toNamed(
+ '/video?bvid=$bvid&cid=$cid',
+ arguments: {'videoItem': resultList.first, 'heroTag': heroTag},
+ );
+ }
+ }
+ }
}
diff --git a/lib/pages/searchPanel/view.dart b/lib/pages/searchPanel/view.dart
index 5dd15b8f..8e1cf3c4 100644
--- a/lib/pages/searchPanel/view.dart
+++ b/lib/pages/searchPanel/view.dart
@@ -25,7 +25,7 @@ class SearchPanel extends StatefulWidget {
class _SearchPanelState extends State
with AutomaticKeepAliveClientMixin {
- late SearchPanelController? _searchPanelController;
+ late SearchPanelController _searchPanelController;
bool _isLoadingMore = false;
late Future _futureBuilderFuture;
@@ -41,21 +41,20 @@ class _SearchPanelState extends State
keyword: widget.keyword,
searchType: widget.searchType,
),
- tag: widget.searchType!.type + widget.tag!,
+ tag: widget.searchType!.type,
);
- ScrollController scrollController =
- _searchPanelController!.scrollController;
+ ScrollController scrollController = _searchPanelController.scrollController;
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
if (!_isLoadingMore) {
_isLoadingMore = true;
- await _searchPanelController!.onSearch(type: 'onLoad');
+ await _searchPanelController.onSearch(type: 'onLoad');
_isLoadingMore = false;
}
}
});
- _futureBuilderFuture = _searchPanelController!.onSearch();
+ _futureBuilderFuture = _searchPanelController.onSearch();
}
@override
@@ -63,7 +62,7 @@ class _SearchPanelState extends State
super.build(context);
return RefreshIndicator(
onRefresh: () async {
- await _searchPanelController!.onRefresh();
+ await _searchPanelController.onRefresh();
},
child: FutureBuilder(
future: _futureBuilderFuture,
@@ -71,7 +70,7 @@ class _SearchPanelState extends State
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
var ctr = _searchPanelController;
- List list = ctr!.resultList;
+ List list = ctr.resultList;
if (data['status']) {
return Obx(() {
switch (widget.searchType) {
diff --git a/lib/pages/searchPanel/widgets/live_panel.dart b/lib/pages/searchPanel/widgets/live_panel.dart
index f00660e6..02b56d3a 100644
--- a/lib/pages/searchPanel/widgets/live_panel.dart
+++ b/lib/pages/searchPanel/widgets/live_panel.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
@@ -32,6 +33,7 @@ class LiveItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ String heroTag = Utils.makeHeroTag(liveItem.roomid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
@@ -40,7 +42,10 @@ class LiveItem extends StatelessWidget {
),
margin: EdgeInsets.zero,
child: InkWell(
- onTap: () {},
+ onTap: () async {
+ Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
+ arguments: {'liveItem': liveItem, 'heroTag': heroTag});
+ },
child: Column(
children: [
ClipRRect(
@@ -58,7 +63,7 @@ class LiveItem extends StatelessWidget {
return Stack(
children: [
Hero(
- tag: Utils.makeHeroTag(liveItem.roomid),
+ tag: heroTag,
child: NetworkImgLayer(
src: liveItem.cover,
type: 'emote',
diff --git a/lib/pages/searchPanel/widgets/media_bangumi_panel.dart b/lib/pages/searchPanel/widgets/media_bangumi_panel.dart
index 9dab4699..b17c74de 100644
--- a/lib/pages/searchPanel/widgets/media_bangumi_panel.dart
+++ b/lib/pages/searchPanel/widgets/media_bangumi_panel.dart
@@ -41,8 +41,13 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
height: 148,
src: i.cover,
),
- pBadge(i.mediaType == 1 ? '番剧' : '国创', context, 6.0, 4.0,
- null, null)
+ PBadge(
+ text: i.mediaType == 1 ? '番剧' : '国创',
+ top: 6.0,
+ right: 4.0,
+ bottom: null,
+ left: null,
+ )
],
),
const SizedBox(width: 10),
diff --git a/lib/pages/searchResult/view.dart b/lib/pages/searchResult/view.dart
index 00c917f7..f2efb33a 100644
--- a/lib/pages/searchResult/view.dart
+++ b/lib/pages/searchResult/view.dart
@@ -88,6 +88,7 @@ class _SearchResultPageState extends State
tag: SearchType.values[index].type)
.animateToTop();
}
+
_searchResultController!.tabIndex = index;
},
),
diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart
index 2a394587..0766340b 100644
--- a/lib/pages/setting/controller.dart
+++ b/lib/pages/setting/controller.dart
@@ -1,16 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/init.dart';
+import 'package:pilipala/models/common/theme_type.dart';
+import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/controller.dart';
+import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class SettingController extends GetxController {
Box user = GStrorage.user;
- RxBool userLogin = false.obs;
- Box userInfoCache = GStrorage.userInfo;
Box setting = GStrorage.setting;
+ Box userInfoCache = GStrorage.userInfo;
+
+ RxBool userLogin = false.obs;
RxBool feedBackEnable = false.obs;
+ RxInt picQuality = 10.obs;
+ Rx themeType = ThemeType.system.obs;
@override
void onInit() {
@@ -18,13 +26,54 @@ class SettingController extends GetxController {
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
feedBackEnable.value =
setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);
+ picQuality.value =
+ setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
+ themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
+ defaultValue: ThemeType.system.code)];
}
loginOut() async {
- await Request.removeCookie();
- await Get.find().resetUserInfo();
- userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
- userInfoCache.put('userInfoCache', null);
+ SmartDialog.show(
+ useSystem: true,
+ animationType: SmartAnimationType.centerFade_otherSlide,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('提示'),
+ content: const Text('确认要退出登录吗'),
+ actions: [
+ TextButton(
+ onPressed: () => SmartDialog.dismiss(),
+ child: const Text('点错了'),
+ ),
+ TextButton(
+ onPressed: () async {
+ // 清空cookie
+ await Request.cookieManager.cookieJar.deleteAll();
+ Request.dio.options.headers['cookie'] = '';
+
+ // 清空本地存储的用户标识
+ userInfoCache.put('userInfoCache', null);
+ user.put(UserBoxKey.accessKey, {'mid': -1, 'value': ''});
+
+ // 更改我的页面登录状态
+ await Get.find().resetUserInfo();
+
+ // 更改主页登录状态
+ HomeController homeCtr = Get.find();
+ homeCtr.updateLoginStatus(false);
+
+ // 事件通知
+ EventBus eventBus = EventBus();
+ eventBus.emit(EventName.loginEvent, {'status': false});
+
+ SmartDialog.dismiss().then((value) => Get.back());
+ },
+ child: const Text('确认'),
+ )
+ ],
+ );
+ },
+ );
}
// 开启关闭震动反馈
diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart
new file mode 100644
index 00000000..6f160f08
--- /dev/null
+++ b/lib/pages/setting/play_setting.dart
@@ -0,0 +1,170 @@
+import 'package:flutter/material.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/models/video/play/quality.dart';
+import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
+import 'package:pilipala/utils/storage.dart';
+
+import 'widgets/switch_item.dart';
+
+class PlaySetting extends StatefulWidget {
+ const PlaySetting({super.key});
+
+ @override
+ State createState() => _PlaySettingState();
+}
+
+class _PlaySettingState extends State {
+ Box setting = GStrorage.setting;
+ late dynamic defaultVideoQa;
+ late dynamic defaultAudioQa;
+ late dynamic defaultDecode;
+ late int defaultFullScreenMode;
+
+ @override
+ void initState() {
+ super.initState();
+ defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
+ defaultValue: VideoQuality.values.last.code);
+ defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
+ defaultValue: AudioQuality.values.last.code);
+ defaultDecode = setting.get(SettingBoxKey.defaultDecode,
+ defaultValue: VideoDecodeFormats.values.last.code);
+ defaultFullScreenMode = setting.get(SettingBoxKey.fullScreenMode,
+ defaultValue: FullScreenMode.values.first.code);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
+ TextStyle subTitleStyle = Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .copyWith(color: Theme.of(context).colorScheme.outline);
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ '播放设置',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ body: ListView(
+ children: [
+ const SetSwitchItem(
+ title: '自动播放',
+ subTitle: '进入详情页自动播放',
+ setKey: SettingBoxKey.autoPlayEnable,
+ defaultVal: true,
+ ),
+ const SetSwitchItem(
+ title: '开启硬解',
+ subTitle: '以较低功耗播放视频',
+ setKey: SettingBoxKey.enableHA,
+ defaultVal: true,
+ ),
+ ListTile(
+ dense: false,
+ title: Text('默认画质', style: titleStyle),
+ subtitle: Text(
+ '当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',
+ style: subTitleStyle,
+ ),
+ trailing: PopupMenuButton(
+ initialValue: defaultVideoQa,
+ icon: const Icon(Icons.more_vert_outlined, size: 22),
+ onSelected: (item) {
+ defaultVideoQa = item;
+ setting.put(SettingBoxKey.defaultVideoQa, item);
+ setState(() {});
+ },
+ itemBuilder: (BuildContext context) => [
+ for (var i in VideoQuality.values.reversed) ...[
+ PopupMenuItem(
+ value: i.code,
+ child: Text(i.description),
+ ),
+ ]
+ ],
+ ),
+ ),
+ ListTile(
+ dense: false,
+ title: Text('默认音质', style: titleStyle),
+ subtitle: Text(
+ '当前音质${AudioQualityCode.fromCode(defaultAudioQa)!.description!}',
+ style: subTitleStyle,
+ ),
+ trailing: PopupMenuButton(
+ initialValue: defaultAudioQa,
+ icon: const Icon(Icons.more_vert_outlined, size: 22),
+ onSelected: (item) {
+ defaultAudioQa = item;
+ setting.put(SettingBoxKey.defaultAudioQa, item);
+ setState(() {});
+ },
+ itemBuilder: (BuildContext context) => [
+ for (var i in AudioQuality.values.reversed) ...[
+ PopupMenuItem(
+ value: i.code,
+ child: Text(i.description),
+ ),
+ ]
+ ],
+ ),
+ ),
+ ListTile(
+ dense: false,
+ title: Text('默认解码格式', style: titleStyle),
+ subtitle: Text(
+ '当前解码格式${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!}',
+ style: subTitleStyle,
+ ),
+ trailing: PopupMenuButton(
+ initialValue: defaultDecode,
+ icon: const Icon(Icons.more_vert_outlined, size: 22),
+ onSelected: (item) {
+ defaultDecode = item;
+ setting.put(SettingBoxKey.defaultDecode, item);
+ setState(() {});
+ },
+ itemBuilder: (BuildContext context) => [
+ for (var i in VideoDecodeFormats.values) ...[
+ PopupMenuItem(
+ value: i.code,
+ child: Text(i.description),
+ ),
+ ]
+ ],
+ ),
+ ),
+ ListTile(
+ dense: false,
+ title: Text('默认全屏方式', style: titleStyle),
+ subtitle: Text(
+ '当前全屏方式:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}',
+ style: subTitleStyle,
+ ),
+ trailing: PopupMenuButton(
+ initialValue: defaultFullScreenMode,
+ icon: const Icon(Icons.more_vert_outlined, size: 22),
+ onSelected: (item) {
+ defaultFullScreenMode = item;
+ setting.put(SettingBoxKey.fullScreenMode, item);
+ setState(() {});
+ },
+ itemBuilder: (BuildContext context) => [
+ for (var i in FullScreenMode.values) ...[
+ PopupMenuItem(
+ value: i.code,
+ child: Text(i.description),
+ ),
+ ]
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/setting/privacy_setting.dart b/lib/pages/setting/privacy_setting.dart
new file mode 100644
index 00000000..5594fc1e
--- /dev/null
+++ b/lib/pages/setting/privacy_setting.dart
@@ -0,0 +1,58 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/utils/storage.dart';
+
+class PrivacySetting extends StatefulWidget {
+ const PrivacySetting({super.key});
+
+ @override
+ State createState() => _PrivacySettingState();
+}
+
+class _PrivacySettingState extends State {
+ bool userLogin = false;
+ Box user = GStrorage.user;
+
+ @override
+ void initState() {
+ super.initState();
+ userLogin = user.get(UserBoxKey.userLogin) ?? false;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
+ TextStyle subTitleStyle = Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .copyWith(color: Theme.of(context).colorScheme.outline);
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ '隐私设置',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ body: Column(
+ children: [
+ ListTile(
+ onTap: () {
+ if (!userLogin) {
+ SmartDialog.showToast('登录后查看');
+ return;
+ }
+ Get.toNamed('/blackListPage');
+ },
+ dense: false,
+ title: Text('黑名单管理', style: titleStyle),
+ subtitle: Text('已拉黑用户', style: subTitleStyle),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart
new file mode 100644
index 00000000..397edbbf
--- /dev/null
+++ b/lib/pages/setting/style_setting.dart
@@ -0,0 +1,191 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/models/common/theme_type.dart';
+import 'package:pilipala/utils/storage.dart';
+
+import 'controller.dart';
+
+class StyleSetting extends StatefulWidget {
+ const StyleSetting({super.key});
+
+ @override
+ State createState() => _StyleSettingState();
+}
+
+class _StyleSettingState extends State {
+ final SettingController settingController = Get.put(SettingController());
+ Box setting = GStrorage.setting;
+ late int picQuality;
+ late ThemeType _tempThemeValue;
+
+ @override
+ void initState() {
+ super.initState();
+ picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
+ _tempThemeValue = settingController.themeType.value;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
+ TextStyle subTitleStyle = Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .copyWith(color: Theme.of(context).colorScheme.outline);
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ '外观设置',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ body: ListView(
+ children: [
+ Obx(
+ () => ListTile(
+ enableFeedback: true,
+ onTap: () => settingController.onOpenFeedBack(),
+ title: const Text('震动反馈'),
+ subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
+ trailing: Transform.scale(
+ scale: 0.8,
+ child: Switch(
+ thumbIcon: MaterialStateProperty.resolveWith(
+ (Set states) {
+ if (states.isNotEmpty &&
+ states.first == MaterialState.selected) {
+ return const Icon(Icons.done);
+ }
+ return null; // All other states will use the default thumbIcon.
+ }),
+ value: settingController.feedBackEnable.value,
+ onChanged: (value) => settingController.onOpenFeedBack()),
+ ),
+ ),
+ ),
+ ListTile(
+ dense: false,
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, StateSetter setState) {
+ final SettingController settingController =
+ Get.put(SettingController());
+ return AlertDialog(
+ title: const Text('图片质量'),
+ contentPadding: const EdgeInsets.only(
+ top: 20, left: 8, right: 8, bottom: 8),
+ content: SizedBox(
+ height: 40,
+ child: Slider(
+ value: picQuality.toDouble(),
+ min: 10,
+ max: 100,
+ divisions: 9,
+ label: '$picQuality%',
+ onChanged: (double val) {
+ picQuality = val.toInt();
+ setState(() {});
+ },
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(),
+ child: Text('取消',
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .outline))),
+ TextButton(
+ onPressed: () {
+ setting.put(
+ SettingBoxKey.defaultPicQa, picQuality);
+ Get.back();
+ settingController.picQuality.value = picQuality;
+ },
+ child: const Text('确定'),
+ )
+ ],
+ );
+ },
+ );
+ },
+ );
+ },
+ title: Text('图片质量', style: titleStyle),
+ subtitle: Text('选择合适的图片清晰度,上限100%', style: subTitleStyle),
+ trailing: Obx(
+ () => Text(
+ '${settingController.picQuality.value}%',
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ ),
+ ),
+ ListTile(
+ dense: false,
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ title: const Text('主题模式'),
+ contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
+ content: StatefulBuilder(
+ builder: (context, StateSetter setState) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ for (var i in ThemeType.values) ...[
+ RadioListTile(
+ value: i,
+ title: Text(i.description, style: titleStyle),
+ groupValue: _tempThemeValue,
+ onChanged: (ThemeType? value) {
+ setState(() {
+ _tempThemeValue = i;
+ });
+ },
+ ),
+ ]
+ ],
+ );
+ }),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: Text(
+ '取消',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline),
+ )),
+ TextButton(
+ onPressed: () {
+ settingController.themeType.value = _tempThemeValue;
+ setting.put(
+ SettingBoxKey.themeMode, _tempThemeValue.code);
+ Get.forceAppUpdate();
+ Get.back();
+ },
+ child: const Text('确定'))
+ ],
+ );
+ },
+ );
+ },
+ title: Text('主题模式', style: titleStyle),
+ subtitle: Obx(() => Text(
+ '当前模式:${settingController.themeType.value.description}',
+ style: subTitleStyle)),
+ trailing: const Icon(Icons.arrow_right_alt_outlined),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart
index 155eec2a..0bddb809 100644
--- a/lib/pages/setting/view.dart
+++ b/lib/pages/setting/view.dart
@@ -7,39 +7,38 @@ class SettingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- TextStyle subTitleStyle = Theme.of(context)
- .textTheme
- .labelMedium!
- .copyWith(color: Theme.of(context).colorScheme.outline);
final SettingController settingController = Get.put(SettingController());
return Scaffold(
appBar: AppBar(
- title: const Text('设置'),
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ '设置',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
),
body: Column(
children: [
- Obx(
- () => ListTile(
- enableFeedback: true,
- onTap: () => settingController.onOpenFeedBack(),
- title: const Text('震动反馈'),
- subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
- trailing: Transform.scale(
- scale: 0.8,
- child: Switch(
- thumbIcon: MaterialStateProperty.resolveWith(
- (Set states) {
- if (states.isNotEmpty &&
- states.first == MaterialState.selected) {
- return const Icon(Icons.done);
- }
- return null; // All other states will use the default thumbIcon.
- }),
- value: settingController.feedBackEnable.value,
- onChanged: (value) => settingController.onOpenFeedBack()),
- ),
- ),
+ ListTile(
+ onTap: () => Get.toNamed('/privacySetting'),
+ dense: false,
+ title: const Text('隐私设置'),
),
+ ListTile(
+ onTap: () => Get.toNamed('/playSetting'),
+ dense: false,
+ title: const Text('播放设置'),
+ ),
+ ListTile(
+ onTap: () => Get.toNamed('/styleSetting'),
+ dense: false,
+ title: const Text('外观设置'),
+ ),
+ // ListTile(
+ // onTap: () {},
+ // dense: false,
+ // title: const Text('其他设置'),
+ // ),
Obx(
() => Visibility(
visible: settingController.userLogin.value,
@@ -50,6 +49,11 @@ class SettingPage extends StatelessWidget {
),
),
),
+ ListTile(
+ onTap: () => Get.toNamed('/about'),
+ dense: false,
+ title: const Text('关于'),
+ ),
],
),
);
diff --git a/lib/pages/setting/widgets/select_item.dart b/lib/pages/setting/widgets/select_item.dart
new file mode 100644
index 00000000..a18754a1
--- /dev/null
+++ b/lib/pages/setting/widgets/select_item.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/material.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/models/video/play/quality.dart';
+import 'package:pilipala/utils/storage.dart';
+
+class SetSelectItem extends StatefulWidget {
+ final String? title;
+ final String? subTitle;
+ final String? setKey;
+ const SetSelectItem({
+ this.title,
+ this.subTitle,
+ this.setKey,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _SetSelectItemState();
+}
+
+class _SetSelectItemState extends State {
+ Box setting = GStrorage.setting;
+ late dynamic currentVal;
+ late int currentIndex;
+ late List menus;
+ late List popMenuItems;
+
+ @override
+ void initState() {
+ super.initState();
+ late String defaultVal;
+ switch (widget.setKey) {
+ case 'defaultVideoQa':
+ defaultVal = VideoQuality.values.last.description;
+ List list = menus = VideoQuality.values.reversed.toList();
+ currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
+ currentIndex =
+ list.firstWhere((i) => i.description == currentVal).index;
+
+ popMenuItems = [
+ for (var i in list) ...[
+ PopupMenuItem(
+ value: i.code,
+ child: Text(i.description),
+ )
+ ]
+ ];
+
+ break;
+ case 'defaultAudioQa':
+ defaultVal = AudioQuality.values.last.description;
+ List list = menus = AudioQuality.values.reversed.toList();
+ currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
+ currentIndex =
+ list.firstWhere((i) => i.description == currentVal).index;
+
+ popMenuItems = [
+ for (var i in list) ...[
+ PopupMenuItem(
+ value: i.index,
+ child: Text(i.description),
+ ),
+ ]
+ ];
+ break;
+ case 'defaultDecode':
+ defaultVal = VideoDecodeFormats.values[0].description;
+ currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
+ List list = menus = VideoDecodeFormats.values;
+
+ currentIndex =
+ list.firstWhere((i) => i.description == currentVal).index;
+
+ popMenuItems = [
+ for (var i in list) ...[
+ PopupMenuItem(
+ value: i.index,
+ child: Text(i.description),
+ ),
+ ]
+ ];
+ break;
+ case 'defaultVideoSpeed':
+ defaultVal = '1.0';
+ currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
+
+ break;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle subTitleStyle = Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .copyWith(color: Theme.of(context).colorScheme.outline);
+ return ListTile(
+ onTap: () {},
+ dense: false,
+ title: Text(widget.title!),
+ subtitle: Text(
+ '当前${widget.title!} $currentVal',
+ style: subTitleStyle,
+ ),
+ trailing: PopupMenuButton(
+ initialValue: currentIndex,
+ icon: const Icon(
+ Icons.arrow_forward_rounded,
+ size: 22,
+ ),
+ onSelected: (item) {
+ currentVal = menus.firstWhere((e) => e.code == item).first;
+ setState(() {});
+ },
+ itemBuilder: (BuildContext context) =>
+ [...popMenuItems],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart
new file mode 100644
index 00000000..6ae642d3
--- /dev/null
+++ b/lib/pages/setting/widgets/switch_item.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/utils/storage.dart';
+
+class SetSwitchItem extends StatefulWidget {
+ final String? title;
+ final String? subTitle;
+ final String? setKey;
+ final bool? defaultVal;
+
+ const SetSwitchItem({
+ this.title,
+ this.subTitle,
+ this.setKey,
+ this.defaultVal,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _SetSwitchItemState();
+}
+
+class _SetSwitchItemState extends State {
+ // ignore: non_constant_identifier_names
+ Box Setting = GStrorage.setting;
+ late bool val;
+
+ @override
+ void initState() {
+ super.initState();
+ val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
+ TextStyle subTitleStyle = Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .copyWith(color: Theme.of(context).colorScheme.outline);
+ return ListTile(
+ enableFeedback: true,
+ onTap: () {
+ Setting.put(widget.setKey, !val);
+ },
+ title: Text(widget.title!, style: titleStyle),
+ subtitle: widget.subTitle != null
+ ? Text(widget.subTitle!, style: subTitleStyle)
+ : null,
+ trailing: Transform.scale(
+ scale: 0.8,
+ child: Switch(
+ thumbIcon: MaterialStateProperty.resolveWith(
+ (Set states) {
+ if (states.isNotEmpty && states.first == MaterialState.selected) {
+ return const Icon(Icons.done);
+ }
+ return null; // All other states will use the default thumbIcon.
+ }),
+ value: val,
+ onChanged: (value) {
+ val = value;
+ Setting.put(widget.setKey, value);
+ setState(() {});
+ }),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart
index adbedcba..fe19ae0d 100644
--- a/lib/pages/video/detail/controller.dart
+++ b/lib/pages/video/detail/controller.dart
@@ -13,36 +13,47 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
+import 'package:pilipala/utils/utils.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
- int tabInitialIndex = 0;
- TabController? tabCtr;
- // tabs
- RxList tabs = ['简介', '评论'].obs;
-
- // 视频aid
+ /// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
- // 视频类型 默认投稿视频
- SearchType videoType = SearchType.video;
-
- late PlayUrlModel data;
- // 当前画质
- late VideoQuality currentVideoQa;
- // 当前音质
- late AudioQuality currentAudioQa;
-
- // 是否预渲染 骨架屏
- bool preRender = false;
-
- // 视频详情 上个页面传入
+ String heroTag = Get.arguments['heroTag'];
+ // 视频详情
Map videoItem = {};
+ // 视频类型 默认投稿视频
+ SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
+ /// tabs相关配置
+ int tabInitialIndex = 0;
+ late TabController tabCtr;
+ RxList tabs = ['简介', '评论'].obs;
+
+ // 请求返回的视频信息
+ late PlayUrlModel data;
// 请求状态
RxBool isLoading = false.obs;
- String heroTag = '';
+ /// 播放器配置 画质 音质 解码格式
+ late VideoQuality currentVideoQa;
+ late AudioQuality currentAudioQa;
+ late VideoDecodeFormats currentDecodeFormats;
+ // PlPlayerController plPlayerController = PlPlayerController();
+ // 是否开始自动播放 存在多p的情况下,第二p需要为true
+ RxBool autoPlay = true.obs;
+ // 视频资源是否有效
+ RxBool isEffective = true.obs;
+ // 封面图的展示
+ RxBool isShowCover = true.obs;
+ // 硬解
+ RxBool enableHA = true.obs;
+
+ /// 本地存储
+ Box user = GStrorage.user;
+ Box localCache = GStrorage.localCache;
+ Box setting = GStrorage.setting;
int oid = 0;
// 评论id 请求楼中楼评论使用
@@ -50,39 +61,41 @@ class VideoDetailController extends GetxController
ReplyItemModel? firstFloor;
final scaffoldKey = GlobalKey();
- Timer? timer;
RxString bgCover = ''.obs;
- Box user = GStrorage.user;
- Box localCache = GStrorage.localCache;
- PlPlayerController plPlayerController = PlPlayerController();
- // 是否开始自动播放 存在多p的情况下,第二p需要为true
- RxBool autoPlay = true.obs;
- // 视频资源是否有效
- RxBool isEffective = true.obs;
- // 封面图的展示
- RxBool isShowCover = true.obs;
+ PlPlayerController plPlayerController = PlPlayerController.getInstance();
+
+ late VideoItem firstVideo;
+ late String videoUrl;
+ late String audioUrl;
+ late Duration defaultST;
+ // 默认记录历史记录
+ bool enableHeart = true;
@override
void onInit() {
super.onInit();
- if (Get.arguments.isNotEmpty) {
- if (Get.arguments.containsKey('videoItem')) {
- preRender = true;
- var args = Get.arguments['videoItem'];
+ Map argMap = Get.arguments;
+ var keys = argMap.keys.toList();
+ if (keys.isNotEmpty) {
+ if (keys.contains('videoItem')) {
+ var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
- bgCover.value = args.pic;
}
}
- if (Get.arguments.containsKey('pic')) {
- videoItem['pic'] = Get.arguments['pic'];
- bgCover.value = Get.arguments['pic'];
+ if (keys.contains('pic')) {
+ videoItem['pic'] = argMap['pic'];
}
- heroTag = Get.arguments['heroTag'];
- videoType = Get.arguments['videoType'] ?? SearchType.video;
}
tabCtr = TabController(length: 2, vsync: this);
- // queryVideoUrl();
+ autoPlay.value =
+ setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
+ enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
+
+ if (user.get(UserBoxKey.userMid) == null ||
+ localCache.get(LocalCacheKey.historyPause) == true) {
+ enableHeart = false;
+ }
}
showReplyReplyPanel() {
@@ -107,7 +120,7 @@ class VideoDetailController extends GetxController
/// 更新画质、音质
/// TODO 继续进度播放
updatePlayer() {
- Duration position = plPlayerController.position.value;
+ defaultST = plPlayerController.position.value;
plPlayerController.removeListeners();
plPlayerController.isBuffering.value = false;
plPlayerController.buffered.value = Duration.zero;
@@ -115,24 +128,30 @@ class VideoDetailController extends GetxController
/// 暂不匹配解码规则
/// 根据currentVideoQa 重新设置videoUrl
- VideoItem firstVideo =
- data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
- // String videoUrl = firstVideo.baseUrl!;
+ // firstVideo =
+ // data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
+ // videoUrl = firstVideo.baseUrl!;
+
+ /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
+ List videoList =
+ data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
+ firstVideo = videoList
+ .firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
+ videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
- String audioUrl = firstAudio.baseUrl ?? '';
+ audioUrl = firstAudio.baseUrl ?? '';
- playerInit(firstVideo, audioUrl, defaultST: position);
+ playerInit();
}
- Future playerInit(firstVideo, audioSource,
- {Duration defaultST = Duration.zero, int duration = 0}) async {
+ Future playerInit({video, audio, seekToTime, duration}) async {
await plPlayerController.setDataSource(
DataSource(
- videoSource: firstVideo.baseUrl,
- audioSource: audioSource,
+ videoSource: video ?? videoUrl,
+ audioSource: audio ?? audioUrl,
type: DataSourceType.network,
httpHeaders: {
'user-agent':
@@ -141,13 +160,19 @@ class VideoDetailController extends GetxController
},
),
// 硬解
- enableHA: true,
+ enableHA: enableHA.value,
autoplay: autoPlay.value,
- seekTo: defaultST,
- duration: Duration(milliseconds: duration),
+ seekTo: seekToTime ?? defaultST,
+ duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),
// 宽>高 水平 否则 垂直
- direction:
- firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical',
+ direction: (firstVideo.width! - firstVideo.height!) > 0
+ ? 'horizontal'
+ : 'vertical',
+ // 默认1倍速
+ speed: 1.0,
+ bvid: bvid,
+ cid: cid,
+ enableHeart: enableHeart,
);
}
@@ -163,58 +188,90 @@ class VideoDetailController extends GetxController
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
- VideoItem firstVideo = data.dash!.video!.first;
- // String videoUrl = firstVideo.baseUrl!;
- //
- currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
+ // firstVideo = data.dash!.video!.first;
+ // videoUrl = firstVideo.baseUrl!;
+ // //
+ // currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
+
+ // /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
+ // AudioItem firstAudio =
+ // data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
+ // audioUrl = firstAudio.baseUrl ?? '';
+
+ List allVideosList = data.dash!.video!;
+
+ try {
+ // 当前可播放的最高质量视频
+ int currentHighVideoQa = allVideosList.first.quality!.code;
+ //
+ int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
+ defaultValue: currentHighVideoQa);
+ int resVideoQa = currentHighVideoQa;
+ if (cacheVideoQa <= currentHighVideoQa) {
+ List numbers = data.acceptQuality!
+ .where((e) => e <= currentHighVideoQa)
+ .toList();
+ resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
+ }
+ currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
+
+ /// 取出符合当前画质的videoList
+ List videosList =
+ allVideosList.where((e) => e.quality!.code == resVideoQa).toList();
+
+ /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
+ List supportFormats = data.supportFormats!;
+ // 根据画质选编码格式
+ List supportDecodeFormats =
+ supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
+
+ try {
+ currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
+ SettingBoxKey.defaultDecode,
+ defaultValue: supportDecodeFormats.first))!;
+ } catch (_) {}
+
+ /// 取出符合当前解码格式的videoItem
+ firstVideo = videosList
+ .firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code));
+ videoUrl = firstVideo.baseUrl!;
+ } catch (_) {}
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
- AudioItem firstAudio =
- data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
- String audioUrl = firstAudio.baseUrl ?? '';
+ late AudioItem firstAudio;
+ List audiosList = data.dash!.audio!;
+ try {
+ if (audiosList.isNotEmpty) {
+ firstAudio = audiosList.first;
+ int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
+ defaultValue: firstAudio.id);
+ // 选择最接近的那个音轨
+ firstAudio = audiosList.firstWhere(
+ (e) => e.id == resultAudioQa,
+ orElse: () => AudioItem(),
+ );
+ } else {
+ firstAudio = AudioItem();
+ }
+ } catch (_) {}
+
+ audioUrl = firstAudio.baseUrl ?? '';
//
if (firstAudio.id != null) {
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
}
- await playerInit(
- firstVideo,
- audioUrl,
- defaultST: Duration(milliseconds: data.lastPlayTime!),
- duration: data.timeLength ?? 0,
- );
+ defaultST = Duration(milliseconds: data.lastPlayTime!);
+ await playerInit();
+
+ // await playerInit(
+ // firstVideo,
+ // audioUrl,
+ // defaultST: Duration(milliseconds: data.lastPlayTime!),
+ // duration: data.timeLength ?? 0,
+ // );
} else {
SmartDialog.showToast(result['msg'].toString());
}
return result;
}
-
- void loopHeartBeat() {
- timer = Timer.periodic(const Duration(seconds: 5), (timer) {
- markHeartBeat();
- });
- }
-
- void markHeartBeat() async {
- if (user.get(UserBoxKey.userMid) == null) {
- return;
- }
- if (localCache.get(LocalCacheKey.historyStatus) == true) {
- return;
- }
- Duration progress = plPlayerController.position.value;
- await VideoHttp.heartBeat(
- bvid: bvid,
- cid: cid,
- progress: progress.inSeconds,
- );
- }
-
- @override
- void onClose() {
- markHeartBeat();
- if (timer != null && timer!.isActive) {
- timer!.cancel();
- }
- super.onClose();
- }
}
diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart
index 9a4039c7..aa5d41c4 100644
--- a/lib/pages/video/detail/introduction/controller.dart
+++ b/lib/pages/video/detail/introduction/controller.dart
@@ -51,6 +51,8 @@ class VideoIntroController extends GetxController {
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
+ RxInt lastPlayCid = 0.obs;
+
@override
void onInit() {
super.onInit();
@@ -76,6 +78,7 @@ class VideoIntroController extends GetxController {
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
+ lastPlayCid.value = int.parse(Get.parameters['cid']!);
}
// 获取视频简介&分p
@@ -83,6 +86,9 @@ class VideoIntroController extends GetxController {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
videoDetail.value = result['data']!;
+ if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
+ lastPlayCid.value = videoDetail.value.pages!.first.cid!;
+ }
Get.find(tag: Get.arguments['heroTag'])
.tabs
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
@@ -408,6 +414,7 @@ class VideoIntroController extends GetxController {
videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {}
this.bvid = bvid;
+ lastPlayCid.value = cid;
await queryVideoIntro();
}
}
diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart
index b34d1109..2528ca18 100644
--- a/lib/pages/video/detail/introduction/view.dart
+++ b/lib/pages/video/detail/introduction/view.dart
@@ -1,4 +1,3 @@
-import 'package:flutter/gestures.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@@ -20,6 +19,7 @@ import 'widgets/action_item.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
+import 'widgets/page.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
@@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
- // return _buildView(context, false, videoDetail);
return Obx(
() => VideoInfo(
loadingStatus: false,
@@ -95,22 +94,35 @@ class VideoInfo extends StatefulWidget {
}
class _VideoInfoState extends State with TickerProviderStateMixin {
- Map videoItem = Get.put(VideoIntroController()).videoItem!;
- final VideoIntroController videoIntroController =
- Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
- bool isExpand = false;
+ final String heroTag = Get.arguments['heroTag'];
+ late final VideoIntroController videoIntroController;
+ late final VideoDetailController videoDetailCtr;
+ late final Map videoItem;
- late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
+ late final bool loadingStatus; // 加载状态
+
+ late final dynamic owner;
+ late final dynamic follower;
+ late final dynamic followStatus;
+
@override
void initState() {
super.initState();
- videoDetailCtr =
- Get.find(tag: Get.arguments['heroTag']);
+ videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
+ videoDetailCtr = Get.find(tag: heroTag);
+ videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight');
+
+ loadingStatus = widget.loadingStatus;
+ owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner;
+ follower = loadingStatus
+ ? '-'
+ : Utils.numFormat(videoIntroController.userStat['follower']);
+ followStatus = videoIntroController.followStatus;
}
// 收藏
@@ -141,24 +153,39 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
);
}
+ // 用户主页
+ onPushMember() {
+ feedBack();
+ int mid = !loadingStatus
+ ? widget.videoDetail!.owner!.mid
+ : videoItem['owner'].mid;
+ String face = !loadingStatus
+ ? widget.videoDetail!.owner!.face
+ : videoItem['owner'].face;
+ Get.toNamed('/member?mid=$mid',
+ arguments: {'face': face, 'heroTag': (mid + 99).toString()});
+ }
+
@override
Widget build(BuildContext context) {
ThemeData t = Theme.of(context);
+ Color outline = t.colorScheme.outline;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
- child: !widget.loadingStatus || videoItem.isNotEmpty
+ child: !loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- InkWell(
+ GestureDetector(
+ behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
Expanded(
child: Text(
- !widget.loadingStatus
+ !loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
@@ -182,14 +209,18 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
return t.highlightColor.withOpacity(0.2);
}),
),
- onPressed: () => showIntroDetail(),
- icon: const Icon(Icons.more_horiz),
+ onPressed: showIntroDetail,
+ icon: Icon(
+ Icons.more_horiz,
+ color: Theme.of(context).colorScheme.primary,
+ ),
),
),
],
),
),
- InkWell(
+ GestureDetector(
+ behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
@@ -237,107 +268,100 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
// 点赞收藏转发 布局样式2
// actionGrid(context, videoIntroController),
// 合集
- if (!widget.loadingStatus &&
+ if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
- SeasonPanel(
- ugcSeason: widget.videoDetail!.ugcSeason!,
- cid: widget.videoDetail!.pages!.first.cid,
- sheetHeight: sheetHeight,
- changeFuc: (bvid, cid, aid) => videoIntroController
- .changeSeasonOrbangu(bvid, cid, aid),
+ Obx(
+ () => SeasonPanel(
+ ugcSeason: widget.videoDetail!.ugcSeason!,
+ cid: videoIntroController.lastPlayCid.value != 0
+ ? videoIntroController.lastPlayCid.value
+ : widget.videoDetail!.pages!.first.cid,
+ sheetHeight: sheetHeight,
+ changeFuc: (bvid, cid, aid) => videoIntroController
+ .changeSeasonOrbangu(bvid, cid, aid),
+ ),
)
],
+ if (!loadingStatus &&
+ widget.videoDetail!.pages != null &&
+ widget.videoDetail!.pages!.length > 1) ...[
+ Obx(() => PagesPanel(
+ pages: widget.videoDetail!.pages!,
+ cid: videoIntroController.lastPlayCid.value,
+ sheetHeight: sheetHeight,
+ changeFuc: (cid) =>
+ videoIntroController.changeSeasonOrbangu(
+ videoIntroController.bvid, cid, null),
+ ))
+ ],
GestureDetector(
- onTap: () {
- feedBack();
- int mid = !widget.loadingStatus
- ? widget.videoDetail!.owner!.mid
- : videoItem['owner'].mid;
- String face = !widget.loadingStatus
- ? widget.videoDetail!.owner!.face
- : videoItem['owner'].face;
- Get.toNamed('/member?mid=$mid', arguments: {
- 'face': face,
- 'heroTag': (mid + 99).toString()
- });
- },
- child: Padding(
- padding: const EdgeInsets.only(
- top: 12, bottom: 12, left: 4, right: 4),
+ onTap: onPushMember,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
- src: !widget.loadingStatus
- ? widget.videoDetail!.owner!.face
- : videoItem['owner'].face,
+ src: loadingStatus
+ ? owner.face
+ : widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
- Text(
- !widget.loadingStatus
- ? widget.videoDetail!.owner!.name
- : videoItem['owner'].name,
- style: const TextStyle(fontSize: 13),
- ),
+ Text(owner.name,
+ style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
- widget.loadingStatus
- ? '-'
- : Utils.numFormat(
- videoIntroController.userStat['follower']),
+ follower,
style: TextStyle(
- fontSize: t.textTheme.labelSmall!.fontSize,
- color: t.colorScheme.outline),
+ fontSize: t.textTheme.labelSmall!.fontSize,
+ color: outline,
+ ),
),
const Spacer(),
AnimatedOpacity(
- opacity: widget.loadingStatus ? 0 : 1,
+ opacity: loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 32,
child: Obx(
- () => videoIntroController
- .followStatus.isNotEmpty
- ? TextButton(
- onPressed: () => videoIntroController
- .actionRelationMod(),
- style: TextButton.styleFrom(
- padding: const EdgeInsets.only(
- left: 8, right: 8),
- foregroundColor:
- videoIntroController.followStatus[
- 'attribute'] !=
- 0
- ? t.colorScheme.outline
- : t.colorScheme.onPrimary,
- backgroundColor: videoIntroController
- .followStatus[
- 'attribute'] !=
- 0
- ? t.colorScheme.onInverseSurface
- : t.colorScheme
- .primary, // 设置按钮背景色
- ),
- child: Text(
- videoIntroController.followStatus[
- 'attribute'] !=
- 0
- ? '已关注'
- : '关注',
- style: TextStyle(
- fontSize: t.textTheme.labelMedium!
- .fontSize),
- ),
- )
- : ElevatedButton(
- onPressed: () => videoIntroController
- .actionRelationMod(),
- child: const Text('关注'),
- ),
+ () =>
+ videoIntroController.followStatus.isNotEmpty
+ ? TextButton(
+ onPressed: videoIntroController
+ .actionRelationMod,
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.only(
+ left: 8, right: 8),
+ foregroundColor:
+ followStatus['attribute'] != 0
+ ? outline
+ : t.colorScheme.onPrimary,
+ backgroundColor:
+ followStatus['attribute'] != 0
+ ? t.colorScheme
+ .onInverseSurface
+ : t.colorScheme
+ .primary, // 设置按钮背景色
+ ),
+ child: Text(
+ followStatus['attribute'] != 0
+ ? '已关注'
+ : '关注',
+ style: TextStyle(
+ fontSize: t.textTheme
+ .labelMedium!.fontSize),
+ ),
+ )
+ : ElevatedButton(
+ onPressed: videoIntroController
+ .actionRelationMod,
+ child: const Text('关注'),
+ ),
),
),
),
@@ -359,66 +383,64 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) {
return LayoutBuilder(builder: (context, constraints) {
- return Padding(
+ return Container(
padding: const EdgeInsets.only(top: 6, bottom: 10),
- child: SizedBox(
- height: constraints.maxWidth / 5 * 0.8,
- child: GridView.count(
- primary: false,
- padding: const EdgeInsets.all(0),
- crossAxisCount: 5,
- childAspectRatio: 1.25,
- children: [
- Obx(
- () => ActionItem(
- icon: const Icon(FontAwesomeIcons.thumbsUp),
- selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
- onTap: () => videoIntroController.actionLikeVideo(),
- selectStatus: videoIntroController.hasLike.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.like!.toString()
- : '-'),
- ),
- ActionItem(
- icon: const Icon(FontAwesomeIcons.clock),
- onTap: () => videoIntroController.actionShareVideo(),
- selectStatus: false,
- loadingStatus: widget.loadingStatus,
- text: '稍后再看'),
- Obx(
- () => ActionItem(
- icon: const Icon(FontAwesomeIcons.b),
- selectIcon: const Icon(FontAwesomeIcons.b),
- onTap: () => videoIntroController.actionCoinVideo(),
- selectStatus: videoIntroController.hasCoin.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.coin!.toString()
- : '-'),
- ),
- Obx(
- () => ActionItem(
- icon: const Icon(FontAwesomeIcons.star),
- selectIcon: const Icon(FontAwesomeIcons.solidStar),
- // onTap: () => videoIntroController.actionFavVideo(),
- onTap: () => showFavBottomSheet(),
- selectStatus: videoIntroController.hasFav.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.favorite!.toString()
- : '-'),
- ),
- ActionItem(
- icon: const Icon(FontAwesomeIcons.shareFromSquare),
- onTap: () => videoIntroController.actionShareVideo(),
- selectStatus: false,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.share!.toString()
+ height: constraints.maxWidth / 5 * 0.8,
+ child: GridView.count(
+ primary: false,
+ padding: const EdgeInsets.all(0),
+ crossAxisCount: 5,
+ childAspectRatio: 1.25,
+ children: [
+ Obx(
+ () => ActionItem(
+ icon: const Icon(FontAwesomeIcons.thumbsUp),
+ selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
+ onTap: () => videoIntroController.actionLikeVideo(),
+ selectStatus: videoIntroController.hasLike.value,
+ loadingStatus: loadingStatus,
+ text: !loadingStatus
+ ? widget.videoDetail!.stat!.like!.toString()
: '-'),
- ],
- ),
+ ),
+ ActionItem(
+ icon: const Icon(FontAwesomeIcons.clock),
+ onTap: () => videoIntroController.actionShareVideo(),
+ selectStatus: false,
+ loadingStatus: loadingStatus,
+ text: '稍后再看'),
+ Obx(
+ () => ActionItem(
+ icon: const Icon(FontAwesomeIcons.b),
+ selectIcon: const Icon(FontAwesomeIcons.b),
+ onTap: () => videoIntroController.actionCoinVideo(),
+ selectStatus: videoIntroController.hasCoin.value,
+ loadingStatus: loadingStatus,
+ text: !loadingStatus
+ ? widget.videoDetail!.stat!.coin!.toString()
+ : '-'),
+ ),
+ Obx(
+ () => ActionItem(
+ icon: const Icon(FontAwesomeIcons.star),
+ selectIcon: const Icon(FontAwesomeIcons.solidStar),
+ // onTap: () => videoIntroController.actionFavVideo(),
+ onTap: () => showFavBottomSheet(),
+ selectStatus: videoIntroController.hasFav.value,
+ loadingStatus: loadingStatus,
+ text: !loadingStatus
+ ? widget.videoDetail!.stat!.favorite!.toString()
+ : '-'),
+ ),
+ ActionItem(
+ icon: const Icon(FontAwesomeIcons.shareFromSquare),
+ onTap: () => videoIntroController.actionShareVideo(),
+ selectStatus: false,
+ loadingStatus: loadingStatus,
+ text: !loadingStatus
+ ? widget.videoDetail!.stat!.share!.toString()
+ : '-'),
+ ],
),
);
});
@@ -431,10 +453,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.like!.toString()
- : '-',
+ loadingStatus: loadingStatus,
+ text:
+ !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
@@ -443,10 +464,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.coin!.toString()
- : '-',
+ loadingStatus: loadingStatus,
+ text:
+ !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
@@ -455,8 +475,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
+ loadingStatus: loadingStatus,
+ text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
@@ -468,57 +488,20 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
- loadingStatus: widget.loadingStatus,
- text: !widget.loadingStatus
- ? widget.videoDetail!.stat!.reply!.toString()
- : '-',
+ loadingStatus: loadingStatus,
+ text:
+ !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
- loadingStatus: widget.loadingStatus,
- // text: !widget.loadingStatus
+ loadingStatus: loadingStatus,
+ // text: !loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
-
- InlineSpan buildContent(BuildContext context, content) {
- String desc = content.desc;
- List descV2 = content.descV2;
- // type
- // 1 普通文本
- // 2 @用户
- List spanChilds = [];
- if (descV2.isNotEmpty) {
- for (var i = 0; i < descV2.length; i++) {
- if (descV2[i].type == 1) {
- spanChilds.add(TextSpan(text: descV2[i].rawText));
- } else if (descV2[i].type == 2) {
- spanChilds.add(
- TextSpan(
- text: '@${descV2[i].rawText}',
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- String heroTag = Utils.makeHeroTag(descV2[i].bizId);
- Get.toNamed(
- '/member?mid=${descV2[i].bizId}',
- arguments: {'face': '', 'heroTag': heroTag},
- );
- },
- ),
- );
- }
- }
- } else {
- spanChilds.add(TextSpan(text: desc));
- }
- return TextSpan(children: spanChilds);
- }
}
diff --git a/lib/pages/video/detail/introduction/widgets/fav_panel.dart b/lib/pages/video/detail/introduction/widgets/fav_panel.dart
index 44502b0e..68f53772 100644
--- a/lib/pages/video/detail/introduction/widgets/fav_panel.dart
+++ b/lib/pages/video/detail/introduction/widgets/fav_panel.dart
@@ -33,24 +33,13 @@ class _FavPanelState extends State {
child: Column(
children: [
AppBar(
- toolbarHeight: 50,
- automaticallyImplyLeading: false,
centerTitle: false,
- elevation: 1,
- title: Text(
- '选择文件夹',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- actions: [
- TextButton(
- onPressed: () async {
- feedBack();
- await widget.ctr!.actionFavVideo();
- },
- child: const Text('完成'),
- ),
- const SizedBox(width: 6),
- ],
+ elevation: 0,
+ leading: IconButton(
+ onPressed: () => Get.back(),
+ icon: const Icon(Icons.close_outlined)),
+ title:
+ Text('添加到收藏夹', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: Material(
@@ -63,45 +52,33 @@ class _FavPanelState extends State {
return Obx(
() => ListView.builder(
itemCount:
- widget.ctr!.favFolderData.value.list!.length + 1,
+ widget.ctr!.favFolderData.value.list!.length,
itemBuilder: (context, index) {
- if (index == 0) {
- return const SizedBox(height: 10);
- } else {
- return ListTile(
- onTap: () => widget.ctr!.onChoose(
- widget.ctr!.favFolderData.value
- .list![index - 1].favState !=
- 1,
- index - 1),
- dense: true,
- leading:
- const Icon(Icons.folder_special_outlined),
- minLeadingWidth: 0,
- title: Text(widget.ctr!.favFolderData.value
- .list![index - 1].title!),
- subtitle: Text(
- '${widget.ctr!.favFolderData.value.list![index - 1].mediaCount}个内容',
- style: TextStyle(
- color:
- Theme.of(context).colorScheme.outline,
- fontSize: Theme.of(context)
- .textTheme
- .labelSmall!
- .fontSize),
+ return ListTile(
+ onTap: () => widget.ctr!.onChoose(
+ widget.ctr!.favFolderData.value.list![index]
+ .favState !=
+ 1,
+ index),
+ dense: true,
+ leading: const Icon(Icons.folder_outlined),
+ minLeadingWidth: 0,
+ title: Text(widget.ctr!.favFolderData.value
+ .list![index].title!),
+ subtitle: Text(
+ '${widget.ctr!.favFolderData.value.list![index].mediaCount}个内容',
+ ),
+ trailing: Transform.scale(
+ scale: 0.9,
+ child: Checkbox(
+ value: widget.ctr!.favFolderData.value
+ .list![index].favState ==
+ 1,
+ onChanged: (bool? checkValue) =>
+ widget.ctr!.onChoose(checkValue!, index),
),
- trailing: Transform.scale(
- scale: 0.9,
- child: Checkbox(
- value: widget.ctr!.favFolderData.value
- .list![index - 1].favState ==
- 1,
- onChanged: (bool? checkValue) => widget.ctr!
- .onChoose(checkValue!, index - 1),
- ),
- ),
- );
- }
+ ),
+ );
},
),
);
@@ -119,6 +96,47 @@ class _FavPanelState extends State {
),
),
),
+ Divider(
+ height: 1,
+ color: Theme.of(context).disabledColor.withOpacity(0.08),
+ ),
+ Padding(
+ padding: EdgeInsets.only(
+ left: 20,
+ right: 20,
+ top: 12,
+ bottom: MediaQuery.of(context).padding.bottom + 12,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () => Get.back(),
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.only(left: 30, right: 30),
+ backgroundColor: Theme.of(context)
+ .colorScheme
+ .onInverseSurface, // 设置按钮背景色
+ ),
+ child: const Text('取消'),
+ ),
+ const SizedBox(width: 10),
+ TextButton(
+ onPressed: () async {
+ feedBack();
+ await widget.ctr!.actionFavVideo();
+ },
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.only(left: 30, right: 30),
+ foregroundColor: Theme.of(context).colorScheme.onPrimary,
+ backgroundColor:
+ Theme.of(context).colorScheme.primary, // 设置按钮背景色
+ ),
+ child: const Text('完成'),
+ ),
+ ],
+ ),
+ ),
],
),
);
diff --git a/lib/pages/video/detail/introduction/widgets/intro_detail.dart b/lib/pages/video/detail/introduction/widgets/intro_detail.dart
index ab07e456..7b1029f3 100644
--- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart
+++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart
@@ -27,19 +27,20 @@ class IntroDetail extends StatelessWidget {
height: sheetHeight,
child: Column(
children: [
- Container(
- height: 35,
- padding: const EdgeInsets.only(bottom: 2),
- child: Center(
- child: Container(
- width: 32,
- height: 3,
- decoration: BoxDecoration(
- color: Theme.of(context)
- .colorScheme
- .onSecondaryContainer
- .withOpacity(0.5),
- borderRadius: const BorderRadius.all(Radius.circular(3))),
+ InkWell(
+ onTap: () => Get.back(),
+ child: Container(
+ height: 35,
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Center(
+ child: Container(
+ width: 32,
+ height: 3,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primary,
+ borderRadius:
+ const BorderRadius.all(Radius.circular(3))),
+ ),
),
),
),
@@ -120,38 +121,33 @@ class IntroDetail extends StatelessWidget {
}
InlineSpan buildContent(BuildContext context, content) {
- String desc = content.desc;
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
- List spanChilds = [];
- if (descV2.isNotEmpty) {
- for (var i = 0; i < descV2.length; i++) {
- if (descV2[i].type == 1) {
- spanChilds.add(TextSpan(text: descV2[i].rawText));
- } else if (descV2[i].type == 2) {
- spanChilds.add(
- TextSpan(
- text: '@${descV2[i].rawText}',
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- String heroTag = Utils.makeHeroTag(descV2[i].bizId);
- Get.toNamed(
- '/member?mid=${descV2[i].bizId}',
- arguments: {'face': '', 'heroTag': heroTag},
- );
- },
- ),
+ List spanChilds = List.generate(descV2.length, (index) {
+ final currentDesc = descV2[index];
+ switch (currentDesc.type) {
+ case 1:
+ return TextSpan(text: currentDesc.rawText);
+ case 2:
+ final colorSchemePrimary = Theme.of(context).colorScheme.primary;
+ final heroTag = Utils.makeHeroTag(currentDesc.bizId);
+ return TextSpan(
+ text: '@${currentDesc.rawText}',
+ style: TextStyle(color: colorSchemePrimary),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ Get.toNamed(
+ '/member?mid=${currentDesc.bizId}',
+ arguments: {'face': '', 'heroTag': heroTag},
+ );
+ },
);
- }
+ default:
+ return const TextSpan();
}
- } else {
- spanChilds.add(TextSpan(text: desc));
- }
+ });
return TextSpan(children: spanChilds);
}
}
diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart
new file mode 100644
index 00000000..261b6227
--- /dev/null
+++ b/lib/pages/video/detail/introduction/widgets/page.dart
@@ -0,0 +1,203 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/models/video_detail_res.dart';
+
+class PagesPanel extends StatefulWidget {
+ final List pages;
+ final int? cid;
+ final double? sheetHeight;
+ final Function? changeFuc;
+
+ const PagesPanel({
+ super.key,
+ required this.pages,
+ this.cid,
+ this.sheetHeight,
+ this.changeFuc,
+ });
+
+ @override
+ State createState() => _PagesPanelState();
+}
+
+class _PagesPanelState extends State {
+ late List episodes;
+ late int currentIndex;
+
+ @override
+ void initState() {
+ super.initState();
+ episodes = widget.pages;
+ currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
+ }
+
+ void changeFucCall(item, i) async {
+ await widget.changeFuc!(
+ item.cid,
+ );
+ currentIndex = i;
+ setState(() {});
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 10, bottom: 2),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('视频选集 '),
+ Expanded(
+ child: Text(
+ ' 正在播放:${widget.pages[currentIndex].pagePart}',
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ),
+ const SizedBox(width: 10),
+ SizedBox(
+ height: 34,
+ child: TextButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () {
+ showBottomSheet(
+ context: context,
+ builder: (_) => Container(
+ height: widget.sheetHeight,
+ color: Theme.of(context).colorScheme.background,
+ child: Column(
+ children: [
+ Container(
+ height: 45,
+ padding:
+ const EdgeInsets.only(left: 14, right: 14),
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '合集(${episodes.length})',
+ style:
+ Theme.of(context).textTheme.titleMedium,
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ ),
+ ),
+ Divider(
+ height: 1,
+ color: Theme.of(context)
+ .dividerColor
+ .withOpacity(0.1),
+ ),
+ Expanded(
+ child: Material(
+ child: ListView.builder(
+ itemCount: episodes.length,
+ itemBuilder: (context, index) {
+ return InkWell(
+ onTap: () {
+ changeFucCall(episodes[index], index);
+ Get.back();
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(
+ top: 10,
+ bottom: 10,
+ left: 15,
+ right: 15),
+ child: Text(
+ episodes[index].pagePart!,
+ style: TextStyle(
+ color: index == currentIndex
+ ? Theme.of(context)
+ .colorScheme
+ .primary
+ : Theme.of(context)
+ .colorScheme
+ .onSurface),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ child: Text(
+ '共${widget.pages.length}集',
+ style: const TextStyle(fontSize: 13),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Container(
+ height: 35,
+ margin: const EdgeInsets.only(bottom: 8),
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: widget.pages.length,
+ itemExtent: 150,
+ itemBuilder: ((context, i) {
+ return Container(
+ width: 150,
+ margin: const EdgeInsets.only(right: 10),
+ child: Material(
+ color: Theme.of(context).colorScheme.onInverseSurface,
+ borderRadius: BorderRadius.circular(6),
+ clipBehavior: Clip.hardEdge,
+ child: InkWell(
+ onTap: () => changeFucCall(widget.pages[i], i),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 8, horizontal: 8),
+ child: Row(
+ children: [
+ if (i == currentIndex) ...[
+ Image.asset(
+ 'assets/images/live.gif',
+ color: Theme.of(context).colorScheme.primary,
+ height: 12,
+ ),
+ const SizedBox(width: 6)
+ ],
+ Expanded(
+ child: Text(
+ widget.pages[i].pagePart!,
+ maxLines: 1,
+ style: TextStyle(
+ fontSize: 13,
+ color: i == currentIndex
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).colorScheme.onSurface),
+ overflow: TextOverflow.ellipsis,
+ ))
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }),
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart
index dd478b83..876dac74 100644
--- a/lib/pages/video/detail/introduction/widgets/season.dart
+++ b/lib/pages/video/detail/introduction/widgets/season.dart
@@ -38,6 +38,7 @@ class _SeasonPanelState extends State {
item.cid,
item.aid,
);
+ currentIndex = i;
Get.back();
}
diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart
index bc2c326c..73c6e289 100644
--- a/lib/pages/video/detail/related/view.dart
+++ b/lib/pages/video/detail/related/view.dart
@@ -6,71 +6,74 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart';
-class RelatedVideoPanel extends GetView {
+class RelatedVideoPanel extends StatefulWidget {
const RelatedVideoPanel({super.key});
+ @override
+ State createState() => _RelatedVideoPanelState();
+}
+
+class _RelatedVideoPanelState extends State {
+ final ReleatedController _releatedController =
+ Get.put(ReleatedController(), tag: Get.arguments['heroTag']);
@override
Widget build(BuildContext context) {
- return GetBuilder(
- init: ReleatedController(),
- id: Get.arguments['heroTag'],
- builder: (context) {
- return FutureBuilder(
- future: ReleatedController().queryRelatedVideo(),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- if (snapshot.data!['status']) {
- // 请求成功
- return SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- if (index == snapshot.data['data'].length) {
- return SizedBox(
- height: MediaQuery.of(context).padding.bottom);
- } else {
- return Material(
- child: VideoCardH(
- videoItem: snapshot.data['data'][index],
- longPress: () {
- try {
- ReleatedController().popupDialog =
- _createPopupDialog(
- snapshot.data['data'][index]);
- Overlay.of(context)
- .insert(ReleatedController().popupDialog!);
- } catch (_) {
- return {};
- }
- },
- longPressEnd: () {
- ReleatedController().popupDialog?.remove();
- },
- ),
- );
- }
- }, childCount: snapshot.data['data'].length + 1));
- } else {
- // 请求错误
- return const Center(
- child: Text('出错了'),
- );
- }
+ return FutureBuilder(
+ future: _releatedController.queryRelatedVideo(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data!['status']) {
+ // 请求成功
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ if (index == snapshot.data['data'].length) {
+ return SizedBox(height: MediaQuery.of(context).padding.bottom);
} else {
- // 骨架屏
- return SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return const VideoCardHSkeleton();
- }, childCount: 5),
+ return Material(
+ child: VideoCardH(
+ videoItem: snapshot.data['data'][index],
+ longPress: () {
+ try {
+ _releatedController.popupDialog =
+ _createPopupDialog(snapshot.data['data'][index]);
+ Overlay.of(context)
+ .insert(_releatedController.popupDialog!);
+ } catch (err) {
+ return {};
+ }
+ },
+ longPressEnd: () {
+ _releatedController.popupDialog?.remove();
+ },
+ ),
);
}
- },
+ }, childCount: snapshot.data['data'].length + 1));
+ } else {
+ // 请求错误
+ return const Center(
+ child: Text('出错了'),
+ );
+ }
+ } else {
+ // 骨架屏
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ return const VideoCardHSkeleton();
+ }, childCount: 5),
);
- });
+ }
+ },
+ );
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
- child: OverlayPop(videoItem: videoItem),
+ closeFn: _releatedController.popupDialog?.remove,
+ child: OverlayPop(
+ videoItem: videoItem,
+ closeFn: _releatedController.popupDialog?.remove),
),
);
}
diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart
index 0f2e35b7..7aab2a13 100644
--- a/lib/pages/video/detail/reply/widgets/reply_item.dart
+++ b/lib/pages/video/detail/reply/widgets/reply_item.dart
@@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
+import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
@@ -139,7 +140,13 @@ class ReplyItem extends StatelessWidget {
height: 11,
),
const SizedBox(width: 6),
- if (replyItem!.isUp!) const UpTag(),
+ if (replyItem!.isUp!)
+ const PBadge(
+ text: 'UP',
+ size: 'small',
+ stack: 'normal',
+ fs: 9,
+ ),
],
),
Positioned(
@@ -208,8 +215,15 @@ class ReplyItem extends StatelessWidget {
children: [
if (replyItem!.isTop!)
const WidgetSpan(
- alignment: PlaceholderAlignment.top,
- child: UpTag(tagText: 'TOP')),
+ alignment: PlaceholderAlignment.top,
+ child: PBadge(
+ text: 'TOP',
+ size: 'small',
+ stack: 'normal',
+ type: 'line',
+ fs: 9,
+ ),
+ ),
buildContent(context, replyItem!, replyReply, null),
],
),
@@ -391,7 +405,13 @@ class ReplyItemRow extends StatelessWidget {
),
if (replies![i].isUp)
const WidgetSpan(
- child: UpTag(),
+ alignment: PlaceholderAlignment.top,
+ child: PBadge(
+ text: 'UP',
+ size: 'small',
+ stack: 'normal',
+ fs: 9,
+ ),
),
buildContent(
context, replies![i], replyReply, replyItem),
@@ -758,32 +778,3 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds);
}
-
-class UpTag extends StatelessWidget {
- final String? tagText;
- const UpTag({super.key, this.tagText = 'UP'});
- @override
- Widget build(BuildContext context) {
- Color primary = Theme.of(context).colorScheme.primary;
- return Container(
- width: 24,
- height: 14,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(3),
- color: tagText == 'UP' ? primary : null,
- border: Border.all(color: primary)),
- margin: const EdgeInsets.only(right: 4),
- child: Center(
- child: Text(
- tagText!,
- style: TextStyle(
- fontSize: 9,
- color: tagText == 'UP'
- ? Theme.of(context).colorScheme.onPrimary
- : primary,
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/video/detail/replyNew/view.dart b/lib/pages/video/detail/replyNew/view.dart
index f86683a0..6f538e4e 100644
--- a/lib/pages/video/detail/replyNew/view.dart
+++ b/lib/pages/video/detail/replyNew/view.dart
@@ -118,7 +118,7 @@ class _VideoReplyNewDialogState extends State
@override
Widget build(BuildContext context) {
return Container(
- height: 400,
+ height: 500,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
diff --git a/lib/pages/video/detail/replyReply/controller.dart b/lib/pages/video/detail/replyReply/controller.dart
index 7bad5069..bd5cfd1f 100644
--- a/lib/pages/video/detail/replyReply/controller.dart
+++ b/lib/pages/video/detail/replyReply/controller.dart
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_type.dart';
-import 'package:pilipala/models/video/reply/data.dart';
import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyReplyController extends GetxController {
diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart
index f1c38b80..ddb53a79 100644
--- a/lib/pages/video/detail/view.dart
+++ b/lib/pages/video/detail/view.dart
@@ -1,11 +1,13 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
+import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
@@ -49,25 +51,7 @@ class _VideoDetailPageState extends State
void initState() {
super.initState();
plPlayerController = videoDetailController.plPlayerController;
- plPlayerController!.onPlayerStatusChanged.listen(
- (PlayerStatus status) {
- videoDetailController.markHeartBeat();
- playerStatus = status;
- if (status == PlayerStatus.playing) {
- videoDetailController.isShowCover.value = false;
- videoDetailController.loopHeartBeat();
- } else {
- videoDetailController.timer!.cancel();
- // 播放完成停止 or 切换下一个
- if (status == PlayerStatus.completed) {
- // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
- plPlayerController!.seekTo(Duration.zero);
- plPlayerController!.onLockControl(false);
- plPlayerController!.videoPlayerController!.pause();
- }
- }
- },
- );
+ playerListener();
appbarStream = StreamController();
@@ -82,6 +66,26 @@ class _VideoDetailPageState extends State
_futureBuilderFuture = videoDetailController.queryVideoUrl();
}
+ // 播放器状态监听
+ void playerListener() {
+ plPlayerController!.onPlayerStatusChanged.listen(
+ (PlayerStatus status) async {
+ playerStatus = status;
+ if (status == PlayerStatus.playing) {
+ videoDetailController.isShowCover.value = false;
+ } else {
+ // 播放完成停止 or 切换下一个
+ if (status == PlayerStatus.completed) {
+ // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
+ plPlayerController!.seekTo(Duration.zero);
+ plPlayerController!.onLockControl(false);
+ plPlayerController!.videoPlayerController!.pause();
+ }
+ }
+ },
+ );
+ }
+
// 继续播放或重新播放
void continuePlay() async {
await _extendNestCtr.animateTo(0,
@@ -91,19 +95,15 @@ class _VideoDetailPageState extends State
@override
void dispose() {
+ plPlayerController!.pause();
plPlayerController!.dispose();
- if (videoDetailController.timer != null) {
- videoDetailController.timer!.cancel();
- }
super.dispose();
}
@override
// 离开当前页面时
void didPushNext() async {
- if (videoDetailController.timer!.isActive) {
- videoDetailController.timer!.cancel();
- }
+ videoDetailController.defaultST = plPlayerController!.position.value;
plPlayerController!.pause();
super.didPushNext();
}
@@ -111,13 +111,11 @@ class _VideoDetailPageState extends State