merge main

This commit is contained in:
guozhigq
2023-08-19 15:33:24 +08:00
208 changed files with 6302 additions and 2205 deletions

23
.github/ISSUE_TEMPLATE/bug-反馈.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug 反馈
about: 描述你所遇到的bug
title: ''
labels: 问题反馈
assignees: guozhigq
---
### 问题描述
请提供一个清晰而简明的问题描述。
### 复现步骤
请提供复现该问题所需的具体步骤。
### 预期行为
请描述你期望的正确行为或结果。
### 系统信息
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
### 相关截图或日志
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。

20
.github/ISSUE_TEMPLATE/功能请求.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: 功能请求
about: 对于功能的一些建议
title: ''
labels: 功能
assignees: guozhigq
---
### 功能描述
请提供对所请求功能的清晰描述。
### 目标
请描述你希望通过这个功能实现的目标。
### 解决方案
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
### 其他
请提供已实现该功能或类似功能的应用

84
.github/workflows/main.yml vendored Normal file
View File

@ -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

132
README.md
View File

@ -1,16 +1,128 @@
# pilipala <div align="center">
<img width="200" height="200" src="https://github.com/guozhigq/pilipala/blob/main/assets/images/logo/logo_android.png">
</div>
A new Flutter project.
## Getting Started <div align="center">
<h1>PiliPala</h1>
<p>使用Flutter开发的BiliBili第三方客户端</p>
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/510shots_so.png" width="32%" alt="home" />
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/174shots_so.png" width="32%" alt="home" />
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" />
<br/>
<br/>
</div>
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 <br/>
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. ### 功能
目前着重移动端(Android、iOS)暂时没有适配桌面端、Pad端、手表端等
<br/>
现有功能及[开发计划](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] 震动反馈(可选)
- [ ] 等等
<br/>
### 下载
可以通过右侧release进行下载或拉取代码到本地进行编译
<br/>
### 声明
此项目PiliPala是个人为了兴趣而开发, 仅用于学习和测试。
所用API皆从官方网站收集, 不提供任何破解内容。
感谢使用
<br/>
### 致谢
- [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)
- 等等
<br/>
<br/>
<br/>

View File

@ -25,6 +25,17 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 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 { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
@ -54,11 +65,24 @@ android {
minSdkVersion 19 minSdkVersion 19
} }
signingConfigs {
// 添加签名配置
release {
// 配置密钥库文件的位置、别名、密码等信息
storeFile _storeFile
storePassword _storePassword
keyAlias _keyAlias
keyPassword _keyPassword
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug signingConfig signingConfigs.release
} }
} }
} }

View File

@ -21,7 +21,7 @@
</intent> </intent>
</queries> </queries>
<application <application
android:label="pilipala" android:label="PiliPala"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"> android:enableOnBackInvokedCallback="true">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
assets/sreenshot/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

BIN
assets/sreenshot/media.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
assets/sreenshot/member.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

BIN
assets/sreenshot/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

BIN
assets/sreenshot/set.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

11
change_log/1.0.0.0817.md Normal file
View File

@ -0,0 +1,11 @@
## 1.0.0
### 初始版本
+ 直播、推荐、动态功能
+ 投稿、番剧播放功能
+ 播放器手势支持
+ 画质、音质、解码格式支持
+ 点赞、投币、收藏功能
+ 关注/取关、用户主页功能
+ 评论功能
+ 历史记录、稍后再看功能

7
change_log/1.0.1.0817.md Normal file
View File

@ -0,0 +1,7 @@
## 1.0.1
### 修复
+ 升级播放器依赖
+ android平台 AV1格式视频支持
+ 视频全屏功能

View File

