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第三方客户端

+
+ home + home + home +
+
+
-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 @override // 返回当前页面时 void didPopNext() async { + videoDetailController.playerInit(); if (_extendNestCtr.position.pixels == 0) { await Future.delayed(const Duration(milliseconds: 300)); plPlayerController!.play(); } - if (!videoDetailController.timer!.isActive) { - videoDetailController.loopHeartBeat(); - } super.didPopNext(); } @@ -156,8 +154,11 @@ class _VideoDetailPageState extends State scrolledUnderElevation: 0, forceElevated: innerBoxIsScrolled, expandedHeight: videoHeight, - // backgroundColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: + MediaQuery.of(Get.context!).platformBrightness == + Brightness.dark + ? Colors.black + : Theme.of(context).colorScheme.background, flexibleSpace: FlexibleSpaceBar( background: Padding( padding: EdgeInsets.only(top: statusBarHeight), @@ -226,10 +227,17 @@ class _VideoDetailPageState extends State backgroundColor: Colors.transparent, actions: [ - /// TODO IconButton( tooltip: '稍后再看', - onPressed: () {}, + onPressed: () async { + var res = await UserHttp + .toViewLater( + bvid: + videoDetailController + .bvid); + SmartDialog.showToast( + res['msg']); + }, icon: const Icon(Icons .history_outlined)) ], @@ -284,39 +292,20 @@ class _VideoDetailPageState extends State children: [ Opacity( opacity: 0, - child: Container( + child: SizedBox( width: double.infinity, height: 0, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context) - .dividerColor - .withOpacity(0.1), - ), + child: Obx( + () => TabBar( + controller: videoDetailController.tabCtr, + dividerColor: Colors.transparent, + indicatorColor: + Theme.of(context).colorScheme.background, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - width: 280, - margin: const EdgeInsets.only(left: 20), - child: Obx( - () => TabBar( - controller: videoDetailController.tabCtr, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).colorScheme.background, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), - ), - ), - ], - ), ), ), Expanded( @@ -333,20 +322,30 @@ class _VideoDetailPageState extends State const VideoIntroPanel(), ] else if (videoDetailController.videoType == SearchType.media_bangumi) ...[ - const BangumiIntroPanel() + BangumiIntroPanel( + cid: videoDetailController.cid) ], - if (videoDetailController.videoType == - SearchType.video) ...[ - SliverPersistentHeader( - floating: true, - pinned: true, - delegate: SliverHeaderDelegate( - height: 50, - child: - const MenuRow(loadingStatus: false), - ), + // if (videoDetailController.videoType == + // SearchType.video) ...[ + // SliverPersistentHeader( + // floating: true, + // pinned: true, + // delegate: SliverHeaderDelegate( + // height: 50, + // child: + // const MenuRow(loadingStatus: false), + // ), + // ), + // ], + SliverToBoxAdapter( + child: Divider( + indent: 12, + endIndent: 12, + color: Theme.of(context) + .dividerColor + .withOpacity(0.06), ), - ], + ), const RelatedVideoPanel(), ], ); diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index bedcd008..353371cd 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -28,6 +28,7 @@ class _HeaderControlState extends State { late PlayUrlModel videoInfo; List playSpeed = PlaySpeed.values; TextStyle subTitleStyle = const TextStyle(fontSize: 12); + TextStyle titleStyle = const TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); @override @@ -81,7 +82,7 @@ class _HeaderControlState extends State { enabled: false, leading: const Icon(Icons.network_cell_outlined, size: 20), - title: const Text('省流模式'), + title: Text('省流模式', style: titleStyle), subtitle: Text('低画质 | 减少视频缓存', style: subTitleStyle), trailing: Transform.scale( scale: 0.75, @@ -99,22 +100,22 @@ class _HeaderControlState extends State { ), ), ), - Obx( - () => ListTile( - onTap: () => {Get.back(), showSetSpeedSheet()}, - dense: true, - leading: const Icon(Icons.speed_outlined, size: 20), - title: const Text('播放速度'), - subtitle: Text( - '当前倍速 x${widget.controller!.playbackSpeed}', - style: subTitleStyle), - ), - ), + // Obx( + // () => ListTile( + // onTap: () => {Get.back(), showSetSpeedSheet()}, + // dense: true, + // leading: const Icon(Icons.speed_outlined, size: 20), + // title: Text('播放速度', style: titleStyle), + // subtitle: Text( + // '当前倍速 x${widget.controller!.playbackSpeed}', + // style: subTitleStyle), + // ), + // ), ListTile( onTap: () => {Get.back(), showSetVideoQa()}, dense: true, leading: const Icon(Icons.play_circle_outline, size: 20), - title: const Text('选择画质'), + title: Text('选择画质', style: titleStyle), subtitle: Text( '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}', style: subTitleStyle), @@ -123,24 +124,33 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetAudioQa()}, dense: true, leading: const Icon(Icons.album_outlined, size: 20), - title: const Text('选择音质'), + title: Text('选择音质', style: titleStyle), subtitle: Text( '当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}', style: subTitleStyle), ), ListTile( - onTap: () {}, + onTap: () => {Get.back(), showSetDecodeFormats()}, dense: true, - enabled: false, - leading: const Icon(Icons.play_circle_outline, size: 20), - title: const Text('播放设置'), + leading: const Icon(Icons.av_timer_outlined, size: 20), + title: Text('解码格式', style: titleStyle), + subtitle: Text( + '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', + style: subTitleStyle), ), + // ListTile( + // onTap: () {}, + // dense: true, + // enabled: false, + // leading: const Icon(Icons.play_circle_outline, size: 20), + // title: Text('播放设置', style: titleStyle), + // ), ListTile( onTap: () {}, dense: true, enabled: false, leading: const Icon(Icons.subtitles_outlined, size: 20), - title: const Text('弹幕设置'), + title: Text('弹幕设置', style: titleStyle), ), ], ), @@ -250,7 +260,7 @@ class _HeaderControlState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('选择画质'), + Text('选择画质', style: titleStyle), const SizedBox(width: 4), Icon( Icons.info_outline, @@ -309,7 +319,6 @@ class _HeaderControlState extends State { /// 选择音质 void showSetAudioQa() { - List videoFormat = videoInfo.supportFormats!; AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa; List audio = videoInfo.dash!.audio!; @@ -329,7 +338,9 @@ class _HeaderControlState extends State { margin: const EdgeInsets.all(12), child: Column( children: [ - const SizedBox(height: 45, child: Center(child: Text('选择音质'))), + SizedBox( + height: 45, + child: Center(child: Text('选择音质', style: titleStyle))), Expanded( child: Material( child: ListView( @@ -370,6 +381,74 @@ class _HeaderControlState extends State { ); } + // 选择解码格式 + void showSetDecodeFormats() { + // 当前选中的解码格式 + VideoDecodeFormats currentDecodeFormats = + widget.videoDetailCtr!.currentDecodeFormats; + // 当前视频可用的解码格式 + List videoFormat = videoInfo.supportFormats!; + List list = videoFormat.first.codecs!; + + showModalBottomSheet( + context: context, + elevation: 0, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + width: double.infinity, + height: 250, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + child: Column( + children: [ + SizedBox( + height: 45, + child: Center(child: Text('选择解码格式', style: titleStyle))), + Expanded( + child: Material( + child: ListView( + children: [ + for (var i in list) ...[ + ListTile( + onTap: () { + widget.videoDetailCtr!.currentDecodeFormats = + VideoDecodeFormatsCode.fromString(i)!; + widget.videoDetailCtr!.updatePlayer(); + Get.back(); + }, + dense: true, + contentPadding: + const EdgeInsets.only(left: 20, right: 20), + title: Text(VideoDecodeFormatsCode.fromString(i)! + .description!), + subtitle: Text( + i!, + style: subTitleStyle, + ), + trailing: i.startsWith(currentDecodeFormats.code) + ? Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ) + : const SizedBox(), + ), + ] + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final _ = widget.controller!; @@ -403,7 +482,11 @@ class _HeaderControlState extends State { size: 15, color: Colors.white, ), - fuc: () => Get.offAll(const MainApp()), + fuc: () { + // 销毁播放器实例 + widget.controller!.dispose(type: 'all'); + Get.offAll(const MainApp()); + }, ), const Spacer(), // ComBtn( diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index 6411ce55..b5b50152 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -1,17 +1,15 @@ // ignore_for_file: avoid_print +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/constants.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/user.dart'; -import 'package:pilipala/pages/dynamics/index.dart'; -import 'package:pilipala/pages/mine/index.dart'; -import 'package:pilipala/pages/rcmd/controller.dart'; +import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/utils/cookie.dart'; +import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/storage.dart'; -import 'package:webview_cookie_manager/webview_cookie_manager.dart'; import 'package:webview_flutter/webview_flutter.dart'; class WebviewController extends GetxController { @@ -21,6 +19,7 @@ class WebviewController extends GetxController { final WebViewController controller = WebViewController(); RxInt loadProgress = 0.obs; RxBool loadShow = true.obs; + EventBus eventBus = EventBus(); @override void onInit() { @@ -29,17 +28,17 @@ class WebviewController extends GetxController { type = Get.parameters['type']!; pageTitle = Get.parameters['pageTitle']!; - webviewInit(); if (type == 'login') { controller.clearCache(); controller.clearLocalStorage(); WebViewCookieManager().clearCookies(); - controller.setUserAgent(Request().headerUa('mob')); } + webviewInit(); } webviewInit() { controller + ..setUserAgent(Request().headerUa('mob')) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( @@ -50,39 +49,53 @@ class WebviewController extends GetxController { }, onPageStarted: (String url) {}, // 加载完成 - onPageFinished: (String url) async { + onUrlChange: (UrlChange urlChange) async { loadShow.value = false; + String url = urlChange.url ?? ''; if (type == 'login' && (url.startsWith( 'https://passport.bilibili.com/web/sso/exchange_cookie') || url.startsWith('https://m.bilibili.com/'))) { try { - var cookies = - await WebviewCookieManager().getCookies(HttpString.baseUrl); - var apiCookies = await WebviewCookieManager() - .getCookies(HttpString.baseApiUrl); - var tCookies = - await WebviewCookieManager().getCookies(HttpString.tUrl); - await SetCookie.onSet(cookies, HttpString.baseUrl); - await SetCookie.onSet(apiCookies, HttpString.baseApiUrl); - await SetCookie.onSet(tCookies, HttpString.tUrl); - await UserHttp.userInfo(); + await SetCookie.onSet(); var result = await UserHttp.userInfo(); + UserHttp.thirdLogin(); print('网页登录: $result'); if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功'); - Box user = GStrorage.user; - user.put(UserBoxKey.userLogin, true); - user.put(UserBoxKey.userName, result['data'].uname); - user.put(UserBoxKey.userFace, result['data'].face); - user.put(UserBoxKey.userMid, result['data'].mid); - Box userInfoCache = GStrorage.userInfo; - userInfoCache.put('userInfoCache', result['data']); - Get.find().userInfo.value = result['data']; - Get.find().onInit(); - Get.find().queryRcmdFeed('onRefresh'); - Get.find().queryFollowDynamic(); + try { + Box user = GStrorage.user; + user.put(UserBoxKey.userLogin, true); + user.put(UserBoxKey.userName, result['data'].uname); + user.put(UserBoxKey.userFace, result['data'].face); + user.put(UserBoxKey.userMid, result['data'].mid); + + Box userInfoCache = GStrorage.userInfo; + userInfoCache.put('userInfoCache', result['data']); + + // 通知更新 + eventBus.emit(EventName.loginEvent, {'status': true}); + + HomeController homeCtr = Get.find(); + homeCtr.updateLoginStatus(true); + } catch (err) { + SmartDialog.show(builder: (context) { + return AlertDialog( + title: const Text('登录遇到问题'), + content: Text(err.toString()), + actions: [ + TextButton( + onPressed: () => controller.reload(), + child: const Text('确认'), + ) + ], + ); + }); + } Get.back(); + } else { + // 获取用户信息失败 + SmartDialog.showToast(result.msg); } } catch (e) { print(e); diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 50be2477..10e8f7dd 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1,19 +1,22 @@ +// ignore_for_file: avoid_print + import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:flutter/painting.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/plugin/pl_player/models/data_source.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; import 'package:universal_platform/universal_platform.dart'; -import 'package:volume_controller/volume_controller.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; +// import 'package:wakelock_plus/wakelock_plus.dart'; import 'models/data_status.dart'; import 'models/play_speed.dart'; @@ -25,6 +28,9 @@ class PlPlayerController { Player? _videoPlayerController; VideoController? _videoController; + // 添加一个私有静态变量来保存实例 + static PlPlayerController? _instance; + // 流事件 监听播放状态变化 StreamSubscription? _playerEventSubs; @@ -34,7 +40,7 @@ class PlPlayerController { /// final PlPlayerDataStatus dataStatus = PlPlayerDataStatus(); - bool controlsEnabled = true; + // bool controlsEnabled = false; /// 响应数据 // 播放位置 @@ -44,6 +50,8 @@ class PlPlayerController { final Rx _duration = Rx(Duration.zero); final Rx _buffered = Rx(Duration.zero); + final Rx _playerCount = Rx(0); + final Rx _playbackSpeed = 1.0.obs; final Rx _currentVolume = 1.0.obs; final Rx _currentBrightness = 0.0.obs; @@ -55,18 +63,27 @@ class PlPlayerController { final Rx _doubleSpeedStatus = false.obs; final Rx _controlsLock = false.obs; final Rx _isFullScreen = false.obs; + // 默认投稿视频格式 + static Rx _videoType = 'archive'.obs; final Rx _direction = 'horizontal'.obs; Rx videoFitChanged = false.obs; - final Rx _videoFit = Rx(BoxFit.fill); + final Rx _videoFit = Rx(BoxFit.contain); /// + // ignore: prefer_final_fields Rx _isSliderMoving = false.obs; PlaylistMode _looping = PlaylistMode.none; bool _autoPlay = false; final bool _listenersInitialized = false; + // 记录历史记录 + String _bvid = ''; + int _cid = 0; + int _heartDuration = 0; + bool _enableHeart = true; + Timer? _timer; Timer? _timerForSeek; Timer? _timerForVolume; @@ -77,13 +94,13 @@ class PlPlayerController { // final Durations durations; - List fits = [ - BoxFit.contain, - BoxFit.cover, - BoxFit.fill, - BoxFit.fitHeight, - BoxFit.fitWidth, - BoxFit.scaleDown + List> videoFitType = [ + {'attr': BoxFit.contain, 'desc': '包含'}, + {'attr': BoxFit.cover, 'desc': '覆盖'}, + {'attr': BoxFit.fill, 'desc': '填充'}, + {'attr': BoxFit.fitHeight, 'desc': '高度适应'}, + {'attr': BoxFit.fitWidth, 'desc': '宽度适应'}, + {'attr': BoxFit.scaleDown, 'desc': '缩小适应'}, ]; PreferredSizeWidget? headerControl; @@ -172,10 +189,27 @@ class PlPlayerController { /// 全屏方向 Rx get direction => _direction; - PlPlayerController({ - // 直播间 传false 关闭控制栏 - this.controlsEnabled = true, - this.fits = const [ + Rx get playerCount => _playerCount; + + /// + Rx get videoType => _videoType; + + // 添加一个私有构造函数 + PlPlayerController._() { + _videoType = videoType; + // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { + // if (status == PlayerStatus.playing) { + // WakelockPlus.enable(); + // } else { + // WakelockPlus.disable(); + // } + // }); + } + + // 获取实例 传参 + static PlPlayerController getInstance({ + String videoType = 'archive', + List fits = const [ BoxFit.contain, BoxFit.cover, BoxFit.fill, @@ -184,14 +218,11 @@ class PlPlayerController { BoxFit.scaleDown ], }) { - controlsEnabled = controlsEnabled; - _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { - if (status == PlayerStatus.playing) { - WakelockPlus.enable(); - } else { - WakelockPlus.enable(); - } - }); + // 如果实例尚未创建,则创建一个新实例 + _instance ??= PlPlayerController._(); + _instance!._playerCount.value += 1; + _videoType.value = videoType; + return _instance!; } // 初始化资源 @@ -211,19 +242,24 @@ class PlPlayerController { Duration? duration, // 方向 String? direction, - // 全屏模式 + // 记录历史记录 + String bvid = '', + int cid = 0, + // 历史记录开关 + bool enableHeart = true, }) async { try { _autoPlay = autoplay; _looping = looping; - // 初始化视频时长 - _duration.value = duration ?? Duration.zero; // 初始化视频倍速 _playbackSpeed.value = speed; // 初始化数据加载状态 dataStatus.status.value = DataStatus.loading; // 初始化全屏方向 _direction.value = direction ?? 'horizontal'; + _bvid = bvid; + _cid = cid; + _enableHeart = enableHeart; if (_videoPlayerController != null && _videoPlayerController!.state.playing) { @@ -234,7 +270,7 @@ class PlPlayerController { _videoPlayerController = await _createVideoController( dataSource, _looping, enableHA, width, height); // 获取视频时长 00:00 - _duration.value = _videoPlayerController!.state.duration; + _duration.value = duration ?? _videoPlayerController!.state.duration; // 数据加载完成 dataStatus.status.value = DataStatus.loaded; @@ -258,6 +294,13 @@ class PlPlayerController { double? width, double? height, ) async { + // 每次配置时先移除监听 + removeListeners(); + isBuffering.value = false; + buffered.value = Duration.zero; + _heartDuration = 0; + _position.value = Duration.zero; + Player player = _videoPlayerController ?? Player( configuration: const PlayerConfiguration( @@ -364,6 +407,7 @@ class PlPlayerController { } else { // playerStatus.status.value = PlayerStatus.paused; } + makeHeartBeat(_position.value.inSeconds, type: 'status'); }), videoPlayerController!.stream.completed.listen((event) { if (event) { @@ -371,12 +415,14 @@ class PlPlayerController { } else { // playerStatus.status.value = PlayerStatus.playing; } + makeHeartBeat(_position.value.inSeconds, type: 'status'); }), videoPlayerController!.stream.position.listen((event) { _position.value = event; if (!isSliderMoving.value) { _sliderPosition.value = event; } + makeHeartBeat(event.inSeconds); }), videoPlayerController!.stream.duration.listen((event) { duration.value = event; @@ -413,7 +459,7 @@ class PlPlayerController { } _position.value = position; if (duration.value.inSeconds != 0) { - // await _videoPlayerController!.stream.buffer.first; + await _videoPlayerController!.stream.buffer.first; await _videoPlayerController?.seek(position); // if (playerStatus.stopped) { // play(); @@ -521,7 +567,10 @@ class PlPlayerController { /// 音量 Future getCurrentVolume() async { - _currentVolume.value = await VolumeController().getVolume(); + // mac try...catch + try { + _currentVolume.value = (await FlutterVolumeController.getVolume())!; + } catch (_) {} } Future setVolume(double volumeNew, @@ -537,7 +586,8 @@ class PlPlayerController { volume.value = volumeNew; try { - VolumeController().setVolume(volumeNew, showSystemUI: false); + FlutterVolumeController.showSystemUI = false; + await FlutterVolumeController.setVolume(volumeNew); } catch (err) { print(err); } @@ -583,10 +633,17 @@ class PlPlayerController { void toggleVideoFit() { videoFitChangedTimer?.cancel(); videoFitChanged.value = true; - if (fits.indexOf(_videoFit.value) < fits.length - 1) { - _videoFit.value = fits[fits.indexOf(_videoFit.value) + 1]; + // 范围内 + List attrs = videoFitType.map((e) => e['attr']).toList(); + if (attrs.indexOf(_videoFit.value) < attrs.length - 1) { + int index = attrs.indexOf(_videoFit.value); + _videoFit.value = attrs[index + 1]; + print(videoFitType[index + 1]['desc']); + SmartDialog.showToast(videoFitType[index + 1]['desc']); } else { - _videoFit.value = fits[0]; + // 默认 contain + _videoFit.value = videoFitType.first['attr']; + SmartDialog.showToast(videoFitType.first['desc']); } videoFitChangedTimer = Timer(const Duration(seconds: 1), () { videoFitChangedTimer = null; @@ -607,9 +664,10 @@ class PlPlayerController { /// 读取fit Future getVideoFit() async { - String fitValue = videoStorage.get(VideoBoxKey.videoBrightness, - defaultValue: 'fitHeight'); - _videoFit.value = fits.firstWhere((element) => element.name == fitValue); + String fitValue = + videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain'); + _videoFit.value = videoFitType + .firstWhere((element) => element['attr'] == fitValue)['attr']; } /// 缓存亮度 @@ -630,9 +688,18 @@ class PlPlayerController { } } - /// 设置长按倍速状态 + /// 设置长按倍速状态 live模式下禁用 void setDoubleSpeedStatus(bool val) { + if (videoType.value == 'live') { + return; + } _doubleSpeedStatus.value = val; + double currentSpeed = playbackSpeed; + if (val) { + setPlaybackSpeed(currentSpeed * 2); + } else { + setPlaybackSpeed(currentSpeed / 2); + } } /// 关闭控制栏 @@ -662,7 +729,41 @@ class PlPlayerController { videoFitChangedTimer?.cancel(); } - Future dispose() async { + // 记录播放记录 + Future makeHeartBeat(progress, {type = 'playing'}) async { + if (!_enableHeart) { + return false; + } + // 播放状态变化时,更新 + if (type == 'status') { + await VideoHttp.heartBeat( + bvid: _bvid, + cid: _cid, + progress: + playerStatus.status.value == PlayerStatus.completed ? -1 : progress, + ); + } else + // 正常播放时,间隔5秒更新一次 + if (progress - _heartDuration >= 5) { + _heartDuration = progress; + await VideoHttp.heartBeat( + bvid: _bvid, + cid: _cid, + progress: progress, + ); + } + } + + Future dispose({String type = 'single'}) async { + // 每次减1,最后销毁 + if (type == 'single') { + _playerCount.value -= 1; + _heartDuration = 0; + if (playerCount.value > 0) { + return; + } + } + _timer?.cancel(); _timerForVolume?.cancel(); _timerForGettingVolume?.cancel(); @@ -685,5 +786,6 @@ class PlPlayerController { removeListeners(); await _videoPlayerController?.dispose(); _videoPlayerController = null; + _instance = null; } } diff --git a/lib/plugin/pl_player/models/fullscreen_mode.dart b/lib/plugin/pl_player/models/fullscreen_mode.dart index 1080b6c6..9b5028e7 100644 --- a/lib/plugin/pl_player/models/fullscreen_mode.dart +++ b/lib/plugin/pl_player/models/fullscreen_mode.dart @@ -7,3 +7,20 @@ enum FullScreenMode { // 始终横屏 horizontal } + +extension FullScreenModeDesc on FullScreenMode { + String get description => ['自适应', '始终竖屏', '始终横屏'][index]; +} + +extension FullScreenModeCode on FullScreenMode { + static final List _codeList = [0, 1, 2]; + int get code => _codeList[index]; + + static FullScreenMode? fromCode(int code) { + final index = _codeList.indexOf(code); + if (index != -1) { + return FullScreenMode.values[index]; + } + return null; + } +} diff --git a/lib/plugin/pl_player/utils/fullscreen.dart b/lib/plugin/pl_player/utils/fullscreen.dart index 6139416d..46540bbd 100644 --- a/lib/plugin/pl_player/utils/fullscreen.dart +++ b/lib/plugin/pl_player/utils/fullscreen.dart @@ -1,16 +1,36 @@ import 'dart:io'; -import 'package:device_info_plus/device_info_plus.dart'; -// import 'package:auto_orientation/auto_orientation.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; //横屏 -/// 低版本xcode不支持auto_orientation -// Future landScape() async { -// if (Platform.isAndroid || Platform.isIOS) { -// await AutoOrientation.landscapeAutoMode(forceSensor: true); -// } -// } +Future landScape() async { + dynamic document; + try { + if (kIsWeb) { + await document.documentElement?.requestFullscreen(); + } else if (Platform.isAndroid || Platform.isIOS) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky, + overlays: [], + ); + await SystemChrome.setPreferredOrientations( + [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ], + ); + } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + await const MethodChannel('com.alexmercerind/media_kit_video') + .invokeMethod( + 'Utils.EnterNativeFullscreen', + ); + } + } catch (exception, stacktrace) { + debugPrint(exception.toString()); + debugPrint(stacktrace.toString()); + } +} //竖屏 Future verticalScreen() async { @@ -27,14 +47,24 @@ Future enterFullScreen() async { //退出全屏显示 Future exitFullScreen() async { - late SystemUiMode mode; - if ((Platform.isAndroid && - (await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) || - !Platform.isAndroid) { - mode = SystemUiMode.edgeToEdge; - } else { - mode = SystemUiMode.manual; + dynamic document; + try { + if (kIsWeb) { + document.exitFullscreen(); + } else if (Platform.isAndroid || Platform.isIOS) { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + await SystemChrome.setPreferredOrientations([]); + } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + await const MethodChannel('com.alexmercerind/media_kit_video') + .invokeMethod( + 'Utils.ExitNativeFullscreen', + ); + } + } catch (exception, stacktrace) { + debugPrint(exception.toString()); + debugPrint(stacktrace.toString()); } - await SystemChrome.setEnabledSystemUIMode(mode, - overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]); } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index aa3349e9..cfb9dad8 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -3,20 +3,23 @@ import 'dart:async'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; -import 'package:pilipala/common/widgets/app_bar_ani.dart'; import 'package:pilipala/plugin/pl_player/controller.dart'; import 'package:pilipala/plugin/pl_player/models/duration.dart'; +import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:pilipala/plugin/pl_player/models/play_status.dart'; import 'package:pilipala/plugin/pl_player/utils.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; -import 'package:volume_controller/volume_controller.dart'; import 'utils/fullscreen.dart'; +import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; @@ -62,6 +65,9 @@ class _PLVideoPlayerState extends State bool _volumeInterceptEventStream = false; + Box setting = GStrorage.setting; + late FullScreenMode mode; + void onDoubleTapSeekBackward() { setState(() { _mountSeekBackwardButton = true; @@ -84,9 +90,9 @@ class _PLVideoPlayerState extends State Future.microtask(() async { try { - VolumeController().showSystemUI = false; - _volumeValue = await VolumeController().getVolume(); - VolumeController().listener((value) { + FlutterVolumeController.showSystemUI = true; + _volumeValue = (await FlutterVolumeController.getVolume())!; + FlutterVolumeController.addListener((value) { if (mounted && !_volumeInterceptEventStream) { setState(() { _volumeValue = value; @@ -112,7 +118,8 @@ class _PLVideoPlayerState extends State Future setVolume(double value) async { try { - VolumeController().setVolume(value); + FlutterVolumeController.showSystemUI = false; + await FlutterVolumeController.setVolume(value); } catch (_) {} setState(() { _volumeValue = value; @@ -149,16 +156,37 @@ class _PLVideoPlayerState extends State Future triggerFullScreen() async { PlPlayerController _ = widget.controller; + mode = FullScreenModeCode.fromCode( + setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!; + if (!_.isFullScreen.value) { /// 按照视频宽高比决定全屏方向 - if (_.direction.value == 'horizontal') { - /// 进入全屏 - await enterFullScreen(); - // 横屏 - // await landScape(); - } else { - // 竖屏 - await verticalScreen(); + switch (mode) { + case FullScreenMode.auto: + if (_.direction.value == 'horizontal') { + /// 进入全屏 + await enterFullScreen(); + // 横屏 + await landScape(); + } else { + // 竖屏 + await verticalScreen(); + } + break; + case FullScreenMode.vertical: + + /// 进入全屏 + await enterFullScreen(); + // 横屏 + await verticalScreen(); + break; + case FullScreenMode.horizontal: + + /// 进入全屏 + await enterFullScreen(); + // 横屏 + await landScape(); + break; } _.toggleFullScreen(true); @@ -235,7 +263,7 @@ class _PLVideoPlayerState extends State ), ), - /// 长按倍速 + /// 长按倍速 toast Obx( () => Align( alignment: Alignment.topCenter, @@ -274,7 +302,7 @@ class _PLVideoPlayerState extends State ), ), - /// 时间进度 + /// 时间进度 toast Obx( () => Align( alignment: Alignment.topCenter, @@ -426,6 +454,24 @@ class _PLVideoPlayerState extends State ), ), + Obx(() { + if (_.buffered.value == Duration.zero) { + return Positioned.fill( + child: Container( + color: Colors.black, + child: Center( + child: Image.asset( + 'assets/images/loading.gif', + height: 25, + ), + ), + ), + ); + } else { + return Container(); + } + }), + /// 手势 Positioned.fill( left: 16, @@ -437,6 +483,10 @@ class _PLVideoPlayerState extends State _.controls = !_.showControls.value; }, onDoubleTapDown: (details) { + // live模式下禁用 + if (_.videoType.value == 'live') { + return; + } final totalWidth = MediaQuery.of(context).size.width; final tapPosition = details.localPosition.dx; final sectionWidth = totalWidth / 3; @@ -456,17 +506,17 @@ class _PLVideoPlayerState extends State }, onLongPressStart: (detail) { feedBack(); - double currentSpeed = _.playbackSpeed; _.setDoubleSpeedStatus(true); - _.setPlaybackSpeed(currentSpeed * 2); }, onLongPressEnd: (details) { - double currentSpeed = _.playbackSpeed; _.setDoubleSpeedStatus(false); - _.setPlaybackSpeed(currentSpeed / 2); }, - // 水平位置 快进 + + /// 水平位置 快进 live模式下禁用 onHorizontalDragUpdate: (DragUpdateDetails details) { + if (_.videoType.value == 'live') { + return; + } final tapPosition = details.localPosition.dx; int curSliderPosition = _.sliderPosition.value.inSeconds; late int result; @@ -485,6 +535,9 @@ class _PLVideoPlayerState extends State _initTapPositoin = tapPosition; }, onHorizontalDragEnd: (DragEndDetails details) { + if (_.videoType.value == 'live') { + return; + } _.onChangedSliderEnd(); _.seekTo(_.sliderPosition.value); }, @@ -517,8 +570,6 @@ class _PLVideoPlayerState extends State _distance = 0.0; } _distance = dy; - - // triggerFullScreen(); } else { // 右边区域 👈 final volume = _volumeValue - delta / 100.0; @@ -531,9 +582,10 @@ class _PLVideoPlayerState extends State ), // 头部、底部控制条 - if (_.controlsEnabled) - Obx( - () => Column( + Obx( + () => Visibility( + visible: _.videoType.value != 'live', + child: Column( children: [ if (widget.headerControl != null) ClipRect( @@ -560,7 +612,9 @@ class _PLVideoPlayerState extends State ], ), ), - // 进度条 + ), + + /// 进度条 live模式下禁用 Obx( () { final int value = _.sliderPosition.value.inSeconds; @@ -613,9 +667,10 @@ class _PLVideoPlayerState extends State ), // 锁 - if (_.controlsEnabled) - Obx( - () => Align( + Obx( + () => Visibility( + visible: _.videoType.value != 'live', + child: Align( alignment: Alignment.centerLeft, child: FractionalTranslation( translation: const Offset(0.5, 0.0), @@ -635,6 +690,7 @@ class _PLVideoPlayerState extends State ), ), ), + ), // Obx(() { if (_.dataStatus.loading || _.isBuffering.value) { @@ -676,7 +732,6 @@ class _PLVideoPlayerState extends State }, child: BackwardSeekIndicator( onChanged: (value) { - print(value); // _seekBarDeltaValueNotifier.value = -value; }, onSubmitted: (value) { diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index e73236fc..1cd4c20d 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -130,15 +130,15 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // ), // ), // ), - // ComBtn( - // icon: const Icon( - // Icons.fit_screen_sharp, - // size: 18, - // color: Colors.white, - // ), - // fuc: () => _.toggleVideoFit(), - // ), - // const SizedBox(width: 4), + ComBtn( + icon: const Icon( + Icons.settings_overscan_outlined, + size: 18, + color: Colors.white, + ), + fuc: () => _.toggleVideoFit(), + ), + const SizedBox(width: 4), // 全屏 Obx( () => ComBtn( diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d6de8ae4..c1482f1a 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,4 +1,6 @@ import 'package:get/get.dart'; +import 'package:pilipala/pages/about/index.dart'; +import 'package:pilipala/pages/blacklist/index.dart'; import 'package:pilipala/pages/dynamics/deatil/index.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/fan/index.dart'; @@ -14,6 +16,9 @@ import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; +import 'package:pilipala/pages/setting/play_setting.dart'; +import 'package:pilipala/pages/setting/privacy_setting.dart'; +import 'package:pilipala/pages/setting/style_setting.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/webview/index.dart'; @@ -68,5 +73,17 @@ class Routes { GetPage(name: '/member', page: () => const MemberPage()), // 二级回复 GetPage(name: '/replyReply', page: () => const VideoReplyReplyPanel()), + + // 播放设置 + GetPage(name: '/playSetting', page: () => const PlaySetting()), + // 外观设置 + GetPage(name: '/styleSetting', page: () => const StyleSetting()), + // 隐私设置 + GetPage(name: '/privacySetting', page: () => const PrivacySetting()), + + // + GetPage(name: '/blackListPage', page: () => const BlackListPage()), + // 关于 + GetPage(name: '/about', page: () => const AboutPage()), ]; } diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart index 8d5c891b..5d4d9cbd 100644 --- a/lib/utils/cookie.dart +++ b/lib/utils/cookie.dart @@ -1,26 +1,22 @@ -import 'dart:io'; -import 'package:cookie_jar/cookie_jar.dart'; +import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/init.dart'; -import 'package:pilipala/utils/utils.dart'; +import 'package:webview_cookie_manager/webview_cookie_manager.dart'; class SetCookie { - static onSet(List cookiesList, String url) async { - // domain url - List jarCookies = []; - if (cookiesList.isNotEmpty) { - for (var i in cookiesList) { - Cookie jarCookie = Cookie(i.name, i.value); - jarCookies.add(jarCookie); - } - } - String cookiePath = await Utils.getCookiePath(); - PersistCookieJar cookieJar = PersistCookieJar( - ignoreExpires: true, - storage: FileStorage(cookiePath), - ); - await cookieJar.saveFromResponse(Uri.parse(url), jarCookies); - // 重新设置 cookie - Request.setCookie(); - return true; + static onSet() async { + var cookies = await WebviewCookieManager().getCookies(HttpString.baseUrl); + await Request.cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseUrl), cookies); + var cookieString = + cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '); + Request.dio.options.headers['cookie'] = cookieString; + + cookies = await WebviewCookieManager().getCookies(HttpString.baseApiUrl); + await Request.cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseApiUrl), cookies); + + cookies = await WebviewCookieManager().getCookies(HttpString.tUrl); + await Request.cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.tUrl), cookies); } } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index e6ab2ae3..e4361f92 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -10,7 +10,11 @@ class Data { static Future historyStatus() async { Box localCache = GStrorage.localCache; + Box user = GStrorage.user; + if (user.get(UserBoxKey.userMid) == null) { + return; + } var res = await UserHttp.historyStatus(); - localCache.put(LocalCacheKey.historyStatus, res.data['data']); + localCache.put(LocalCacheKey.historyPause, res.data['data']); } } diff --git a/lib/utils/download.dart b/lib/utils/download.dart new file mode 100644 index 00000000..830464b2 --- /dev/null +++ b/lib/utils/download.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class DownloadUtils { + // 获取存储全县 + static requestStoragePer() async { + Map statuses = await [ + Permission.storage, + Permission.photos, + ].request(); + statuses[Permission.storage].toString(); + } + + static Future downloadImg(String imgUrl) async { + await requestStoragePer(); + SmartDialog.showLoading(msg: '保存中'); + var response = await Dio() + .get(imgUrl, options: Options(responseType: ResponseType.bytes)); + String picName = + "plpl_cover_${DateTime.now().toString().split('-').join()}.png"; + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 100, + name: picName, + ); + SmartDialog.dismiss(); + if (result != null) { + if (result['isSuccess']) { + // ignore: avoid_print + await SmartDialog.showToast('「$picName」已保存 '); + } + } + return true; + } +} diff --git a/lib/utils/event_bus.dart b/lib/utils/event_bus.dart new file mode 100644 index 00000000..cd010ef9 --- /dev/null +++ b/lib/utils/event_bus.dart @@ -0,0 +1,53 @@ +// 订阅者回调签名 +typedef void EventCallback(arg); + +class EventBus { + // 私有构造函数 + EventBus._internal(); + + // 保存单例 + static final EventBus _singleton = EventBus._internal(); + + // 工厂构造函数 + factory EventBus() => _singleton; + + // 保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列 + final _emap = >{}; + + // 添加订阅者 + void on(eventName, EventCallback f) { + _emap[eventName] ??= []; + _emap[eventName]!.add(f); + } + + // 移除订阅者 + void off(eventName, [EventCallback? f]) { + var list = _emap[eventName]; + if (eventName == null || list == null) return; + if (f == null) { + _emap[eventName] = []; + } else { + list.remove(f); + } + } + + // 触发事件,事件触发后该事件所有订阅者会被调用 + void emit(eventName, [arg]) { + var list = _emap[eventName]; + if (list == null) return; + List tempList = List.from(list); + for (var callback in tempList) { + callback(arg); + } + } + + // 获取订阅者数量 + int getSubscriberCount(eventName) { + var list = _emap[eventName]; + return list?.length ?? 0; + } +} + +class EventName { + static const String loginEvent = 'loginEvent'; +} diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 227e58a7..8e2e6d70 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -46,4 +46,28 @@ class IdUtils { } return (r - ADD) ^ XOR; } + + // 匹配 + static Map matchAvorBv({String? input}) { + Map result = {}; + if (input == null || input == '') { + return result; + } + RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false); + RegExp avRegex = RegExp(r'AV\d+', caseSensitive: false); + + Iterable bvMatches = bvRegex.allMatches(input); + Iterable avMatches = avRegex.allMatches(input); + + List bvs = bvMatches.map((match) => match.group(0)!).toList(); + List avs = avMatches.map((match) => match.group(0)!).toList(); + + if (bvs.isNotEmpty) { + result['BV'] = bvs[0].substring(0, 2).toUpperCase() + bvs[0].substring(2); + } + if (avs.isNotEmpty) { + result['AV'] = avs[0].substring(2); + } + return result; + } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index b39dcbfc..7fd8b3a5 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,8 +1,8 @@ // import 'package:hive/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:pilipala/models/home/rcmd/result.dart'; import 'package:pilipala/models/model_owner.dart'; -import 'package:pilipala/models/model_rec_video_item.dart'; import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/user/info.dart'; @@ -17,26 +17,46 @@ class GStrorage { static late final Box video; static Future init() async { - final dir = await getApplicationDocumentsDirectory(); + final dir = await getApplicationSupportDirectory(); final path = dir.path; await Hive.initFlutter('$path/hive'); regAdapter(); // 用户信息 user = await Hive.openBox('user'); // 首页推荐视频 - recVideo = await Hive.openBox('recVideo'); + recVideo = await Hive.openBox( + 'recVideo', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 20; + }, + ); // 登录用户信息 userInfo = await Hive.openBox('userInfo'); // 本地缓存 localCache = await Hive.openBox('localCache'); // 设置 setting = await Hive.openBox('setting'); + // 热搜关键词 + hotKeyword = await Hive.openBox( + 'hotKeyword', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 10; + }, + ); + // 搜索历史 + historyword = await Hive.openBox( + 'historyWord', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 10; + }, + ); } static regAdapter() { - Hive.registerAdapter(RecVideoItemModelAdapter()); + Hive.registerAdapter(RecVideoItemAppModelAdapter()); Hive.registerAdapter(RcmdReasonAdapter()); - Hive.registerAdapter(StatAdapter()); + Hive.registerAdapter(RcmdStatAdapter()); + Hive.registerAdapter(RcmdOwnerAdapter()); Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(LevelInfoAdapter()); @@ -45,13 +65,28 @@ class GStrorage { } static Future lazyInit() async { - // 热搜关键词 - hotKeyword = await Hive.openBox('hotKeyword'); - // 搜索历史 - historyword = await Hive.openBox('historyWord'); // 视频设置 video = await Hive.openBox('video'); } + + static Future close() async { + user.compact(); + user.close(); + recVideo.compact(); + recVideo.close(); + userInfo.compact(); + userInfo.close(); + hotKeyword.compact(); + hotKeyword.close(); + historyword.compact(); + historyword.close(); + localCache.compact(); + localCache.close(); + setting.compact(); + setting.close(); + video.compact(); + video.close(); + } } // 约定 key @@ -63,16 +98,32 @@ class UserBoxKey { static const String userMid = 'userMid'; // 登录状态 static const String userLogin = 'userLogin'; + // 凭证 + static const String accessKey = 'accessKey'; } class SettingBoxKey { static const String themeMode = 'themeMode'; static const String feedBackEnable = 'feedBackEnable'; + static const String defaultFontSize = 'fontSize'; + static const String defaultVideoQa = 'defaultVideoQa'; + static const String defaultAudioQa = 'defaultAudioQa'; + static const String defaultDecode = 'defaultDecode'; + static const String defaultVideoSpeed = 'defaultVideoSpeed'; + static const String autoUpgradeEnable = 'autoUpgradeEnable'; + static const String autoPlayEnable = 'autoPlayEnable'; + static const String enableHA = 'enableHA'; + static const String defaultPicQa = 'defaultPicQa'; + + static const String danmakuEnable = 'danmakuEnable'; + static const String fullScreenMode = 'fullScreenMode'; + + static const String blackMidsList = 'blackMidsList'; } class LocalCacheKey { - // 历史记录暂停状态 默认false - static const String historyStatus = 'historyStatus'; + // 历史记录暂停状态 默认false 记录 + static const String historyPause = 'historyPause'; } class VideoBoxKey { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index fdf5009e..4a539888 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -162,4 +162,36 @@ class Utils { } return 0; } + + static int findClosestNumber(int target, List numbers) { + int minDiff = 127; + late int closestNumber; + try { + for (int number in numbers) { + int diff = (number - target).abs(); + + if (diff < minDiff) { + minDiff = diff; + closestNumber = number; + } + } + } catch (_) {} + return closestNumber; + } + + // 版本对比 + static bool needUpdate(localVersion, remoteVersion) { + List localVersionList = localVersion.split('.'); + List remoteVersionList = remoteVersion.split('v')[1].split('.'); + for (int i = 0; i < localVersionList.length; i++) { + int localVersion = int.parse(localVersionList[i]); + int remoteVersion = int.parse(remoteVersionList[i]); + if (remoteVersion > localVersion) { + return true; + } else if (remoteVersion < localVersion) { + return false; + } + } + return false; + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 82b739b5..c3b56ecd 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin"); + flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 047b2c8e..70cdeb4b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + flutter_volume_controller media_kit_libs_linux media_kit_video url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bc042145..2363b89f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import connectivity_plus import device_info_plus import dynamic_color +import flutter_volume_controller import media_kit_libs_macos_video import media_kit_video import package_info_plus @@ -15,12 +16,14 @@ import path_provider_foundation import screen_brightness_macos import share_plus import sqflite +import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) @@ -28,5 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 00000000..3c36eea5 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,114 @@ +PODS: + - connectivity_plus (0.0.1): + - FlutterMacOS + - ReachabilitySwift + - device_info_plus (0.0.1): + - FlutterMacOS + - dynamic_color (0.0.2): + - FlutterMacOS + - flutter_volume_controller (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - media_kit_libs_macos_video (1.0.4): + - FlutterMacOS + - media_kit_native_event_loop (1.0.0): + - FlutterMacOS + - media_kit_video (0.0.1): + - FlutterMacOS + - package_info_plus (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - ReachabilitySwift (5.0.0) + - screen_brightness_macos (0.1.0): + - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) + - flutter_volume_controller (from `Flutter/ephemeral/.symlinks/plugins/flutter_volume_controller/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) + - media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`) + - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + +SPEC REPOS: + trunk: + - FMDB + - ReachabilitySwift + +EXTERNAL SOURCES: + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + dynamic_color: + :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos + flutter_volume_controller: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_volume_controller/macos + FlutterMacOS: + :path: Flutter/ephemeral + media_kit_libs_macos_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos + media_kit_native_event_loop: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos + media_kit_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_brightness_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + +SPEC CHECKSUMS: + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f + flutter_volume_controller: 25d09126b0d695560f11c80b1311d5063fed882f + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 + media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 + media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 + +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 + +COCOAPODS: 1.12.1 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6d05c56b..14953b60 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 70B7435992536DF7EF916102 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA6434EEFB6EABF56163956B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* pilipala.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "pilipala.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* pilipala.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pilipala.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -66,8 +67,12 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 55ED1760F98F03A067237962 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A5BA27756D8018CEC961D98E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + BA6434EEFB6EABF56163956B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C7AC3B7DF8D09FAFBD7AC6AD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 70B7435992536DF7EF916102 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 3BC3551DE517F6B6D0C82543 /* Pods */, ); sourceTree = ""; }; @@ -145,9 +152,21 @@ path = Runner; sourceTree = ""; }; + 3BC3551DE517F6B6D0C82543 /* Pods */ = { + isa = PBXGroup; + children = ( + A5BA27756D8018CEC961D98E /* Pods-Runner.debug.xcconfig */, + C7AC3B7DF8D09FAFBD7AC6AD /* Pods-Runner.release.xcconfig */, + 55ED1760F98F03A067237962 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + BA6434EEFB6EABF56163956B /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 5190B88ACAD3AAF5B1766AE1 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 02C468425C99B15D8DFFA40D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -233,6 +254,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 02C468425C99B15D8DFFA40D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -271,6 +309,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 5190B88ACAD3AAF5B1766AE1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -345,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f1..96d3fee1 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d9..94704c86 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eba..28b42e42 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa4..9d7e5bcb 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb57226..70ac520b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318e..85e8a020 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e72..132682a1 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632cf..1b9d43ec 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a30..08c3ab17 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a4..ee95ab7e 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/pubspec.lock b/pubspec.lock index db30ccca..198c430f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + animations: + dependency: "direct main" + description: + name: animations + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" + source: hosted + version: "2.0.7" archive: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -285,10 +301,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "3866d67f93523161b643187af65f5ac08bc991a5bcdaf41a2d587fe4ccb49993" + sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.3.2" dio_cookie_manager: dependency: "direct main" description: @@ -414,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -427,6 +451,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" flutter_smart_dialog: dependency: "direct main" description: @@ -440,6 +472,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_volume_controller: + dependency: "direct main" + description: + name: flutter_volume_controller + sha256: "7f88cb046b00fd80e98bcb7926b9e3879f004f30905109fdf6c5d09b8d28eb2e" + url: "https://pub.dev" + source: hosted + version: "1.2.7" flutter_web_plugins: dependency: transitive description: flutter @@ -649,50 +689,50 @@ packages: dependency: "direct main" description: name: media_kit - sha256: eb47c2d51b7c06b319d8c776b47be912c5a4f53af7ab4814927a52a76442609e + sha256: "0a89e7037002a62701ec319c375586849f9ef8e681820e1dd4a4ff7b843f7542" url: "https://pub.dev" source: hosted - version: "1.1.2+1" + version: "1.1.4+1" media_kit_libs_android_video: dependency: "direct main" description: name: media_kit_libs_android_video - sha256: ddb0d26ecba72bf7117e37e29b6a50f4ba198bbccb4e47246cae1812087dc721 + sha256: "142d389bf3efcf8469594a9c7a06a92fc25843fc6c0c3247f76cdcf70b3b29de" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" media_kit_libs_ios_video: dependency: "direct main" description: name: media_kit_libs_ios_video - sha256: b65d1f22442e6074948f501e7156c310fbf133cd63aa914c84d0d4db1c683ee0 + sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" media_kit_libs_linux: dependency: "direct main" description: name: media_kit_libs_linux - sha256: "838b9e8041d376873cc938872c75812989d0feb247ad93afd8dbc92bf052680a" + sha256: "570bf18ebbd1221caec082657468be05d180510385d3515ec38e0be44fdcc859" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" media_kit_libs_macos_video: dependency: "direct main" description: name: media_kit_libs_macos_video - sha256: dcabf2731b6b7dee143c18617fd32a063146303e7388f87dd4e974e03c9c795a + sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" media_kit_libs_windows_video: dependency: "direct main" description: name: media_kit_libs_windows_video - sha256: b343e644927982a2ef3db63877b36d84bdda8173d8318ca0d1c68c1ea8a35982 + sha256: f33aabd8414470d99e2c91dd98d605e6a5f1c4b8082dd933c10951bc961b9124 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.7" media_kit_native_event_loop: dependency: "direct main" description: @@ -705,10 +745,10 @@ packages: dependency: "direct main" description: name: media_kit_video - sha256: "4b627683ef9e7f5cd049fd01be4b5deb5f31effc878503f108c81b19fd745040" + sha256: e7fcbe426d42a78ad6696f8f557adb9cbdc012177829026d04992cc106a1c815 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" meta: dependency: transitive description: @@ -750,13 +790,13 @@ packages: source: hosted version: "2.1.0" package_info_plus: - dependency: transitive + dependency: "direct main" description: name: package_info_plus - sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1170,6 +1210,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + url: "https://pub.dev" + source: hosted + version: "6.1.12" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + url: "https://pub.dev" + source: hosted + version: "6.0.38" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -1178,6 +1242,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + url: "https://pub.dev" + source: hosted + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: @@ -1227,7 +1299,7 @@ packages: source: hosted version: "0.4.0+2" volume_controller: - dependency: "direct main" + dependency: transitive description: name: volume_controller sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9" diff --git a/pubspec.yaml b/pubspec.yaml index 0b47f807..0d53ab33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,17 +91,17 @@ dependencies: crypto: ^3.0.3 # 视频播放器 - media_kit: ^1.1.2+1 # Primary package. - media_kit_video: ^1.1.2 # For video rendering. + media_kit: ^1.1.4 # Primary package. + media_kit_video: ^1.1.5 # For video rendering. media_kit_native_event_loop: ^1.0.7 # Support for higher number of concurrent instances & better performance. - media_kit_libs_android_video: ^1.3.0 # Android package for video native libraries. - media_kit_libs_ios_video: ^1.1.1 # iOS package for video native libraries. - media_kit_libs_macos_video: ^1.1.1 # macOS package for video native libraries. - media_kit_libs_windows_video: ^1.0.5 # Windows package for video native libraries. - media_kit_libs_linux: ^1.1.0 + media_kit_libs_android_video: ^1.3.2 # Android package for video native libraries. + media_kit_libs_ios_video: ^1.1.3 # iOS package for video native libraries. + media_kit_libs_macos_video: ^1.1.3 # macOS package for video native libraries. + media_kit_libs_windows_video: ^1.0.7 # Windows package for video native libraries. + media_kit_libs_linux: ^1.1.1 # 音量、亮度、屏幕控制 - volume_controller: ^2.0.7 + flutter_volume_controller: ^1.2.7 screen_brightness: ^0.2.2 wakelock_plus: ^1.1.1 universal_platform: ^1.0.0+1 @@ -109,6 +109,11 @@ dependencies: audio_video_progress_bar: ^1.0.1 # auto_orientation: ^2.3.1 protobuf: ^3.0.0 + animations: ^2.0.7 + + # 获取appx信息 + package_info_plus: ^4.1.0 + url_launcher: ^6.1.12 dev_dependencies: flutter_test: @@ -120,6 +125,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 + flutter_launcher_icons: ^0.13.1 # flutter_launcher_icons: # git: # url: https://github.com/nvi9/flutter_launcher_icons.git @@ -127,16 +133,19 @@ dev_dependencies: hive_generator: ^2.0.0 build_runner: ^2.3.3 -flutter_icons: +flutter_launcher_icons: android: true ios: true - remove_alpha_ios: false - image_path: assets/images/logo/logo_android.png - image_path_android: assets/images/logo/logo_android.png + remove_alpha_ios: true + image_path: assets/images/logo/logo_android_2.png + image_path_android: assets/images/logo/logo_android_2.png image_path_ios: assets/images/logo/logo_ios.png adaptive_icon_background: "#ffffff" - adaptive_icon_foreground: assets/images/logo/logo_android.png - adaptive_icon_monochrome: assets/images/logo/logo_android.png + adaptive_icon_foreground: assets/images/logo/logo_android_2.png + adaptive_icon_monochrome: assets/images/logo/logo_android_2.png + macos: + generate: true + image_path: assets/images/logo/logo_ios.png # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -171,9 +180,9 @@ flutter: - family: Jura-Bold fonts: - asset: assets/fonts/Jura-Bold.ttf - - family: HarmonyOS - fonts: - - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf + # - family: HarmonyOS + # fonts: + # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf # For details regarding fonts from package dependencies, diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5483cc28..d2cff3b2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + FlutterVolumeControllerPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index cd0b6c2a..5d25e134 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color + flutter_volume_controller media_kit_libs_windows_video media_kit_video permission_handler_windows