merge main
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 描述你所遇到的bug
|
||||
title: ''
|
||||
labels: 问题反馈
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 问题描述
|
||||
请提供一个清晰而简明的问题描述。
|
||||
|
||||
### 复现步骤
|
||||
请提供复现该问题所需的具体步骤。
|
||||
|
||||
### 预期行为
|
||||
请描述你期望的正确行为或结果。
|
||||
|
||||
### 系统信息
|
||||
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
|
||||
|
||||
### 相关截图或日志
|
||||
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。
|
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 对于功能的一些建议
|
||||
title: ''
|
||||
labels: 功能
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 功能描述
|
||||
请提供对所请求功能的清晰描述。
|
||||
|
||||
### 目标
|
||||
请描述你希望通过这个功能实现的目标。
|
||||
|
||||
### 解决方案
|
||||
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
|
||||
|
||||
### 其他
|
||||
请提供已实现该功能或类似功能的应用
|
84
.github/workflows/main.yml
vendored
Normal 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
@ -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
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
<br/>
|
||||
|
||||
### 功能
|
||||
|
||||
目前着重移动端(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/>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:label="pilipala"
|
||||
android:label="PiliPala"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 650 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 485 B |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 805 B |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 1.4 KiB |
@ -2,5 +2,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 398 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 447 B |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 542 B |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
BIN
assets/images/logo/logo_android_2.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 25 KiB |
BIN
assets/sreenshot/174shots_so.png
Normal file
After Width: | Height: | Size: 526 KiB |
BIN
assets/sreenshot/510shots_so.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/sreenshot/850shots_so.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/sreenshot/bangumi.png
Normal file
After Width: | Height: | Size: 659 KiB |
BIN
assets/sreenshot/bangumi_detail.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
assets/sreenshot/dynamic.png
Normal file
After Width: | Height: | Size: 217 KiB |
BIN
assets/sreenshot/home.png
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
assets/sreenshot/media.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
assets/sreenshot/member.png
Normal file
After Width: | Height: | Size: 407 KiB |
BIN
assets/sreenshot/search.png
Normal file
After Width: | Height: | Size: 522 KiB |
BIN
assets/sreenshot/set.png
Normal file
After Width: | Height: | Size: 44 KiB |
11
change_log/1.0.0.0817.md
Normal file
@ -0,0 +1,11 @@
|
||||
## 1.0.0
|
||||
|
||||
### 初始版本
|
||||
+ 直播、推荐、动态功能
|
||||
+ 投稿、番剧播放功能
|
||||
+ 播放器手势支持
|
||||
+ 画质、音质、解码格式支持
|
||||
+ 点赞、投币、收藏功能
|
||||
+ 关注/取关、用户主页功能
|
||||
+ 评论功能
|
||||
+ 历史记录、稍后再看功能
|
7
change_log/1.0.1.0817.md
Normal file
@ -0,0 +1,7 @@
|
||||
## 1.0.1
|
||||
|
||||
### 修复
|
||||
+ 升级播放器依赖
|
||||
+ android平台 AV1格式视频支持
|
||||
+ 视频全屏功能
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1020 B After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Pilipala</string>
|
||||
<string>PiliPala</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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<StatefulWidget> createState() => AnimatedDialogState();
|
||||
@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<RcmdController>().crossAxisCount,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<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))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -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<RcmdController>().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<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(
|
||||
// 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<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
|
||||
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(
|
||||
|
@ -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';
|
||||
}
|
||||
|
36
lib/http/bangumi.dart
Normal 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
@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
|
@ -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<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);
|
||||
}
|
||||
|
||||
|
@ -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<dynamic> 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<HotVideoItemModel> 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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<RecVideoItemAppModel> list = [];
|
||||
List<int> 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<HotVideoItemModel> list = [];
|
||||
List<int> 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 {
|
||||
|
@ -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: <TargetPlatform, PageTransitionsBuilder>{
|
||||
@ -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 [
|
||||
|
90
lib/models/bangumi/list.dart
Normal 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'];
|
||||
}
|
||||
}
|
55
lib/models/common/tab_type.dart
Normal 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(),
|
||||
},
|
||||
];
|