@ -5,6 +5,8 @@ PODS:
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_volume_controller (0.0.1):
- Flutter
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
@ -31,6 +33,8 @@ PODS:
- sqflite (0.0.3): - sqflite (0.0.3):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1): - volume_controller (0.0.1):
- Flutter - Flutter
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
@ -44,6 +48,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/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_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -54,6 +59,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`) - webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
@ -71,6 +77,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
image_gallery_saver: image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios" :path: ".symlinks/plugins/image_gallery_saver/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
@ -91,6 +99,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller: volume_controller:
:path: ".symlinks/plugins/volume_controller/ios" :path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus: wakelock_plus:
@ -104,6 +114,7 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
@ -116,6 +127,7 @@ SPEC CHECKSUMS:
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1020 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Pilipala</string> <string>PiliPala</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

View File

@ -7,3 +7,10 @@ class StyleString {
static const Radius imgRadius = Radius.circular(10); static const Radius imgRadius = Radius.circular(10);
static const double aspectRatio = 16 / 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';
}

View File

@ -14,7 +14,7 @@ class VideoCardHSkeleton extends StatelessWidget {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
double width = double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2; (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox( return SizedBox(
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
child: Row( child: Row(

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget { 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 Widget child;
final Function? closeFn;
@override @override
State<StatefulWidget> createState() => AnimatedDialogState(); State<StatefulWidget> createState() => AnimatedDialogState();
@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Colors.black.withOpacity(opacityAnimation!.value), color: Colors.black.withOpacity(opacityAnimation!.value),
child: Center( child: InkWell(
child: FadeTransition( splashColor: Colors.transparent,
opacity: scaleAnimation!, onTap: () => widget.closeFn!(),
child: ScaleTransition( child: Center(
scale: scaleAnimation!, child: FadeTransition(
child: widget.child, opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
),
), ),
), ),
), ),

View File

@ -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<Offset>(
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: <Color>[
Colors.transparent,
Colors.black45,
],
tileMode: TileMode.clamp,
)
: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black45,
],
tileMode: TileMode.mirror,
),
),
child: child,
),
);
}
}

View File

@ -1,33 +1,120 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
Widget pBadge( // Widget pBadge(
text, // text,
context, // context,
double? top, // double? top,
double? right, // double? right,
double? bottom, // double? bottom,
double? left, { // double? left, {
type = 'primary', // type = 'primary',
}) { // }) {
Color bgColor = Theme.of(context).colorScheme.primary; // Color bgColor = Theme.of(context).colorScheme.primary;
Color color = Theme.of(context).colorScheme.onPrimary; // Color color = Theme.of(context).colorScheme.onPrimary;
if (type == 'gray') { // if (type == 'gray') {
bgColor = Colors.black54.withOpacity(0.4); // bgColor = Colors.black54.withOpacity(0.4);
color = Colors.white; // color = Colors.white;
} // }
return Positioned( // return Positioned(
top: top, // top: top,
left: left, // left: left,
right: right, // right: right,
bottom: bottom, // bottom: bottom,
child: Container( // child: Container(
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6), // padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
decoration: // decoration:
BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor), // BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
child: Text( // child: Text(
text, // text,
style: TextStyle(fontSize: 11, color: color), // 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,
);
}
}
} }

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class LiveCard extends StatelessWidget { class LiveCard extends StatelessWidget {
@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
liveItem.title, liveItem.title,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: Get.find<RcmdController>().crossAxisCount, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox( SizedBox(

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/utils/storage.dart';
Box setting = GStrorage.setting;
class NetworkImgLayer extends StatelessWidget { class NetworkImgLayer extends StatelessWidget {
final String? src; final String? src;
@ -24,12 +28,14 @@ class NetworkImgLayer extends StatelessWidget {
this.fadeOutDuration, this.fadeOutDuration,
this.fadeInDuration, this.fadeInDuration,
// 图片质量 默认1% // 图片质量 默认1%
this.quality = 1, this.quality,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double pr = MediaQuery.of(context).devicePixelRatio; double pr = MediaQuery.of(context).devicePixelRatio;
int picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
// double pr = 2; // double pr = 2;
return src != '' return src != ''
? ClipRRect( ? ClipRRect(
@ -41,7 +47,7 @@ class NetworkImgLayer extends StatelessWidget {
: StyleString.imgRadius.x), : StyleString.imgRadius.x),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: imageUrl:
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality}q.webp', '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp',
width: width ?? double.infinity, width: width ?? double.infinity,
height: height ?? double.infinity, height: height ?? double.infinity,
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -1,42 +1,82 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/download.dart';
class OverlayPop extends StatelessWidget { class OverlayPop extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const OverlayPop({super.key, this.videoItem}); final Function? closeFn;
const OverlayPop({super.key, this.videoItem, this.closeFn});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0), margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(10.0),
), ),
child: ClipRRect( child: Column(
borderRadius: BorderRadius.circular(6.0), mainAxisSize: MainAxisSize.min,
child: Column( mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, Stack(
children: [ children: [
NetworkImgLayer( NetworkImgLayer(
width: (MediaQuery.of(context).size.width - 16), width: imgWidth,
height: (MediaQuery.of(context).size.width - 16) / height: imgWidth / StyleString.aspectRatio,
StyleString.aspectRatio, src: videoItem.pic!,
src: videoItem.pic!, quality: 100,
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
child: Text(
videoItem.title!,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
), ),
), 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),
)
],
)),
],
), ),
); );
} }

View File

@ -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),
),
),
);
}
}

View File

@ -16,12 +16,14 @@ class VideoCardH extends StatelessWidget {
final videoItem; final videoItem;
final Function()? longPress; final Function()? longPress;
final Function()? longPressEnd; final Function()? longPressEnd;
final String source;
const VideoCardH({ const VideoCardH({
Key? key, Key? key,
required this.videoItem, required this.videoItem,
this.longPress, this.longPress,
this.longPressEnd, this.longPressEnd,
this.source = 'normal',
}) : super(key: key); }) : super(key: key);
@override @override
@ -35,11 +37,11 @@ class VideoCardH extends StatelessWidget {
longPress!(); longPress!();
} }
}, },
onLongPressEnd: (details) { // onLongPressEnd: (details) {
if (longPressEnd != null) { // if (longPressEnd != null) {
longPressEnd!(); // longPressEnd!();
} // }
}, // },
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
try { try {
@ -57,8 +59,9 @@ class VideoCardH extends StatelessWidget {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
double width = double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2; (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox( return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -80,19 +83,24 @@ class VideoCardH extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
pBadge(Utils.timeFormat(videoItem.duration!), PBadge(
context, null, 6.0, 6.0, null, text: Utils.timeFormat(videoItem.duration!),
type: 'gray'), top: null,
if (videoItem.rcmdReason != null && right: 6.0,
videoItem.rcmdReason.content != '') bottom: 6.0,
pBadge(videoItem.rcmdReason.content, context, left: null,
6.0, 6.0, null, 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 { class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final videoItem; final videoItem;
const VideoContent({super.key, required this.videoItem}); final String source;
const VideoContent(
{super.key, required this.videoItem, this.source = 'normal'});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -124,7 +134,6 @@ class VideoContent extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0.3,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -198,26 +207,62 @@ class VideoContent extends StatelessWidget {
// color: Theme.of(context).colorScheme.outline), // color: Theme.of(context).colorScheme.outline),
// ) // )
const Spacer(), const Spacer(),
SizedBox( // SizedBox(
width: 20, // width: 20,
height: 20, // height: 20,
child: IconButton( // child: IconButton(
tooltip: '稍后再看', // tooltip: '稍后再看',
style: ButtonStyle( // style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), // padding: MaterialStateProperty.all(EdgeInsets.zero),
), // ),
onPressed: () async { // onPressed: () async {
var res = // var res =
await UserHttp.toViewLater(bvid: videoItem.bvid); // await UserHttp.toViewLater(bvid: videoItem.bvid);
SmartDialog.showToast(res['msg']); // SmartDialog.showToast(res['msg']);
}, // },
icon: Icon( // icon: Icon(
Icons.more_vert_outlined, // Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline, // color: Theme.of(context).colorScheme.outline,
size: 14, // size: 14,
// ),
// ),
// ),
if (source == 'normal')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
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) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
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))
],
),
),
],
), ),
), ),
),
], ],
), ),
], ],

View File

@ -2,18 +2,19 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.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/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局 // 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget { class VideoCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables final dynamic videoItem;
final videoItem;
final Function()? longPress; final Function()? longPress;
final Function()? longPressEnd; final Function()? longPressEnd;
@ -24,6 +25,54 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd, this.longPressEnd,
}) : super(key: key); }) : 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id); String heroTag = Utils.makeHeroTag(videoItem.id);
@ -40,61 +89,29 @@ class VideoCardV extends StatelessWidget {
longPress!(); longPress!();
} }
}, },
onLongPressEnd: (details) { // onLongPressEnd: (details) {
if (longPressEnd != null) { // if (longPressEnd != null) {
longPressEnd!(); // longPressEnd!();
} // }
}, // },
child: InkWell( child: InkWell(
onTap: () async { onTap: () async => onPushDetail(heroTag),
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
child: Column( child: Column(
children: [ children: [
ClipRRect( AspectRatio(
borderRadius: const BorderRadius.only( aspectRatio: StyleString.aspectRatio,
topLeft: StyleString.imgRadius, child: LayoutBuilder(builder: (context, boxConstraints) {
topRight: StyleString.imgRadius, double maxWidth = boxConstraints.maxWidth;
bottomLeft: StyleString.imgRadius, double maxHeight = boxConstraints.maxHeight;
bottomRight: StyleString.imgRadius, return Hero(
), tag: heroTag,
child: AspectRatio( child: NetworkImgLayer(
aspectRatio: StyleString.aspectRatio, src: videoItem.pic,
child: LayoutBuilder(builder: (context, boxConstraints) { width: maxWidth,
double maxWidth = boxConstraints.maxWidth; height: maxHeight,
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,
// ),
// ),
// ),
],
);
}),
),
), ),
VideoContent(videoItem: videoItem) VideoContent(videoItem: videoItem)
], ],
@ -106,113 +123,151 @@ class VideoCardV extends StatelessWidget {
} }
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables final dynamic videoItem;
final videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key); const VideoContent({Key? key, required this.videoItem}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Padding( child: Padding(
// 多列 padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
videoItem.title, videoItem.title,
textAlign: TextAlign.start, style: const TextStyle(fontSize: 13),
style: const TextStyle( maxLines: 2,
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: Get.find<RcmdController>().crossAxisCount,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Row( Row(
children: [ children: [
if (videoItem.goto == 'bangumi') ...[
PBadge(
text: videoItem.bangumiBadge,
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
if (videoItem.rcmdReason != null && if (videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != '' || videoItem.rcmdReason.content != '') ...[
videoItem.isFollowed == 1) ...[ PBadge(
Container( text: videoItem.rcmdReason.content,
padding: const EdgeInsets.fromLTRB(3, 0, 3, 0), stack: 'normal',
decoration: BoxDecoration( size: 'small',
color: Theme.of(context) type: 'color',
.colorScheme )
.primaryContainer ],
.withOpacity(0.6), if (videoItem.goto == 'picture') ...[
borderRadius: BorderRadius.circular(3)), const PBadge(
child: Center( text: '动态',
child: Text( stack: 'normal',
videoItem.rcmdReason != null && size: 'small',
videoItem.rcmdReason.content != '' type: 'line',
? videoItem.rcmdReason.content fs: 9,
: '已关注', )
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
)),
const SizedBox(width: 4)
], ],
Expanded( Expanded(
child: LayoutBuilder(builder: child: Text(
(BuildContext context, BoxConstraints constraints) { videoItem.owner.name,
return SizedBox( maxLines: 1,
width: constraints.maxWidth, style: TextStyle(
child: Text( fontSize:
videoItem.owner.name, Theme.of(context).textTheme.labelMedium!.fontSize,
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,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 14,
), ),
), ),
), ),
if (videoItem.goto == 'av')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
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) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
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( // Row(
// children: [ // children: [
// const SizedBox(width: 1),
// StatView( // StatView(
// theme: 'black', // theme: 'gray',
// view: videoItem.stat.view, // view: videoItem.stat.view,
// ), // ),
// const SizedBox(width: 6), // const SizedBox(width: 10),
// StatDanMu( // StatDanMu(
// theme: 'black', // theme: 'gray',
// danmu: videoItem.stat.danmaku, // danmu: videoItem.stat.danmaku,
// ), // ),
// const Spacer(),
// SizedBox(
// width: 24,
// height: 24,
// child: PopupMenuButton<String>(
// 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) =>
// <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 45, height: 48,
padding: const EdgeInsets.only(top: 22, left: 6, right: 6), padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(

View File

@ -248,9 +248,44 @@ class Api {
// 移除已观看 // 移除已观看
static const String toViewDel = '/x/v2/history/toview/del'; 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 bangumiAdd = '/pgc/web/follow/add';
// 取消追番 // 取消追番
static const String bangumiDel = '/pgc/web/follow/del'; 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&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的订阅
static const String bangumiFollow =
'/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
// 黑名单
static const String blackLst = '/x/relation/blacks';
// github 获取最新版
static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
} }

36
lib/http/bangumi.dart Normal file
View File

@ -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'],
};
}
}
}

26
lib/http/black.dart Normal file
View File

@ -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'],
};
}
}
}

View File

@ -15,20 +15,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request { class Request {
static final Request _instance = Request._internal(); static final Request _instance = Request._internal();
static late CookieManager cookieManager; static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance; factory Request() => _instance;
static Dio dio = Dio()
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
/// 设置cookie /// 设置cookie
static setCookie() async { static setCookie() async {
Box user = GStrorage.user;
var cookiePath = await Utils.getCookiePath(); var cookiePath = await Utils.getCookiePath();
var cookieJar = PersistCookieJar( var cookieJar = PersistCookieJar(
ignoreExpires: true, ignoreExpires: true,
@ -38,8 +30,18 @@ class Request {
dio.interceptors.add(cookieManager); dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)); .loadForRequest(Uri.parse(HttpString.baseUrl));
var cookie2 = await cookieManager.cookieJar if (user.get(UserBoxKey.userMid) != null) {
.loadForRequest(Uri.parse(HttpString.tUrl)); 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) { if (cookie.isEmpty) {
try { try {
await Request().get(HttpString.baseUrl); await Request().get(HttpString.baseUrl);
@ -47,23 +49,9 @@ class Request {
log("setCookie, ${e.toString()}"); log("setCookie, ${e.toString()}");
} }
} }
if (cookie2.isEmpty) { var cookieString =
try { cookie.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
await Request().get(HttpString.tUrl); dio.options.headers['cookie'] = cookieString;
} 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);
} }
// 从cookie中获取 csrf token // 从cookie中获取 csrf token
@ -95,28 +83,38 @@ class Request {
//Http请求头. //Http请求头.
headers: { headers: {
// 'cookie': '', // '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; Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) != null) { if (user.get(UserBoxKey.userMid) != null) {
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString(); 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 dio.interceptors.add(ApiInterceptor());
..add(ApiInterceptor())
// 日志拦截器 输出请求、响应内容 // 日志拦截器 输出请求、响应内容
..add(LogInterceptor( dio.interceptors.add(LogInterceptor(
request: false, request: false,
requestHeader: false, requestHeader: false,
responseHeader: false, responseHeader: false,
)); ));
dio.transformer = BackgroundTransformer(); dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) { dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || status == 304 || status == 302; return status! >= 200 && status < 300 || status == 304 || status == 302;
@ -161,7 +159,7 @@ class Request {
* post请求 * post请求
*/ */
post(url, {data, queryParameters, options, cancelToken, extra}) async { post(url, {data, queryParameters, options, cancelToken, extra}) async {
print('post-data: $data'); // print('post-data: $data');
Response response; Response response;
try { try {
response = await dio.post( response = await dio.post(
@ -171,7 +169,7 @@ class Request {
options: options, options: options,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
print('post success: ${response.data}'); // print('post success: ${response.data}');
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
print('post error: $e'); print('post error: $e');

View File

@ -1,6 +1,10 @@
// ignore_for_file: avoid_print
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
// import 'package:get/get.dart' hide Response; // import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@ -13,8 +17,26 @@ class ApiInterceptor extends Interceptor {
handler.next(options); handler.next(options);
} }
Box user = GStrorage.user;
@override @override
void onResponse(Response response, ResponseInterceptorHandler handler) { void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
if (response.statusCode == 302) {
List<String> 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); handler.next(response);
} }

View File

@ -1,3 +1,4 @@
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_hot_video_item.dart';
@ -84,6 +85,12 @@ class UserHttp {
static Future<dynamic> seeYouLater() async { static Future<dynamic> seeYouLater() async {
var res = await Request().get(Api.seeYouLater); var res = await Request().get(Api.seeYouLater);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
if (res.data['data']['count'] == 0) {
return {
'status': true,
'data': {'list': [], 'count': 0}
};
}
List<HotVideoItemModel> list = []; List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) { for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i)); list.add(HotVideoItemModel.fromJson(i));
@ -179,4 +186,35 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; 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']};
}
}
} }

View File

@ -1,5 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/reply_type.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/user/fav_folder.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/storage.dart';
/// res.data['code'] == 0 请求正常返回结果 /// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果 /// res.data['data'] 为结果
/// 返回{'status': bool, 'data': List} /// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑 /// view层根据 status 判断渲染逻辑
class VideoHttp { class VideoHttp {
static Box user = GStrorage.user;
static Box setting = GStrorage.setting;
// 首页推荐视频 // 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async { static Future rcmdVideoList({required int ps, required int freshIdx}) async {
try { try {
@ -42,8 +48,7 @@ class VideoHttp {
} }
} }
static Future rcmdVideoListApp( static Future rcmdVideoListApp({int? ps, required int freshIdx}) async {
{required int ps, required int freshIdx}) async {
try { try {
var res = await Request().get( var res = await Request().get(
Api.recommendListApp, Api.recommendListApp,
@ -55,12 +60,22 @@ class VideoHttp {
'device_type': 0, 'device_type': 0,
'device_name': 'vivo', 'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false', 'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key':
user.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = []; List<RecVideoItemAppModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['items']) { 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}; return {'status': true, 'data': list};
} else { } else {
@ -80,8 +95,12 @@ class VideoHttp {
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<HotVideoItemModel> list = []; List<HotVideoItemModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['list']) { 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}; return {'status': true, 'data': list};
} else { } else {

View File

@ -4,8 +4,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart'; import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.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/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/router/app_pages.dart';
@ -33,19 +35,38 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( 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; // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp( return GetMaterialApp(
title: 'PiLiPaLa', title: 'PiLiPaLa',
theme: ThemeData( theme: ThemeData(
fontFamily: 'HarmonyOS', // fontFamily: 'HarmonyOS',
colorScheme: lightDynamic ?? colorScheme: currentThemeValue == ThemeType.dark
ColorScheme.fromSeed( ? darkColorScheme
seedColor: Colors.green, : lightColorScheme,
brightness: Brightness.light,
),
useMaterial3: true, useMaterial3: true,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{ builders: <TargetPlatform, PageTransitionsBuilder>{
@ -56,12 +77,10 @@ class MyApp extends StatelessWidget {
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
fontFamily: 'HarmonyOS', // fontFamily: 'HarmonyOS',
colorScheme: darkDynamic ?? colorScheme: currentThemeValue == ThemeType.light
ColorScheme.fromSeed( ? lightColorScheme
seedColor: Colors.green, : darkColorScheme,
brightness: Brightness.dark,
),
useMaterial3: true, useMaterial3: true,
), ),
localizationsDelegates: const [ localizationsDelegates: const [

View File

@ -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<String, dynamic> json) {
hasNext = json['has_next'];
list = json['list'] != null
? json['list']
.map<BangumiListItemModel>((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<String, dynamic> 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'];
}
}

View File

@ -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<LiveController>,
'page': const LivePage(),
},
{
'icon': const Icon(
Icons.thumb_up_off_alt_outlined,
size: 15,
),
'label': '推荐',
'type': TabType.rcmd,
'ctr': Get.find<RcmdController>,
'page': const RcmdPage(),
},
{
'icon': const Icon(
Icons.whatshot_outlined,
size: 15,
),
'label': '热门',
'type': TabType.hot,
'ctr': Get.find<HotController>,
'page': const HotPage(),
},
{
'icon': const Icon(
Icons.play_circle_outlined,
size: 15,
),
'label': '番剧',
'type': TabType.bangumi,
'ctr': Get.find<BangumiController>,
'page': const BangumiPage(),
},
];

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