mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-11 20:47:52 +08:00
Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047e3cd26f | ||
|
|
a04da4c34a | ||
|
|
95c35cac58 | ||
|
|
9429225029 | ||
|
|
9cf9867fac | ||
|
|
2fad0d38a7 | ||
|
|
130bf36115 | ||
|
|
f571121b66 | ||
|
|
12a236f397 | ||
|
|
226d554249 | ||
|
|
f8dc7ece2d | ||
|
|
3d7583d376 | ||
|
|
e645274609 | ||
|
|
0817b183a4 | ||
|
|
591078d4e4 | ||
|
|
4075978dc4 | ||
|
|
9b8b9bd1ab | ||
|
|
ba192a0356 | ||
|
|
2dbbf28d13 | ||
|
|
b62d3a03bc | ||
|
|
32d2ec8ca0 | ||
|
|
26b437f5df | ||
|
|
25a38450c3 | ||
|
|
0c0dfc6fbe | ||
|
|
2d338450f4 | ||
|
|
54d13fe7f2 | ||
|
|
efcbd5b1a4 | ||
|
|
6a5faba5fd | ||
|
|
c29db2bc4f | ||
|
|
11c6d5ac7e | ||
|
|
c8fc48e24d | ||
|
|
d0687f6d5a | ||
|
|
d0dc89ab54 | ||
|
|
ca10033a7d | ||
|
|
69463d4945 | ||
|
|
7b3d132802 | ||
|
|
96fb17e6b1 | ||
|
|
cc74cec7ba | ||
|
|
a1bfe1f4ee | ||
|
|
ae88700b96 | ||
|
|
84bc0d55b7 | ||
|
|
358a14b3f6 | ||
|
|
0be2cac3a0 | ||
|
|
86dd54991a | ||
|
|
0ece0bd7fa | ||
|
|
c9fe3c6485 | ||
|
|
093b84ca3a | ||
|
|
6c4cf5a139 | ||
|
|
794fb9c7e4 | ||
|
|
57043166a9 | ||
|
|
9ab6dcff23 | ||
|
|
eec4aa50f0 | ||
|
|
3d94a15d5d | ||
|
|
dc75d59ccd | ||
|
|
ff00ade786 | ||
|
|
43fcc36165 | ||
|
|
33465db0c3 | ||
|
|
6d9651070e | ||
|
|
c8ad94343a | ||
|
|
df81a33ae0 | ||
|
|
f4261dd8f7 | ||
|
|
5ea594b747 | ||
|
|
e8c82f69d4 | ||
|
|
41dfab371e | ||
|
|
7e400701df | ||
|
|
5b1ec83a34 | ||
|
|
adf4b6fa5d | ||
|
|
d4e33c9636 | ||
|
|
e8396bd313 | ||
|
|
96ea6d60e3 | ||
|
|
f9c365011b | ||
|
|
8ce5026778 | ||
|
|
56350b181f | ||
|
|
5982fd312b | ||
|
|
4d35dfe2f0 | ||
|
|
0eac1b2c69 | ||
|
|
89050c7ca8 | ||
|
|
ae16771b5e | ||
|
|
847f42fee3 | ||
|
|
8d4294ba75 | ||
|
|
0b9d4d970a | ||
|
|
34c024239d | ||
|
|
71daa6df29 | ||
|
|
20c1112a10 | ||
|
|
31e8c36653 | ||
|
|
e06a3d8f22 | ||
|
|
c77ceea262 | ||
|
|
28b6b769b2 | ||
|
|
57722eb579 | ||
|
|
d4e381380a | ||
|
|
21fdcdb2bb | ||
|
|
1a30e542a9 | ||
|
|
c1ce704e4e | ||
|
|
30a5889215 | ||
|
|
75a242de2a | ||
|
|
a0afbb2615 | ||
|
|
da3c087ade | ||
|
|
4dc0389624 | ||
|
|
488cb58b85 | ||
|
|
f5b50ffcb0 | ||
|
|
d9474a79c1 | ||
|
|
3a15353bc4 | ||
|
|
b239737498 | ||
|
|
5001f3b6d2 | ||
|
|
3d803cce9f | ||
|
|
d0046d0faf | ||
|
|
d59c364ba6 | ||
|
|
fee161e99b | ||
|
|
5a481dbaaf | ||
|
|
f3279b4177 | ||
|
|
242fde92f6 | ||
|
|
a9c542ac4e | ||
|
|
4aebc0aac5 | ||
|
|
51bf59e329 | ||
|
|
39716cc1d4 | ||
|
|
50cf99720b | ||
|
|
214239a6f8 | ||
|
|
0d63d6102f | ||
|
|
47e79ee7d8 | ||
|
|
22e6e19500 | ||
|
|
8ae92b859f | ||
|
|
78180a1dd1 | ||
|
|
f47c500c5b | ||
|
|
2e65b65b1d | ||
|
|
88578393c2 | ||
|
|
1643db4656 | ||
|
|
e4b8dfcada | ||
|
|
1a3f5414c6 | ||
|
|
789d8a77dd | ||
|
|
5efbdda107 | ||
|
|
2aa109b089 | ||
|
|
22abc4488b | ||
|
|
0d41731681 | ||
|
|
f467532f9d | ||
|
|
daf01df5aa | ||
|
|
738c057304 | ||
|
|
cf76cb6f63 | ||
|
|
27e39d4de5 | ||
|
|
58fd373e8c | ||
|
|
76b37437d3 | ||
|
|
8186307f98 | ||
|
|
be42ce97f8 | ||
|
|
5f6dcc9569 | ||
|
|
4539e0e5c5 | ||
|
|
d066262cdd | ||
|
|
7ac4a32468 | ||
|
|
9cf74c0db6 | ||
|
|
14f2c34d21 | ||
|
|
b7b4432d71 | ||
|
|
0be609db3d | ||
|
|
321b7933d7 | ||
|
|
1d51db0a62 | ||
|
|
18ee1d4e18 | ||
|
|
413a49bcb1 | ||
|
|
fd1bb0af30 | ||
|
|
f808012ec2 | ||
|
|
51e436faed | ||
|
|
168bb22670 | ||
|
|
1232116d22 | ||
|
|
621239551f | ||
|
|
f1a10a786d | ||
|
|
d0ef75bce7 | ||
|
|
3919e42b59 | ||
|
|
eafaa1b045 | ||
|
|
6e08735421 | ||
|
|
a5823e1e90 | ||
|
|
665f5cdeef | ||
|
|
28c2323ef1 | ||
|
|
d30dd96bbd | ||
|
|
1026fc79e1 | ||
|
|
1073d82008 | ||
|
|
30f3440b90 | ||
|
|
45e1282a0e | ||
|
|
2e480518b7 | ||
|
|
1e7ff89341 | ||
|
|
269fb033e0 | ||
|
|
dbc93883e8 | ||
|
|
144a9b604a | ||
|
|
dda0fc15c7 | ||
|
|
1dd7b9ed0a | ||
|
|
b7768e5886 | ||
|
|
7df4c5c4c7 | ||
|
|
952fd5fc38 | ||
|
|
cde0ea244b | ||
|
|
098e2220cc | ||
|
|
df41729d74 | ||
|
|
273e5649c3 | ||
|
|
de3edcfa13 | ||
|
|
1215d126cc | ||
|
|
20a89fbccb | ||
|
|
cbe814fdd6 | ||
|
|
04583e92b7 | ||
|
|
ae6c6431f3 | ||
|
|
2973299e29 | ||
|
|
52f9b0f83c | ||
|
|
2a1849d24c | ||
|
|
991ae8518a | ||
|
|
bef7a28229 | ||
|
|
753fdeea03 | ||
|
|
ef8d57ddfd | ||
|
|
582574a605 | ||
|
|
43583be6da | ||
|
|
836f1a9b06 | ||
|
|
90176a4787 | ||
|
|
a6cb49fd02 | ||
|
|
85733e071b | ||
|
|
bdd927e7e3 | ||
|
|
e2f8cb89a9 | ||
|
|
8fd51da8da | ||
|
|
0edb7f44a7 | ||
|
|
882f16bdab | ||
|
|
b6217f6e6e | ||
|
|
e9945ab63c | ||
|
|
eca69f3d6d | ||
|
|
f854e949cd | ||
|
|
e34fce6d0e | ||
|
|
b00708b498 | ||
|
|
d6ed1edc6f | ||
|
|
93560a6fb2 | ||
|
|
07307a666c | ||
|
|
0e253ecb83 | ||
|
|
8545a3cbe6 | ||
|
|
dbd8b80507 | ||
|
|
6260809e40 | ||
|
|
820c7aa324 | ||
|
|
ec8c010c96 | ||
|
|
de91bdff74 | ||
|
|
51f87cc49c | ||
|
|
821a6ad4b2 | ||
|
|
cbf0d050f8 | ||
|
|
7fab59acd2 | ||
|
|
fedb67c809 | ||
|
|
0e8502b087 | ||
|
|
64672dbdf9 | ||
|
|
329eb31387 | ||
|
|
8e8dc273aa | ||
|
|
91fc383723 | ||
|
|
e4f4a088ce | ||
|
|
45a965135e | ||
|
|
81a23ea59d | ||
|
|
79da08b285 | ||
|
|
d3c7b3830f | ||
|
|
5e0a46f268 | ||
|
|
5d1c1494dd | ||
|
|
ed3036cc43 | ||
|
|
5410a5cecc | ||
|
|
65be638b66 | ||
|
|
563edbb07c | ||
|
|
5664447e15 | ||
|
|
eee7eda1a2 | ||
|
|
513a3d2175 | ||
|
|
11dde3a887 | ||
|
|
234017cc8a | ||
|
|
f6406f47a6 | ||
|
|
a7fb8f6007 | ||
|
|
6810aaeba1 | ||
|
|
6acba93c2c | ||
|
|
169ae7d562 | ||
|
|
c371d74a0c | ||
|
|
00681e95b5 | ||
|
|
5eed75e353 | ||
|
|
9223f40f6d | ||
|
|
34bceeea39 | ||
|
|
36ee59c7da | ||
|
|
c23f15b195 | ||
|
|
94c077a4fe | ||
|
|
23ba9ad8c1 | ||
|
|
f29e49dc4c | ||
|
|
7603a72101 | ||
|
|
569cf6b4a3 | ||
|
|
e2b30200bf | ||
|
|
07d8504f91 | ||
|
|
952f1429eb | ||
|
|
c79364cef2 | ||
|
|
3ee1c9fdcd | ||
|
|
385ebd01cc | ||
|
|
a8d40b4aea | ||
|
|
dffea51223 | ||
|
|
812170ce38 | ||
|
|
c8e89653ed | ||
|
|
521c24f762 | ||
|
|
47641eeb28 | ||
|
|
ff8f6da0bb | ||
|
|
9536b5db6f | ||
|
|
a9e4f2081d | ||
|
|
9e8d34e0dc | ||
|
|
47241897de | ||
|
|
aed3b12b09 | ||
|
|
0fde99dc68 | ||
|
|
0ae2665c56 | ||
|
|
0b311d37c8 | ||
|
|
a01d54cd80 | ||
|
|
fe2b4f6735 | ||
|
|
6f5bd626b4 | ||
|
|
a7ffc3b05f | ||
|
|
45b4f9570b | ||
|
|
1d9f7f052d | ||
|
|
47fd90e4a5 | ||
|
|
ed4d2685b4 | ||
|
|
b9aa968a2e | ||
|
|
3852e21571 | ||
|
|
5a69e6abb0 | ||
|
|
726fd0b338 | ||
|
|
4aadc9b050 | ||
|
|
41c9367c42 | ||
|
|
52f888167f | ||
|
|
fee1ad56f7 | ||
|
|
4e7cf0a1bd | ||
|
|
bc0914e146 | ||
|
|
b898a78e62 | ||
|
|
1b71fd4ca6 | ||
|
|
94d055610e | ||
|
|
be371e002a | ||
|
|
7905f51067 | ||
|
|
c7fef4e998 | ||
|
|
5d8b42a928 | ||
|
|
22f668245d | ||
|
|
ce89a5fdb9 | ||
|
|
63a12ba6ed | ||
|
|
7cdfe26a26 | ||
|
|
dfd67219e3 | ||
|
|
979df1585e | ||
|
|
731a7dd3e5 | ||
|
|
4ec7a628a6 | ||
|
|
48d4e3ed34 | ||
|
|
c8a4be00ce | ||
|
|
98158c4f0c | ||
|
|
49fe27176d | ||
|
|
3d7583e010 | ||
|
|
64ff4e0d5c |
223
.github/workflows/CI.yml
vendored
223
.github/workflows/CI.yml
vendored
@@ -1,223 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - 'main'
|
||||
# paths-ignore:
|
||||
# - '**.md'
|
||||
# - '**.txt'
|
||||
# - '.github/**'
|
||||
# - '.idea/**'
|
||||
# - '!.github/workflows/CI.yml'
|
||||
|
||||
jobs:
|
||||
update_version:
|
||||
name: Read and update version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
# 定义输出变量 version,以便在其他job中引用
|
||||
new_version: ${{ steps.version.outputs.new_version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
#- name: 获取first parent commit次数
|
||||
# id: get-first-parent-commit-count
|
||||
# run: |
|
||||
# version=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
# recent_release_tag=$(git tag -l | grep $version | egrep -v "[-|+]" || true)
|
||||
# if [[ "x$recent_release_tag" == "x" ]]; then
|
||||
# echo "当前版本tag不存在,请手动生成tag."
|
||||
# exit 1
|
||||
# fi
|
||||
# git log --oneline HEAD
|
||||
# first_parent_commit_count=$(git rev-list --first-parent --count $recent_release_tag..HEAD)
|
||||
# echo "count=$first_parent_commit_count" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 从tag获取之前的version_code与beta版本号
|
||||
id: get-previous-codes
|
||||
run: |
|
||||
version=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
last_tag=$(git tag --sort=committerdate | tail -1)
|
||||
if (echo $last_tag | grep -v "+"); then
|
||||
echo "Tag格式不正确"
|
||||
exit 1
|
||||
elif (echo $last_tag | grep -v $version); then
|
||||
echo "当前版本tag不存在,请手动添加tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version_code=$(echo $last_tag | cut -d "+" -f 2)
|
||||
beta_code=$(echo $last_tag | cut -d "+" -f 1 | cut -d "." -f 4)
|
||||
beta_code=${beta_code:-0}
|
||||
|
||||
echo "beta-code=$beta_code" >> $GITHUB_OUTPUT
|
||||
echo "version-code=$version_code" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 更新版本号
|
||||
id: version
|
||||
run: |
|
||||
# 读取版本号
|
||||
version_name=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
let beta_code=${{ steps.get-previous-codes.outputs.beta-code }}+1
|
||||
let version_code=${{ steps.get-previous-codes.outputs.version-code }}+1
|
||||
# 构建新版本号
|
||||
NEW_VERSION=${version_name}-beta.${beta_code}+${version_code}
|
||||
|
||||
# 输出新版本号
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# 设置新版本号为输出变量
|
||||
echo "new_version=$NEW_VERSION" >>$GITHUB_OUTPUT
|
||||
|
||||
android:
|
||||
name: Build CI (Android)
|
||||
needs: update_version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- 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
|
||||
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.24.0
|
||||
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: 更新版本号
|
||||
id: version
|
||||
run: |
|
||||
# 更新pubspec.yaml文件中的版本号
|
||||
sed -i "s/version: .*/version: ${{ needs.update_version.outputs.new_version }}/g" pubspec.yaml
|
||||
|
||||
- name: flutter build apk
|
||||
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: flutter build apk
|
||||
run: |
|
||||
sed -i "s/version: .*/version: ${{ needs.update_version.outputs.new_version }}0/g" pubspec.yaml
|
||||
flutter build apk --release
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
|
||||
|
||||
- name: 重命名应用
|
||||
run: |
|
||||
version_name=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
for file in build/app/outputs/flutter-apk/app-*.apk; do
|
||||
if [[ $file =~ app-(.?*)release.apk ]]; then
|
||||
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}${version_name}.apk"
|
||||
mv "$file" "$new_file_name"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Pilipala-CI
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/Pili-*.apk
|
||||
|
||||
iOS:
|
||||
name: Build CI (iOS)
|
||||
needs: update_version
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 安装Flutter
|
||||
if: steps.cache-flutter.outputs.cache-hit != 'true'
|
||||
uses: subosito/flutter-action@v2.10.0
|
||||
with:
|
||||
cache: true
|
||||
flutter-version: 3.24.0
|
||||
|
||||
- name: 更新版本号
|
||||
id: version
|
||||
run: |
|
||||
# 更新pubspec.yaml文件中的版本号
|
||||
sed -i "" "s/version: .*/version: ${{ needs.update_version.outputs.new_version }}/g" pubspec.yaml
|
||||
|
||||
- name: flutter build ipa
|
||||
run: |
|
||||
flutter build ios --release --no-codesign
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 app.ipa Payload/runner.app
|
||||
|
||||
- name: 重命名应用
|
||||
run: |
|
||||
version_name=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
for file in app.ipa; do
|
||||
new_file_name="build/Pili-${version_name}.ipa"
|
||||
mv "$file" "$new_file_name"
|
||||
done
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Pilipala-CI
|
||||
path: |
|
||||
build/Pili-*.ipa
|
||||
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs:
|
||||
- update_version
|
||||
- android
|
||||
- iOS
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Pilipala-CI
|
||||
path: ./Pilipala-CI
|
||||
|
||||
- name: Upload Pre-release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ needs.update_version.outputs.new_version }}
|
||||
token: ${{ secrets.GIT_TOKEN }}
|
||||
commit: main
|
||||
tag: ${{ needs.update_version.outputs.new_version }}
|
||||
prerelease: true
|
||||
allowUpdates: true
|
||||
artifacts: Pilipala-CI/*
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建Java环境
|
||||
uses: actions/setup-java@v4
|
||||
@@ -28,12 +30,30 @@ jobs:
|
||||
if: steps.cache-flutter.outputs.cache-hit != 'true'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.24.0
|
||||
channel: any
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: 修复3.24的stable显示中文不正确问题 // from orz12
|
||||
run: |
|
||||
version=$(grep -m 1 'flutter:' pubspec.yaml | awk '{print $2}')
|
||||
if [ "$(echo "$version < 3.27.0" | awk '{print ($1 < $2)}')" -eq 1 ]; then
|
||||
cd $FLUTTER_ROOT
|
||||
git config --global user.name "orz12"
|
||||
git config --global user.email "orz12@test.com"
|
||||
git cherry-pick d4124bd --strategy-option theirs
|
||||
# flutter precache
|
||||
flutter --version
|
||||
cd -
|
||||
fi
|
||||
|
||||
- name: 下载项目依赖
|
||||
run: flutter pub get
|
||||
|
||||
- name: 更新版本号
|
||||
run: |
|
||||
version_name=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
sed -i "s/version: .*/version: $version_name-$(git rev-parse --short HEAD)+$(git rev-list --count HEAD)/g" pubspec.yaml
|
||||
|
||||
- name: Write key
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
@@ -45,17 +65,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: flutter build apk
|
||||
run: flutter build apk --release --target-platform=android-arm64
|
||||
|
||||
- name: flutter build apk
|
||||
run: flutter build apk --release --split-per-abi
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-release
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-release.apk
|
||||
run: |
|
||||
chmod +x lib/scripts/build.sh
|
||||
lib/scripts/build.sh
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
130
.github/workflows/build-ios.yml
vendored
130
.github/workflows/build-ios.yml
vendored
@@ -1,130 +0,0 @@
|
||||
name: Build iOS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'build-ios'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.txt'
|
||||
- '.github/**'
|
||||
- '.idea/**'
|
||||
- '!.github/workflows/build-ios.yml'
|
||||
|
||||
jobs:
|
||||
update_version:
|
||||
name: Read latest version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
# 定义输出变量 version,以便在其他job中引用
|
||||
new_version: ${{ steps.get-last-tag.outputs.tag}}
|
||||
last_commit: ${{ steps.get-last-commit.outputs.last_commit }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 获取最后一次提交
|
||||
id: get-last-commit
|
||||
run: |
|
||||
last_commit=$(git log -1 --pretty="%h %s" --first-parent)
|
||||
echo "last_commit=$last_commit" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 获取最后一个tag
|
||||
id: get-last-tag
|
||||
run: |
|
||||
version=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
|
||||
last_tag=$(git tag --sort=committerdate | tail -1)
|
||||
if (echo $last_tag | grep -v "+"); then
|
||||
echo "Illegal tag!"
|
||||
exit 1
|
||||
elif (echo $last_tag | grep -v $version); then
|
||||
echo "No tags for current version in the repo, please add one manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tag=$last_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
iOS:
|
||||
name: Build CI (iOS)
|
||||
needs: update_version
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: 安装Flutter
|
||||
if: steps.cache-flutter.outputs.cache-hit != 'true'
|
||||
uses: subosito/flutter-action@v2.10.0
|
||||
with:
|
||||
cache: true
|
||||
flutter-version: 3.24.0
|
||||
|
||||
- name: 更新版本号
|
||||
id: version
|
||||
run: |
|
||||
# 更新pubspec.yaml文件中的版本号
|
||||
sed -i "" "s/version: .*/version: ${{ needs.update_version.outputs.new_version }}/g" pubspec.yaml
|
||||
|
||||
- name: flutter build ipa
|
||||
run: |
|
||||
flutter build ios --release --no-codesign
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 app.ipa Payload/runner.app
|
||||
|
||||
- name: 重命名应用
|
||||
run: |
|
||||
for file in app.ipa; do
|
||||
new_file_name="build/Pili-${{ needs.update_version.outputs.new_version }}.ipa"
|
||||
mv "$file" "$new_file_name"
|
||||
done
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: PiliPalaX-iOS
|
||||
path: |
|
||||
build/Pili-*.ipa
|
||||
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs:
|
||||
- update_version
|
||||
- iOS
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: PiliPalaX-iOS
|
||||
path: ./PiliPalaX-iOS
|
||||
|
||||
# - name: Upload Pre-release
|
||||
# uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# name: ${{ needs.update_version.outputs.new_version }}
|
||||
# token: ${{ secrets.GIT_TOKEN }}
|
||||
# commit: main
|
||||
# tag: ${{ needs.update_version.outputs.new_version }}
|
||||
# prerelease: true
|
||||
# allowUpdates: true
|
||||
# artifacts: Pilipala-CI/*
|
||||
|
||||
- name: 发送到Telegram频道
|
||||
uses: xireiki/channel-post@v1.0.7
|
||||
with:
|
||||
bot_token: ${{ secrets.BOT_TOKEN }}
|
||||
chat_id: ${{ secrets.CHAT_ID }}
|
||||
large_file: false
|
||||
method: sendFile
|
||||
path: PiliPalaX-iOS/*
|
||||
parse_mode: Markdown
|
||||
context: "*v${{ needs.update_version.outputs.new_version }}*\n${{ needs.update_version.outputs.last_commit }}"
|
||||
10
.github/workflows/ios.yml
vendored
10
.github/workflows/ios.yml
vendored
@@ -16,17 +16,23 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Set up xcode
|
||||
uses: BoundfoxStudios/action-xcode-select@v1
|
||||
- name: 更新版本号
|
||||
run: |
|
||||
version_name=$(yq e '.version' pubspec.yaml | cut -d "+" -f 1)
|
||||
sed -i '' "s/version: .*/version: $version_name+$(git rev-list --count HEAD)/" pubspec.yaml
|
||||
|
||||
- name: Build iOS
|
||||
run: |
|
||||
chmod +x lib/scripts/build.sh
|
||||
lib/scripts/build.sh
|
||||
flutter build ios --release --no-codesign
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 ios-release-no-sign.ipa Payload/runner.app
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -133,4 +133,6 @@ app.*.symbols
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
!.vscode/settings.json
|
||||
!.vscode/settings.json
|
||||
|
||||
/lib/build_config.dart
|
||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -5,18 +5,18 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "pilipala",
|
||||
"name": "piliplus",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "pilipala (profile mode)",
|
||||
"name": "piliplus (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "pilipala (release mode)",
|
||||
"name": "piliplus (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
|
||||
21
README.md
21
README.md
@@ -1,24 +1,24 @@
|
||||
<div align="center">
|
||||
<img width="200" height="200" src="https://github.com/orz12/pilipala/blob/main/assets/images/logo/logo_android.png">
|
||||
<img width="200" height="200" src="https://github.com/bggRGjQaUbCoE/PiliPlus/blob/main/assets/images/logo/logo_android.png">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1>PiliPalaX</h1>
|
||||
<h1>PiliPlus</h1>
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
</div>
|
||||
<p>使用Flutter开发的BiliBili第三方客户端</p>
|
||||
|
||||
<img src="https://github.com/orz12/pilipala/blob/main/assets/screenshots/510shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/orz12/pilipala/blob/main/assets/screenshots/174shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/orz12/pilipala/blob/main/assets/screenshots/850shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/bggRGjQaUbCoE/PiliPlus/blob/main/assets/screenshots/510shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/bggRGjQaUbCoE/PiliPlus/blob/main/assets/screenshots/174shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/bggRGjQaUbCoE/PiliPlus/blob/main/assets/screenshots/850shots_so.png" width="32%" alt="home" />
|
||||
<br/>
|
||||
<img src="https://github.com/orz12/pilipala/blob/main/assets/screenshots/main_screen.png" width="96%" alt="home" />
|
||||
<img src="https://github.com/bggRGjQaUbCoE/PiliPlus/blob/main/assets/screenshots/main_screen.png" width="96%" alt="home" />
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
@@ -186,9 +186,10 @@
|
||||
|
||||
## 声明
|
||||
|
||||
此项目(PiliPalaX)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
||||
此项目(PiliPlus)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
||||
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
||||
本仓库做了更激进的修改,感谢原作者的开源精神。
|
||||
|
||||
感谢使用
|
||||
|
||||
@@ -35,7 +35,7 @@ def _keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties["keyAlias"]
|
||||
def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"]
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
|
||||
namespace 'com.example.pilipalax'
|
||||
ndkVersion flutter.ndkVersion
|
||||
@@ -61,7 +61,7 @@ android {
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
minSdkVersion 21
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
|
||||
<application
|
||||
android:label="PiliPalaX Debug"
|
||||
android:label="PiliPlus Debug"
|
||||
tools:replace="android:label">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -36,7 +36,7 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPalaX Debug">
|
||||
<intent-filter android:label="PiliPlus Debug">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -60,7 +60,7 @@
|
||||
<!--<data android:host="bangumi.bilibili.com"/>-->
|
||||
<!--<data android:host="space.bilibili.com"/>-->
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPalaX Debug">
|
||||
<intent-filter android:label="PiliPlus Debug">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="PiliPalaX"
|
||||
android:label="PiliPlus"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
@@ -67,7 +67,7 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPalaX+">
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -91,7 +91,7 @@
|
||||
<!--<data android:host="bangumi.bilibili.com"/>-->
|
||||
<!--<data android:host="space.bilibili.com"/>-->
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPalaX+">
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -16,7 +16,8 @@ class MainActivity : AudioServiceActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
methodChannel = MethodChannel(flutterEngine!!.getDartExecutor()!!.getBinaryMessenger(), CHANNEL)
|
||||
|
||||
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "PiliPlus")
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
if (call.method == "back") {
|
||||
back()
|
||||
@@ -53,10 +54,6 @@ class MainActivity : AudioServiceActivity() {
|
||||
methodChannel.invokeMethod("onUserLeaveHint", null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL = "onUserLeaveHint"
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel(
|
||||
@@ -7,6 +7,7 @@
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#212121</item>
|
||||
<item name="android:defaultFocusHighlightEnabled">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:defaultFocusHighlightEnabled">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#ffffff</item>
|
||||
<item name="android:defaultFocusHighlightEnabled">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:defaultFocusHighlightEnabled">false</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
@@ -1,5 +1,5 @@
|
||||
PiliPalaX is a third-party Bilibili client developed in Flutter,
|
||||
fork from PiliPala (https://github.com/guozhigq/pilipala).
|
||||
PiliPlus is a third-party Bilibili client developed in Flutter,
|
||||
fork from PiliPalaX (https://github.com/orz12/PiliPalaX).
|
||||
|
||||
Top Features:
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
PiliPalaX
|
||||
PiliPlus
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PiliPalaX 是使用 Flutter 开发的 BiliBili 第三方客户端,
|
||||
是由PiliPala仓库fork并进行了差异化开发的版本
|
||||
PiliPlus 是使用 Flutter 开发的 BiliBili 第三方客户端,
|
||||
是由PiliPalaX仓库fork并进行了差异化开发的版本
|
||||
|
||||
主要功能:
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
PiliPalaX
|
||||
PiliPlus
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>PiliPalaX</string>
|
||||
<string>PiliPlus</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>PiliPalaX</string>
|
||||
<string>PiliPlus</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -42,7 +42,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
||||
@@ -21,13 +21,10 @@ class Constants {
|
||||
static const String traceId =
|
||||
'11111111111111111111111111111111:1111111111111111:0:0';
|
||||
static const String userAgent =
|
||||
'Mozilla/5.0 BiliDroid/1.46.2 (bbcallen@gmail.com) os/android model/vivo mobi_app/android build/1462100 channel/bili innerVer/1462100 osVer/14 network/2';
|
||||
'Mozilla/5.0 BiliDroid/1.46.2 (bbcallen@gmail.com) os/android model/vivo mobi_app/android_hd build/2001100 channel/yingyongbao innerVer/2001100 osVer/14 network/2';
|
||||
static const String statistics =
|
||||
'%7B%22appId%22%3A5%2C%22platform%22%3A3%2C%22version%22%3A%221.46.2%22%2C%22abtest%22%3A%22%22%7D';
|
||||
// jsonEncode(
|
||||
// {"appId": 5, "platform": 3, "version": "1.46.2", "abtest": ""});
|
||||
// Uri.encodeComponent(
|
||||
// '{"appId": 5,"platform": 3,"version": "1.46.2","abtest": ""}');
|
||||
//Uri.encodeComponent('{"appId": 5,"platform": 3,"version": "1.46.2","abtest": ""}');
|
||||
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static const List<Map<String, dynamic>> internationalDialingPrefix = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
|
||||
import 'skeleton.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
@@ -8,79 +8,88 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Skeleton(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius:
|
||||
BorderRadius.circular(StyleString.imgRadius.x),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// VideoContent(videoItem: videoItem)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius:
|
||||
BorderRadius.circular(StyleString.imgRadius.x),
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// VideoContent(videoItem: videoItem)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
@@ -45,6 +45,17 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
Container(
|
||||
width: 110,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
Container(
|
||||
width: 75,
|
||||
height: 13,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPalaX/common/widgets/no_splash_factory.dart';
|
||||
import 'package:PiliPalaX/common/widgets/overlay_pop.dart';
|
||||
import 'package:PiliPlus/common/widgets/no_splash_factory.dart';
|
||||
import 'package:PiliPlus/common/widgets/overlay_pop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedDialog extends StatefulWidget {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPalaX/models/dynamics/article_content_model.dart';
|
||||
import 'package:PiliPalaX/pages/preview/view.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:flutter_html/flutter_html.dart';
|
||||
Widget articleContent({
|
||||
required BuildContext context,
|
||||
required List<ArticleContentModel> list,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
List<String>? imgList = list
|
||||
.where((item) => item.pic != null)
|
||||
@@ -17,7 +17,7 @@ Widget articleContent({
|
||||
.toList();
|
||||
return SliverList.separated(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) {
|
||||
itemBuilder: (context, index) {
|
||||
ArticleContentModel item = list[index];
|
||||
if (item.text != null) {
|
||||
List<InlineSpan> spanList = [];
|
||||
@@ -56,25 +56,29 @@ Widget articleContent({
|
||||
);
|
||||
} else if (item.pic != null) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) => GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(
|
||||
builder: (context, constraints) => Hero(
|
||||
tag: item.pic!.pics!.first.url!,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback(
|
||||
imgList,
|
||||
imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
);
|
||||
} else {
|
||||
context.imageView(
|
||||
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
imgList: imgList,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth *
|
||||
item.pic!.pics!.first.height! /
|
||||
item.pic!.pics!.first.width!,
|
||||
src: item.pic!.pics!.first.url,
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth *
|
||||
item.pic!.pics!.first.height! /
|
||||
item.pic!.pics!.first.width!,
|
||||
src: item.pic!.pics!.first.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -42,8 +42,8 @@ class PBadge extends StatelessWidget {
|
||||
color = Colors.white;
|
||||
}
|
||||
if (type == 'color') {
|
||||
bgColor = t.primaryContainer.withOpacity(0.5);
|
||||
color = t.primary;
|
||||
bgColor = t.secondaryContainer.withOpacity(0.5);
|
||||
color = t.onSecondaryContainer;
|
||||
}
|
||||
if (type == 'line') {
|
||||
bgColor = Colors.transparent;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverPersistentHeaderDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
CustomSliverPersistentHeaderDelegate({
|
||||
required this.child,
|
||||
required this.bgColor,
|
||||
double extent = 45,
|
||||
}) : _minExtent = extent,
|
||||
_maxExtent = extent;
|
||||
final double _minExtent;
|
||||
final double _maxExtent;
|
||||
final Widget child;
|
||||
final Color bgColor;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
//创建child子组件
|
||||
//shrinkOffset:child偏移值minExtent~maxExtent
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
|
||||
return ColoredBox(
|
||||
color: bgColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
//SliverPersistentHeader最大高度
|
||||
@override
|
||||
double get maxExtent => _maxExtent;
|
||||
|
||||
//SliverPersistentHeader最小高度
|
||||
@override
|
||||
double get minExtent => _minExtent;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(
|
||||
covariant CustomSliverPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.bgColor != bgColor;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
|
||||
Box<dynamic> setting = GStorage.setting;
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
|
||||
class CustomToast extends StatelessWidget {
|
||||
const CustomToast({super.key, required this.msg});
|
||||
@@ -11,8 +8,8 @@ class CustomToast extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double toastOpacity =
|
||||
setting.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;
|
||||
final double toastOpacity = GStorage.setting
|
||||
.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;
|
||||
return Container(
|
||||
margin:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),
|
||||
@@ -28,7 +25,7 @@ class CustomToast extends StatelessWidget {
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -133,32 +133,9 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
}
|
||||
if (_height == 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
// Padding which centers the flexible space within the app bar
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: MediaQuery.paddingOf(context).top / 2),
|
||||
child: Container(
|
||||
key: _childKey,
|
||||
child:
|
||||
widget.flexibleSpace ?? SizedBox(height: kToolbarHeight)),
|
||||
),
|
||||
Positioned.fill(
|
||||
// 10 is the magic number which the app bar is pushed down within the sliver app bar. Couldnt find exactly where this number
|
||||
// comes from and found it through trial and error.
|
||||
top: 10,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: widget.leading,
|
||||
actions: widget.actions,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: Container(
|
||||
key: _childKey,
|
||||
child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/pages/preview/view.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'network_img_layer.dart';
|
||||
@@ -9,6 +9,7 @@ Widget htmlRender({
|
||||
int? imgCount,
|
||||
List<String>? imgList,
|
||||
required double constrainedWidth,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
return SelectionArea(
|
||||
child: Html(
|
||||
@@ -40,29 +41,29 @@ Widget htmlRender({
|
||||
// extensionContext.element!.previousElementSibling == null ||
|
||||
// extensionContext.element!.nextElementSibling == null;
|
||||
// imgUrl = Utils().imageUrl(imgUrl!);
|
||||
// return Image.network(
|
||||
// imgUrl,
|
||||
// return CachedNetworkImage(
|
||||
// imageUrl: imgUrl,
|
||||
// width: isEmote ? 22 : null,
|
||||
// height: isEmote ? 22 : null,
|
||||
// );
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(
|
||||
initialPage: 0,
|
||||
return Hero(
|
||||
tag: imgUrl,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback([imgUrl], 0);
|
||||
} else {
|
||||
context.imageView(
|
||||
imgList: [imgUrl],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: isEmote ? 22 : constrainedWidth,
|
||||
height: isEmote ? 22 : 200,
|
||||
src: imgUrl,
|
||||
ignoreHeight: !isEmote,
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: isEmote ? 22 : constrainedWidth,
|
||||
height: isEmote ? 22 : 200,
|
||||
src: imgUrl,
|
||||
ignoreHeight: !isEmote,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -73,7 +74,7 @@ Widget htmlRender({
|
||||
],
|
||||
style: {
|
||||
'html': Style(
|
||||
fontSize: FontSize(17),
|
||||
fontSize: FontSize(16),
|
||||
lineHeight: LineHeight.percent(160),
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@@ -109,7 +110,7 @@ Widget htmlRender({
|
||||
margin: Margins.only(bottom: 8),
|
||||
),
|
||||
'h3,h4,h5': Style(
|
||||
fontSize: FontSize(17),
|
||||
fontSize: FontSize(16),
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 4),
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ class HttpError extends StatelessWidget {
|
||||
this.errMsg,
|
||||
this.callback,
|
||||
this.btnText,
|
||||
this.extraWidget,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -14,6 +15,7 @@ class HttpError extends StatelessWidget {
|
||||
final String? errMsg;
|
||||
final Function()? callback;
|
||||
final String? btnText;
|
||||
final Widget? extraWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -44,8 +46,13 @@ class HttpError extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
if (extraWidget != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
extraWidget!,
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
if (callback != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
if (extraWidget == null) const SizedBox(height: 20),
|
||||
FilledButton.tonal(
|
||||
onPressed: callback,
|
||||
style: ButtonStyle(
|
||||
|
||||
@@ -6,6 +6,7 @@ Widget iconButton({
|
||||
required IconData icon,
|
||||
required VoidCallback? onPressed,
|
||||
double size = 36,
|
||||
double? iconSize,
|
||||
Color? bgColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
@@ -17,7 +18,7 @@ Widget iconButton({
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: size / 2,
|
||||
size: iconSize ?? size / 2,
|
||||
color: iconColor ?? Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
@@ -28,3 +29,22 @@ Widget iconButton({
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget mediumButton({
|
||||
String? tooltip,
|
||||
IconData? icon,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(icon),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
96
lib/common/widgets/image_save.dart
Normal file
96
lib/common/widgets/image_save.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void imageSaveDialog({
|
||||
required BuildContext context,
|
||||
required String? title,
|
||||
required String? cover,
|
||||
}) {
|
||||
final double imgWidth = min(Get.width, Get.height) - 8 * 2;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) => Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[cover ?? ''],
|
||||
);
|
||||
// 保存成功,自动关闭弹窗
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 20),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/common/widgets/badge.dart';
|
||||
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPalaX/common/widgets/nine_grid_view.dart';
|
||||
import 'package:PiliPalaX/pages/preview/view.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/nine_grid_view.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageModel {
|
||||
@@ -23,10 +23,13 @@ class ImageModel {
|
||||
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
|
||||
}
|
||||
|
||||
Widget image(
|
||||
Widget imageview(
|
||||
double maxWidth,
|
||||
List<ImageModel> picArr,
|
||||
) {
|
||||
List<ImageModel> picArr, {
|
||||
VoidCallback? onViewImage,
|
||||
ValueChanged<int>? onDismissed,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
double imageWidth = (maxWidth - 2 * 5) / 3;
|
||||
double imageHeight = imageWidth;
|
||||
if (picArr.length == 1) {
|
||||
@@ -53,39 +56,43 @@ Widget image(
|
||||
height: picArr.length == 1 ? imageHeight : null,
|
||||
width: picArr.length == 1 ? imageWidth : maxWidth,
|
||||
itemCount: picArr.length,
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(
|
||||
itemBuilder: (context, index) => Hero(
|
||||
tag: picArr[index].url,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback(picArr.map((item) => item.url).toList(), index);
|
||||
} else {
|
||||
onViewImage?.call();
|
||||
context.imageView(
|
||||
initialPage: index,
|
||||
imgList: picArr.map((item) => item.url).toList(),
|
||||
onDismissed: onDismissed,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: NetworkImgLayer(
|
||||
src: picArr[index].url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
isLongPic: () => picArr[index].isLongPic,
|
||||
callback: () =>
|
||||
picArr[index].safeWidth <= picArr[index].safeHeight,
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: NetworkImgLayer(
|
||||
src: picArr[index].url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
isLongPic: () => picArr[index].isLongPic,
|
||||
callback: () =>
|
||||
picArr[index].safeWidth <= picArr[index].safeHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (picArr[index].isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
],
|
||||
if (picArr[index].isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// A [PageRoute] with a semi transparent background.
|
||||
///
|
||||
/// Similar to calling [showDialog] except it can be used with a [Navigator] to
|
||||
/// show a [Hero] animation.
|
||||
class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
HeroDialogRoute({
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget child = builder(context);
|
||||
final Widget result = Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: child,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
1515
lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
Normal file
1515
lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// A callback for the [InteractiveViewerBoundary] that is called when the scale
|
||||
/// changed.
|
||||
typedef ScaleChanged = void Function(double scale);
|
||||
|
||||
/// Builds an [InteractiveViewer] and provides callbacks that are called when a
|
||||
/// horizontal boundary has been hit.
|
||||
///
|
||||
/// The callbacks are called when an interaction ends by listening to the
|
||||
/// [InteractiveViewer.onInteractionEnd] callback.
|
||||
class InteractiveViewerBoundary extends StatefulWidget {
|
||||
const InteractiveViewerBoundary({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.boundaryWidth,
|
||||
this.controller,
|
||||
this.onScaleChanged,
|
||||
this.onLeftBoundaryHit,
|
||||
this.onRightBoundaryHit,
|
||||
this.onNoBoundaryHit,
|
||||
required this.maxScale,
|
||||
required this.minScale,
|
||||
this.onDismissed,
|
||||
this.onReset,
|
||||
this.dismissThreshold = 0.2,
|
||||
});
|
||||
|
||||
final VoidCallback? onReset;
|
||||
final double dismissThreshold;
|
||||
final VoidCallback? onDismissed;
|
||||
|
||||
final Widget child;
|
||||
|
||||
/// The max width this widget can have.
|
||||
///
|
||||
/// If the [InteractiveViewer] can take up the entire screen width, this
|
||||
/// should be set to `MediaQuery.of(context).size.width`.
|
||||
final double boundaryWidth;
|
||||
|
||||
/// The [TransformationController] for the [InteractiveViewer].
|
||||
final custom.TransformationController? controller;
|
||||
|
||||
/// Called when the scale changed after an interaction ended.
|
||||
final ScaleChanged? onScaleChanged;
|
||||
|
||||
/// Called when the left boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onLeftBoundaryHit;
|
||||
|
||||
/// Called when the right boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onRightBoundaryHit;
|
||||
|
||||
/// Called when no boundary has been hit after an interaction ended.
|
||||
final VoidCallback? onNoBoundaryHit;
|
||||
|
||||
final double maxScale;
|
||||
|
||||
final double minScale;
|
||||
|
||||
@override
|
||||
InteractiveViewerBoundaryState createState() =>
|
||||
InteractiveViewerBoundaryState();
|
||||
}
|
||||
|
||||
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
with SingleTickerProviderStateMixin {
|
||||
custom.TransformationController? _controller;
|
||||
|
||||
double? _scale;
|
||||
|
||||
late AnimationController _animateController;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<Decoration> _opacityAnimation;
|
||||
|
||||
Offset _offset = Offset.zero;
|
||||
bool _dragging = false;
|
||||
|
||||
bool get _isActive => _dragging || _animateController.isAnimating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = widget.controller ?? custom.TransformationController();
|
||||
|
||||
_animateController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_updateMoveAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller!.dispose();
|
||||
_animateController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateMoveAnimation() {
|
||||
final double endX = _offset.dx.sign * (_offset.dx.abs() / _offset.dy.abs());
|
||||
final double endY = _offset.dy.sign;
|
||||
|
||||
_slideAnimation = _animateController.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: Offset(endX, endY),
|
||||
),
|
||||
);
|
||||
|
||||
_scaleAnimation = _animateController.drive(
|
||||
Tween<double>(
|
||||
begin: 1,
|
||||
end: 0.25,
|
||||
),
|
||||
);
|
||||
|
||||
_opacityAnimation = _animateController.drive(
|
||||
DecorationTween(
|
||||
begin: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
),
|
||||
end: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDragStart(ScaleStartDetails details) {
|
||||
_dragging = true;
|
||||
|
||||
if (_animateController.isAnimating) {
|
||||
_animateController.stop();
|
||||
} else {
|
||||
_offset = Offset.zero;
|
||||
_animateController.value = 0.0;
|
||||
}
|
||||
setState(_updateMoveAnimation);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(ScaleUpdateDetails details) {
|
||||
if (!_isActive || _animateController.isAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
_offset += details.focalPointDelta;
|
||||
|
||||
setState(_updateMoveAnimation);
|
||||
|
||||
if (!_animateController.isAnimating) {
|
||||
_animateController.value = _offset.dy.abs() / context.size!.height;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragEnd(ScaleEndDetails details) {
|
||||
if (!_isActive || _animateController.isAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
_dragging = false;
|
||||
|
||||
if (_animateController.isCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_animateController.isDismissed) {
|
||||
// if the dragged value exceeded the dismissThreshold, call onDismissed
|
||||
// else animate back to initial position.
|
||||
if (_animateController.value > widget.dismissThreshold) {
|
||||
widget.onDismissed?.call();
|
||||
} else {
|
||||
_animateController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateBoundaryDetection() {
|
||||
final double scale = _controller!.value.row0[0];
|
||||
|
||||
if (_scale != scale) {
|
||||
// the scale changed
|
||||
_scale = scale;
|
||||
widget.onScaleChanged?.call(scale);
|
||||
}
|
||||
|
||||
if (scale <= 1.01) {
|
||||
// cant hit any boundaries when the child is not scaled
|
||||
return;
|
||||
}
|
||||
|
||||
final double xOffset = _controller!.value.row0[3];
|
||||
final double boundaryWidth = widget.boundaryWidth;
|
||||
final double boundaryEnd = boundaryWidth * scale;
|
||||
final double xPos = boundaryEnd + xOffset;
|
||||
|
||||
if (boundaryEnd.round() == xPos.round()) {
|
||||
// left boundary hit
|
||||
widget.onLeftBoundaryHit?.call();
|
||||
} else if (boundaryWidth.round() == xPos.round()) {
|
||||
// right boundary hit
|
||||
widget.onRightBoundaryHit?.call();
|
||||
} else {
|
||||
widget.onNoBoundaryHit?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Widget get content => DecoratedBoxTransition(
|
||||
decoration: _opacityAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return custom.InteractiveViewer(
|
||||
maxScale: widget.maxScale,
|
||||
minScale: widget.minScale,
|
||||
transformationController: _controller,
|
||||
onInteractionEnd: (_) => _updateBoundaryDetection(),
|
||||
onPanStart: _handleDragStart,
|
||||
onPanUpdate: _handleDragUpdate,
|
||||
onPanEnd: _handleDragEnd,
|
||||
onReset: widget.onReset,
|
||||
isAnimating: () => _animateController.value != 0,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:status_bar_control/status_bar_control.dart';
|
||||
import 'interactive_viewer_boundary.dart';
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
/// Builds a carousel controlled by a [PageView] for the tweet media sources.
|
||||
///
|
||||
/// Used for showing a full screen view of the [TweetMedia] sources.
|
||||
///
|
||||
/// The sources can be panned and zoomed interactively using an
|
||||
/// [InteractiveViewer].
|
||||
/// An [InteractiveViewerBoundary] is used to detect when the boundary of the
|
||||
/// source is hit after zooming in to disable or enable the swiping gesture of
|
||||
/// the [PageView].
|
||||
///
|
||||
typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||
BuildContext context, int index, bool isFocus, bool enablePageView);
|
||||
|
||||
typedef IndexedTagStringBuilder = String Function(int index);
|
||||
|
||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
required this.sources,
|
||||
required this.initIndex,
|
||||
this.itemBuilder,
|
||||
this.maxScale = 8,
|
||||
this.minScale = 1.0,
|
||||
this.onPageChanged,
|
||||
this.onDismissed,
|
||||
this.setStatusBar,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final VoidCallback? onClose;
|
||||
|
||||
final bool? setStatusBar;
|
||||
|
||||
/// The sources to show.
|
||||
final List<String> sources;
|
||||
|
||||
/// The index of the first source in [sources] to show.
|
||||
final int initIndex;
|
||||
|
||||
/// The item content
|
||||
final IndexedFocusedWidgetBuilder? itemBuilder;
|
||||
|
||||
final double maxScale;
|
||||
|
||||
final double minScale;
|
||||
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
final ValueChanged<int>? onDismissed;
|
||||
|
||||
@override
|
||||
State<InteractiveviewerGallery> createState() =>
|
||||
_InteractiveviewerGalleryState();
|
||||
}
|
||||
|
||||
class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
with SingleTickerProviderStateMixin {
|
||||
PageController? _pageController;
|
||||
custom.TransformationController? _transformationController;
|
||||
|
||||
/// The controller to animate the transformation value of the
|
||||
/// [InteractiveViewer] when it should reset.
|
||||
late AnimationController _animationController;
|
||||
Animation<Matrix4>? _animation;
|
||||
|
||||
/// `true` when an source is zoomed in and not at the at a horizontal boundary
|
||||
/// to disable the [PageView].
|
||||
bool _enablePageView = true;
|
||||
|
||||
late Offset _doubleTapLocalPosition;
|
||||
|
||||
int? currentIndex;
|
||||
|
||||
late List<bool> _thumbList;
|
||||
late int _quality;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_quality =
|
||||
GStorage.setting.get(SettingBoxKey.previewQuality, defaultValue: 80);
|
||||
_thumbList = List.generate(widget.sources.length, (_) => true);
|
||||
|
||||
_pageController = PageController(initialPage: widget.initIndex);
|
||||
|
||||
_transformationController = custom.TransformationController();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..addListener(() {
|
||||
_transformationController!.value =
|
||||
_animation?.value ?? Matrix4.identity();
|
||||
});
|
||||
|
||||
currentIndex = widget.initIndex;
|
||||
if (widget.setStatusBar != false) {
|
||||
setStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
setStatusBar() async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
await StatusBarControl.setHidden(
|
||||
true,
|
||||
animation: StatusBarAnimation.FADE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
_pageController?.dispose();
|
||||
_animationController.removeListener(() {});
|
||||
_animationController.dispose();
|
||||
if (widget.setStatusBar != false) {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// When the source gets scaled up, the swipe up / down to dismiss gets
|
||||
/// disabled.
|
||||
///
|
||||
/// When the scale resets, the dismiss and the page view swiping gets enabled.
|
||||
void _onScaleChanged(double scale) {
|
||||
final bool initialScale = scale <= widget.minScale;
|
||||
|
||||
if (initialScale) {
|
||||
if (!_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When the left boundary has been hit after scaling up the source, the page
|
||||
/// view swiping gets enabled if it has a page to swipe to.
|
||||
void _onLeftBoundaryHit() {
|
||||
if (!_enablePageView && _pageController!.page!.floor() > 0) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the right boundary has been hit after scaling up the source, the page
|
||||
/// view swiping gets enabled if it has a page to swipe to.
|
||||
void _onRightBoundaryHit() {
|
||||
if (!_enablePageView &&
|
||||
_pageController!.page!.floor() < widget.sources.length - 1) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the source has been scaled up and no horizontal boundary has been hit,
|
||||
/// the page view swiping gets disabled.
|
||||
void _onNoBoundaryHit() {
|
||||
if (_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// When the page view changed its page, the source will animate back into the
|
||||
/// original scale if it was scaled up.
|
||||
///
|
||||
/// Additionally the swipe up / down to dismiss gets enabled.
|
||||
void _onPageChanged(int page) {
|
||||
setState(() {
|
||||
currentIndex = page;
|
||||
});
|
||||
widget.onPageChanged?.call(page);
|
||||
if (_transformationController!.value != Matrix4.identity()) {
|
||||
// animate the reset for the transformation of the interactive viewer
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: Matrix4.identity(),
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
String _getActualUrl(int index) => _thumbList[index] && _quality != 100
|
||||
? '${widget.sources[index]}@${_quality}q.webp'.http2https
|
||||
: widget.sources[index].http2https;
|
||||
|
||||
void onClose() {
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!();
|
||||
} else {
|
||||
Get.back();
|
||||
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
InteractiveViewerBoundary(
|
||||
controller: _transformationController,
|
||||
boundaryWidth: MediaQuery.of(context).size.width,
|
||||
onScaleChanged: _onScaleChanged,
|
||||
onLeftBoundaryHit: _onLeftBoundaryHit,
|
||||
onRightBoundaryHit: _onRightBoundaryHit,
|
||||
onNoBoundaryHit: _onNoBoundaryHit,
|
||||
maxScale: widget.maxScale,
|
||||
minScale: widget.minScale,
|
||||
onDismissed: onClose,
|
||||
onReset: () {
|
||||
if (!_enablePageView) {
|
||||
setState(() {
|
||||
_enablePageView = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: PageView.builder(
|
||||
onPageChanged: _onPageChanged,
|
||||
controller: _pageController,
|
||||
physics:
|
||||
_enablePageView ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.sources.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
onDoubleTapDown: (TapDownDetails details) {
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
index,
|
||||
index == currentIndex,
|
||||
_enablePageView,
|
||||
)
|
||||
: _itemBuilder(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
8,
|
||||
20,
|
||||
MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
decoration: _enablePageView
|
||||
? BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.3)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: onClose,
|
||||
),
|
||||
widget.sources.length > 1
|
||||
? Text(
|
||||
"${currentIndex! + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)
|
||||
: const SizedBox(),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
onTap: () => onShareImg(widget.sources[currentIndex!]),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
onTap: () {
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources as List<String>,
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 图片分享
|
||||
void onShareImg(String imgUrl) async {
|
||||
SmartDialog.showLoading();
|
||||
var response = await Request()
|
||||
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
SmartDialog.dismiss();
|
||||
String imgName =
|
||||
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
File(path).writeAsBytesSync(response.data);
|
||||
Share.shareXFiles([XFile(path)], subject: imgUrl);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(index) {
|
||||
return Center(
|
||||
child: Hero(
|
||||
tag: widget.sources[index],
|
||||
child: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: _getActualUrl(index),
|
||||
// fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(value: progress.progress ?? 0),
|
||||
),
|
||||
);
|
||||
},
|
||||
// errorListener: (value) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// setState(() {
|
||||
// _thumbList[index] = false;
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
Matrix4 matrix = _transformationController!.value.clone();
|
||||
double currentScale = matrix.row0.x;
|
||||
|
||||
double targetScale = widget.minScale;
|
||||
|
||||
if (currentScale <= widget.minScale) {
|
||||
targetScale = widget.maxScale * 0.4;
|
||||
}
|
||||
|
||||
double offSetX = targetScale == 1.0
|
||||
? 0.0
|
||||
: -_doubleTapLocalPosition.dx * (targetScale - 1);
|
||||
double offSetY = targetScale == 1.0
|
||||
? 0.0
|
||||
: -_doubleTapLocalPosition.dy * (targetScale - 1);
|
||||
|
||||
matrix = Matrix4.fromList([
|
||||
targetScale,
|
||||
matrix.row1.x,
|
||||
matrix.row2.x,
|
||||
matrix.row3.x,
|
||||
matrix.row0.y,
|
||||
targetScale,
|
||||
matrix.row2.y,
|
||||
matrix.row3.y,
|
||||
matrix.row0.z,
|
||||
matrix.row1.z,
|
||||
targetScale,
|
||||
matrix.row3.z,
|
||||
offSetX,
|
||||
offSetY,
|
||||
matrix.row2.w,
|
||||
matrix.row3.w
|
||||
]);
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: matrix,
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animationController
|
||||
.forward(from: 0)
|
||||
.whenComplete(() => _onScaleChanged(targetScale));
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
onShareImg(widget.sources[currentIndex!]);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('分享', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources,
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi;
|
||||
import 'package:PiliPalaX/models/video_detail_res.dart' as video;
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
|
||||
import 'package:PiliPlus/models/video_detail_res.dart' as video;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
@@ -17,13 +18,18 @@ import '../../utils/utils.dart';
|
||||
class ListSheetContent extends StatefulWidget {
|
||||
const ListSheetContent({
|
||||
super.key,
|
||||
this.index,
|
||||
this.index, // tab index
|
||||
this.season,
|
||||
required this.episodes,
|
||||
this.episodes,
|
||||
this.bvid,
|
||||
this.aid,
|
||||
required this.currentCid,
|
||||
required this.changeFucCall,
|
||||
this.onClose,
|
||||
this.onReverse,
|
||||
this.showTitle,
|
||||
this.isSupportReverse,
|
||||
this.isReversed,
|
||||
});
|
||||
|
||||
final dynamic index;
|
||||
@@ -33,6 +39,11 @@ class ListSheetContent extends StatefulWidget {
|
||||
final int? aid;
|
||||
final int currentCid;
|
||||
final Function changeFucCall;
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onReverse;
|
||||
final bool? showTitle;
|
||||
final bool? isSupportReverse;
|
||||
final bool? isReversed;
|
||||
|
||||
@override
|
||||
State<ListSheetContent> createState() => _ListSheetContentState();
|
||||
@@ -41,26 +52,63 @@ class ListSheetContent extends StatefulWidget {
|
||||
class _ListSheetContentState extends State<ListSheetContent>
|
||||
with TickerProviderStateMixin {
|
||||
late List<ItemScrollController> itemScrollController = [];
|
||||
late final int currentIndex =
|
||||
widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ??
|
||||
0;
|
||||
late int currentIndex = _currentIndex;
|
||||
late List<bool> reverse;
|
||||
|
||||
int get _index => widget.index ?? 0;
|
||||
bool get _isList =>
|
||||
widget.season != null &&
|
||||
late final bool _isList = widget.season != null &&
|
||||
widget.season?.sections is List &&
|
||||
widget.season.sections.length > 1;
|
||||
dynamic get episodes =>
|
||||
widget.episodes ?? widget.season?.sections[_index].episodes;
|
||||
TabController? _ctr;
|
||||
StreamController? _indexStream;
|
||||
int? _seasonFav;
|
||||
StreamController? _favStream;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ListSheetContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.showTitle != false) {
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = _currentIndex;
|
||||
|
||||
void jumpToCurrent() {
|
||||
if (this.currentIndex != currentIndex) {
|
||||
this.currentIndex = currentIndex;
|
||||
try {
|
||||
itemScrollController[_index].jumpTo(
|
||||
index: currentIndex,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// jump to current
|
||||
if (_ctr != null && widget.index != _ctr?.index) {
|
||||
_ctr?.animateTo(_index, duration: const Duration(milliseconds: 200));
|
||||
Future.delayed(const Duration(milliseconds: 300)).then((_) {
|
||||
jumpToCurrent();
|
||||
});
|
||||
} else {
|
||||
jumpToCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
int get _currentIndex => max(
|
||||
0,
|
||||
_isList
|
||||
? widget.season.sections[_index].episodes
|
||||
.indexWhere((e) => e.cid == widget.currentCid)
|
||||
: episodes.indexWhere((e) => e.cid == widget.currentCid));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isList) {
|
||||
_indexStream = StreamController<int>();
|
||||
_indexStream ??= StreamController<int>.broadcast();
|
||||
_ctr = TabController(
|
||||
vsync: this,
|
||||
length: widget.season.sections.length,
|
||||
@@ -76,11 +124,8 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
reverse = _isList
|
||||
? List.generate(widget.season.sections.length, (_) => false)
|
||||
: [false];
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
itemScrollController[_index].jumpTo(index: currentIndex);
|
||||
});
|
||||
if (widget.bvid != null && widget.season != null) {
|
||||
_favStream = StreamController<int>();
|
||||
_favStream ??= StreamController<int>();
|
||||
() async {
|
||||
dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid);
|
||||
if (result['status']) {
|
||||
@@ -89,12 +134,19 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
}
|
||||
}();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
try {
|
||||
itemScrollController[_index].jumpTo(index: currentIndex);
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_favStream?.close();
|
||||
_favStream = null;
|
||||
_indexStream?.close();
|
||||
_indexStream = null;
|
||||
_ctr?.removeListener(() {});
|
||||
_ctr?.dispose();
|
||||
super.dispose();
|
||||
@@ -136,7 +188,8 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
}
|
||||
}
|
||||
SmartDialog.showToast('切换到:$title');
|
||||
Get.back();
|
||||
widget.onClose?.call();
|
||||
currentIndex = index;
|
||||
widget.changeFucCall(
|
||||
episode is bangumi.EpisodeItem ? episode.epId : null,
|
||||
episode.runtimeType.toString() == "EpisodeItem"
|
||||
@@ -170,7 +223,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
)
|
||||
: null,
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) => NetworkImgLayer(
|
||||
builder: (context, constraints) => NetworkImgLayer(
|
||||
radius: 6,
|
||||
src: episode is video.EpisodeItem
|
||||
? episode.arc?.pic
|
||||
@@ -229,17 +282,19 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
children: [
|
||||
Container(
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.showTitle != false ? 14 : 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'合集(${_isList ? widget.season.epCount : widget.episodes!.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (widget.showTitle != false)
|
||||
Text(
|
||||
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _favStream?.stream,
|
||||
builder: (_, snapshot) => snapshot.hasData
|
||||
? _mediumButton(
|
||||
builder: (context, snapshot) => snapshot.hasData
|
||||
? mediumButton(
|
||||
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
|
||||
icon: _seasonFav == 1
|
||||
? Icons.notifications_off_outlined
|
||||
@@ -261,39 +316,43 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
_mediumButton(
|
||||
mediumButton(
|
||||
tooltip: '跳至顶部',
|
||||
icon: Icons.vertical_align_top,
|
||||
onPressed: () {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? 0
|
||||
: _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: widget.episodes.length - 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? 0
|
||||
: _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: episodes.length - 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
_mediumButton(
|
||||
mediumButton(
|
||||
tooltip: '跳至底部',
|
||||
icon: Icons.vertical_align_bottom,
|
||||
onPressed: () {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: widget.episodes.length - 1
|
||||
: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: episodes.length - 1
|
||||
: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
_mediumButton(
|
||||
mediumButton(
|
||||
tooltip: '跳至当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () async {
|
||||
@@ -301,21 +360,36 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
_ctr?.animateTo(_index);
|
||||
await Future.delayed(const Duration(milliseconds: 225));
|
||||
}
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: currentIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: currentIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
if (widget.isSupportReverse == true)
|
||||
if (!_isList)
|
||||
_reverseButton
|
||||
else
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
initialData: _index,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.data == _index
|
||||
? _reverseButton
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
initialData: 0,
|
||||
builder: (_, snapshot) => _mediumButton(
|
||||
tooltip: reverse[snapshot.data] ? '正序' : '反序',
|
||||
initialData: _index,
|
||||
builder: (context, snapshot) => mediumButton(
|
||||
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
|
||||
icon: !reverse[snapshot.data]
|
||||
? MdiIcons.sortAscending
|
||||
: MdiIcons.sortDescending,
|
||||
? MdiIcons.sortNumericAscending
|
||||
: MdiIcons.sortNumericDescending,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
|
||||
@@ -323,11 +397,12 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
},
|
||||
),
|
||||
),
|
||||
_mediumButton(
|
||||
tooltip: '关闭',
|
||||
icon: Icons.close,
|
||||
onPressed: Get.back,
|
||||
),
|
||||
if (widget.onClose != null)
|
||||
mediumButton(
|
||||
tooltip: '关闭',
|
||||
icon: Icons.close,
|
||||
onPressed: widget.onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -358,36 +433,41 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
index, widget.season.sections[index].episodes),
|
||||
),
|
||||
)
|
||||
: _buildBody(null, widget.episodes),
|
||||
: _buildBody(null, episodes),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mediumButton({
|
||||
String? tooltip,
|
||||
IconData? icon,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(icon),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget get _reverseButton => mediumButton(
|
||||
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
|
||||
icon: widget.isReversed == true
|
||||
? MdiIcons.sortDescending
|
||||
: MdiIcons.sortAscending,
|
||||
onPressed: () async {
|
||||
if (widget.showTitle == false) {
|
||||
// jump to current
|
||||
if (_ctr != null && _ctr?.index != (_index)) {
|
||||
_ctr?.animateTo(_index);
|
||||
await Future.delayed(const Duration(milliseconds: 225));
|
||||
}
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: currentIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
widget.onReverse?.call();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildBody(i, episodes) => Material(
|
||||
child: ScrollablePositionedList.separated(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
reverse: reverse[i ?? 0],
|
||||
itemCount: episodes.length,
|
||||
@@ -404,7 +484,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
||||
);
|
||||
},
|
||||
itemScrollController: itemScrollController[i ?? 0],
|
||||
separatorBuilder: (_, index) => Divider(
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
|
||||
@@ -80,7 +80,7 @@ class LiveCard extends StatelessWidget {
|
||||
Text(
|
||||
liveItem.title as String,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w400),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPalaX/common/widgets/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/http_error.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget get loadingWidget => Center(child: CircularProgressIndicator());
|
||||
@@ -8,3 +9,36 @@ Widget errorWidget({errMsg, callback}) => HttpError(
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView(
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget replyErrorWidget(context, isSliver, errMsg, onReload) => HttpError(
|
||||
isSliver: isSliver,
|
||||
errMsg:
|
||||
'${errMsg.startsWith('gRPC Error') ? '如无法加载评论:\n关闭代理\n或设置中关闭使用gRPC加载评论\n\n' : ''}$errMsg',
|
||||
callback: onReload,
|
||||
extraWidget: errMsg.startsWith('gRPC Error') && GlobalData().grpcReply
|
||||
? FilledButton.tonal(
|
||||
onPressed: () {
|
||||
GlobalData().grpcReply = false;
|
||||
onReload();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context).colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
'暂时关闭gRPC加载评论',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPalaX/utils/global_data.dart';
|
||||
import '../../utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
Box<dynamic> setting = GStorage.setting;
|
||||
|
||||
class NetworkImgLayer extends StatelessWidget {
|
||||
const NetworkImgLayer({
|
||||
super.key,
|
||||
@@ -67,7 +63,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
child: Builder(
|
||||
builder: (context) => CachedNetworkImage(
|
||||
imageUrl:
|
||||
'${src?.startsWith('//') == true ? 'https:$src' : src?.http2https}${thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}',
|
||||
'${src?.startsWith('//') == true ? 'https:$src' : src?.http2https}${type != 'emote' && thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}',
|
||||
width: width,
|
||||
height: ignoreHeight == null || ignoreHeight == false
|
||||
? height
|
||||
@@ -83,17 +79,17 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
fadeInDuration:
|
||||
fadeInDuration ?? const Duration(milliseconds: 120),
|
||||
filterQuality: FilterQuality.low,
|
||||
errorWidget: (BuildContext context, String url, Object error) =>
|
||||
placeholder(context),
|
||||
// errorWidget: (BuildContext context, String url, Object error) =>
|
||||
// placeholder(context),
|
||||
placeholder: (BuildContext context, String url) =>
|
||||
placeholder(context),
|
||||
imageBuilder: imageBuilder,
|
||||
errorListener: (value) {
|
||||
thumbnail = false;
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
},
|
||||
// errorListener: (value) {
|
||||
// thumbnail = false;
|
||||
// if (context.mounted) {
|
||||
// (context as Element).markNeedsBuild();
|
||||
// }
|
||||
// },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPalaX/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../utils/download.dart';
|
||||
@@ -83,14 +82,8 @@ class OverlayPop extends StatelessWidget {
|
||||
context,
|
||||
[
|
||||
videoItem is card.Card
|
||||
? (videoItem as card.Card)
|
||||
.smallCoverV5
|
||||
.base
|
||||
.cover
|
||||
.http2https
|
||||
: (videoItem.pic != null
|
||||
? (videoItem.pic as String).http2https
|
||||
: (videoItem.cover as String).http2https)
|
||||
? (videoItem as card.Card).smallCoverV5.base.cover
|
||||
: videoItem.pic ?? videoItem.cover
|
||||
],
|
||||
);
|
||||
closeFn?.call();
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
// ignore_for_file: depend_on_referenced_packages
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
|
||||
double get maxDragOffset => 100;
|
||||
double hideHeight = maxDragOffset / 2.3;
|
||||
double refreshHeight = maxDragOffset / 1.5;
|
||||
|
||||
class PullToRefreshHeader extends StatelessWidget {
|
||||
const PullToRefreshHeader(
|
||||
this.info,
|
||||
this.lastRefreshTime, {
|
||||
this.color,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PullToRefreshScrollNotificationInfo? info;
|
||||
final DateTime? lastRefreshTime;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PullToRefreshScrollNotificationInfo? infos = info;
|
||||
if (infos == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
String text = '';
|
||||
if (infos.mode == PullToRefreshIndicatorMode.armed) {
|
||||
text = 'Release to refresh';
|
||||
} else if (infos.mode == PullToRefreshIndicatorMode.refresh ||
|
||||
infos.mode == PullToRefreshIndicatorMode.snap) {
|
||||
text = 'Loading...';
|
||||
} else if (infos.mode == PullToRefreshIndicatorMode.done) {
|
||||
text = 'Refresh completed.';
|
||||
} else if (infos.mode == PullToRefreshIndicatorMode.drag) {
|
||||
text = 'Pull to refresh';
|
||||
} else if (infos.mode == PullToRefreshIndicatorMode.canceled) {
|
||||
text = 'Cancel refresh';
|
||||
}
|
||||
|
||||
final TextStyle ts = const TextStyle(
|
||||
color: Colors.grey,
|
||||
).copyWith(fontSize: 14);
|
||||
|
||||
final double dragOffset = info?.dragOffset ?? 0.0;
|
||||
|
||||
final DateTime time = lastRefreshTime ?? DateTime.now();
|
||||
final double top = -hideHeight + dragOffset;
|
||||
return Container(
|
||||
height: dragOffset,
|
||||
color: color ?? Colors.transparent,
|
||||
// padding: EdgeInsets.only(top: dragOffset / 3),
|
||||
// padding: EdgeInsets.only(bottom: 5.0),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
top: top,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
child: RefreshImage(top: top),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Text(text, style: ts),
|
||||
Text(
|
||||
'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}',
|
||||
style: ts.copyWith(fontSize: 14),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RefreshImage extends StatelessWidget {
|
||||
const RefreshImage({
|
||||
super.key,
|
||||
required this.top,
|
||||
});
|
||||
|
||||
final double top;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double imageSize = 30;
|
||||
return ExtendedImage.asset(
|
||||
'assets/flutterCandies_grey.png',
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
|
||||
final double imageHeight = image.height.toDouble();
|
||||
final double imageWidth = image.width.toDouble();
|
||||
final Size size = rect.size;
|
||||
final double y =
|
||||
(1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight;
|
||||
|
||||
canvas.drawImageRect(
|
||||
image,
|
||||
Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
|
||||
Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
|
||||
size.width, (imageHeight - y) / imageHeight * size.height),
|
||||
Paint()
|
||||
..colorFilter =
|
||||
const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
|
||||
..isAntiAlias = false
|
||||
..filterQuality = FilterQuality.low,
|
||||
);
|
||||
|
||||
//canvas.restore();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,668 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
|
||||
Widget refreshIndicator({
|
||||
required RefreshCallback onRefresh,
|
||||
required Widget child,
|
||||
}) {
|
||||
return RefreshIndicator(
|
||||
displacement: 20,
|
||||
displacement: displacement,
|
||||
onRefresh: onRefresh,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// The over-scroll distance that moves the indicator to its maximum
|
||||
// displacement, as a percentage of the scrollable's container extent.
|
||||
|
||||
double displacement = 20;
|
||||
double kDragContainerExtentPercentage = 0.25;
|
||||
|
||||
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
|
||||
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
|
||||
const double _kDragSizeFactorLimit = 1.5;
|
||||
|
||||
// When the scroll ends, the duration of the refresh indicator's animation
|
||||
// to the RefreshIndicator's displacement.
|
||||
const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
|
||||
|
||||
// The duration of the ScaleTransition that starts when the refresh action
|
||||
// has completed.
|
||||
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
|
||||
|
||||
/// The signature for a function that's called when the user has dragged a
|
||||
/// [RefreshIndicator] far enough to demonstrate that they want the app to
|
||||
/// refresh. The returned [Future] must complete when the refresh operation is
|
||||
/// finished.
|
||||
///
|
||||
/// Used by [RefreshIndicator.onRefresh].
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
|
||||
// The state machine moves through these modes only when the scrollable
|
||||
// identified by scrollableKey has been scrolled to its min or max limit.
|
||||
enum _RefreshIndicatorMode {
|
||||
drag, // Pointer is down.
|
||||
armed, // Dragged far enough that an up event will run the onRefresh callback.
|
||||
snap, // Animating to the indicator's final "displacement".
|
||||
refresh, // Running the refresh callback.
|
||||
done, // Animating the indicator's fade-out after refreshing.
|
||||
canceled, // Animating the indicator's fade-out after not arming.
|
||||
}
|
||||
|
||||
/// Used to configure how [RefreshIndicator] can be triggered.
|
||||
enum RefreshIndicatorTriggerMode {
|
||||
/// The indicator can be triggered regardless of the scroll position
|
||||
/// of the [Scrollable] when the drag starts.
|
||||
anywhere,
|
||||
|
||||
/// The indicator can only be triggered if the [Scrollable] is at the edge
|
||||
/// when the drag starts.
|
||||
onEdge,
|
||||
}
|
||||
|
||||
enum _IndicatorType { material, adaptive }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
|
||||
///
|
||||
/// When the child's [Scrollable] descendant overscrolls, an animated circular
|
||||
/// progress indicator is faded into view. When the scroll ends, if the
|
||||
/// indicator has been dragged far enough for it to become completely opaque,
|
||||
/// the [onRefresh] callback is called. The callback is expected to update the
|
||||
/// scrollable's contents and then complete the [Future] it returns. The refresh
|
||||
/// indicator disappears after the callback's [Future] has completed.
|
||||
///
|
||||
/// The trigger mode is configured by [RefreshIndicator.triggerMode].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how [RefreshIndicator] can be triggered in different ways.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to trigger [RefreshIndicator] in a nested scroll view using
|
||||
/// the [notificationPredicate] property.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Troubleshooting
|
||||
///
|
||||
/// ### Refresh indicator does not show up
|
||||
///
|
||||
/// The [RefreshIndicator] will appear if its scrollable descendant can be
|
||||
/// overscrolled, i.e. if the scrollable's content is bigger than its viewport.
|
||||
/// To ensure that the [RefreshIndicator] will always appear, even if the
|
||||
/// scrollable's content fits within its viewport, set the scrollable's
|
||||
/// [Scrollable.physics] property to [AlwaysScrollableScrollPhysics]:
|
||||
///
|
||||
/// ```dart
|
||||
/// ListView(
|
||||
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||
/// // ...
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// A [RefreshIndicator] can only be used with a vertical scroll view.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://material.io/design/platform-guidance/android-swipe-to-refresh.html>
|
||||
/// * [RefreshIndicatorState], can be used to programmatically show the refresh indicator.
|
||||
/// * [RefreshProgressIndicator], widget used by [RefreshIndicator] to show
|
||||
/// the inner circular progress spinner during refreshes.
|
||||
/// * [CupertinoSliverRefreshControl], an iOS equivalent of the pull-to-refresh pattern.
|
||||
/// Must be used as a sliver inside a [CustomScrollView] instead of wrapping
|
||||
/// around a [ScrollView] because it's a part of the scrollable instead of
|
||||
/// being overlaid on top of it.
|
||||
class RefreshIndicator extends StatefulWidget {
|
||||
/// Creates a refresh indicator.
|
||||
///
|
||||
/// The [onRefresh], [child], and [notificationPredicate] arguments must be
|
||||
/// non-null. The default
|
||||
/// [displacement] is 40.0 logical pixels.
|
||||
///
|
||||
/// The [semanticsLabel] is used to specify an accessibility label for this widget.
|
||||
/// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel].
|
||||
/// An empty string may be passed to avoid having anything read by screen reading software.
|
||||
/// The [semanticsValue] may be used to specify progress on the widget.
|
||||
const RefreshIndicator({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
}) : _indicatorType = _IndicatorType.material;
|
||||
|
||||
/// Creates an adaptive [RefreshIndicator] based on whether the target
|
||||
/// platform is iOS or macOS, following Material design's
|
||||
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
|
||||
///
|
||||
/// When the descendant overscrolls, a different spinning progress indicator
|
||||
/// is shown depending on platform. On iOS and macOS,
|
||||
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
|
||||
/// [CircularProgressIndicator] appears.
|
||||
///
|
||||
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
|
||||
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
|
||||
///
|
||||
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
||||
///
|
||||
/// Notably the scrollable widget itself will have slightly different behavior
|
||||
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
|
||||
const RefreshIndicator.adaptive({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
}) : _indicatorType = _IndicatorType.adaptive;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// The refresh indicator will be stacked on top of this child. The indicator
|
||||
/// will appear when child's Scrollable descendant is over-scrolled.
|
||||
///
|
||||
/// Typically a [ListView] or [CustomScrollView].
|
||||
final Widget child;
|
||||
|
||||
/// The distance from the child's top or bottom [edgeOffset] where
|
||||
/// the refresh indicator will settle. During the drag that exposes the refresh
|
||||
/// indicator, its actual displacement may significantly exceed this value.
|
||||
///
|
||||
/// In most cases, [displacement] distance starts counting from the parent's
|
||||
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
|
||||
/// value is calculated from that offset instead of the parent's edge.
|
||||
final double displacement;
|
||||
|
||||
/// The offset where [RefreshProgressIndicator] starts to appear on drag start.
|
||||
///
|
||||
/// Depending whether the indicator is showing on the top or bottom, the value
|
||||
/// of this variable controls how far from the parent's edge the progress
|
||||
/// indicator starts to appear. This may come in handy when, for example, the
|
||||
/// UI contains a top [Widget] which covers the parent's edge where the progress
|
||||
/// indicator would otherwise appear.
|
||||
///
|
||||
/// By default, the edge offset is set to 0.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [displacement], can be used to change the distance from the edge that
|
||||
/// the indicator settles.
|
||||
final double edgeOffset;
|
||||
|
||||
/// A function that's called when the user has dragged the refresh indicator
|
||||
/// far enough to demonstrate that they want the app to refresh. The returned
|
||||
/// [Future] must complete when the refresh operation is finished.
|
||||
final RefreshCallback onRefresh;
|
||||
|
||||
/// The progress indicator's foreground color. The current theme's
|
||||
/// [ColorScheme.primary] by default.
|
||||
final Color? color;
|
||||
|
||||
/// The progress indicator's background color. The current theme's
|
||||
/// [ThemeData.canvasColor] by default.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// A check that specifies whether a [ScrollNotification] should be
|
||||
/// handled by this widget.
|
||||
///
|
||||
/// By default, checks whether `notification.depth == 0`. Set it to something
|
||||
/// else for more complicated layouts.
|
||||
final ScrollNotificationPredicate notificationPredicate;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
|
||||
///
|
||||
/// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
|
||||
/// if it is null.
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Defines [strokeWidth] for `RefreshIndicator`.
|
||||
///
|
||||
/// By default, the value of [strokeWidth] is 2.0 pixels.
|
||||
final double strokeWidth;
|
||||
|
||||
final _IndicatorType _indicatorType;
|
||||
|
||||
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
|
||||
///
|
||||
/// The [RefreshIndicator] can be pulled out in two cases,
|
||||
/// 1, Keep dragging if the scrollable widget at the edge with zero scroll position
|
||||
/// when the drag starts.
|
||||
/// 2, Keep dragging after overscroll occurs if the scrollable widget has
|
||||
/// a non-zero scroll position when the drag starts.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered.
|
||||
///
|
||||
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
|
||||
final RefreshIndicatorTriggerMode triggerMode;
|
||||
|
||||
@override
|
||||
RefreshIndicatorState createState() => RefreshIndicatorState();
|
||||
}
|
||||
|
||||
/// Contains the state for a [RefreshIndicator]. This class can be used to
|
||||
/// programmatically show the refresh indicator, see the [show] method.
|
||||
class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
with TickerProviderStateMixin<RefreshIndicator> {
|
||||
late AnimationController _positionController;
|
||||
late AnimationController _scaleController;
|
||||
late Animation<double> _positionFactor;
|
||||
late Animation<double> _scaleFactor;
|
||||
late Animation<double> _value;
|
||||
late Animation<Color?> _valueColor;
|
||||
|
||||
_RefreshIndicatorMode? _mode;
|
||||
late Future<void> _pendingRefreshFuture;
|
||||
bool? _isIndicatorAtTop;
|
||||
double? _dragOffset;
|
||||
late Color _effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
|
||||
static final Animatable<double> _threeQuarterTween =
|
||||
Tween<double>(begin: 0.0, end: 0.75);
|
||||
static final Animatable<double> _kDragSizeFactorLimitTween =
|
||||
Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
|
||||
static final Animatable<double> _oneToZeroTween =
|
||||
Tween<double>(begin: 1.0, end: 0.0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_positionController = AnimationController(vsync: this);
|
||||
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
|
||||
_value = _positionController.drive(
|
||||
_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
|
||||
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
_scaleFactor = _scaleController.drive(_oneToZeroTween);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_setupColorTween();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RefreshIndicator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.color != widget.color) {
|
||||
_setupColorTween();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_positionController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setupColorTween() {
|
||||
// Reset the current value color.
|
||||
_effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
final Color color = _effectiveValueColor;
|
||||
if (color.alpha == 0x00) {
|
||||
// Set an always stopped animation instead of a driven tween.
|
||||
_valueColor = AlwaysStoppedAnimation<Color>(color);
|
||||
} else {
|
||||
// Respect the alpha of the given color.
|
||||
_valueColor = _positionController.drive(
|
||||
ColorTween(
|
||||
begin: color.withAlpha(0),
|
||||
end: color.withAlpha(color.alpha),
|
||||
).chain(
|
||||
CurveTween(
|
||||
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldStart(ScrollNotification notification) {
|
||||
// If the notification.dragDetails is null, this scroll is not triggered by
|
||||
// user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll.
|
||||
// In this case, we don't want to trigger the refresh indicator.
|
||||
return ((notification is ScrollStartNotification &&
|
||||
notification.dragDetails != null) ||
|
||||
(notification is ScrollUpdateNotification &&
|
||||
notification.dragDetails != null &&
|
||||
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
|
||||
((notification.metrics.axisDirection == AxisDirection.up &&
|
||||
notification.metrics.extentAfter == 0.0) ||
|
||||
(notification.metrics.axisDirection == AxisDirection.down &&
|
||||
notification.metrics.extentBefore == 0.0)) &&
|
||||
_mode == null &&
|
||||
_start(notification.metrics.axisDirection);
|
||||
}
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (!widget.notificationPredicate(notification)) {
|
||||
return false;
|
||||
}
|
||||
if (_shouldStart(notification)) {
|
||||
setState(() {
|
||||
_mode = _RefreshIndicatorMode.drag;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
final bool? indicatorAtTopNow =
|
||||
switch (notification.metrics.axisDirection) {
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
if (indicatorAtTopNow != _isIndicatorAtTop) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
}
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.scrollDelta!;
|
||||
}
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
if (_mode == _RefreshIndicatorMode.armed &&
|
||||
notification.dragDetails == null) {
|
||||
// On iOS start the refresh when the Scrollable bounces back from the
|
||||
// overscroll (ScrollNotification indicating this don't have dragDetails
|
||||
// because the scroll activity is not directly triggered by a drag).
|
||||
_show();
|
||||
}
|
||||
} else if (notification is OverscrollNotification) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.overscroll;
|
||||
}
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
switch (_mode) {
|
||||
case _RefreshIndicatorMode.armed:
|
||||
if (_positionController.value < 1.0) {
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
} else {
|
||||
_show();
|
||||
}
|
||||
case _RefreshIndicatorMode.drag:
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
case _RefreshIndicatorMode.canceled:
|
||||
case _RefreshIndicatorMode.done:
|
||||
case _RefreshIndicatorMode.refresh:
|
||||
case _RefreshIndicatorMode.snap:
|
||||
case null:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _handleIndicatorNotification(
|
||||
OverscrollIndicatorNotification notification) {
|
||||
if (notification.depth != 0 || !notification.leading) {
|
||||
return false;
|
||||
}
|
||||
if (_mode == _RefreshIndicatorMode.drag) {
|
||||
notification.disallowIndicator();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _start(AxisDirection direction) {
|
||||
assert(_mode == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
assert(_dragOffset == null);
|
||||
switch (direction) {
|
||||
case AxisDirection.down:
|
||||
case AxisDirection.up:
|
||||
_isIndicatorAtTop = true;
|
||||
case AxisDirection.left:
|
||||
case AxisDirection.right:
|
||||
_isIndicatorAtTop = null;
|
||||
// we do not support horizontal scroll views.
|
||||
return false;
|
||||
}
|
||||
_dragOffset = 0.0;
|
||||
_scaleController.value = 0.0;
|
||||
_positionController.value = 0.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _checkDragOffset(double containerExtent) {
|
||||
assert(_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed);
|
||||
double newValue =
|
||||
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
|
||||
if (_mode == _RefreshIndicatorMode.armed) {
|
||||
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
|
||||
}
|
||||
_positionController.value =
|
||||
clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
|
||||
if (_mode == _RefreshIndicatorMode.drag &&
|
||||
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
|
||||
_mode = _RefreshIndicatorMode.armed;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
|
||||
await Future<void>.value();
|
||||
// This can only be called from _show() when refreshing and
|
||||
// _handleScrollNotification in response to a ScrollEndNotification or
|
||||
// direction change.
|
||||
assert(newMode == _RefreshIndicatorMode.canceled ||
|
||||
newMode == _RefreshIndicatorMode.done);
|
||||
setState(() {
|
||||
_mode = newMode;
|
||||
});
|
||||
switch (_mode!) {
|
||||
case _RefreshIndicatorMode.done:
|
||||
await _scaleController.animateTo(1.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.canceled:
|
||||
await _positionController.animateTo(0.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.armed:
|
||||
case _RefreshIndicatorMode.drag:
|
||||
case _RefreshIndicatorMode.refresh:
|
||||
case _RefreshIndicatorMode.snap:
|
||||
assert(false);
|
||||
}
|
||||
if (mounted && _mode == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_mode = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _show() {
|
||||
assert(_mode != _RefreshIndicatorMode.refresh);
|
||||
assert(_mode != _RefreshIndicatorMode.snap);
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_pendingRefreshFuture = completer.future;
|
||||
_mode = _RefreshIndicatorMode.snap;
|
||||
_positionController
|
||||
.animateTo(1.0 / _kDragSizeFactorLimit,
|
||||
duration: _kIndicatorSnapDuration)
|
||||
.then<void>((void value) {
|
||||
if (mounted && _mode == _RefreshIndicatorMode.snap) {
|
||||
setState(() {
|
||||
// Show the indeterminate progress indicator.
|
||||
_mode = _RefreshIndicatorMode.refresh;
|
||||
});
|
||||
|
||||
final Future<void> refreshResult = widget.onRefresh();
|
||||
refreshResult.whenComplete(() {
|
||||
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
|
||||
completer.complete();
|
||||
_dismiss(_RefreshIndicatorMode.done);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Show the refresh indicator and run the refresh callback as if it had
|
||||
/// been started interactively. If this method is called while the refresh
|
||||
/// callback is running, it quietly does nothing.
|
||||
///
|
||||
/// Creating the [RefreshIndicator] with a [GlobalKey<RefreshIndicatorState>]
|
||||
/// makes it possible to refer to the [RefreshIndicatorState].
|
||||
///
|
||||
/// The future returned from this method completes when the
|
||||
/// [RefreshIndicator.onRefresh] callback's future completes.
|
||||
///
|
||||
/// If you await the future returned by this function from a [State], you
|
||||
/// should check that the state is still [mounted] before calling [setState].
|
||||
///
|
||||
/// When initiated in this manner, the refresh indicator is independent of any
|
||||
/// actual scroll view. It defaults to showing the indicator at the top. To
|
||||
/// show it at the bottom, set `atTop` to false.
|
||||
Future<void> show({bool atTop = true}) {
|
||||
if (_mode != _RefreshIndicatorMode.refresh &&
|
||||
_mode != _RefreshIndicatorMode.snap) {
|
||||
if (_mode == null) {
|
||||
_start(atTop ? AxisDirection.down : AxisDirection.up);
|
||||
}
|
||||
_show();
|
||||
}
|
||||
return _pendingRefreshFuture;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final Widget child = NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: NotificationListener<OverscrollIndicatorNotification>(
|
||||
onNotification: _handleIndicatorNotification,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
assert(() {
|
||||
if (_mode == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
assert(_dragOffset != null);
|
||||
assert(_isIndicatorAtTop != null);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
final bool showIndeterminateIndicator =
|
||||
_mode == _RefreshIndicatorMode.refresh ||
|
||||
_mode == _RefreshIndicatorMode.done;
|
||||
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
child,
|
||||
if (_mode != null)
|
||||
Positioned(
|
||||
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: SizeTransition(
|
||||
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
|
||||
sizeFactor: _positionFactor, // this is what brings it down
|
||||
child: Container(
|
||||
padding: _isIndicatorAtTop!
|
||||
? EdgeInsets.only(top: widget.displacement)
|
||||
: EdgeInsets.only(bottom: widget.displacement),
|
||||
alignment: _isIndicatorAtTop!
|
||||
? Alignment.topCenter
|
||||
: Alignment.bottomCenter,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final Widget materialIndicator = RefreshProgressIndicator(
|
||||
semanticsLabel: widget.semanticsLabel ??
|
||||
MaterialLocalizations.of(context)
|
||||
.refreshIndicatorSemanticLabel,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value: showIndeterminateIndicator ? null : _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
);
|
||||
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
|
||||
switch (widget._indicatorType) {
|
||||
case _IndicatorType.material:
|
||||
return materialIndicator;
|
||||
|
||||
case _IndicatorType.adaptive:
|
||||
{
|
||||
final ThemeData theme = Theme.of(context);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return materialIndicator;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return cupertinoIndicator;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,10 @@ class Segment {
|
||||
}
|
||||
|
||||
class SegmentProgressBar extends CustomPainter {
|
||||
final double progress;
|
||||
final List<Segment> segmentColors;
|
||||
double? _defHeight;
|
||||
late double _defHeight;
|
||||
|
||||
SegmentProgressBar({
|
||||
required this.progress,
|
||||
required this.segmentColors,
|
||||
});
|
||||
|
||||
@@ -38,52 +36,44 @@ class SegmentProgressBar extends CustomPainter {
|
||||
paint.color = segmentColors[i].color;
|
||||
final segmentStart = segmentColors[i].start * size.width;
|
||||
final segmentEnd = segmentColors[i].end * size.width;
|
||||
final progressEnd = progress * size.width;
|
||||
|
||||
if (progressEnd < segmentStart) {
|
||||
break;
|
||||
}
|
||||
|
||||
final segmentWidth =
|
||||
(progressEnd < segmentEnd ? progressEnd : segmentEnd) - segmentStart;
|
||||
if (segmentWidth >= 0) {
|
||||
if (segmentEnd > segmentStart ||
|
||||
(segmentEnd == segmentStart && segmentStart > 0)) {
|
||||
if (segmentColors[i].title != null) {
|
||||
double fontSize = 8;
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(color: Colors.white, fontSize: fontSize),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
_defHeight ??= textPainter.height;
|
||||
double fontSize = 10;
|
||||
|
||||
double? prevStart;
|
||||
TextPainter getTextPainter() => TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
TextPainter textPainter = getTextPainter();
|
||||
if (i == 0) {
|
||||
_defHeight = textPainter.height;
|
||||
}
|
||||
|
||||
late double prevStart;
|
||||
if (i != 0) {
|
||||
prevStart = segmentColors[i - 1].start * size.width;
|
||||
}
|
||||
double width = i == 0 ? segmentStart : segmentStart - prevStart!;
|
||||
double width = i == 0 ? segmentStart : segmentStart - prevStart;
|
||||
|
||||
while (textPainter.width > width - 2 && fontSize >= 2) {
|
||||
fontSize -= 1;
|
||||
textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
fontSize -= 0.5;
|
||||
textPainter = getTextPainter();
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(
|
||||
0,
|
||||
-_defHeight!,
|
||||
-_defHeight,
|
||||
size.width,
|
||||
0,
|
||||
),
|
||||
@@ -94,26 +84,26 @@ class SegmentProgressBar extends CustomPainter {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
-_defHeight!,
|
||||
segmentWidth == 0 ? 2 : segmentWidth,
|
||||
size.height + _defHeight!,
|
||||
-_defHeight,
|
||||
segmentEnd == segmentStart ? 2 : segmentEnd - segmentStart,
|
||||
size.height + _defHeight,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
|
||||
double textX = i == 0
|
||||
? (segmentStart - textPainter.width) / 2
|
||||
: (segmentStart - prevStart! - textPainter.width) / 2 +
|
||||
: (segmentStart - prevStart - textPainter.width) / 2 +
|
||||
prevStart +
|
||||
1;
|
||||
double textY = -_defHeight! / 2 - textPainter.height / 2;
|
||||
double textY = (-_defHeight - textPainter.height) / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
} else {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
0,
|
||||
segmentWidth == 0 ? 2 : segmentWidth,
|
||||
segmentEnd == segmentStart ? 2 : segmentEnd - segmentStart,
|
||||
size.height,
|
||||
),
|
||||
paint,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
|
||||
Widget statDanMu({
|
||||
required BuildContext context,
|
||||
@@ -24,7 +24,6 @@ Widget statDanMu({
|
||||
Text(
|
||||
Utils.numFormat(danmu!),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: size == 'medium' ? 12 : 11,
|
||||
color: color,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
|
||||
Widget statView({
|
||||
required BuildContext context,
|
||||
@@ -27,7 +27,6 @@ Widget statView({
|
||||
Text(
|
||||
Utils.numFormat(view!),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: size == 'medium' ? 12 : 11,
|
||||
color: color,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/models/model_hot_video_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../http/search.dart';
|
||||
@@ -16,24 +17,22 @@ class VideoCardH extends StatelessWidget {
|
||||
const VideoCardH({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
this.source = 'normal',
|
||||
this.showOwner = true,
|
||||
this.showView = true,
|
||||
this.showDanmaku = true,
|
||||
this.showPubdate = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
final dynamic videoItem;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
final String source;
|
||||
final bool showOwner;
|
||||
final bool showView;
|
||||
final bool showDanmaku;
|
||||
final bool showPubdate;
|
||||
final GestureTapCallback? onTap;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -43,21 +42,33 @@ class VideoCardH extends StatelessWidget {
|
||||
try {
|
||||
type = videoItem.type;
|
||||
} catch (_) {}
|
||||
List<VideoCustomAction> actions =
|
||||
VideoCustomActions(videoItem, context).actions;
|
||||
final String heroTag = Utils.makeHeroTag(aid);
|
||||
return Stack(children: [
|
||||
Semantics(
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
for (var item in actions)
|
||||
CustomSemanticsAction(
|
||||
label: item.title.isEmpty ? 'label' : item.title): item.onTap!,
|
||||
},
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(
|
||||
// label: item.title.isEmpty ? 'label' : item.title): item.onTap!,
|
||||
// },
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onLongPress: longPress,
|
||||
onLongPress: () {
|
||||
if (onLongPress != null) {
|
||||
onLongPress!();
|
||||
} else {
|
||||
imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title is String
|
||||
? videoItem.title
|
||||
: videoItem.title is List
|
||||
? (videoItem.title as List)
|
||||
.map((item) => item['text'])
|
||||
.join()
|
||||
: '',
|
||||
cover: videoItem.pic,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: () async {
|
||||
if (onTap != null) {
|
||||
onTap?.call();
|
||||
@@ -67,76 +78,93 @@ class VideoCardH extends StatelessWidget {
|
||||
SmartDialog.showToast('课堂视频暂不支持播放');
|
||||
return;
|
||||
}
|
||||
if (videoItem is HotVideoItemModel &&
|
||||
videoItem.redirectUrl?.isNotEmpty == true) {
|
||||
if (Utils.viewPgcFromUri(videoItem.redirectUrl!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
final int cid =
|
||||
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||
Get.toNamed('/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
'videoItem': videoItem,
|
||||
'heroTag': Utils.makeHeroTag(aid)
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints boxConstraints) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic as String,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder:
|
||||
(BuildContext context, BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.pic as String,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (videoItem is HotVideoItemModel &&
|
||||
videoItem.pgcLabel?.isNotEmpty == true)
|
||||
PBadge(
|
||||
text: videoItem.pgcLabel,
|
||||
top: 6.0,
|
||||
right: 6.0,
|
||||
),
|
||||
if (videoItem.duration != 0)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(videoItem.duration!),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
if (type != 'video')
|
||||
PBadge(
|
||||
text: type,
|
||||
left: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'primary',
|
||||
),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// pBadge(videoItem.rcmdReason.content, context,
|
||||
// 6.0, 6.0, null, null),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (videoItem.duration != 0)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(videoItem.duration!),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
if (type != 'video')
|
||||
PBadge(
|
||||
text: type,
|
||||
left: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'primary',
|
||||
),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// pBadge(videoItem.rcmdReason.content, context,
|
||||
// 6.0, 6.0, null, null),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (source == 'normal')
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
right: 12,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
actions: actions,
|
||||
videoItem: videoItem,
|
||||
),
|
||||
),
|
||||
]);
|
||||
@@ -153,13 +181,12 @@ class VideoCardH extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (videoItem.title is String) ...[
|
||||
if (videoItem.title is String)
|
||||
Expanded(
|
||||
child: Text(
|
||||
videoItem.title as String,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
@@ -167,19 +194,19 @@ class VideoCardH extends StatelessWidget {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
textScaler: MediaQuery.textScalerOf(context),
|
||||
text: TextSpan(
|
||||
children: [
|
||||
for (final i in videoItem.title) ...[
|
||||
TextSpan(
|
||||
text: i['text'] as String,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
@@ -195,7 +222,6 @@ class VideoCardH extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// const Spacer(),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
@@ -221,7 +247,6 @@ class VideoCardH extends StatelessWidget {
|
||||
"$pubdate ${showOwner ? videoItem.owner.name : ''}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPalaX/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPalaX/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../utils/utils.dart';
|
||||
@@ -12,8 +13,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
const VideoCardHGrpc({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
this.source = 'normal',
|
||||
this.showOwner = true,
|
||||
this.showView = true,
|
||||
@@ -21,8 +20,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
this.showPubdate = false,
|
||||
});
|
||||
final card.Card videoItem;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
final String source;
|
||||
final bool showOwner;
|
||||
final bool showView;
|
||||
@@ -50,7 +47,11 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
// },
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onLongPress: longPress,
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.smallCoverV5.base.title,
|
||||
cover: videoItem.smallCoverV5.base.cover,
|
||||
),
|
||||
onTap: () async {
|
||||
if (type == 'ketang') {
|
||||
SmartDialog.showToast('课堂视频暂不支持播放');
|
||||
@@ -58,13 +59,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
}
|
||||
try {
|
||||
PiliScheme.routePush(Uri.parse(videoItem.smallCoverV5.base.uri));
|
||||
// final int cid =
|
||||
// videoItem.smallCoverV5.base.playerArgs.cid.toInt() ??
|
||||
// await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||
// Get.toNamed('/video?bvid=$bvid&cid=$cid',
|
||||
// arguments: {'heroTag': heroTag});
|
||||
// Get.toNamed('/video?bvid=$bvid&cid=$cid',
|
||||
// arguments: {'videoItem': videoItem, 'heroTag': heroTag});
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
@@ -150,7 +144,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
videoItem.smallCoverV5.base.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
@@ -183,7 +176,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
videoItem.smallCoverV5.rightDesc1,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
@@ -195,7 +187,6 @@ class VideoCardHGrpc extends StatelessWidget {
|
||||
videoItem.smallCoverV5.rightDesc2,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:PiliPalaX/common/widgets/stat/danmu.dart';
|
||||
import 'package:PiliPalaX/common/widgets/stat/view.dart';
|
||||
import 'package:PiliPalaX/common/widgets/video_popup_menu.dart';
|
||||
import 'package:PiliPalaX/models/space_archive/item.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/danmu.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/view.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -15,90 +16,110 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
const VideoCardHMemberVideo({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
this.onTap,
|
||||
this.bvid,
|
||||
});
|
||||
final Item videoItem;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
final VoidCallback? onTap;
|
||||
final dynamic bvid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final int aid = int.tryParse(videoItem.param ?? '') ?? -1;
|
||||
final String bvid = videoItem.bvid ?? '';
|
||||
final String heroTag = Utils.makeHeroTag(aid);
|
||||
return Stack(children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onLongPress: longPress,
|
||||
onTap: () async {
|
||||
try {
|
||||
Get.toNamed('/video?bvid=$bvid&cid=${videoItem.firstCid}',
|
||||
arguments: {'heroTag': heroTag});
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints boxConstraints) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder:
|
||||
(BuildContext context, BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
// videoItem.season?['cover'] ?? videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
// if (videoItem.season != null)
|
||||
// PBadge(
|
||||
// text: '合集: ${videoItem.season?['count']}',
|
||||
// right: 6.0,
|
||||
// bottom: 6.0,
|
||||
// type: 'gray',
|
||||
// )
|
||||
// else
|
||||
if (videoItem.duration != null)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
);
|
||||
return Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
),
|
||||
onTap: () async {
|
||||
if (onTap != null) {
|
||||
onTap!();
|
||||
return;
|
||||
}
|
||||
if (videoItem.isPgc == true && videoItem.uri?.isNotEmpty == true) {
|
||||
if (Utils.viewPgcFromUri(videoItem.uri!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=${videoItem.firstCid}',
|
||||
arguments: {
|
||||
'heroTag': Utils.makeHeroTag(aid),
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints boxConstraints) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
// videoItem.season?['cover'] ?? videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
// if (videoItem.season != null)
|
||||
// PBadge(
|
||||
// text: '合集: ${videoItem.season?['count']}',
|
||||
// right: 6.0,
|
||||
// bottom: 6.0,
|
||||
// type: 'gray',
|
||||
// )
|
||||
// else
|
||||
if (videoItem.duration != null)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
actions: VideoCustomActions(videoItem, context).actions,
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 12,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoContent(context) {
|
||||
@@ -115,10 +136,13 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
videoItem.title ?? '',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontWeight: videoItem.bvid == bvid ? FontWeight.bold : null,
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
color: videoItem.bvid == bvid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -131,7 +155,6 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
: videoItem.publishTimeText ?? '',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:PiliPalaX/http/search.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../models/home/rcmd/result.dart';
|
||||
@@ -18,14 +18,12 @@ import 'video_popup_menu.dart';
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardV extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
const VideoCardV({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
bool isStringNumeric(String str) {
|
||||
@@ -37,12 +35,11 @@ class VideoCardV extends StatelessWidget {
|
||||
String goto = videoItem.goto;
|
||||
switch (goto) {
|
||||
case 'bangumi':
|
||||
if (videoItem.bangumiBadge == '电影') {
|
||||
SmartDialog.showToast('暂不支持电影观看');
|
||||
return;
|
||||
}
|
||||
int epId = videoItem.param;
|
||||
Utils.viewBangumi(epId: epId);
|
||||
// if (videoItem.bangumiBadge == '电影') {
|
||||
// SmartDialog.showToast('暂不支持电影观看');
|
||||
// return;
|
||||
// }
|
||||
Utils.viewBangumi(epId: videoItem.param);
|
||||
// SmartDialog.showLoading(msg: '资源获取中');
|
||||
// var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
|
||||
// SmartDialog.dismiss();
|
||||
@@ -83,11 +80,14 @@ class VideoCardV extends StatelessWidget {
|
||||
if (cid == -1) {
|
||||
cid = await SearchHttp.ab2c(aid: videoItem.aid, bvid: bvid);
|
||||
}
|
||||
Get.toNamed('/video?bvid=$bvid&cid=$cid', arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.pic,
|
||||
'heroTag': heroTag,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.pic,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 动态
|
||||
case 'picture':
|
||||
@@ -134,36 +134,30 @@ class VideoCardV extends StatelessWidget {
|
||||
break;
|
||||
default:
|
||||
SmartDialog.showToast(videoItem.goto);
|
||||
Get.toNamed(
|
||||
'/webviewnew',
|
||||
parameters: {
|
||||
'url': videoItem.uri,
|
||||
'type': 'url',
|
||||
'pageTitle': videoItem.title,
|
||||
},
|
||||
);
|
||||
Utils.handleWebview(videoItem.uri);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(videoItem.id);
|
||||
List<VideoCustomAction> actions =
|
||||
VideoCustomActions(videoItem, context).actions;
|
||||
return Stack(children: [
|
||||
Semantics(
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
for (var item in actions)
|
||||
CustomSemanticsAction(label: item.title): item.onTap!,
|
||||
},
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(label: item.title): item.onTap!,
|
||||
// },
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () async => onPushDetail(heroTag),
|
||||
onLongPress: longPress,
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.id)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.pic,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -173,13 +167,10 @@ class VideoCardV extends StatelessWidget {
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
@@ -203,13 +194,15 @@ class VideoCardV extends StatelessWidget {
|
||||
),
|
||||
if (videoItem.goto == 'av')
|
||||
Positioned(
|
||||
right: -5,
|
||||
bottom: -2,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
actions: actions,
|
||||
)),
|
||||
right: -5,
|
||||
bottom: -2,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
onRemove: onRemove,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -229,7 +222,6 @@ class VideoCardV extends StatelessWidget {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.38,
|
||||
)),
|
||||
),
|
||||
@@ -247,7 +239,8 @@ class VideoCardV extends StatelessWidget {
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.rcmdReason != null) ...[
|
||||
PBadge(
|
||||
@@ -255,7 +248,8 @@ class VideoCardV extends StatelessWidget {
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'color',
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.goto == 'picture') ...[
|
||||
const PBadge(
|
||||
@@ -264,7 +258,8 @@ class VideoCardV extends StatelessWidget {
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.isFollowed == 1) ...[
|
||||
const PBadge(
|
||||
@@ -272,7 +267,8 @@ class VideoCardV extends StatelessWidget {
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'color',
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
Expanded(
|
||||
flex: 1,
|
||||
@@ -307,7 +303,7 @@ class VideoCardV extends StatelessWidget {
|
||||
view: videoItem.stat.view,
|
||||
goto: videoItem.goto,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: 4),
|
||||
if (videoItem.goto != 'picture')
|
||||
statDanMu(
|
||||
context: context,
|
||||
@@ -320,6 +316,7 @@ class VideoCardV extends StatelessWidget {
|
||||
flex: 0,
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
textScaler: MediaQuery.textScalerOf(context),
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
@@ -342,6 +339,7 @@ class VideoCardV extends StatelessWidget {
|
||||
flex: 0,
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
textScaler: MediaQuery.textScalerOf(context),
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPalaX/models/space/item.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/models/space/item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -10,26 +11,22 @@ import 'network_img_layer.dart';
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardVMemberHome extends StatelessWidget {
|
||||
final Item videoItem;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
|
||||
const VideoCardVMemberHome({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
});
|
||||
|
||||
void onPushDetail(heroTag) async {
|
||||
String goto = videoItem.goto ?? '';
|
||||
switch (goto) {
|
||||
// case 'bangumi':
|
||||
// if (videoItem.bangumiBadge == '电影') {
|
||||
// SmartDialog.showToast('暂不支持电影观看');
|
||||
// return;
|
||||
// }
|
||||
// int epId = videoItem.param;
|
||||
// Utils.viewBangumi(epId: epId);
|
||||
case 'bangumi':
|
||||
// if (videoItem.bangumiBadge == '电影') {
|
||||
// SmartDialog.showToast('暂不支持电影观看');
|
||||
// return;
|
||||
// }
|
||||
// int epId = videoItem.param;
|
||||
Utils.viewBangumi(epId: videoItem.param);
|
||||
|
||||
// SmartDialog.showLoading(msg: '资源获取中');
|
||||
// var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
|
||||
@@ -66,12 +63,20 @@ class VideoCardVMemberHome extends StatelessWidget {
|
||||
// }
|
||||
// break;
|
||||
case 'av':
|
||||
if (videoItem.isPgc == true && videoItem.uri?.isNotEmpty == true) {
|
||||
if (Utils.viewPgcFromUri(videoItem.uri!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
String bvid = videoItem.bvid ?? '';
|
||||
Get.toNamed('/video?bvid=$bvid&cid=${videoItem.firstCid}', arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.cover,
|
||||
'heroTag': heroTag,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=${videoItem.firstCid}',
|
||||
arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.cover,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 动态
|
||||
// case 'picture':
|
||||
@@ -118,20 +123,12 @@ class VideoCardVMemberHome extends StatelessWidget {
|
||||
// break;
|
||||
default:
|
||||
SmartDialog.showToast(goto);
|
||||
Get.toNamed(
|
||||
'/webviewnew',
|
||||
parameters: {
|
||||
'url': videoItem.uri ?? '',
|
||||
'type': 'url',
|
||||
'pageTitle': videoItem.title ?? '',
|
||||
},
|
||||
);
|
||||
Utils.handleWebview(videoItem.uri ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(videoItem.bvid);
|
||||
// List<VideoCustomAction> actions =
|
||||
// VideoCustomActions(videoItem, context).actions;
|
||||
return Stack(children: [
|
||||
@@ -146,8 +143,12 @@ class VideoCardVMemberHome extends StatelessWidget {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () async => onPushDetail(heroTag),
|
||||
onLongPress: longPress,
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.bvid)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -157,13 +158,10 @@ class VideoCardVMemberHome extends StatelessWidget {
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if ((videoItem.duration ?? -1) > 0)
|
||||
PBadge(
|
||||
@@ -214,70 +212,69 @@ Widget videoContent(BuildContext context, Item videoItem) {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.38,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
// const Spacer(),
|
||||
// const SizedBox(height: 2),
|
||||
// VideoStat(
|
||||
// videoItem: videoItem,
|
||||
// ),
|
||||
Row(
|
||||
children: [
|
||||
// if (videoItem.goto == 'bangumi') ...[
|
||||
// PBadge(
|
||||
// text: videoItem.bangumiBadge,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'line',
|
||||
// fs: 9,
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.rcmdReason != null) ...[
|
||||
// PBadge(
|
||||
// text: videoItem.rcmdReason,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
if (videoItem.goto == 'picture') ...[
|
||||
const PBadge(
|
||||
text: '动态',
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
)
|
||||
],
|
||||
// if (videoItem.isFollowed == 1) ...[
|
||||
// const PBadge(
|
||||
// text: '已关注',
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
videoItem.author ?? '',
|
||||
// semanticsLabel: "Up主:${videoItem.owner.name}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av') const SizedBox(width: 10)
|
||||
],
|
||||
),
|
||||
// Row(
|
||||
// children: [
|
||||
// if (videoItem.goto == 'bangumi') ...[
|
||||
// PBadge(
|
||||
// text: videoItem.bangumiBadge,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'line',
|
||||
// fs: 9,
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.rcmdReason != null) ...[
|
||||
// PBadge(
|
||||
// text: videoItem.rcmdReason,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.goto == 'picture') ...[
|
||||
// const PBadge(
|
||||
// text: '动态',
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'line',
|
||||
// fs: 9,
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.isFollowed == 1) ...[
|
||||
// const PBadge(
|
||||
// text: '已关注',
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
// Expanded(
|
||||
// flex: 1,
|
||||
// child: Text(
|
||||
// videoItem.author ?? '',
|
||||
// // semanticsLabel: "Up主:${videoItem.owner.name}",
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.clip,
|
||||
// style: TextStyle(
|
||||
// height: 1.5,
|
||||
// fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// if (videoItem.goto == 'av') const SizedBox(width: 10)
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -9,7 +10,7 @@ import '../../http/video.dart';
|
||||
import '../../models/home/rcmd/result.dart';
|
||||
import '../../pages/mine/controller.dart';
|
||||
import '../../utils/storage.dart';
|
||||
import 'package:PiliPalaX/models/space_archive/item.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
|
||||
class VideoCustomAction {
|
||||
String title;
|
||||
@@ -23,7 +24,9 @@ class VideoCustomActions {
|
||||
dynamic videoItem;
|
||||
BuildContext context;
|
||||
late List<VideoCustomAction> actions;
|
||||
VideoCustomActions(this.videoItem, this.context) {
|
||||
VoidCallback? onRemove;
|
||||
|
||||
VideoCustomActions(this.videoItem, this.context, [this.onRemove]) {
|
||||
actions = [
|
||||
if ((videoItem.bvid as String?)?.isNotEmpty == true)
|
||||
VideoCustomAction(
|
||||
@@ -82,12 +85,10 @@ class VideoCustomActions {
|
||||
return;
|
||||
}
|
||||
Widget actionButton(DislikeReason? r, FeedbackReason? f) {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0, vertical: 0.0),
|
||||
),
|
||||
onPressed: () async {
|
||||
return SearchText(
|
||||
text: r?.name ?? f?.name ?? '未知',
|
||||
onTap: (_) async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislike(
|
||||
reasonId: r?.id,
|
||||
@@ -97,10 +98,12 @@ class VideoCustomActions {
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? (r?.toast ?? f?.toast) : res['msg']);
|
||||
Get.back();
|
||||
res['status'] ? (r?.toast ?? f?.toast) : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
child: Text(r?.name ?? f?.name ?? '未知'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,53 +111,57 @@ class VideoCustomActions {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('请选择'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (tp.dislikeReasons != null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text('我不想看'),
|
||||
),
|
||||
if (tp.dislikeReasons != null)
|
||||
if (tp.dislikeReasons != null) ...[
|
||||
Text('我不想看'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
runSpacing: 2.0,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.dislikeReasons!.map((item) {
|
||||
return actionButton(item, null);
|
||||
}).toList(),
|
||||
),
|
||||
if (tp.feedbacks != null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text('反馈'),
|
||||
),
|
||||
if (tp.feedbacks != null)
|
||||
],
|
||||
if (tp.feedbacks != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
Text('反馈'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
runSpacing: 2.0,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.feedbacks!.map((item) {
|
||||
return actionButton(null, item);
|
||||
}).toList(),
|
||||
),
|
||||
//分割线
|
||||
],
|
||||
const Divider(),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislikeCancel(
|
||||
// reasonId: r?.id,
|
||||
// feedbackId: f?.id,
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "成功" : res['msg']);
|
||||
Get.back();
|
||||
},
|
||||
child: const Text("撤销"),
|
||||
Center(
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislikeCancel(
|
||||
// reasonId: r?.id,
|
||||
// feedbackId: f?.id,
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "成功" : res['msg']);
|
||||
Get.back();
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text("撤销"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -167,7 +174,6 @@ class VideoCustomActions {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('点踩该视频?'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -178,20 +184,31 @@ class VideoCustomActions {
|
||||
spacing: 5.0,
|
||||
runSpacing: 2.0,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String, type: true);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "点踩成功" : res['msg']);
|
||||
Get.back();
|
||||
res['status'] ? "点踩成功" : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text("点踩"),
|
||||
),
|
||||
ElevatedButton(
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String,
|
||||
@@ -199,8 +216,13 @@ class VideoCustomActions {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "取消踩" : res['msg']);
|
||||
Get.back();
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text("撤销"),
|
||||
),
|
||||
],
|
||||
@@ -254,10 +276,10 @@ class VideoCustomActions {
|
||||
);
|
||||
}),
|
||||
VideoCustomAction(
|
||||
"${MineController.anonymity ? '退出' : '进入'}无痕模式",
|
||||
"${MineController.anonymity.value ? '退出' : '进入'}无痕模式",
|
||||
'anonymity',
|
||||
Icon(
|
||||
MineController.anonymity
|
||||
MineController.anonymity.value
|
||||
? MdiIcons.incognitoOff
|
||||
: MdiIcons.incognito,
|
||||
size: 16,
|
||||
@@ -270,14 +292,16 @@ class VideoCustomActions {
|
||||
class VideoPopupMenu extends StatelessWidget {
|
||||
final double? size;
|
||||
final double? iconSize;
|
||||
final List<VideoCustomAction> actions;
|
||||
final double menuItemHeight = 45;
|
||||
final dynamic videoItem;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
const VideoPopupMenu({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.iconSize,
|
||||
required this.actions,
|
||||
required this.videoItem,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -295,7 +319,8 @@ class VideoPopupMenu extends StatelessWidget {
|
||||
),
|
||||
position: PopupMenuPosition.under,
|
||||
onSelected: (String type) {},
|
||||
itemBuilder: (BuildContext context) => actions.map((e) {
|
||||
itemBuilder: (BuildContext context) =>
|
||||
VideoCustomActions(videoItem, context, onRemove).actions.map((e) {
|
||||
return PopupMenuItem<String>(
|
||||
value: e.value,
|
||||
height: menuItemHeight,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:PiliPalaX/grpc/app/dynamic/v1/dynamic.pbgrpc.dart' as v1;
|
||||
import 'package:PiliPalaX/grpc/app/dynamic/v2/dynamic.pbgrpc.dart' as v2;
|
||||
import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pbgrpc.dart';
|
||||
import 'package:PiliPalaX/grpc/app/playeronline/v1/playeronline.pbgrpc.dart';
|
||||
import 'package:PiliPalaX/grpc/app/show/popular/v1/popular.pbgrpc.dart';
|
||||
import 'package:PiliPlus/grpc/app/dynamic/v1/dynamic.pbgrpc.dart' as v1;
|
||||
import 'package:PiliPlus/grpc/app/dynamic/v2/dynamic.pbgrpc.dart' as v2;
|
||||
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pbgrpc.dart';
|
||||
import 'package:PiliPlus/grpc/app/playeronline/v1/playeronline.pbgrpc.dart';
|
||||
import 'package:PiliPlus/grpc/app/show/popular/v1/popular.pbgrpc.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
|
||||
class GrpcClient {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/grpc/app/dynamic/v1/dynamic.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/app/dynamic/v2/dynamic.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/app/playeronline/v1/playeronline.pbgrpc.dart';
|
||||
import 'package:PiliPalaX/grpc/app/show/popular/v1/popular.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/device/device.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/fawkes/fawkes.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/grpc_client.dart';
|
||||
import 'package:PiliPalaX/grpc/locale/locale.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/metadata/metadata.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/network/network.pb.dart' as network;
|
||||
import 'package:PiliPalaX/grpc/restriction/restriction.pb.dart';
|
||||
import 'package:PiliPalaX/utils/login.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/grpc/app/dynamic/v1/dynamic.pb.dart';
|
||||
import 'package:PiliPlus/grpc/app/dynamic/v2/dynamic.pb.dart';
|
||||
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPlus/grpc/app/playeronline/v1/playeronline.pbgrpc.dart';
|
||||
import 'package:PiliPlus/grpc/app/show/popular/v1/popular.pb.dart';
|
||||
import 'package:PiliPlus/grpc/device/device.pb.dart';
|
||||
import 'package:PiliPlus/grpc/fawkes/fawkes.pb.dart';
|
||||
import 'package:PiliPlus/grpc/grpc_client.dart';
|
||||
import 'package:PiliPlus/grpc/locale/locale.pb.dart';
|
||||
import 'package:PiliPlus/grpc/metadata/metadata.pb.dart';
|
||||
import 'package:PiliPlus/grpc/network/network.pb.dart' as network;
|
||||
import 'package:PiliPlus/grpc/restriction/restriction.pb.dart';
|
||||
import 'package:PiliPlus/utils/login.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:fixnum/src/int64.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
@@ -112,8 +112,8 @@ class GrpcRepo {
|
||||
e.details?.firstOrNull?.getFieldOrNull(2),
|
||||
allowMalformed: true,
|
||||
);
|
||||
msg = msg.replaceAll(
|
||||
RegExp(r"[^a-zA-Z0-9\u4e00-\u9fa5,.;!?,。;!?]"), '');
|
||||
msg =
|
||||
msg.replaceAll(RegExp(r"[^a-zA-Z0-9\u4e00-\u9fa5,.;?,。;!?]"), '');
|
||||
if (msg.isNotEmpty) {
|
||||
return {'status': false, 'msg': msg};
|
||||
} else {
|
||||
|
||||
@@ -19,8 +19,8 @@ class Api {
|
||||
static const String videoUrl = '/x/player/wbi/playurl';
|
||||
|
||||
// 番剧视频流
|
||||
// https://api.bilibili.com/pgc/player/web/playurl?cid=104236640&bvid=BV13t411n7ex
|
||||
static const String bangumiVideoUrl = '/pgc/player/web/playurl';
|
||||
// https://api.bilibili.com/pgc/player/web/v2/playurl?cid=104236640&bvid=BV13t411n7ex
|
||||
static const String bangumiVideoUrl = '/pgc/player/web/v2/playurl';
|
||||
|
||||
// 字幕
|
||||
// aid, cid
|
||||
@@ -247,6 +247,8 @@ class Api {
|
||||
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
|
||||
static const String heartBeat = '/x/click-interface/web/heartbeat';
|
||||
|
||||
static const String mediaListHistory = '/x/v1/medialist/history';
|
||||
|
||||
// 查询视频分P列表 (avid/bvid转cid)
|
||||
static const String ab2c = '/x/player/pagelist';
|
||||
|
||||
@@ -388,9 +390,8 @@ class Api {
|
||||
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';
|
||||
// 我的追番/追剧 ?type=1&pn=1&ps=15
|
||||
static const String bangumiFollow = '/x/space/bangumi/follow/list';
|
||||
|
||||
// 黑名单
|
||||
static const String blackLst = '/x/relation/blacks';
|
||||
@@ -400,7 +401,7 @@ class Api {
|
||||
|
||||
// github 获取最新版
|
||||
static const String latestApp =
|
||||
'https://api.github.com/repos/orz12/pilipala/releases';
|
||||
'https://api.github.com/repos/bggRGjQaUbCoE/PiliPlus/releases';
|
||||
|
||||
// 多少人在看
|
||||
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
|
||||
@@ -442,6 +443,11 @@ class Api {
|
||||
// 获取指定分组下的up
|
||||
static const String followUpGroup = '/x/relation/tag';
|
||||
|
||||
// 获取未读私信数
|
||||
// https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread
|
||||
static const String msgUnread =
|
||||
'${HttpString.tUrl}/session_svr/v1/session_svr/single_unread';
|
||||
|
||||
// 获取消息中心未读信息
|
||||
static const String msgFeedUnread = '/x/msgfeed/unread';
|
||||
//https://api.bilibili.com/x/msgfeed/reply?platform=web&build=0&mobi_app=web
|
||||
@@ -587,6 +593,9 @@ class Api {
|
||||
static const String safeCenterSmsVerify =
|
||||
'${HttpString.passBaseUrl}/x/safecenter/login/tel/verify';
|
||||
|
||||
static const String oauth2AccessToken =
|
||||
'${HttpString.passBaseUrl}/x/passport-login/oauth2/access_token';
|
||||
|
||||
/// 密码加密密钥
|
||||
/// disable_rcmd
|
||||
/// local_id
|
||||
@@ -690,4 +699,11 @@ class Api {
|
||||
static const String videoRelation = '/x/web-interface/archive/relation';
|
||||
|
||||
static const String seasonFav = '/x/v3/fav/season/'; // + fav unfav
|
||||
|
||||
/// 稍后再看&收藏夹视频列表
|
||||
static const String mediaList = '/x/v2/medialist/resource/list';
|
||||
|
||||
/// 我的关注 - 正在直播
|
||||
static const String getFollowingLive =
|
||||
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
|
||||
import '../models/bangumi/list.dart';
|
||||
import 'index.dart';
|
||||
|
||||
class BangumiHttp {
|
||||
static Future<LoadingState> bangumiList({int? page}) async {
|
||||
var res = await Request().get(Api.bangumiList, data: {'page': page});
|
||||
static Future<LoadingState> bangumiList({
|
||||
int? page,
|
||||
int? indexType,
|
||||
}) async {
|
||||
var res = await Request().get(Api.bangumiList, queryParameters: {
|
||||
'page': page,
|
||||
if (indexType != null) 'index_type': indexType,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
BangumiListDataModel data =
|
||||
BangumiListDataModel.fromJson(res.data['data']);
|
||||
@@ -15,12 +21,20 @@ class BangumiHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> bangumiFollow({int? mid}) async {
|
||||
var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
|
||||
static Future<LoadingState> bangumiFollow({
|
||||
dynamic mid,
|
||||
required int type,
|
||||
required int pn,
|
||||
}) async {
|
||||
var res = await Request().get(Api.bangumiFollow, queryParameters: {
|
||||
'vmid': mid,
|
||||
'type': type,
|
||||
'pn': pn,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
BangumiListDataModel data =
|
||||
BangumiListDataModel.fromJson(res.data['data']);
|
||||
return LoadingState.success(data.list);
|
||||
return LoadingState.success(data);
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
|
||||
import '../models/user/black.dart';
|
||||
import 'index.dart';
|
||||
|
||||
class BlackHttp {
|
||||
static Future<LoadingState> blackList({required int pn, int? ps}) async {
|
||||
var res = await Request().get(Api.blackLst, data: {
|
||||
var res = await Request().get(Api.blackLst, queryParameters: {
|
||||
'pn': pn,
|
||||
'ps': ps ?? 50,
|
||||
're_version': 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'index.dart';
|
||||
|
||||
class CommonHttp {
|
||||
static Future unReadDynamic() async {
|
||||
var res = await Request().get(Api.getUnreadDynamic, data: {
|
||||
var res = await Request().get(Api.getUnreadDynamic, queryParameters: {
|
||||
'alltype_offset': 0,
|
||||
'video_offset': 0,
|
||||
'article_offset': 0,
|
||||
|
||||
@@ -16,7 +16,7 @@ class DanmakaHttp {
|
||||
};
|
||||
var response = await Request().get(
|
||||
Api.webDanmaku,
|
||||
data: params,
|
||||
queryParameters: params,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
@@ -29,8 +29,8 @@ class DanmakaHttp {
|
||||
int type = 1, //弹幕类选择(1:视频弹幕 2:漫画弹幕)
|
||||
required int oid, // 视频cid
|
||||
required String msg, //弹幕文本(长度小于 100 字符)
|
||||
int mode =
|
||||
1, // 弹幕类型(1:滚动弹幕 4:底端弹幕 5:顶端弹幕 6:逆向弹幕(不能使用) 7:高级弹幕 8:代码弹幕(不能使用) 9:BAS弹幕(pool必须为2))
|
||||
// 弹幕类型(1:滚动弹幕 4:底端弹幕 5:顶端弹幕 6:逆向弹幕(不能使用) 7:高级弹幕 8:代码弹幕(不能使用) 9:BAS弹幕(pool必须为2))
|
||||
int mode = 1,
|
||||
// String? aid,// 稿件avid
|
||||
// String? bvid,// bvid与aid必须有一个
|
||||
required String bvid,
|
||||
@@ -47,7 +47,6 @@ class DanmakaHttp {
|
||||
// 构建参数对象
|
||||
// assert(aid != null || bvid != null);
|
||||
// assert(csrf != null || access_key != null);
|
||||
assert(msg.length < 100);
|
||||
// 构建参数对象
|
||||
var params = <String, dynamic>{
|
||||
'type': type,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
|
||||
import '../models/dynamics/result.dart';
|
||||
import '../models/dynamics/up.dart';
|
||||
@@ -20,7 +20,7 @@ class DynamicsHttp {
|
||||
data['host_mid'] = mid;
|
||||
data.remove('timezone_offset');
|
||||
}
|
||||
var res = await Request().get(Api.followDynamic, data: data);
|
||||
var res = await Request().get(Api.followDynamic, queryParameters: data);
|
||||
if (res.data['code'] == 0) {
|
||||
try {
|
||||
DynamicsDataModel data = DynamicsDataModel.fromJson(res.data['data']);
|
||||
@@ -80,7 +80,7 @@ class DynamicsHttp {
|
||||
static Future dynamicDetail({
|
||||
String? id,
|
||||
}) async {
|
||||
var res = await Request().get(Api.dynamicDetail, data: {
|
||||
var res = await Request().get(Api.dynamicDetail, queryParameters: {
|
||||
'timezone_offset': -480,
|
||||
'id': id,
|
||||
'features': 'itemOpusStyle',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
|
||||
import '../models/fans/result.dart';
|
||||
import 'index.dart';
|
||||
@@ -6,7 +6,7 @@ import 'index.dart';
|
||||
class FanHttp {
|
||||
static Future<LoadingState> fans(
|
||||
{int? vmid, int? pn, int? ps, String? orderType}) async {
|
||||
var res = await Request().get(Api.fans, data: {
|
||||
var res = await Request().get(Api.fans, queryParameters: {
|
||||
'vmid': vmid,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'index.dart';
|
||||
class FollowHttp {
|
||||
static Future followings(
|
||||
{int? vmid, int? pn, int? ps, String? orderType}) async {
|
||||
var res = await Request().get(Api.followings, data: {
|
||||
var res = await Request().get(Api.followings, queryParameters: {
|
||||
'vmid': vmid,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPalaX/models/dynamics/article_content_model.dart';
|
||||
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// ignore_for_file: avoid_print
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
@@ -9,23 +8,20 @@ import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import '../utils/storage.dart';
|
||||
import '../utils/utils.dart';
|
||||
import 'api.dart';
|
||||
import 'constants.dart';
|
||||
import 'interceptor.dart';
|
||||
import 'interceptor_anonymity.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web;
|
||||
|
||||
class Request {
|
||||
static final Request _instance = Request._internal();
|
||||
static late CookieManager cookieManager;
|
||||
static late final Dio dio;
|
||||
factory Request() => _instance;
|
||||
Box setting = GStorage.setting;
|
||||
static Box localCache = GStorage.localCache;
|
||||
late bool enableSystemProxy;
|
||||
late String systemProxyHost;
|
||||
late String systemProxyPort;
|
||||
@@ -34,7 +30,6 @@ class Request {
|
||||
|
||||
/// 设置cookie
|
||||
static setCookie() async {
|
||||
Box userInfoCache = GStorage.userInfo;
|
||||
final String cookiePath = await Utils.getCookiePath();
|
||||
final PersistCookieJar cookieJar = PersistCookieJar(
|
||||
ignoreExpires: true,
|
||||
@@ -43,9 +38,20 @@ class Request {
|
||||
cookieManager = CookieManager(cookieJar);
|
||||
dio.interceptors.add(cookieManager);
|
||||
dio.interceptors.add(AnonymityInterceptor());
|
||||
// final List<Cookie> cookie = await cookieManager.cookieJar
|
||||
// .loadForRequest(Uri.parse(HttpString.baseUrl));
|
||||
final userInfo = userInfoCache.get('userInfoCache');
|
||||
final List<Cookie> cookies = await cookieManager.cookieJar
|
||||
.loadForRequest(Uri.parse(HttpString.baseUrl));
|
||||
for (Cookie item in cookies) {
|
||||
await web.CookieManager().setCookie(
|
||||
url: web.WebUri(item.domain ?? ''),
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
path: item.path ?? '',
|
||||
domain: item.domain,
|
||||
isSecure: item.secure,
|
||||
isHttpOnly: item.httpOnly,
|
||||
);
|
||||
}
|
||||
final userInfo = GStorage.userInfo.get('userInfoCache');
|
||||
if (userInfo != null && userInfo.mid != null) {
|
||||
final List<Cookie> cookie2 = await cookieManager.cookieJar
|
||||
.loadForRequest(Uri.parse(HttpString.tUrl));
|
||||
@@ -65,7 +71,7 @@ class Request {
|
||||
log("setCookie, ${e.toString()}");
|
||||
}
|
||||
|
||||
// final String cookieString = cookie
|
||||
// final String cookieString = cookies
|
||||
// .map((Cookie cookie) => '${cookie.name}=${cookie.value}')
|
||||
// .join('; ');
|
||||
// dio.options.headers['cookie'] = cookieString;
|
||||
@@ -134,12 +140,12 @@ class Request {
|
||||
headers: {},
|
||||
);
|
||||
|
||||
enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy,
|
||||
defaultValue: false) as bool;
|
||||
enableSystemProxy = GStorage.setting
|
||||
.get(SettingBoxKey.enableSystemProxy, defaultValue: false) as bool;
|
||||
systemProxyHost =
|
||||
setting.get(SettingBoxKey.systemProxyHost, defaultValue: '');
|
||||
GStorage.setting.get(SettingBoxKey.systemProxyHost, defaultValue: '');
|
||||
systemProxyPort =
|
||||
setting.get(SettingBoxKey.systemProxyPort, defaultValue: '');
|
||||
GStorage.setting.get(SettingBoxKey.systemProxyPort, defaultValue: '');
|
||||
|
||||
dio = Dio(options);
|
||||
|
||||
@@ -147,7 +153,7 @@ class Request {
|
||||
// ..httpClientAdapter = Http2Adapter(
|
||||
// ConnectionManager(
|
||||
// idleTimeout: const Duration(milliseconds: 10000),
|
||||
// onClientCreate: (_, ClientSetting config) =>
|
||||
// onClientCreate: (context, ClientSetting config) =>
|
||||
// config.onBadCertificate = (_) => true,
|
||||
// ),
|
||||
// );
|
||||
@@ -189,7 +195,8 @@ class Request {
|
||||
/*
|
||||
* get请求
|
||||
*/
|
||||
Future<Response> get(url, {data, options, cancelToken, extra}) async {
|
||||
Future<Response> get(url,
|
||||
{queryParameters, options, cancelToken, extra}) async {
|
||||
Response response;
|
||||
if (extra != null) {
|
||||
if (extra['ua'] != null) {
|
||||
@@ -202,7 +209,7 @@ class Request {
|
||||
try {
|
||||
response = await dio.get(
|
||||
url,
|
||||
queryParameters: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,8 +25,7 @@ class ApiInterceptor extends Interceptor {
|
||||
// final String? accessKey = uri.queryParameters['access_key'];
|
||||
// final String? mid = uri.queryParameters['mid'];
|
||||
// try {
|
||||
// Box localCache = GStorage.localCache;
|
||||
// localCache.put(LocalCacheKey.accessKey,
|
||||
// GStorage.localCache.put(LocalCacheKey.accessKey,
|
||||
// <String, String?>{'mid': mid, 'value': accessKey});
|
||||
// } catch (_) {}
|
||||
// }
|
||||
@@ -47,10 +44,13 @@ class ApiInterceptor extends Interceptor {
|
||||
// handler.next(err);
|
||||
String url = err.requestOptions.uri.toString();
|
||||
debugPrint('🌹🌹ApiInterceptor: $url');
|
||||
// 屏蔽弹幕、心跳、人数请求的错误提示
|
||||
if (!url.contains('heartbeat') &&
|
||||
!url.contains('seg.so') &&
|
||||
!url.contains('online/total')) {
|
||||
if (url.contains('heartbeat') ||
|
||||
url.contains('seg.so') ||
|
||||
url.contains('online/total') ||
|
||||
url.contains('github') ||
|
||||
(url.contains('skipSegments') && err.requestOptions.method == 'GET')) {
|
||||
// skip
|
||||
} else {
|
||||
SmartDialog.showToast(
|
||||
await dioError(err) + url,
|
||||
displayType: SmartToastType.onlyRefresh,
|
||||
@@ -77,29 +77,13 @@ class ApiInterceptor extends Interceptor {
|
||||
case DioExceptionType.sendTimeout:
|
||||
return '发送请求超时,请检查网络设置';
|
||||
case DioExceptionType.unknown:
|
||||
final String res = await checkConnect();
|
||||
final String res =
|
||||
(await Connectivity().checkConnectivity()).first.title;
|
||||
return '$res网络异常 ${error.error}';
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> checkConnect() async {
|
||||
final List<ConnectivityResult> connectivityResult =
|
||||
await Connectivity().checkConnectivity();
|
||||
switch (connectivityResult.first) {
|
||||
case ConnectivityResult.mobile:
|
||||
return '流量';
|
||||
case ConnectivityResult.wifi:
|
||||
return 'Wi-Fi';
|
||||
case ConnectivityResult.ethernet:
|
||||
return '局域';
|
||||
case ConnectivityResult.vpn:
|
||||
return '代理';
|
||||
case ConnectivityResult.other:
|
||||
return '其他';
|
||||
case ConnectivityResult.none:
|
||||
return '无';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _ConnectivityResultExt on ConnectivityResult {
|
||||
String get title => ['蓝牙', 'Wi-Fi', '局域', '流量', '无', '代理', '其他'][index];
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ class AnonymityInterceptor extends Interceptor {
|
||||
Api.dynamicDetail,
|
||||
Api.aiConclusion,
|
||||
Api.getSeasonDetailApi,
|
||||
Api.liveRoomDmToken,
|
||||
Api.liveRoomDmPrefetch,
|
||||
];
|
||||
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (MineController.anonymity) {
|
||||
if (MineController.anonymity.value) {
|
||||
String uri = options.uri.toString();
|
||||
for (var i in anonymityList) {
|
||||
// 如果请求的url包含无痕列表中的url,则清空cookie
|
||||
@@ -39,7 +40,7 @@ class AnonymityInterceptor extends Interceptor {
|
||||
if (uri.lastIndexOf('/') >= index + i.length) continue;
|
||||
//SmartDialog.showToast('触发无痕模式\n\n$i\n\n${options.uri}');
|
||||
options.headers[HttpHeaders.cookieHeader] = "";
|
||||
if(options.data != null && options.data.csrf != null) {
|
||||
if (options.data != null && options.data.csrf != null) {
|
||||
options.data.csrf = "";
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/models/live/danmu_info.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/live/danmu_info.dart';
|
||||
import 'package:PiliPlus/models/live/follow.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/live/item.dart';
|
||||
import '../models/live/room_info.dart';
|
||||
@@ -12,7 +13,7 @@ class LiveHttp {
|
||||
static Future<LoadingState> liveList(
|
||||
{int? vmid, int? pn, int? ps, String? orderType}) async {
|
||||
var res = await Request().get(Api.liveList,
|
||||
data: {'page': pn, 'page_size': 30, 'platform': 'web'});
|
||||
queryParameters: {'page': pn, 'page_size': 30, 'platform': 'web'});
|
||||
if (res.data['code'] == 0) {
|
||||
List<LiveItemModel> list = res.data['data']['list']
|
||||
.map<LiveItemModel>((e) => LiveItemModel.fromJson(e))
|
||||
@@ -67,7 +68,7 @@ class LiveHttp {
|
||||
}
|
||||
|
||||
static Future liveRoomInfo({roomId, qn}) async {
|
||||
var res = await Request().get(Api.liveRoomInfo, data: {
|
||||
var res = await Request().get(Api.liveRoomInfo, queryParameters: {
|
||||
'room_id': roomId,
|
||||
'protocol': '0, 1',
|
||||
'format': '0, 1, 2',
|
||||
@@ -90,7 +91,7 @@ class LiveHttp {
|
||||
}
|
||||
|
||||
static Future liveRoomInfoH5({roomId, qn}) async {
|
||||
var res = await Request().get(Api.liveRoomInfoH5, data: {
|
||||
var res = await Request().get(Api.liveRoomInfoH5, queryParameters: {
|
||||
'room_id': roomId,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -108,7 +109,7 @@ class LiveHttp {
|
||||
}
|
||||
|
||||
static Future liveRoomDanmaPrefetch({roomId}) async {
|
||||
var res = await Request().get(Api.liveRoomDmPrefetch, data: {
|
||||
var res = await Request().get(Api.liveRoomDmPrefetch, queryParameters: {
|
||||
'roomid': roomId,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -123,7 +124,7 @@ class LiveHttp {
|
||||
}
|
||||
|
||||
static Future liveRoomGetDanmakuToken({roomId}) async {
|
||||
var res = await Request().get(Api.liveRoomDmToken, data: {
|
||||
var res = await Request().get(Api.liveRoomDmToken, queryParameters: {
|
||||
'id': roomId,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -136,4 +137,29 @@ class LiveHttp {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 我的关注 正在直播
|
||||
static Future liveFollowing({required int pn, required int ps}) async {
|
||||
var res = await Request().get(
|
||||
Api.getFollowingLive,
|
||||
queryParameters: {
|
||||
'page': pn,
|
||||
'page_size': ps,
|
||||
'platform': 'web',
|
||||
'ignoreRecord': 1,
|
||||
'hit_ab': true,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': LiveFollowingModel.fromJson(res.data['data'])
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../common/constants.dart';
|
||||
import '../models/login/index.dart';
|
||||
import '../utils/login.dart';
|
||||
@@ -43,6 +42,7 @@ class LoginHttp {
|
||||
);
|
||||
var res = await Request()
|
||||
.post(Api.getTVCode, queryParameters: {...params, 'sign': sign});
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -107,9 +107,10 @@ class LoginHttp {
|
||||
int timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
var data = {
|
||||
'appkey': Constants.appKey,
|
||||
'build': '1462100',
|
||||
'build': '2001100',
|
||||
'buvid': buvid,
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'cid': cid,
|
||||
// if (deviceTouristId != null) 'device_tourist_id': deviceTouristId,
|
||||
'disable_rcmd': '0',
|
||||
@@ -142,6 +143,7 @@ class LoginHttp {
|
||||
headers: headers,
|
||||
),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0 && res.data['data']['recaptcha_url'] == "") {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -158,7 +160,7 @@ class LoginHttp {
|
||||
// dynamic publicKey = RSAKeyParser().parse(key);
|
||||
// var params = {
|
||||
// 'appkey': Constants.appKey,
|
||||
// 'build': '1462100',
|
||||
// 'build': '2001100',
|
||||
// 'buvid': buvid,
|
||||
// 'c_locale': 'zh_CN',
|
||||
// 'channel': 'yingyongbao',
|
||||
@@ -185,7 +187,7 @@ class LoginHttp {
|
||||
// contentType: Headers.formUrlEncodedContentType,
|
||||
// headers: headers,
|
||||
// ));
|
||||
// debugPrint("getGuestId: $res");
|
||||
// print("getGuestId: $res");
|
||||
// if (res.data['code'] == 0) {
|
||||
// return {'status': true, 'data': res.data['data']};
|
||||
// } else {
|
||||
@@ -211,7 +213,7 @@ class LoginHttp {
|
||||
Map<String, String> data = {
|
||||
'appkey': Constants.appKey,
|
||||
'bili_local_id': deviceId,
|
||||
'build': '1462100',
|
||||
'build': '2001100',
|
||||
'buvid': buvid,
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
@@ -247,7 +249,6 @@ class LoginHttp {
|
||||
);
|
||||
data['sign'] = sign;
|
||||
data.map((key, value) {
|
||||
debugPrint('$key: $value');
|
||||
return MapEntry<String, dynamic>(key, value);
|
||||
});
|
||||
var res = await Request().post(
|
||||
@@ -259,6 +260,7 @@ class LoginHttp {
|
||||
//responseType: ResponseType.plain
|
||||
),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
@@ -287,7 +289,7 @@ class LoginHttp {
|
||||
Map<String, String> data = {
|
||||
'appkey': Constants.appKey,
|
||||
'bili_local_id': deviceId,
|
||||
'build': '1462100',
|
||||
'build': '2001100',
|
||||
'buvid': buvid,
|
||||
'c_locale': 'zh_CN',
|
||||
'captcha_key': captchaKey,
|
||||
@@ -321,7 +323,6 @@ class LoginHttp {
|
||||
);
|
||||
data['sign'] = sign;
|
||||
data.map((key, value) {
|
||||
debugPrint('$key: $value');
|
||||
return MapEntry<String, dynamic>(key, value);
|
||||
});
|
||||
var res = await Request().post(
|
||||
@@ -333,6 +334,7 @@ class LoginHttp {
|
||||
//responseType: ResponseType.plain
|
||||
),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -349,9 +351,12 @@ class LoginHttp {
|
||||
static Future safeCenterGetInfo({
|
||||
required String tmpCode,
|
||||
}) async {
|
||||
var res = await Request().get(Api.safeCenterGetInfo, data: {
|
||||
'tmp_code': tmpCode,
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.safeCenterGetInfo,
|
||||
queryParameters: {
|
||||
'tmp_code': tmpCode,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -364,9 +369,10 @@ class LoginHttp {
|
||||
}
|
||||
}
|
||||
|
||||
// 风控验证手机前的验证码
|
||||
// 风控验证手机前的极验验证码
|
||||
static Future preCapture() async {
|
||||
var res = await Request().post(Api.preCapture);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -379,23 +385,40 @@ class LoginHttp {
|
||||
}
|
||||
}
|
||||
|
||||
// 风控验证手机
|
||||
// 风控验证手机:发送短信验证码
|
||||
static Future safeCenterSmsCode({
|
||||
String? smsType,
|
||||
required String tmpCode,
|
||||
required String geeChallenge,
|
||||
required String geeSeccode,
|
||||
required String geeValidate,
|
||||
required String recaptchaToken,
|
||||
String? geeChallenge,
|
||||
String? geeSeccode,
|
||||
String? geeValidate,
|
||||
String? recaptchaToken,
|
||||
required String refererUrl,
|
||||
}) async {
|
||||
var res = await Request().post(Api.safeCenterSmsCode, data: {
|
||||
Map<String, String> data = {
|
||||
'disable_rcmd': '0',
|
||||
'sms_type': smsType ?? 'loginTelCheck',
|
||||
'tmp_code': tmpCode,
|
||||
'gee_challenge': geeChallenge,
|
||||
'gee_seccode': geeSeccode,
|
||||
'gee_validate': geeValidate,
|
||||
'recaptcha_token': recaptchaToken,
|
||||
});
|
||||
if (geeChallenge != null) 'gee_challenge': geeChallenge,
|
||||
if (geeSeccode != null) 'gee_seccode': geeSeccode,
|
||||
if (geeValidate != null) 'gee_validate': geeValidate,
|
||||
if (recaptchaToken != null) 'recaptcha_token': recaptchaToken,
|
||||
};
|
||||
String sign = Utils.appSign(
|
||||
data,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
data['sign'] = sign;
|
||||
var res = await Request().post(
|
||||
Api.safeCenterSmsCode,
|
||||
data: data,
|
||||
options:
|
||||
Options(contentType: Headers.formUrlEncodedContentType, headers: {
|
||||
"Referer": refererUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -408,21 +431,93 @@ class LoginHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future safeCenterSmsVerify(
|
||||
{String? type,
|
||||
required String code,
|
||||
required String tmpCode,
|
||||
required String requestId,
|
||||
required String source,
|
||||
required String captchaKey}) async {
|
||||
var res = await Request().post(Api.safeCenterSmsVerify, data: {
|
||||
// 风控验证手机:提交短信验证码
|
||||
static Future safeCenterSmsVerify({
|
||||
String? type,
|
||||
required String code,
|
||||
required String tmpCode,
|
||||
required String requestId,
|
||||
required String source,
|
||||
required String captchaKey,
|
||||
required String refererUrl,
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
'type': type ?? 'loginTelCheck',
|
||||
'code': code,
|
||||
'tmp_code': tmpCode,
|
||||
'request_id': requestId,
|
||||
'source': source,
|
||||
'captcha_key': captchaKey,
|
||||
};
|
||||
String sign = Utils.appSign(
|
||||
data,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
data['sign'] = sign;
|
||||
var res = await Request().post(
|
||||
Api.safeCenterSmsVerify,
|
||||
data: data,
|
||||
options:
|
||||
Options(contentType: Headers.formUrlEncodedContentType, headers: {
|
||||
"Referer": refererUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'code': res.data['code'],
|
||||
'msg': res.data['message'],
|
||||
'data': res.data['data']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 风控验证手机:用oauthCode换回accessToken
|
||||
static Future oauth2AccessToken({
|
||||
required String code,
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
'appkey': Constants.appKey,
|
||||
'build': '2001100',
|
||||
'buvid': buvid,
|
||||
// 'c_locale': 'zh_CN',
|
||||
// 'channel': 'yingyongbao',
|
||||
'code': code,
|
||||
// 'device': 'phone',
|
||||
// 'device_id': deviceId,
|
||||
// 'device_name': 'vivo',
|
||||
// 'device_platform': 'Android14vivo',
|
||||
'disable_rcmd': '0',
|
||||
'grant_type': 'authorization_code',
|
||||
'local_id': buvid,
|
||||
'mobi_app': 'android_hd',
|
||||
'platform': 'android',
|
||||
// 's_locale': 'zh_CN',
|
||||
// 'statistics': Constants.statistics,
|
||||
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
|
||||
};
|
||||
String sign = Utils.appSign(
|
||||
data,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
data['sign'] = sign;
|
||||
data.map((key, value) {
|
||||
return MapEntry<String, dynamic>(key, value);
|
||||
});
|
||||
var res = await Request().post(
|
||||
Api.oauth2AccessToken,
|
||||
data: data,
|
||||
options: Options(
|
||||
contentType: Headers.formUrlEncodedContentType,
|
||||
headers: headers,
|
||||
),
|
||||
);
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPalaX/http/constants.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/models/space/data.dart';
|
||||
import 'package:PiliPalaX/models/space_fav/space_fav.dart';
|
||||
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute.dart'
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/space/data.dart';
|
||||
import 'package:PiliPlus/models/space_fav/space_fav.dart';
|
||||
import 'package:PiliPlus/pages/member/new/content/member_contribute/member_contribute.dart'
|
||||
show ContributeType;
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -91,7 +91,7 @@ class MemberHttp {
|
||||
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
|
||||
dynamic res = await Request().get(
|
||||
Api.spaceArticle,
|
||||
data: data,
|
||||
queryParameters: data,
|
||||
options: Options(
|
||||
headers: {
|
||||
'env': 'prod',
|
||||
@@ -136,7 +136,7 @@ class MemberHttp {
|
||||
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
|
||||
dynamic res = await Request().get(
|
||||
Api.spaceFav,
|
||||
data: data,
|
||||
queryParameters: data,
|
||||
options: Options(
|
||||
headers: {
|
||||
'env': 'prod',
|
||||
@@ -206,7 +206,7 @@ class MemberHttp {
|
||||
: type == ContributeType.series
|
||||
? Api.spaceSeries
|
||||
: Api.spaceBangumi,
|
||||
data: data,
|
||||
queryParameters: data,
|
||||
options: Options(
|
||||
headers: {
|
||||
'env': 'prod',
|
||||
@@ -251,7 +251,7 @@ class MemberHttp {
|
||||
int? _mid = GStorage.userInfo.get('userInfoCache')?.mid;
|
||||
dynamic res = await Request().get(
|
||||
Api.space,
|
||||
data: data,
|
||||
queryParameters: data,
|
||||
options: Options(
|
||||
headers: {
|
||||
'env': 'prod',
|
||||
@@ -284,7 +284,7 @@ class MemberHttp {
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.memberInfo,
|
||||
data: params,
|
||||
queryParameters: params,
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -302,7 +302,7 @@ class MemberHttp {
|
||||
}
|
||||
|
||||
static Future memberStat({int? mid}) async {
|
||||
var res = await Request().get(Api.userStat, data: {'vmid': mid});
|
||||
var res = await Request().get(Api.userStat, queryParameters: {'vmid': mid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -316,7 +316,7 @@ class MemberHttp {
|
||||
|
||||
static Future memberCardInfo({int? mid}) async {
|
||||
var res = await Request()
|
||||
.get(Api.memberCardInfo, data: {'mid': mid, 'photo': true});
|
||||
.get(Api.memberCardInfo, queryParameters: {'mid': mid, 'photo': true});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -336,6 +336,7 @@ class MemberHttp {
|
||||
String? keyword,
|
||||
String order = 'pubdate',
|
||||
bool orderAvoided = true,
|
||||
dynamic wwebid,
|
||||
}) async {
|
||||
String dmImgStr = Utils.base64EncodeRandomString(16, 64);
|
||||
String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
|
||||
@@ -353,10 +354,11 @@ class MemberHttp {
|
||||
'dm_img_str': dmImgStr,
|
||||
'dm_cover_img_str': dmCoverImgStr,
|
||||
'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}',
|
||||
'w_webid': wwebid,
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.memberArchive,
|
||||
data: params,
|
||||
queryParameters: params,
|
||||
extra: {'ua': 'Mozilla/5.0'},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -393,7 +395,7 @@ class MemberHttp {
|
||||
'x-bili-device-req-json': jsonEncode({"platform": "web", "device": "pc"}),
|
||||
'x-bili-web-req-json': jsonEncode({"spm_id": "333.999"}),
|
||||
});
|
||||
var res = await Request().get(Api.memberDynamic, data: params);
|
||||
var res = await Request().get(Api.memberDynamic, queryParameters: params);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(DynamicsDataModel.fromJson(res.data['data']));
|
||||
} else {
|
||||
@@ -412,7 +414,7 @@ class MemberHttp {
|
||||
int? mid,
|
||||
required String keyword,
|
||||
}) async {
|
||||
var res = await Request().get(Api.memberDynamicSearch, data: {
|
||||
var res = await Request().get(Api.memberDynamicSearch, queryParameters: {
|
||||
'keyword': keyword,
|
||||
'mid': mid,
|
||||
'pn': pn,
|
||||
@@ -508,7 +510,7 @@ class MemberHttp {
|
||||
int? pn,
|
||||
int? ps,
|
||||
) async {
|
||||
var res = await Request().get(Api.followUpGroup, data: {
|
||||
var res = await Request().get(Api.followUpGroup, queryParameters: {
|
||||
'mid': mid,
|
||||
'tagid': tagid,
|
||||
'pn': pn,
|
||||
@@ -552,7 +554,7 @@ class MemberHttp {
|
||||
|
||||
// 获取uo专栏
|
||||
static Future getMemberSeasons(int? mid, int? pn, int? ps) async {
|
||||
var res = await Request().get(Api.getMemberSeasonsApi, data: {
|
||||
var res = await Request().get(Api.getMemberSeasonsApi, queryParameters: {
|
||||
'mid': mid,
|
||||
'page_num': pn,
|
||||
'page_size': ps,
|
||||
@@ -580,7 +582,7 @@ class MemberHttp {
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.getRecentCoinVideoApi,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'vmid': mid,
|
||||
'gaia_source': 'main_web',
|
||||
'web_location': 333.999,
|
||||
@@ -613,7 +615,7 @@ class MemberHttp {
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.getRecentLikeVideoApi,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'vmid': mid,
|
||||
'gaia_source': 'main_web',
|
||||
'web_location': 333.999,
|
||||
@@ -645,7 +647,7 @@ class MemberHttp {
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.getSeasonDetailApi,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'mid': mid,
|
||||
'season_id': seasonId,
|
||||
'sort_reverse': sortReverse,
|
||||
@@ -673,7 +675,8 @@ class MemberHttp {
|
||||
|
||||
// 获取up播放数、点赞数
|
||||
static Future memberView({required int mid}) async {
|
||||
var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});
|
||||
var res = await Request()
|
||||
.get(Api.getMemberViewApi, queryParameters: {'mid': mid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -703,7 +706,7 @@ class MemberHttp {
|
||||
'web_location': 333.999,
|
||||
};
|
||||
Map params = await WbiSign().makSign(data);
|
||||
var res = await Request().get(Api.followSearch, data: {
|
||||
var res = await Request().get(Api.followSearch, queryParameters: {
|
||||
...data,
|
||||
'w_rid': params['w_rid'],
|
||||
'wts': params['wts'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'package:PiliPalaX/http/constants.dart';
|
||||
import 'package:PiliPalaX/pages/dynamics/view.dart' show ReplyOption;
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/view.dart' show ReplyOption;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'init.dart';
|
||||
|
||||
class MsgHttp {
|
||||
static Future msgFeedReplyMe({int cursor = -1, int cursorTime = -1}) async {
|
||||
var res = await Request().get(Api.msgFeedReply, data: {
|
||||
var res = await Request().get(Api.msgFeedReply, queryParameters: {
|
||||
'id': cursor == -1 ? null : cursor,
|
||||
'reply_time': cursorTime == -1 ? null : cursorTime,
|
||||
});
|
||||
@@ -31,7 +31,7 @@ class MsgHttp {
|
||||
}
|
||||
|
||||
static Future msgFeedAtMe({int cursor = -1, int cursorTime = -1}) async {
|
||||
var res = await Request().get(Api.msgFeedAt, data: {
|
||||
var res = await Request().get(Api.msgFeedAt, queryParameters: {
|
||||
'id': cursor == -1 ? null : cursor,
|
||||
'at_time': cursorTime == -1 ? null : cursorTime,
|
||||
});
|
||||
@@ -50,7 +50,7 @@ class MsgHttp {
|
||||
}
|
||||
|
||||
static Future msgFeedLikeMe({int cursor = -1, int cursorTime = -1}) async {
|
||||
var res = await Request().get(Api.msgFeedLike, data: {
|
||||
var res = await Request().get(Api.msgFeedLike, queryParameters: {
|
||||
'id': cursor == -1 ? null : cursor,
|
||||
'like_time': cursorTime == -1 ? null : cursorTime,
|
||||
});
|
||||
@@ -70,7 +70,7 @@ class MsgHttp {
|
||||
|
||||
static Future msgFeedSysUserNotify() async {
|
||||
String csrf = await Request.getCsrf();
|
||||
var res = await Request().get(Api.msgSysUserNotify, data: {
|
||||
var res = await Request().get(Api.msgSysUserNotify, queryParameters: {
|
||||
'csrf': csrf,
|
||||
'page_size': 20,
|
||||
});
|
||||
@@ -90,7 +90,7 @@ class MsgHttp {
|
||||
|
||||
static Future msgFeedSysUnifiedNotify() async {
|
||||
String csrf = await Request.getCsrf();
|
||||
var res = await Request().get(Api.msgSysUnifiedNotify, data: {
|
||||
var res = await Request().get(Api.msgSysUnifiedNotify, queryParameters: {
|
||||
'csrf': csrf,
|
||||
'page_size': 10,
|
||||
});
|
||||
@@ -110,7 +110,7 @@ class MsgHttp {
|
||||
|
||||
static Future msgSysUpdateCursor(int cursor) async {
|
||||
String csrf = await Request.getCsrf();
|
||||
var res = await Request().get(Api.msgSysUpdateCursor, data: {
|
||||
var res = await Request().get(Api.msgSysUpdateCursor, queryParameters: {
|
||||
'csrf': csrf,
|
||||
'cursor': cursor,
|
||||
});
|
||||
@@ -149,6 +149,7 @@ class MsgHttp {
|
||||
List? pics,
|
||||
int? publishTime,
|
||||
ReplyOption replyOption = ReplyOption.allow,
|
||||
int? privatePub,
|
||||
}) async {
|
||||
String csrf = await Request.getCsrf();
|
||||
var res = await Request().post(
|
||||
@@ -181,6 +182,10 @@ class MsgHttp {
|
||||
: pics != null
|
||||
? 2
|
||||
: 1,
|
||||
if (privatePub != null)
|
||||
'create_option': {
|
||||
'private_pub': privatePub,
|
||||
},
|
||||
if (pics != null) 'pics': pics,
|
||||
"attach_card": null,
|
||||
"upload_id":
|
||||
@@ -400,7 +405,7 @@ class MsgHttp {
|
||||
}
|
||||
|
||||
Map signParams = await WbiSign().makSign(params);
|
||||
var res = await Request().get(Api.sessionList, data: signParams);
|
||||
var res = await Request().get(Api.sessionList, queryParameters: signParams);
|
||||
if (res.data['code'] == 0) {
|
||||
try {
|
||||
return {
|
||||
@@ -424,7 +429,7 @@ class MsgHttp {
|
||||
}
|
||||
|
||||
static Future accountList(uids) async {
|
||||
var res = await Request().get(Api.sessionAccountList, data: {
|
||||
var res = await Request().get(Api.sessionAccountList, queryParameters: {
|
||||
'uids': uids,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
@@ -460,7 +465,7 @@ class MsgHttp {
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
});
|
||||
var res = await Request().get(Api.sessionMsg, data: params);
|
||||
var res = await Request().get(Api.sessionMsg, queryParameters: params);
|
||||
if (res.data['code'] == 0) {
|
||||
try {
|
||||
return {
|
||||
@@ -494,7 +499,7 @@ class MsgHttp {
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf
|
||||
});
|
||||
var res = await Request().get(Api.ackSessionMsg, data: params);
|
||||
var res = await Request().get(Api.ackSessionMsg, queryParameters: params);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPalaX/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPlus/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../models/video/reply/data.dart';
|
||||
@@ -28,7 +29,7 @@ class ReplyHttp {
|
||||
var res = !isLogin
|
||||
? await Request().get(
|
||||
'${HttpString.apiBaseUrl}${Api.replyList}/main',
|
||||
data: {
|
||||
queryParameters: {
|
||||
'oid': oid,
|
||||
'type': type,
|
||||
'pagination_str':
|
||||
@@ -39,7 +40,7 @@ class ReplyHttp {
|
||||
)
|
||||
: await Request().get(
|
||||
'${HttpString.apiBaseUrl}${Api.replyList}',
|
||||
data: {
|
||||
queryParameters: {
|
||||
'oid': oid,
|
||||
'type': type,
|
||||
'sort': sort,
|
||||
@@ -49,7 +50,44 @@ class ReplyHttp {
|
||||
options: options,
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(ReplyData.fromJson(res.data['data']));
|
||||
ReplyData replyData = ReplyData.fromJson(res.data['data']);
|
||||
String banWordForReply = GStorage.banWordForReply;
|
||||
if (banWordForReply.isNotEmpty) {
|
||||
// topReplies
|
||||
if (replyData.topReplies?.isNotEmpty == true) {
|
||||
replyData.topReplies!.removeWhere((item) {
|
||||
bool hasMatch = RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content?.message ?? '');
|
||||
// remove subreplies
|
||||
if (hasMatch.not) {
|
||||
if (item.replies?.isNotEmpty == true) {
|
||||
item.replies!.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content?.message ?? ''));
|
||||
}
|
||||
}
|
||||
return hasMatch;
|
||||
});
|
||||
}
|
||||
|
||||
// replies
|
||||
if (replyData.replies?.isNotEmpty == true) {
|
||||
replyData.replies!.removeWhere((item) {
|
||||
bool hasMatch = RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content?.message ?? '');
|
||||
// remove subreplies
|
||||
if (hasMatch.not) {
|
||||
if (item.replies?.isNotEmpty == true) {
|
||||
item.replies!.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content?.message ?? ''));
|
||||
}
|
||||
}
|
||||
return hasMatch;
|
||||
});
|
||||
}
|
||||
}
|
||||
return LoadingState.success(replyData);
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
@@ -62,10 +100,36 @@ class ReplyHttp {
|
||||
}) async {
|
||||
dynamic res = await GrpcRepo.mainList(type: type, oid: oid, cursor: cursor);
|
||||
if (res['status']) {
|
||||
return LoadingState.success(res['data']);
|
||||
MainListReply mainListReply = res['data'];
|
||||
String banWordForReply = GStorage.banWordForReply;
|
||||
if (banWordForReply.isNotEmpty) {
|
||||
// upTop
|
||||
if (mainListReply.hasUpTop() &&
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(mainListReply.upTop.content.message)) {
|
||||
mainListReply.clearUpTop();
|
||||
}
|
||||
|
||||
// replies
|
||||
if (mainListReply.replies.isNotEmpty) {
|
||||
mainListReply.replies.removeWhere((item) {
|
||||
bool hasMatch = RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content.message);
|
||||
// remove subreplies
|
||||
if (hasMatch.not) {
|
||||
if (item.replies.isNotEmpty) {
|
||||
item.replies.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content.message));
|
||||
}
|
||||
}
|
||||
return hasMatch;
|
||||
});
|
||||
}
|
||||
}
|
||||
return LoadingState.success(mainListReply);
|
||||
} else {
|
||||
return LoadingState.error(
|
||||
'${res['msg'].startsWith('gRPC Error') ? '如无法加载评论:\n关闭代理\n或设置中关闭使用gRPC加载评论\n\n' : ''}${res['msg']}');
|
||||
return LoadingState.error(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +146,7 @@ class ReplyHttp {
|
||||
: null;
|
||||
var res = await Request().get(
|
||||
'${HttpString.apiBaseUrl}${Api.replyReplyList}',
|
||||
data: {
|
||||
queryParameters: {
|
||||
'oid': oid,
|
||||
'root': root,
|
||||
'pn': pageNum,
|
||||
@@ -93,33 +157,21 @@ class ReplyHttp {
|
||||
options: options,
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(ReplyReplyData.fromJson(res.data['data']));
|
||||
ReplyReplyData replyData = ReplyReplyData.fromJson(res.data['data']);
|
||||
String banWordForReply = GStorage.banWordForReply;
|
||||
if (banWordForReply.isNotEmpty) {
|
||||
if (replyData.replies?.isNotEmpty == true) {
|
||||
replyData.replies!.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content?.message ?? ''));
|
||||
}
|
||||
}
|
||||
return LoadingState.success(replyData);
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> dialogListGrpc({
|
||||
int type = 1,
|
||||
required int oid,
|
||||
required int root,
|
||||
required int rpid,
|
||||
required CursorReq cursor,
|
||||
}) async {
|
||||
dynamic res = await GrpcRepo.dialogList(
|
||||
type: type,
|
||||
oid: oid,
|
||||
root: root,
|
||||
rpid: rpid,
|
||||
cursor: cursor,
|
||||
);
|
||||
if (res['status']) {
|
||||
return LoadingState.success(res['data']);
|
||||
} else {
|
||||
return LoadingState.error(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> replyReplyListGrpc({
|
||||
int type = 1,
|
||||
required int oid,
|
||||
@@ -135,7 +187,46 @@ class ReplyHttp {
|
||||
cursor: cursor,
|
||||
);
|
||||
if (res['status']) {
|
||||
return LoadingState.success(res['data']);
|
||||
DetailListReply detailListReply = res['data'];
|
||||
String banWordForReply = GStorage.banWordForReply;
|
||||
if (banWordForReply.isNotEmpty) {
|
||||
if (detailListReply.root.replies.isNotEmpty) {
|
||||
detailListReply.root.replies.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content.message));
|
||||
}
|
||||
}
|
||||
return LoadingState.success(detailListReply);
|
||||
} else {
|
||||
return LoadingState.error(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState> dialogListGrpc({
|
||||
int type = 1,
|
||||
required int oid,
|
||||
required int root,
|
||||
required int rpid,
|
||||
required CursorReq cursor,
|
||||
}) async {
|
||||
dynamic res = await GrpcRepo.dialogList(
|
||||
type: type,
|
||||
oid: oid,
|
||||
root: root,
|
||||
rpid: rpid,
|
||||
cursor: cursor,
|
||||
);
|
||||
if (res['status']) {
|
||||
DialogListReply dialogListReply = res['data'];
|
||||
String banWordForReply = GStorage.banWordForReply;
|
||||
if (banWordForReply.isNotEmpty) {
|
||||
if (dialogListReply.replies.isNotEmpty) {
|
||||
dialogListReply.replies.removeWhere((item) =>
|
||||
RegExp(banWordForReply, caseSensitive: false)
|
||||
.hasMatch(item.content.message));
|
||||
}
|
||||
}
|
||||
return LoadingState.success(dialogListReply);
|
||||
} else {
|
||||
return LoadingState.error(res['msg']);
|
||||
}
|
||||
@@ -200,7 +291,7 @@ class ReplyHttp {
|
||||
}
|
||||
|
||||
static Future<LoadingState> getEmoteList({String? business}) async {
|
||||
var res = await Request().get(Api.myEmote, data: {
|
||||
var res = await Request().get(Api.myEmote, queryParameters: {
|
||||
'business': business ?? 'reply',
|
||||
'web_location': '333.1245',
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import '../models/bangumi/info.dart';
|
||||
import '../models/common/search_type.dart';
|
||||
import '../models/search/hot.dart';
|
||||
@@ -12,7 +11,6 @@ import '../utils/storage.dart';
|
||||
import 'index.dart';
|
||||
|
||||
class SearchHttp {
|
||||
static Box localCache = GStorage.localCache;
|
||||
static Future hotSearchList() async {
|
||||
var res = await Request().get(Api.hotSearchList);
|
||||
if (res.data is String) {
|
||||
@@ -40,7 +38,7 @@ class SearchHttp {
|
||||
// 获取搜索建议
|
||||
static Future searchSuggest({required term}) async {
|
||||
var res = await Request().get(Api.searchSuggest,
|
||||
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
||||
queryParameters: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
||||
if (res.data is String) {
|
||||
Map<String, dynamic> resultMap = json.decode(res.data);
|
||||
if (resultMap['code'] == 0) {
|
||||
@@ -84,7 +82,7 @@ class SearchHttp {
|
||||
int? pubEnd,
|
||||
}) async {
|
||||
var reqData = {
|
||||
'search_type': searchType.type,
|
||||
'search_type': searchType.name,
|
||||
'keyword': keyword,
|
||||
// 'order_sort': 0,
|
||||
// 'user_type': 0,
|
||||
@@ -98,8 +96,8 @@ class SearchHttp {
|
||||
if (pubBegin != null) 'pubtime_begin_s': pubBegin,
|
||||
if (pubEnd != null) 'pubtime_end_s': pubEnd,
|
||||
};
|
||||
var res = await Request().get(Api.searchByType, data: reqData);
|
||||
if (res.data['code'] == 0) {
|
||||
var res = await Request().get(Api.searchByType, queryParameters: reqData);
|
||||
if (res.data['code'] is int && res.data['code'] == 0) {
|
||||
dynamic data;
|
||||
try {
|
||||
switch (searchType) {
|
||||
@@ -119,7 +117,7 @@ class SearchHttp {
|
||||
case SearchType.bili_user:
|
||||
data = SearchUserModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.media_bangumi:
|
||||
case SearchType.media_bangumi || SearchType.media_ft:
|
||||
data = SearchMBangumiModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.article:
|
||||
@@ -139,15 +137,15 @@ class SearchHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> ab2c({int? aid, String? bvid}) async {
|
||||
static Future<int> ab2c({dynamic aid, dynamic bvid}) async {
|
||||
Map<String, dynamic> data = {};
|
||||
if (aid != null) {
|
||||
data['aid'] = aid;
|
||||
} else if (bvid != null) {
|
||||
data['bvid'] = bvid;
|
||||
}
|
||||
final dynamic res =
|
||||
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
|
||||
final dynamic res = await Request()
|
||||
.get(Api.ab2c, queryParameters: <String, dynamic>{...data});
|
||||
if (res.data['code'] == 0) {
|
||||
return res.data['data'].first['cid'];
|
||||
} else {
|
||||
@@ -159,7 +157,7 @@ class SearchHttp {
|
||||
static Future<LoadingState> bangumiInfoNew({int? seasonId, int? epId}) async {
|
||||
final dynamic res = await Request().get(
|
||||
Api.bangumiInfo,
|
||||
data: {
|
||||
queryParameters: {
|
||||
if (seasonId != null) 'season_id': seasonId,
|
||||
if (epId != null) 'ep_id': epId,
|
||||
},
|
||||
@@ -182,8 +180,9 @@ class SearchHttp {
|
||||
} else if (epId != null) {
|
||||
data['ep_id'] = epId;
|
||||
}
|
||||
final dynamic res =
|
||||
await Request().get(Api.bangumiInfo, data: <String, dynamic>{...data});
|
||||
final dynamic res = await Request()
|
||||
.get(Api.bangumiInfo, queryParameters: <String, dynamic>{...data});
|
||||
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/video/later.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../common/constants.dart';
|
||||
@@ -15,7 +16,7 @@ import 'init.dart';
|
||||
|
||||
class UserHttp {
|
||||
static Future<dynamic> userStat({required int mid}) async {
|
||||
var res = await Request().get(Api.userStat, data: {'vmid': mid});
|
||||
var res = await Request().get(Api.userStat, queryParameters: {'vmid': mid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -47,9 +48,9 @@ class UserHttp {
|
||||
static Future<LoadingState> userfavFolder({
|
||||
required int pn,
|
||||
required int ps,
|
||||
required int mid,
|
||||
required dynamic mid,
|
||||
}) async {
|
||||
var res = await Request().get(Api.userFavFolder, data: {
|
||||
var res = await Request().get(Api.userFavFolder, queryParameters: {
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
'up_mid': mid,
|
||||
@@ -113,7 +114,7 @@ class UserHttp {
|
||||
static Future folderInfo({
|
||||
dynamic mediaId,
|
||||
}) async {
|
||||
var res = await Request().get(Api.folderInfo, data: {
|
||||
var res = await Request().get(Api.folderInfo, queryParameters: {
|
||||
'media_id': mediaId,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -130,7 +131,7 @@ class UserHttp {
|
||||
String keyword = '',
|
||||
String order = 'mtime',
|
||||
int type = 0}) async {
|
||||
var res = await Request().get(Api.userFavFolderDetail, data: {
|
||||
var res = await Request().get(Api.userFavFolderDetail, queryParameters: {
|
||||
'media_id': mediaId,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
@@ -172,7 +173,7 @@ class UserHttp {
|
||||
int? max,
|
||||
int? viewAt,
|
||||
}) async {
|
||||
var res = await Request().get(Api.historyList, data: {
|
||||
var res = await Request().get(Api.historyList, queryParameters: {
|
||||
'type': 'all',
|
||||
'ps': 20,
|
||||
'max': max ?? 0,
|
||||
@@ -265,7 +266,7 @@ class UserHttp {
|
||||
static Future thirdLogin() async {
|
||||
var res = await Request().get(
|
||||
'https://passport.bilibili.com/login/app/third',
|
||||
data: {
|
||||
queryParameters: {
|
||||
'appkey': Constants.appKey,
|
||||
'api': Constants.thirdApi,
|
||||
'sign': Constants.thirdSign,
|
||||
@@ -319,7 +320,7 @@ class UserHttp {
|
||||
static Future hasFollow(int mid) async {
|
||||
var res = await Request().get(
|
||||
Api.hasFollow,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'fid': mid,
|
||||
},
|
||||
);
|
||||
@@ -359,7 +360,7 @@ class UserHttp {
|
||||
{required int pn, required String keyword}) async {
|
||||
var res = await Request().get(
|
||||
Api.searchHistory,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'pn': pn,
|
||||
'keyword': keyword,
|
||||
'business': 'all',
|
||||
@@ -373,24 +374,25 @@ class UserHttp {
|
||||
}
|
||||
|
||||
// 我的订阅
|
||||
static Future userSubFolder({
|
||||
static Future<LoadingState> userSubFolder({
|
||||
required int mid,
|
||||
required int pn,
|
||||
required int ps,
|
||||
}) async {
|
||||
var res = await Request().get(Api.userSubFolder, data: {
|
||||
'up_mid': mid,
|
||||
'ps': ps,
|
||||
'pn': pn,
|
||||
'platform': 'web',
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': SubFolderModelData.fromJson(res.data['data'])
|
||||
};
|
||||
var res = await Request().get(
|
||||
Api.userSubFolder,
|
||||
queryParameters: {
|
||||
'up_mid': mid,
|
||||
'ps': ps,
|
||||
'pn': pn,
|
||||
'platform': 'web',
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0 && res.data['data'] is Map) {
|
||||
return LoadingState.success(
|
||||
SubFolderModelData.fromJson(res.data['data']).list);
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +401,7 @@ class UserHttp {
|
||||
required int pn,
|
||||
required int ps,
|
||||
}) async {
|
||||
var res = await Request().get(Api.favSeasonList, data: {
|
||||
var res = await Request().get(Api.favSeasonList, queryParameters: {
|
||||
'season_id': id,
|
||||
'ps': ps,
|
||||
'pn': pn,
|
||||
@@ -419,7 +421,7 @@ class UserHttp {
|
||||
required int pn,
|
||||
required int ps,
|
||||
}) async {
|
||||
var res = await Request().get(Api.favResourceList, data: {
|
||||
var res = await Request().get(Api.favResourceList, queryParameters: {
|
||||
'media_id': id,
|
||||
'ps': ps,
|
||||
'pn': pn,
|
||||
@@ -463,11 +465,118 @@ class UserHttp {
|
||||
}
|
||||
|
||||
static videoTags({required String bvid}) async {
|
||||
var res = await Request().get(Api.videoTags, data: {'bvid': bvid});
|
||||
var res =
|
||||
await Request().get(Api.videoTags, queryParameters: {'bvid': bvid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': false};
|
||||
}
|
||||
}
|
||||
|
||||
// 稍后再看播放全部
|
||||
// static Future toViewPlayAll({required int oid, required String bvid}) async {
|
||||
// var res = await Request().get(
|
||||
// Api.watchLaterHtml,
|
||||
// data: {
|
||||
// 'oid': oid,
|
||||
// 'bvid': bvid,
|
||||
// },
|
||||
// );
|
||||
// String scriptContent =
|
||||
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||
// int startIndex = scriptContent.indexOf('{');
|
||||
// int endIndex = scriptContent.lastIndexOf('};');
|
||||
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||
// // 解析JSON字符串为Map
|
||||
// Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||
// // 输出解析后的数据
|
||||
// return {
|
||||
// 'status': true,
|
||||
// 'data': jsonData['resourceList']
|
||||
// .map((e) => MediaVideoItemModel.fromJson(e))
|
||||
// .toList()
|
||||
// };
|
||||
// }
|
||||
static List<String> extractScriptContents(String htmlContent) {
|
||||
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
|
||||
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
|
||||
List<String> scriptContents = [];
|
||||
for (Match match in matches) {
|
||||
String scriptContent = match.group(1)!;
|
||||
scriptContents.add(scriptContent);
|
||||
}
|
||||
return scriptContents;
|
||||
}
|
||||
|
||||
// 稍后再看列表
|
||||
static Future getMediaList({
|
||||
required dynamic type,
|
||||
required int bizId,
|
||||
required int ps,
|
||||
dynamic oid,
|
||||
int? otype,
|
||||
bool withCurrent = false,
|
||||
bool desc = true,
|
||||
dynamic sortField = 1,
|
||||
bool direction = false,
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.mediaList,
|
||||
queryParameters: {
|
||||
'mobi_app': 'web',
|
||||
'type': type,
|
||||
'biz_id': bizId,
|
||||
if (oid != null) 'oid': oid,
|
||||
if (otype != null) 'otype': otype, // video:2 // bangumi: 24
|
||||
'ps': ps,
|
||||
'direction': direction,
|
||||
'desc': desc,
|
||||
'sort_field': sortField,
|
||||
'tid': 0,
|
||||
'with_current': withCurrent,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data']['media_list'] != null
|
||||
? res.data['data']['media_list']
|
||||
.map<MediaVideoItemModel>(
|
||||
(e) => MediaVideoItemModel.fromJson(e))
|
||||
.toList()
|
||||
: []
|
||||
};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 解析收藏夹视频
|
||||
// static Future parseFavVideo({
|
||||
// required int mediaId,
|
||||
// required int oid,
|
||||
// required String bvid,
|
||||
// }) async {
|
||||
// var res = await Request().get(
|
||||
// 'https://www.bilibili.com/list/ml$mediaId',
|
||||
// queryParameters: {
|
||||
// 'oid': mediaId,
|
||||
// 'bvid': bvid,
|
||||
// },
|
||||
// );
|
||||
// String scriptContent =
|
||||
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||
// int startIndex = scriptContent.indexOf('{');
|
||||
// int endIndex = scriptContent.lastIndexOf('};');
|
||||
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||
// // 解析JSON字符串为Map
|
||||
// Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||
// return {
|
||||
// 'status': true,
|
||||
// 'data': jsonData['resourceList']
|
||||
// .map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
|
||||
// .toList()
|
||||
// };
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:PiliPalaX/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPalaX/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPlus/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import '../common/constants.dart';
|
||||
import '../models/common/reply_type.dart';
|
||||
import '../models/home/rcmd/result.dart';
|
||||
@@ -30,18 +29,15 @@ import 'login.dart';
|
||||
/// 返回{'status': bool, 'data': List}
|
||||
/// view层根据 status 判断渲染逻辑
|
||||
class VideoHttp {
|
||||
static Box localCache = GStorage.localCache;
|
||||
static Box setting = GStorage.setting;
|
||||
static bool enableRcmdDynamic =
|
||||
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
|
||||
static Box userInfoCache = GStorage.userInfo;
|
||||
GStorage.setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
|
||||
|
||||
// 首页推荐视频
|
||||
static Future<LoadingState> rcmdVideoList(
|
||||
{required int ps, required int freshIdx}) async {
|
||||
var res = await Request().get(
|
||||
Api.recommendListWeb,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'version': 1,
|
||||
'feed_version': 'V8',
|
||||
'homepage_ver': 1,
|
||||
@@ -76,7 +72,7 @@ class VideoHttp {
|
||||
{bool loginStatus = true, required int freshIdx}) async {
|
||||
Map<String, String> data = {
|
||||
'access_key': loginStatus
|
||||
? (localCache
|
||||
? (GStorage.localCache
|
||||
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
|
||||
'')
|
||||
: '',
|
||||
@@ -119,7 +115,7 @@ class VideoHttp {
|
||||
|
||||
var res = await Request().get(
|
||||
Api.recommendListApp,
|
||||
data: data,
|
||||
queryParameters: data,
|
||||
options: Options(headers: {
|
||||
'Host': 'app.bilibili.com',
|
||||
'buvid': LoginHttp.buvid,
|
||||
@@ -148,6 +144,12 @@ class VideoHttp {
|
||||
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
|
||||
(i['args'] != null &&
|
||||
!blackMidsList.contains(i['args']['up_id']))) {
|
||||
String banWordForZone = GStorage.banWordForZone;
|
||||
if (banWordForZone.isNotEmpty &&
|
||||
RegExp(banWordForZone, caseSensitive: false)
|
||||
.hasMatch(i['args']['rname'])) {
|
||||
continue;
|
||||
}
|
||||
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
|
||||
if (!RecommendFilter.filter(videoItem)) {
|
||||
list.add(videoItem);
|
||||
@@ -165,13 +167,22 @@ class VideoHttp {
|
||||
{required int pn, required int ps}) async {
|
||||
var res = await Request().get(
|
||||
Api.hotList,
|
||||
data: {'pn': pn, 'ps': ps},
|
||||
queryParameters: {'pn': pn, 'ps': ps},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
if (!blackMidsList.contains(i['owner']['mid']) &&
|
||||
!RecommendFilter.filterTitle(i['title']) &&
|
||||
!RecommendFilter.filterLikeRatio(
|
||||
i['stat']['like'], i['stat']['view'])) {
|
||||
String banWordForZone = GStorage.banWordForZone;
|
||||
if (banWordForZone.isNotEmpty &&
|
||||
RegExp(banWordForZone, caseSensitive: false)
|
||||
.hasMatch(i['tname'])) {
|
||||
continue;
|
||||
}
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
}
|
||||
}
|
||||
@@ -198,25 +209,29 @@ class VideoHttp {
|
||||
}
|
||||
|
||||
// 视频流
|
||||
static Future videoUrl(
|
||||
{int? avid, String? bvid, required int cid, int? qn}) async {
|
||||
static Future videoUrl({
|
||||
int? avid,
|
||||
String? bvid,
|
||||
required int cid,
|
||||
int? qn,
|
||||
dynamic epid,
|
||||
dynamic seasonId,
|
||||
}) async {
|
||||
Map<String, dynamic> data = {
|
||||
if (avid != null) 'avid': avid,
|
||||
if (bvid != null) 'bvid': bvid,
|
||||
if (epid != null) 'ep_id': epid,
|
||||
if (seasonId != null) 'season_id': seasonId,
|
||||
'cid': cid,
|
||||
'qn': qn ?? 80,
|
||||
// 获取所有格式的视频
|
||||
'fnval': 4048,
|
||||
};
|
||||
if (avid != null) {
|
||||
data['avid'] = avid;
|
||||
}
|
||||
if (bvid != null) {
|
||||
data['bvid'] = bvid;
|
||||
}
|
||||
|
||||
// 免登录查看1080p
|
||||
if ((userInfoCache.get('userInfoCache') == null ||
|
||||
MineController.anonymity) &&
|
||||
setting.get(SettingBoxKey.p1080, defaultValue: true)) {
|
||||
if ((GStorage.userInfo.get('userInfoCache') == null ||
|
||||
MineController.anonymity.value) &&
|
||||
GStorage.setting.get(SettingBoxKey.p1080, defaultValue: true)) {
|
||||
data['try_look'] = 1;
|
||||
}
|
||||
|
||||
@@ -228,12 +243,24 @@ class VideoHttp {
|
||||
'web_location': 1550101,
|
||||
});
|
||||
|
||||
late final isLogin = GStorage.isLogin;
|
||||
|
||||
try {
|
||||
var res = await Request().get(Api.videoUrl, data: params);
|
||||
var res = await Request().get(
|
||||
epid != null && isLogin ? Api.bangumiVideoUrl : Api.videoUrl,
|
||||
queryParameters: params);
|
||||
if (res.data['code'] == 0) {
|
||||
late PlayUrlModel data;
|
||||
if (epid != null && isLogin) {
|
||||
data = PlayUrlModel.fromJson(res.data['result']['video_info'])
|
||||
..lastPlayTime = res.data['result']['play_view_business_info']
|
||||
['user_status']['watch_progress']['current_watch_progress'];
|
||||
} else {
|
||||
data = PlayUrlModel.fromJson(res.data['data']);
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': PlayUrlModel.fromJson(res.data['data'])
|
||||
'data': data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -250,7 +277,8 @@ class VideoHttp {
|
||||
|
||||
// 视频信息 标题、简介
|
||||
static Future videoIntro({required String bvid}) async {
|
||||
var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
|
||||
var res =
|
||||
await Request().get(Api.videoIntro, queryParameters: {'bvid': bvid});
|
||||
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
|
||||
if (result.code == 0) {
|
||||
return {
|
||||
@@ -305,7 +333,7 @@ class VideoHttp {
|
||||
static Future videoRelation({required dynamic bvid}) async {
|
||||
var res = await Request().get(
|
||||
Api.videoRelation,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'aid': IdUtils.bv2av(bvid),
|
||||
'bvid': bvid,
|
||||
},
|
||||
@@ -325,7 +353,8 @@ class VideoHttp {
|
||||
|
||||
// 相关视频
|
||||
static Future<LoadingState> relatedVideoList({required String bvid}) async {
|
||||
var res = await Request().get(Api.relatedList, data: {'bvid': bvid});
|
||||
var res =
|
||||
await Request().get(Api.relatedList, queryParameters: {'bvid': bvid});
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
for (var i in res.data['data']) {
|
||||
@@ -344,7 +373,7 @@ class VideoHttp {
|
||||
static Future bangumiLikeCoinFav({dynamic epId}) async {
|
||||
var res = await Request().get(
|
||||
Api.bangumiLikeCoinFav,
|
||||
data: {'ep_id': epId},
|
||||
queryParameters: {'ep_id': epId},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
@@ -355,7 +384,8 @@ class VideoHttp {
|
||||
|
||||
// 获取点赞状态
|
||||
static Future hasLikeVideo({required String bvid}) async {
|
||||
var res = await Request().get(Api.hasLikeVideo, data: {'bvid': bvid});
|
||||
var res =
|
||||
await Request().get(Api.hasLikeVideo, queryParameters: {'bvid': bvid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -365,7 +395,8 @@ class VideoHttp {
|
||||
|
||||
// 获取投币状态
|
||||
static Future hasCoinVideo({required String bvid}) async {
|
||||
var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
|
||||
var res =
|
||||
await Request().get(Api.hasCoinVideo, queryParameters: {'bvid': bvid});
|
||||
debugPrint('res: $res');
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
@@ -397,7 +428,8 @@ class VideoHttp {
|
||||
|
||||
// 获取收藏状态
|
||||
static Future hasFavVideo({required int aid}) async {
|
||||
var res = await Request().get(Api.hasFavVideo, data: {'aid': aid});
|
||||
var res =
|
||||
await Request().get(Api.hasFavVideo, queryParameters: {'aid': aid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -507,7 +539,7 @@ class VideoHttp {
|
||||
return {'status': false, 'msg': "请退出账号后重新登录"};
|
||||
}
|
||||
assert((reasonId != null) ^ (feedbackId != null));
|
||||
var res = await Request().get(Api.feedDislike, data: {
|
||||
var res = await Request().get(Api.feedDislike, queryParameters: {
|
||||
'goto': goto,
|
||||
'id': id,
|
||||
// 'mid': mid,
|
||||
@@ -537,7 +569,7 @@ class VideoHttp {
|
||||
return {'status': false, 'msg': "请退出账号后重新登录"};
|
||||
}
|
||||
// assert ((reasonId != null) ^ (feedbackId != null));
|
||||
var res = await Request().get(Api.feedDislikeCancel, data: {
|
||||
var res = await Request().get(Api.feedDislikeCancel, queryParameters: {
|
||||
'goto': goto,
|
||||
'id': id,
|
||||
// 'mid': mid,
|
||||
@@ -637,7 +669,7 @@ class VideoHttp {
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.videoInFolder,
|
||||
data: {
|
||||
queryParameters: {
|
||||
'up_mid': mid,
|
||||
'rid': rid,
|
||||
if (type != null) 'type': type,
|
||||
@@ -712,7 +744,7 @@ class VideoHttp {
|
||||
|
||||
// 查询是否关注up
|
||||
static Future hasFollow({required int mid}) async {
|
||||
var res = await Request().get(Api.hasFollow, data: {'fid': mid});
|
||||
var res = await Request().get(Api.hasFollow, queryParameters: {'fid': mid});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
@@ -737,18 +769,36 @@ class VideoHttp {
|
||||
}
|
||||
|
||||
// 视频播放进度
|
||||
static Future heartBeat({bvid, cid, progress, realtime}) async {
|
||||
static Future heartBeat({
|
||||
bvid,
|
||||
cid,
|
||||
progress,
|
||||
epid,
|
||||
seasonId,
|
||||
subType,
|
||||
}) async {
|
||||
await Request().post(Api.heartBeat, queryParameters: {
|
||||
// 'aid': aid,
|
||||
'bvid': bvid,
|
||||
'cid': cid,
|
||||
// 'epid': '',
|
||||
// 'sid': '',
|
||||
// 'mid': '',
|
||||
if (epid != null) 'epid': epid,
|
||||
if (seasonId != null) 'sid': seasonId,
|
||||
if (epid != null) 'type': 4,
|
||||
if (subType != null) 'sub_type': subType,
|
||||
'played_time': progress,
|
||||
// 'realtime': realtime,
|
||||
// 'type': '',
|
||||
// 'sub_type': '',
|
||||
'csrf': await Request.getCsrf(),
|
||||
});
|
||||
}
|
||||
|
||||
static Future medialistHistory({
|
||||
required int desc,
|
||||
required dynamic oid,
|
||||
required dynamic upperMid,
|
||||
}) async {
|
||||
await Request().post(Api.mediaListHistory, queryParameters: {
|
||||
'desc': desc,
|
||||
'oid': oid,
|
||||
'upper_mid': upperMid,
|
||||
'csrf': await Request.getCsrf(),
|
||||
});
|
||||
}
|
||||
@@ -818,7 +868,7 @@ class VideoHttp {
|
||||
|
||||
// 查看视频同时在看人数
|
||||
static Future onlineTotal({int? aid, String? bvid, int? cid}) async {
|
||||
var res = await Request().get(Api.onlineTotal, data: {
|
||||
var res = await Request().get(Api.onlineTotal, queryParameters: {
|
||||
'aid': aid,
|
||||
'bvid': bvid,
|
||||
'cid': cid,
|
||||
@@ -840,7 +890,7 @@ class VideoHttp {
|
||||
'cid': cid,
|
||||
'up_mid': upMid,
|
||||
});
|
||||
var res = await Request().get(Api.aiConclusion, data: params);
|
||||
var res = await Request().get(Api.aiConclusion, queryParameters: params);
|
||||
if (res.data['code'] == 0 && res.data['data']['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
@@ -856,7 +906,7 @@ class VideoHttp {
|
||||
assert(aid != null || bvid != null);
|
||||
var res = await Request().get(
|
||||
Api.subtitleUrl,
|
||||
data: {
|
||||
queryParameters: {
|
||||
if (aid != null) 'aid': aid,
|
||||
if (bvid != null) 'bvid': bvid,
|
||||
'cid': cid,
|
||||
@@ -884,6 +934,8 @@ class VideoHttp {
|
||||
'status': true,
|
||||
'data': subtitlesJson,
|
||||
'view_points': data['view_points'],
|
||||
// 'last_play_time': data['last_play_time'],
|
||||
'last_play_cid': data['last_play_cid'],
|
||||
};
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||
@@ -973,7 +1025,16 @@ class VideoHttp {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList = GStorage.blackMidsList;
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
if (!blackMidsList.contains(i['owner']['mid']) &&
|
||||
!RecommendFilter.filterTitle(i['title']) &&
|
||||
!RecommendFilter.filterLikeRatio(
|
||||
i['stat']['like'], i['stat']['view'])) {
|
||||
String banWordForZone = GStorage.banWordForZone;
|
||||
if (banWordForZone.isNotEmpty &&
|
||||
RegExp(banWordForZone, caseSensitive: false)
|
||||
.hasMatch(i['tname'])) {
|
||||
continue;
|
||||
}
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
}
|
||||
}
|
||||
|
||||
113
lib/main.dart
113
lib/main.dart
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPalaX/utils/cache_manage.dart';
|
||||
import 'package:PiliPlus/build_config.dart';
|
||||
import 'package:PiliPlus/utils/cache_manage.dart';
|
||||
import 'package:flex_seed_scheme/flex_seed_scheme.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -9,18 +11,18 @@ import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/common/widgets/custom_toast.dart';
|
||||
import 'package:PiliPalaX/http/init.dart';
|
||||
import 'package:PiliPalaX/models/common/color_type.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
||||
import 'package:PiliPalaX/router/app_pages.dart';
|
||||
import 'package:PiliPalaX/pages/main/view.dart';
|
||||
import 'package:PiliPalaX/services/service_locator.dart';
|
||||
import 'package:PiliPalaX/utils/app_scheme.dart';
|
||||
import 'package:PiliPalaX/utils/data.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_toast.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/models/common/color_type.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/index.dart';
|
||||
import 'package:PiliPlus/router/app_pages.dart';
|
||||
import 'package:PiliPlus/pages/main/view.dart';
|
||||
import 'package:PiliPlus/services/service_locator.dart';
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/data.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
|
||||
import 'package:PiliPalaX/utils/recommend_filter.dart';
|
||||
import 'package:PiliPlus/utils/recommend_filter.dart';
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import './services/loggeer.dart';
|
||||
|
||||
@@ -50,13 +52,19 @@ void main() async {
|
||||
],
|
||||
);
|
||||
}
|
||||
if (GStorage.badCertificateCallback) {
|
||||
HttpOverrides.global = _CustomHttpOverrides();
|
||||
}
|
||||
await setupServiceLocator();
|
||||
Request();
|
||||
await Request.setCookie();
|
||||
RecommendFilter();
|
||||
SmartDialog.config.toast =
|
||||
SmartConfigToast(displayType: SmartToastType.normal);
|
||||
SmartDialog.config.loading =
|
||||
SmartConfigLoading(backType: SmartBackType.normal);
|
||||
// 异常捕获 logo记录
|
||||
final String buildConfig = '''\n
|
||||
Build Time: ${BuildConfig.buildTime}
|
||||
Commit Hash: ${BuildConfig.commitHash}''';
|
||||
final Catcher2Options debugConfig = Catcher2Options(
|
||||
SilentReportMode(),
|
||||
[
|
||||
@@ -64,13 +72,25 @@ void main() async {
|
||||
ConsoleHandler(
|
||||
enableDeviceParameters: false,
|
||||
enableApplicationParameters: false,
|
||||
enableCustomParameters: true,
|
||||
)
|
||||
],
|
||||
customParameters: {
|
||||
'BuildConfig': buildConfig,
|
||||
},
|
||||
);
|
||||
|
||||
final Catcher2Options releaseConfig = Catcher2Options(
|
||||
SilentReportMode(),
|
||||
[FileHandler(await getLogsPath())],
|
||||
[
|
||||
FileHandler(await getLogsPath()),
|
||||
ConsoleHandler(
|
||||
enableCustomParameters: true,
|
||||
)
|
||||
],
|
||||
customParameters: {
|
||||
'BuildConfig': buildConfig,
|
||||
},
|
||||
);
|
||||
|
||||
Catcher2(
|
||||
@@ -96,9 +116,10 @@ void main() async {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
Box get setting => GStorage.setting;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Box setting = GStorage.setting;
|
||||
// 主题色
|
||||
Color defaultColor =
|
||||
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
|
||||
@@ -110,8 +131,10 @@ class MyApp extends StatelessWidget {
|
||||
// 字体缩放大小
|
||||
double textScale =
|
||||
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
|
||||
DynamicSchemeVariant dynamicSchemeVariant =
|
||||
DynamicSchemeVariant.values[GStorage.schemeVariant];
|
||||
// DynamicSchemeVariant dynamicSchemeVariant =
|
||||
// DynamicSchemeVariant.values[GStorage.schemeVariant];
|
||||
FlexSchemeVariant variant =
|
||||
FlexSchemeVariant.values[GStorage.schemeVariant];
|
||||
|
||||
// 强制设置高帧率
|
||||
if (Platform.isAndroid) {
|
||||
@@ -139,29 +162,36 @@ class MyApp extends StatelessWidget {
|
||||
darkColorScheme = darkDynamic.harmonized();
|
||||
} else {
|
||||
// dynamic取色失败,采用品牌色
|
||||
lightColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: brandColor,
|
||||
lightColorScheme = SeedColorScheme.fromSeeds(
|
||||
primaryKey: brandColor,
|
||||
brightness: Brightness.light,
|
||||
dynamicSchemeVariant: dynamicSchemeVariant,
|
||||
variant: variant,
|
||||
// dynamicSchemeVariant: dynamicSchemeVariant,
|
||||
// tones: FlexTones.soft(Brightness.light),
|
||||
);
|
||||
darkColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: brandColor,
|
||||
darkColorScheme = SeedColorScheme.fromSeeds(
|
||||
primaryKey: brandColor,
|
||||
brightness: Brightness.dark,
|
||||
dynamicSchemeVariant: dynamicSchemeVariant,
|
||||
variant: variant,
|
||||
// dynamicSchemeVariant: dynamicSchemeVariant,
|
||||
// tones: FlexTones.soft(Brightness.dark),
|
||||
);
|
||||
}
|
||||
// 图片缓存
|
||||
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
|
||||
return GetMaterialApp(
|
||||
// showSemanticsDebugger: true,
|
||||
title: 'PiliPalaX',
|
||||
title: 'PiliPlus',
|
||||
theme: _getThemeData(
|
||||
colorScheme: lightColorScheme,
|
||||
isDynamic: lightDynamic != null && isDynamicColor,
|
||||
variant: variant,
|
||||
),
|
||||
darkTheme: _getThemeData(
|
||||
colorScheme: darkColorScheme,
|
||||
isDynamic: darkDynamic != null && isDynamicColor,
|
||||
isDark: true,
|
||||
variant: variant,
|
||||
),
|
||||
themeMode: GStorage.themeMode,
|
||||
localizationsDelegates: const [
|
||||
@@ -196,9 +226,9 @@ class MyApp extends StatelessWidget {
|
||||
ThemeData _getThemeData({
|
||||
required ColorScheme colorScheme,
|
||||
required bool isDynamic,
|
||||
bool isDark = false,
|
||||
required FlexSchemeVariant variant,
|
||||
}) {
|
||||
Color surfaceTintColor =
|
||||
isDynamic ? colorScheme.surfaceTint : colorScheme.surfaceContainer;
|
||||
return ThemeData(
|
||||
colorScheme: colorScheme,
|
||||
useMaterial3: true,
|
||||
@@ -211,15 +241,13 @@ class MyApp extends StatelessWidget {
|
||||
titleTextStyle: TextStyle(fontSize: 16, color: colorScheme.onSurface),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
surfaceTintColor: isDynamic ? colorScheme.onSurfaceVariant : null,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
actionTextColor: colorScheme.primary,
|
||||
backgroundColor: colorScheme.secondaryContainer,
|
||||
closeIconColor: colorScheme.secondary,
|
||||
contentTextStyle: TextStyle(
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
contentTextStyle: TextStyle(color: colorScheme.secondary),
|
||||
elevation: 20,
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
@@ -230,19 +258,32 @@ class MyApp extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
surfaceTintColor: isDynamic ? colorScheme.onSurfaceVariant : null,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 1,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
surfaceTintColor: isDynamic
|
||||
? colorScheme.onSurfaceVariant
|
||||
: isDark
|
||||
? colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
),
|
||||
// dialogTheme: DialogTheme(
|
||||
// surfaceTintColor: isDark ? colorScheme.onSurfaceVariant : null,
|
||||
// ),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(
|
||||
refreshBackgroundColor: colorScheme.onSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class BangumiInfoModel {
|
||||
int? type;
|
||||
UserStatus? userStatus;
|
||||
String? staff;
|
||||
List<Section>? section;
|
||||
|
||||
BangumiInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
activity = json['activity'];
|
||||
@@ -89,8 +90,8 @@ class BangumiInfoModel {
|
||||
bkgCover = json['bkg_cover'];
|
||||
cover = json['cover'];
|
||||
enableVt = json['enableVt'];
|
||||
episodes = json['episodes']
|
||||
.map<EpisodeItem>((e) => EpisodeItem.fromJson(e))
|
||||
episodes = (json['episodes'] as List?)
|
||||
?.map<EpisodeItem>((e) => EpisodeItem.fromJson(e))
|
||||
.toList();
|
||||
evaluate = json['evaluate'];
|
||||
freya = json['freya'];
|
||||
@@ -125,6 +126,22 @@ class BangumiInfoModel {
|
||||
userStatus = UserStatus.fromJson(json['user_status']);
|
||||
}
|
||||
staff = json['staff'];
|
||||
section = (json['section'] as List?)
|
||||
?.map((item) => Section.fromJson(item))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class Section {
|
||||
Section({
|
||||
this.episodes,
|
||||
});
|
||||
List<EpisodeItem>? episodes;
|
||||
|
||||
Section.fromJson(Map<String, dynamic> json) {
|
||||
episodes = (json['episodes'] as List?)
|
||||
?.map<EpisodeItem>((e) => EpisodeItem.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final List<Map<String, dynamic>> colorThemeTypes = [
|
||||
{'color': const Color.fromARGB(255, 92, 182, 123), 'label': '默认绿'},
|
||||
{'color': const Color.fromARGB(255, 251, 114, 153), 'label': '粉红色'},
|
||||
{'color': const Color(0xFF5CB67B), 'label': '默认绿'},
|
||||
{'color': const Color(0xFFFF7299), 'label': '粉红色'},
|
||||
{'color': Colors.red, 'label': '红色'},
|
||||
{'color': Colors.orange, 'label': '橙色'},
|
||||
{'color': Colors.amber, 'label': '琥珀色'},
|
||||
|
||||
@@ -4,6 +4,8 @@ extension DynamicBadgeModeDesc on DynamicBadgeMode {
|
||||
String get description => ['隐藏', '红点', '数字'][index];
|
||||
}
|
||||
|
||||
extension DynamicBadgeModeCode on DynamicBadgeMode {
|
||||
int get code => [0, 1, 2][index];
|
||||
enum MsgUnReadType { pm, reply, at, like, sysMsg }
|
||||
|
||||
extension MsgUnReadTypeExt on MsgUnReadType {
|
||||
String get title => ['私信', '回复我的', '@我', '收到的赞', '系统通知'][index];
|
||||
}
|
||||
|
||||
@@ -11,35 +11,35 @@ extension BusinessTypeExtension on DynamicsType {
|
||||
String get labels => ['全部', '投稿', '番剧', '专栏', 'UP'][index];
|
||||
}
|
||||
|
||||
List tabsConfig = [
|
||||
{
|
||||
'tag': 'all',
|
||||
'value': DynamicsType.all,
|
||||
'label': '全部',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'video',
|
||||
'value': DynamicsType.video,
|
||||
'label': '投稿',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'pgc',
|
||||
'value': DynamicsType.pgc,
|
||||
'label': '番剧',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'article',
|
||||
'value': DynamicsType.article,
|
||||
'label': '专栏',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'up',
|
||||
'value': DynamicsType.up,
|
||||
'label': 'UP',
|
||||
'enabled': true,
|
||||
},
|
||||
];
|
||||
List get tabsConfig => [
|
||||
{
|
||||
'tag': 'all',
|
||||
'value': DynamicsType.all,
|
||||
'label': '全部',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'video',
|
||||
'value': DynamicsType.video,
|
||||
'label': '投稿',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'pgc',
|
||||
'value': DynamicsType.pgc,
|
||||
'label': '番剧',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'article',
|
||||
'value': DynamicsType.article,
|
||||
'label': '专栏',
|
||||
'enabled': true,
|
||||
},
|
||||
{
|
||||
'tag': 'up',
|
||||
'value': DynamicsType.up,
|
||||
'label': 'UP',
|
||||
'enabled': true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
List defaultNavigationBars = [
|
||||
{
|
||||
'id': 0,
|
||||
'icon': const Icon(
|
||||
Icons.home_outlined,
|
||||
size: 23,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.home,
|
||||
size: 23,
|
||||
),
|
||||
'label': "首页",
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'id': 1,
|
||||
'icon': const Icon(
|
||||
Icons.motion_photos_on_outlined,
|
||||
size: 21,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.motion_photos_on,
|
||||
size: 21,
|
||||
),
|
||||
'label': "动态",
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'icon': const Icon(
|
||||
Icons.video_collection_outlined,
|
||||
size: 21,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.video_collection,
|
||||
size: 21,
|
||||
),
|
||||
'label': "媒体库",
|
||||
'count': 0,
|
||||
}
|
||||
];
|
||||
List get defaultNavigationBars => [
|
||||
{
|
||||
'id': 0,
|
||||
'icon': const Icon(
|
||||
Icons.home_outlined,
|
||||
size: 23,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.home,
|
||||
size: 23,
|
||||
),
|
||||
'label': "首页",
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'id': 1,
|
||||
'icon': const Icon(
|
||||
Icons.motion_photos_on_outlined,
|
||||
size: 21,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.motion_photos_on,
|
||||
size: 21,
|
||||
),
|
||||
'label': "动态",
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'icon': const Icon(
|
||||
Icons.video_collection_outlined,
|
||||
size: 21,
|
||||
),
|
||||
'selectIcon': const Icon(
|
||||
Icons.video_collection,
|
||||
size: 21,
|
||||
),
|
||||
'label': "媒体库",
|
||||
'count': 0,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@ enum SearchType {
|
||||
// 番剧:media_bangumi,
|
||||
media_bangumi,
|
||||
// 影视:media_ft
|
||||
// media_ft,
|
||||
media_ft,
|
||||
// 直播间及主播:live
|
||||
// live,
|
||||
// 直播间:live_room
|
||||
@@ -23,9 +23,7 @@ enum SearchType {
|
||||
}
|
||||
|
||||
extension SearchTypeExtension on SearchType {
|
||||
String get type =>
|
||||
['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
|
||||
String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
|
||||
String get label => ['视频', '番剧', '影视', '直播间', '用户', '专栏'][index];
|
||||
}
|
||||
|
||||
// 搜索类型为视频、专栏及相簿时
|
||||
|
||||
@@ -1,67 +1,78 @@
|
||||
import 'package:PiliPalaX/pages/rank/index.dart';
|
||||
import 'package:PiliPlus/pages/live/view.dart';
|
||||
import 'package:PiliPlus/pages/rank/index.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPalaX/pages/bangumi/index.dart';
|
||||
import 'package:PiliPalaX/pages/hot/index.dart';
|
||||
import 'package:PiliPalaX/pages/live/index.dart';
|
||||
import 'package:PiliPalaX/pages/rcmd/index.dart';
|
||||
import 'package:PiliPlus/pages/bangumi/index.dart';
|
||||
import 'package:PiliPlus/pages/hot/index.dart';
|
||||
import 'package:PiliPlus/pages/live/index.dart';
|
||||
import 'package:PiliPlus/pages/rcmd/index.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
enum TabType { live, rcmd, hot, rank, bangumi }
|
||||
enum TabType { live, rcmd, hot, rank, bangumi, cinema }
|
||||
|
||||
extension TabTypeDesc on TabType {
|
||||
String get description => ['直播', '推荐', '热门', '分区', '番剧'][index];
|
||||
String get id => ['live', 'rcmd', 'hot', 'rank', 'bangumi'][index];
|
||||
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 RcmdPage(tabType: TabType.live),
|
||||
},
|
||||
{
|
||||
'icon': const Icon(
|
||||
Icons.thumb_up_off_alt_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '推荐',
|
||||
'type': TabType.rcmd,
|
||||
'ctr': Get.find<RcmdController>,
|
||||
'page': const RcmdPage(tabType: TabType.rcmd),
|
||||
},
|
||||
{
|
||||
'icon': const Icon(
|
||||
Icons.whatshot_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '热门',
|
||||
'type': TabType.hot,
|
||||
'ctr': Get.find<HotController>,
|
||||
'page': const HotPage(),
|
||||
},
|
||||
{
|
||||
'icon': const Icon(
|
||||
Icons.category_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '分区',
|
||||
'type': TabType.rank,
|
||||
'ctr': Get.find<RankController>,
|
||||
'page': const RankPage(),
|
||||
},
|
||||
{
|
||||
'icon': const Icon(
|
||||
Icons.play_circle_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '番剧',
|
||||
'type': TabType.bangumi,
|
||||
'ctr': Get.find<BangumiController>,
|
||||
'page': const BangumiPage(),
|
||||
},
|
||||
];
|
||||
List get 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.category_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '分区',
|
||||
'type': TabType.rank,
|
||||
'ctr': Get.find<RankController>,
|
||||
'page': const RankPage(),
|
||||
},
|
||||
{
|
||||
'icon': const Icon(
|
||||
Icons.play_circle_outlined,
|
||||
size: 15,
|
||||
),
|
||||
'label': '番剧',
|
||||
'type': TabType.bangumi,
|
||||
'ctr': Get.find<BangumiController>,
|
||||
'page': const BangumiPage(tabType: TabType.bangumi),
|
||||
},
|
||||
{
|
||||
'icon': Icon(
|
||||
MdiIcons.theater,
|
||||
size: 15,
|
||||
),
|
||||
'label': '影视',
|
||||
'type': TabType.cinema,
|
||||
'ctr': Get.find<BangumiController>,
|
||||
'page': const BangumiPage(tabType: TabType.cinema),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -416,6 +416,8 @@ class DynamicMajorModel {
|
||||
this.none,
|
||||
this.type,
|
||||
this.courses,
|
||||
this.common,
|
||||
this.music,
|
||||
});
|
||||
|
||||
DynamicArchiveModel? archive;
|
||||
@@ -431,6 +433,8 @@ class DynamicMajorModel {
|
||||
// MAJOR_TYPE_OPUS 图文/文章
|
||||
String? type;
|
||||
Map? courses;
|
||||
Map? common;
|
||||
Map? music;
|
||||
|
||||
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
|
||||
archive = json['archive'] != null
|
||||
@@ -454,6 +458,8 @@ class DynamicMajorModel {
|
||||
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
|
||||
type = json['type'];
|
||||
courses = json['courses'] ?? {};
|
||||
common = json['common'] ?? {};
|
||||
music = json['music'] ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ class FollowUpModel {
|
||||
this.upList,
|
||||
});
|
||||
|
||||
String? errMsg;
|
||||
LiveUsers? liveUsers;
|
||||
List<UpItem>? upList;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
|
||||
class RecVideoItemAppModel {
|
||||
RecVideoItemAppModel({
|
||||
|
||||
119
lib/models/live/follow.dart
Normal file
119
lib/models/live/follow.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
class LiveFollowingModel {
|
||||
int? count;
|
||||
List<LiveFollowingItemModel>? list;
|
||||
int? liveCount;
|
||||
int? neverLivedCount;
|
||||
List? neverLivedFaces;
|
||||
int? pageSize;
|
||||
String? title;
|
||||
int? totalPage;
|
||||
|
||||
LiveFollowingModel({
|
||||
this.count,
|
||||
this.list,
|
||||
this.liveCount,
|
||||
this.neverLivedCount,
|
||||
this.neverLivedFaces,
|
||||
this.pageSize,
|
||||
this.title,
|
||||
this.totalPage,
|
||||
});
|
||||
|
||||
LiveFollowingModel.fromJson(Map<String, dynamic> json) {
|
||||
count = json['count'];
|
||||
list = (json['list'] as List?)
|
||||
?.map((item) => LiveFollowingItemModel.fromJson(item))
|
||||
.toList() ??
|
||||
<LiveFollowingItemModel>[];
|
||||
liveCount = json['live_count'];
|
||||
neverLivedCount = json['never_lived_count'];
|
||||
neverLivedFaces = json['never_lived_faces'];
|
||||
pageSize = json['pageSize'];
|
||||
title = json['title'];
|
||||
totalPage = json['totalPage'];
|
||||
}
|
||||
}
|
||||
|
||||
class LiveFollowingItemModel {
|
||||
int? roomId;
|
||||
int? uid;
|
||||
String? uname;
|
||||
String? title;
|
||||
String? face;
|
||||
int? liveStatus;
|
||||
int? recordNum;
|
||||
String? recentRecordId;
|
||||
int? isAttention;
|
||||
int? clipNum;
|
||||
int? fansNum;
|
||||
String? areaName;
|
||||
String? areaValue;
|
||||
String? tags;
|
||||
String? recentRecordIdV2;
|
||||
int? recordNumV2;
|
||||
int? recordLiveTime;
|
||||
String? areaNameV2;
|
||||
String? roomNews;
|
||||
String? watchIcon;
|
||||
String? textSmall;
|
||||
String? roomCover;
|
||||
String? pic;
|
||||
int? parentAreaId;
|
||||
int? areaId;
|
||||
|
||||
LiveFollowingItemModel({
|
||||
this.roomId,
|
||||
this.uid,
|
||||
this.uname,
|
||||
this.title,
|
||||
this.face,
|
||||
this.liveStatus,
|
||||
this.recordNum,
|
||||
this.recentRecordId,
|
||||
this.isAttention,
|
||||
this.clipNum,
|
||||
this.fansNum,
|
||||
this.areaName,
|
||||
this.areaValue,
|
||||
this.tags,
|
||||
this.recentRecordIdV2,
|
||||
this.recordNumV2,
|
||||
this.recordLiveTime,
|
||||
this.areaNameV2,
|
||||
this.roomNews,
|
||||
this.watchIcon,
|
||||
this.textSmall,
|
||||
this.roomCover,
|
||||
this.pic,
|
||||
this.parentAreaId,
|
||||
this.areaId,
|
||||
});
|
||||
|
||||
LiveFollowingItemModel.fromJson(Map<String, dynamic> json) {
|
||||
roomId = json['roomid'];
|
||||
uid = json['uid'];
|
||||
uname = json['uname'];
|
||||
title = json['title'];
|
||||
face = json['face'];
|
||||
liveStatus = json['live_status'];
|
||||
recordNum = json['record_num'];
|
||||
recentRecordId = json['recent_record_id'];
|
||||
isAttention = json['is_attention'];
|
||||
clipNum = json['clipnum'];
|
||||
fansNum = json['fans_num'];
|
||||
areaName = json['area_name'];
|
||||
areaValue = json['area_value'];
|
||||
tags = json['tags'];
|
||||
recentRecordIdV2 = json['recent_record_id_v2'];
|
||||
recordNumV2 = json['record_num_v2'];
|
||||
recordLiveTime = json['record_live_time'];
|
||||
areaNameV2 = json['area_name_v2'];
|
||||
roomNews = json['room_news'];
|
||||
watchIcon = json['watch_icon'];
|
||||
textSmall = json['text_small'];
|
||||
roomCover = json['room_cover'];
|
||||
pic = json['room_cover'];
|
||||
parentAreaId = json['parent_area_id'];
|
||||
areaId = json['area_id'];
|
||||
}
|
||||
}
|
||||
43
lib/models/live/quality.dart
Normal file
43
lib/models/live/quality.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
enum LiveQuality {
|
||||
dolby,
|
||||
super4K,
|
||||
origin,
|
||||
veryHigh,
|
||||
bluRay,
|
||||
superHD,
|
||||
smooth,
|
||||
flunt,
|
||||
}
|
||||
|
||||
extension LiveQualityCode on LiveQuality {
|
||||
static final List<int> _codeList = [
|
||||
30000,
|
||||
20000,
|
||||
10000,
|
||||
400,
|
||||
250,
|
||||
150,
|
||||
80,
|
||||
];
|
||||
int get code => _codeList[index];
|
||||
|
||||
static LiveQuality? fromCode(int code) {
|
||||
final index = _codeList.indexOf(code);
|
||||
if (index != -1) {
|
||||
return LiveQuality.values[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoQualityDesc on LiveQuality {
|
||||
static final List<String> _descList = [
|
||||
'杜比',
|
||||
'4K',
|
||||
'原画',
|
||||
'蓝光',
|
||||
'超清',
|
||||
'流畅',
|
||||
];
|
||||
get description => _descList[index];
|
||||
}
|
||||
@@ -27,7 +27,9 @@ class HotVideoItemModel {
|
||||
this.seasontype,
|
||||
this.isOgv,
|
||||
this.rcmdReason,
|
||||
required this.checked,
|
||||
this.checked,
|
||||
this.pgcLabel,
|
||||
this.redirectUrl,
|
||||
});
|
||||
|
||||
int? aid;
|
||||
@@ -55,7 +57,9 @@ class HotVideoItemModel {
|
||||
int? seasontype;
|
||||
bool? isOgv;
|
||||
RcmdReason? rcmdReason;
|
||||
late bool checked;
|
||||
bool? checked;
|
||||
String? pgcLabel;
|
||||
String? redirectUrl;
|
||||
|
||||
HotVideoItemModel.fromJson(Map<String, dynamic> json) {
|
||||
aid = json["aid"];
|
||||
@@ -85,7 +89,8 @@ class HotVideoItemModel {
|
||||
rcmdReason = json['rcmd_reason'] != '' && json['rcmd_reason'] != null
|
||||
? RcmdReason.fromJson(json['rcmd_reason'])
|
||||
: null;
|
||||
checked = false;
|
||||
pgcLabel = json['pgc_label'];
|
||||
redirectUrl = json['redirect_url'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class AccountListModel {
|
||||
mid = json['mid'];
|
||||
name = json['name'] ?? '';
|
||||
sex = json['sex'];
|
||||
face = json['face'];
|
||||
face = json['face'] ?? json['pic_url'];
|
||||
sign = json['sign'];
|
||||
rank = json['rank'];
|
||||
level = json['level'];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPalaX/models/msg/account.dart';
|
||||
import 'package:PiliPlus/models/msg/account.dart';
|
||||
|
||||
class SessionDataModel {
|
||||
SessionDataModel({
|
||||
@@ -47,6 +47,7 @@ class SessionList {
|
||||
this.liveStatus,
|
||||
this.bizMsgUnreadCount,
|
||||
// this.userLabel,
|
||||
this.accountInfo,
|
||||
});
|
||||
|
||||
int? talkerId;
|
||||
@@ -105,6 +106,9 @@ class SessionList {
|
||||
liveStatus = json["live_status"];
|
||||
bizMsgUnreadCount = json["biz_msg_unread_count"];
|
||||
// userLabel = json["user_label"];
|
||||
accountInfo = json['account_info'] == null
|
||||
? null
|
||||
: AccountListModel.fromJson(json['account_info']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPalaX/utils/em.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/em.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
|
||||
class SearchVideoModel {
|
||||
SearchVideoModel({
|
||||
|
||||
@@ -29,16 +29,16 @@ class SearchSuggestItem {
|
||||
String? value;
|
||||
String? term;
|
||||
int? spid;
|
||||
Widget? textRich;
|
||||
dynamic textRich;
|
||||
|
||||
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
|
||||
value = json['value'];
|
||||
term = json['term'];
|
||||
textRich = highlightText(json['name']);
|
||||
textRich = json['name'];
|
||||
}
|
||||
}
|
||||
|
||||
Widget highlightText(String str) {
|
||||
Widget highlightText(BuildContext context, String str) {
|
||||
// 创建正则表达式,匹配 <em class="suggest_high_light">...</em> 格式的文本
|
||||
RegExp regex = RegExp(r'<em class="suggest_high_light">(.*?)<\/em>');
|
||||
|
||||
@@ -72,7 +72,7 @@ Widget highlightText(String str) {
|
||||
text: highlightedText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(Get.context!).colorScheme.primary),
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
));
|
||||
|
||||
// 更新当前索引位置
|
||||
@@ -86,7 +86,7 @@ Widget highlightText(String str) {
|
||||
// 将剩余的普通文本部分添加到 children 列表中
|
||||
children.add(TextSpan(
|
||||
text: remainingText,
|
||||
style: DefaultTextStyle.of(Get.context!).style,
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/models/space/space_tag_bottom.dart';
|
||||
import 'package:PiliPlus/models/space/space_tag_bottom.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'achieve.dart';
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'series.dart';
|
||||
import 'setting.dart';
|
||||
import 'tab.dart';
|
||||
import 'tab2.dart';
|
||||
import 'package:PiliPalaX/models/space_article/data.dart' as space;
|
||||
import 'package:PiliPlus/models/space_article/data.dart' as space;
|
||||
|
||||
part 'data.g.dart';
|
||||
|
||||
@@ -52,6 +52,7 @@ class Data {
|
||||
@JsonKey(name: 'digital_button')
|
||||
dynamic digitalButton;
|
||||
dynamic entry;
|
||||
dynamic live;
|
||||
|
||||
Data({
|
||||
this.relation,
|
||||
@@ -77,6 +78,7 @@ class Data {
|
||||
this.nftFaceButton,
|
||||
this.digitalButton,
|
||||
this.entry,
|
||||
this.live,
|
||||
});
|
||||
|
||||
factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);
|
||||
|
||||
@@ -61,6 +61,7 @@ Data _$DataFromJson(Map<String, dynamic> json) => Data(
|
||||
nftFaceButton: json['nft_face_button'],
|
||||
digitalButton: json['digital_button'],
|
||||
entry: json['entry'],
|
||||
live: json['live'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DataToJson(Data instance) => <String, dynamic>{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:PiliPalaX/models/space_archive/item.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'season.g.dart';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user