Compare commits
181 Commits
1.1.4.8
...
1.1.5-pre6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
646888c06f | ||
|
|
332f6f1bb4 | ||
|
|
aaab5371b2 | ||
|
|
ad931d7ea2 | ||
|
|
377e430d74 | ||
|
|
a797467606 | ||
|
|
5ee83d902d | ||
|
|
27ae296b28 | ||
|
|
e589f27195 | ||
|
|
c89d6a5a59 | ||
|
|
861365930d | ||
|
|
0d4d92a202 | ||
|
|
4c6ad0e385 | ||
|
|
ad45e995e2 | ||
|
|
50a035a479 | ||
|
|
c0dbd6cbb2 | ||
|
|
686af4a330 | ||
|
|
46aad06e34 | ||
|
|
3921b2304d | ||
|
|
bca5b0419c | ||
|
|
9754b061dd | ||
|
|
407b31c5c1 | ||
|
|
37b1228552 | ||
|
|
0acd9ca767 | ||
|
|
8f3c9f029c | ||
|
|
9310732343 | ||
|
|
e767e506f3 | ||
|
|
ef3a612338 | ||
|
|
d66a42a0aa | ||
|
|
0f06de0047 | ||
|
|
963181fef2 | ||
|
|
ffd4f9ee73 | ||
|
|
976622df89 | ||
|
|
13c220338c | ||
|
|
1291dc77c8 | ||
|
|
08e5477e74 | ||
|
|
c4c6a2243e | ||
|
|
58791e3e91 | ||
|
|
d5bb4bc149 | ||
|
|
3d1199363b | ||
|
|
f225fa33e1 | ||
|
|
e85c8b3dde | ||
|
|
737be8dcac | ||
|
|
77dd939172 | ||
|
|
0a5965a423 | ||
|
|
a53be6814c | ||
|
|
415b8e9da3 | ||
|
|
f034c24d13 | ||
|
|
1ac93d6269 | ||
|
|
906c8f7999 | ||
|
|
c904a5ded8 | ||
|
|
0c9486f6b4 | ||
|
|
576740a502 | ||
|
|
b3f9f43b57 | ||
|
|
e7424bcc66 | ||
|
|
209ec70ea9 | ||
|
|
3b4e251034 | ||
|
|
86beb879a2 | ||
|
|
321d434141 | ||
|
|
b9d17e27b1 | ||
|
|
2f6f6da6c0 | ||
|
|
c3d3fa67f7 | ||
|
|
032dfd69be | ||
|
|
e9dc154642 | ||
|
|
b43840b636 | ||
|
|
1a9d8e35ba | ||
|
|
ccb61415f5 | ||
|
|
08944241bb | ||
|
|
63030147ea | ||
|
|
8ff71c44ca | ||
|
|
4eaf16f500 | ||
|
|
1a9c8a62f2 | ||
|
|
4256c2b023 | ||
|
|
bbcf0dec1b | ||
|
|
da52cac2c6 | ||
|
|
e8a32a6149 | ||
|
|
a71a7b66f8 | ||
|
|
9808f50816 | ||
|
|
cf86bb7e13 | ||
|
|
ff065254ae | ||
|
|
39b4c1a59b | ||
|
|
28f10e0a4b | ||
|
|
12c0ed5baf | ||
|
|
23272d285b | ||
|
|
67b4ed65ab | ||
|
|
7524b3d168 | ||
|
|
340a933e70 | ||
|
|
488ca29fc1 | ||
|
|
cc00b2cc39 | ||
|
|
287cea4d6c | ||
|
|
39e556891a | ||
|
|
0ae4157384 | ||
|
|
6e1ceb1277 | ||
|
|
71a170deb5 | ||
|
|
9482a706da | ||
|
|
0804484a49 | ||
|
|
cdb9bb3dbc | ||
|
|
6ca0de96f4 | ||
|
|
d908f58528 | ||
|
|
1368733a24 | ||
|
|
32e71dbf65 | ||
|
|
c9ce1af2c6 | ||
|
|
416f9e6a8d | ||
|
|
3ae3955f53 | ||
|
|
464f008023 | ||
|
|
52498b3e34 | ||
|
|
57c57b02a5 | ||
|
|
b8c6868043 | ||
|
|
8200fbf512 | ||
|
|
8650c96b7b | ||
|
|
15fe7787ba | ||
|
|
d83076cb07 | ||
|
|
8b3b4c28a5 | ||
|
|
740c001e2f | ||
|
|
096b057f81 | ||
|
|
a161fa5e58 | ||
|
|
bebf34db23 | ||
|
|
b95061434a | ||
|
|
f2a05bb970 | ||
|
|
6c361a047b | ||
|
|
3fb9e22378 | ||
|
|
b2fb4c9afe | ||
|
|
0862c0fc87 | ||
|
|
77ec78e3fe | ||
|
|
fb59c208e3 | ||
|
|
112a06f92a | ||
|
|
c10c4a6f89 | ||
|
|
669c807b23 | ||
|
|
c9de79532a | ||
|
|
32ce2b87db | ||
|
|
4cfcf18bc9 | ||
|
|
14ae61f891 | ||
|
|
a2d5ecc51e | ||
|
|
84f972a3ab | ||
|
|
5249ceccdb | ||
|
|
5035495043 | ||
|
|
25483d71e9 | ||
|
|
c3fa976b26 | ||
|
|
43beb518f4 | ||
|
|
11edabb890 | ||
|
|
019cd9fda0 | ||
|
|
9d747c8e2c | ||
|
|
4cf1c25b36 | ||
|
|
6c6ed46aea | ||
|
|
e1473a453e | ||
|
|
9f6ef0281a | ||
|
|
84d5a24bc3 | ||
|
|
ed8c39aa76 | ||
|
|
23d235b8f4 | ||
|
|
8bea09b78a | ||
|
|
897fda875a | ||
|
|
510bfe01be | ||
|
|
f6ca007815 | ||
|
|
35b34cb2d4 | ||
|
|
5197cca69c | ||
|
|
e5f0742bf6 | ||
|
|
88d207cc24 | ||
|
|
931fcb6f8f | ||
|
|
e4a960ecf9 | ||
|
|
e44419e088 | ||
|
|
16f577f3fd | ||
|
|
a65edab7d1 | ||
|
|
c0bbf8400a | ||
|
|
1dc2da68ac | ||
|
|
3d49529272 | ||
|
|
41768656b4 | ||
|
|
c7e7b3f9c5 | ||
|
|
e0b0a98f0f | ||
|
|
ca0eb1716f | ||
|
|
06d8296939 | ||
|
|
322885f284 | ||
|
|
4553b86cb4 | ||
|
|
904756b6ea | ||
|
|
2bfa1bb6c2 | ||
|
|
8439a3d85c | ||
|
|
454d6b9de1 | ||
|
|
44c7c44a27 | ||
|
|
40e5e2f372 | ||
|
|
138739781c | ||
|
|
355d897ef0 | ||
|
|
a06aef2b25 |
8
.github/ISSUE_TEMPLATE/bug-反馈.yml
vendored
@@ -9,10 +9,16 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的 bug report。
|
||||
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同问题
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
- label: 已排除网络问题
|
||||
required: true
|
||||
- label: 已排除账号问题
|
||||
required: true
|
||||
- label: 已排除设置问题
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: assign
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/功能请求.yml
vendored
@@ -9,10 +9,12 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的功能请求。
|
||||
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同功能请求
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
- label: 设置中未搜索到该功能
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: assign
|
||||
|
||||
84
.github/workflows/android.yml
vendored
@@ -1,84 +0,0 @@
|
||||
name: Android Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建Java环境
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
cache: "gradle"
|
||||
cache-dependency-path: |
|
||||
android/*.gradle*
|
||||
android/**/gradle-wrapper.properties
|
||||
|
||||
- name: 安装Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
id: flutter-action
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
|
||||
# - name: 下载项目依赖
|
||||
# run: flutter pub get
|
||||
|
||||
- name: Write key
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
|
||||
echo storeFile='key.jks' >> android/key.properties
|
||||
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
|
||||
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
|
||||
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
|
||||
fi
|
||||
|
||||
- name: flutter build apk
|
||||
run: |
|
||||
dart lib/scripts/build.dart "android"
|
||||
flutter build apk --release --split-per-abi --pub
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-arm64-v8a
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-armeabi-v7a
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-x86_64
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
168
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_android:
|
||||
description: "Build Android"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_ios:
|
||||
description: "Build iOS"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_mac:
|
||||
description: "Build Mac"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_win_x64:
|
||||
description: "Build Win-x64"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_linux_x64:
|
||||
description: "Build Linux-x64"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
android:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
|
||||
name: Release Android
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建Java环境
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
cache: "gradle"
|
||||
cache-dependency-path: |
|
||||
android/*.gradle*
|
||||
android/**/gradle-wrapper.properties
|
||||
|
||||
- name: 安装Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
id: flutter-action
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
- name: Write key
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
|
||||
echo storeFile='key.jks' >> android/key.properties
|
||||
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
|
||||
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
|
||||
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
|
||||
fi
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1 android
|
||||
|
||||
- name: flutter build apk
|
||||
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
|
||||
|
||||
- name: rename
|
||||
run: |
|
||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
||||
mv "$file" "PiliPlus_android_${{ env.version }}_${abi}.apk"
|
||||
done
|
||||
shell: bash
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_android_*.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Android_arm64-v8a
|
||||
path: |
|
||||
PiliPlus_android_*_arm64-v8a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Android_armeabi-v7a
|
||||
path: |
|
||||
PiliPlus_android_*_armeabi-v7a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Android_x86_64
|
||||
path: |
|
||||
PiliPlus_android_*_x86_64.apk
|
||||
|
||||
ios:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
|
||||
uses: ./.github/workflows/ios.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
mac:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_mac == 'true' }}
|
||||
uses: ./.github/workflows/mac.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
win_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
|
||||
uses: ./.github/workflows/win_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
linux_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_x64 == 'true' }}
|
||||
uses: ./.github/workflows/linux_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
41
.github/workflows/ios.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: Build for iOS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: "main"
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-macos-app:
|
||||
@@ -23,7 +18,6 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
@@ -32,16 +26,27 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Build iOS
|
||||
run: |
|
||||
chmod +x lib/scripts/build.dart
|
||||
dart lib/scripts/build.dart
|
||||
flutter build ios --release --no-codesign
|
||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 ios-release-no-sign.ipa Payload/runner.app
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_ios_*.ipa
|
||||
|
||||
- name: Upload ios release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-release
|
||||
path: ios-release-no-sign.ipa
|
||||
name: iOS-release
|
||||
path: PiliPlus_ios_*.ipa
|
||||
|
||||
123
.github/workflows/linux.yml
vendored
@@ -1,123 +0,0 @@
|
||||
name: Build for Linux
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
default: "main"
|
||||
|
||||
jobs:
|
||||
build-linux-app:
|
||||
name: Release Linux
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev
|
||||
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
|
||||
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
|
||||
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
|
||||
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
|
||||
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
|
||||
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
|
||||
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
|
||||
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
|
||||
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
|
||||
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
|
||||
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
|
||||
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
|
||||
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
|
||||
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
|
||||
shell: bash
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: Set and Extract version
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
VERSION=$(cat pubspec.yaml | grep 'version:' | sed 's/version: //g' | tr -d '[:space:]')
|
||||
echo "version=$VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v --pub
|
||||
|
||||
- name: Package .tar.gz
|
||||
run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle .
|
||||
|
||||
- name: Packege deb
|
||||
run: |
|
||||
printf "建立构建目录...\n"
|
||||
mkdir "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
pushd "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
mkdir -p opt/PiliPlus
|
||||
mkdir -p usr/share/applications
|
||||
mkdir -p usr/share/icons/hicolor/512x512/apps
|
||||
|
||||
printf "复制文件...\n"
|
||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||
cp -r ../assets/linux/DEBIAN .
|
||||
cp ../assets/linux/piliplus.desktop usr/share/applications
|
||||
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
printf "修改控制文件...\n"
|
||||
# 替换版本号
|
||||
sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control
|
||||
# 计算安装大小并替换
|
||||
SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}')
|
||||
SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}')))
|
||||
SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}')
|
||||
printf "\t安装大小: %s KB\n" "$SIZE_KB"
|
||||
sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control
|
||||
|
||||
printf "生成并写入 md5sums ...\n"
|
||||
md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums
|
||||
|
||||
printf "设置权限...\n"
|
||||
chmod 0644 DEBIAN/control
|
||||
chmod 0644 DEBIAN/md5sums
|
||||
chmod 0755 DEBIAN/postinst
|
||||
chmod 0755 DEBIAN/postrm
|
||||
chmod 0755 DEBIAN/prerm
|
||||
|
||||
printf "打包 deb 文件...\n"
|
||||
popd
|
||||
dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
printf "完成: PiliPlus_linux_%s_amd64.deb\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Upload linux targz package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_targz_packege
|
||||
path: PiliPlus_linux_*.tar.gz
|
||||
|
||||
- name: Upload linux deb package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_deb_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
260
.github/workflows/linux_x64.yml
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
name: Build for Linux x64
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux-app:
|
||||
name: Release Linux x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev rpm patchelf
|
||||
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
|
||||
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
|
||||
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
|
||||
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
|
||||
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
|
||||
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
|
||||
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
|
||||
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
|
||||
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
|
||||
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
|
||||
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
|
||||
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
|
||||
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
|
||||
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
|
||||
shell: bash
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Package .tar.gz
|
||||
run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle .
|
||||
|
||||
- name: Packege deb
|
||||
run: |
|
||||
printf "建立构建目录...\n"
|
||||
mkdir "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
pushd "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
mkdir -p opt/PiliPlus
|
||||
mkdir -p usr/share/applications
|
||||
mkdir -p usr/share/icons/hicolor/512x512/apps
|
||||
|
||||
printf "复制文件...\n"
|
||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||
cp -r ../assets/linux/DEBIAN .
|
||||
cp ../assets/linux/piliplus.desktop usr/share/applications
|
||||
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
printf "修改控制文件...\n"
|
||||
# 替换版本号
|
||||
sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control
|
||||
# 计算安装大小并替换
|
||||
SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}')
|
||||
SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}')))
|
||||
SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}')
|
||||
printf "\t安装大小: %s KB\n" "$SIZE_KB"
|
||||
sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control
|
||||
|
||||
printf "生成并写入 md5sums ...\n"
|
||||
md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums
|
||||
|
||||
printf "设置权限...\n"
|
||||
chmod 0644 DEBIAN/control
|
||||
chmod 0644 DEBIAN/md5sums
|
||||
chmod 0755 DEBIAN/postinst
|
||||
chmod 0755 DEBIAN/postrm
|
||||
chmod 0755 DEBIAN/prerm
|
||||
|
||||
printf "打包 deb 文件...\n"
|
||||
popd
|
||||
dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
printf "完成: PiliPlus_linux_%s_amd64.deb\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Packege rpm
|
||||
run: |
|
||||
printf "建立 RPM 构建目录...\n"
|
||||
RPM_BUILD_ROOT="$PWD/rpm_build"
|
||||
mkdir -p "$RPM_BUILD_ROOT/BUILD" "$RPM_BUILD_ROOT/RPMS" "$RPM_BUILD_ROOT/SOURCES" "$RPM_BUILD_ROOT/SPECS" "$RPM_BUILD_ROOT/SRPMS"
|
||||
|
||||
printf "准备源码归档(仅包含运行时与元数据)...\n"
|
||||
DATE="$(date '+%a %b %d %Y')"
|
||||
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
||||
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
||||
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
|
||||
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
|
||||
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
|
||||
|
||||
printf "生成 spec 文件...\n"
|
||||
cat > "$RPM_BUILD_ROOT/SPECS/piliplus.spec" <<EOF
|
||||
Name: piliplus
|
||||
Version: ${{ env.version }}
|
||||
Release: 1%{?dist}
|
||||
Summary: PiliPlus Linux Version
|
||||
License: GPL-3.0
|
||||
Source0: piliplus-${{ env.version }}.tar.gz
|
||||
Requires: desktop-file-utils, hicolor-icon-theme
|
||||
|
||||
%description
|
||||
使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
|
||||
%prep
|
||||
%setup -q -n piliplus-${{ env.version }}
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}/opt/PiliPlus
|
||||
cp -r bundle/* %{buildroot}/opt/PiliPlus/
|
||||
|
||||
# 二进制权限与命令行入口
|
||||
chmod 755 %{buildroot}/opt/PiliPlus/piliplus
|
||||
mkdir -p %{buildroot}/usr/bin
|
||||
ln -sf /opt/PiliPlus/piliplus %{buildroot}/usr/bin/piliplus
|
||||
|
||||
# 桌面集成
|
||||
mkdir -p %{buildroot}/usr/share/applications
|
||||
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
|
||||
|
||||
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
|
||||
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%post
|
||||
update-desktop-database -q || true
|
||||
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
|
||||
|
||||
%postun
|
||||
update-desktop-database -q || true
|
||||
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
|
||||
|
||||
%files
|
||||
/opt/PiliPlus
|
||||
/usr/bin/piliplus
|
||||
/usr/share/applications/piliplus.desktop
|
||||
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%changelog
|
||||
* DATE - ${{ env.version }}-1
|
||||
- Initial RPM release
|
||||
EOF
|
||||
|
||||
sed -i "s/DATE/${DATE}/g" "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
|
||||
|
||||
printf "构建 RPM 包...\n"
|
||||
rpmbuild --define "_topdir $RPM_BUILD_ROOT" -bb "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
|
||||
|
||||
printf "移动生成的 RPM...\n"
|
||||
find "$RPM_BUILD_ROOT/RPMS" -name "*.rpm" -exec mv {} "PiliPlus_linux_${{ env.version }}_amd64.rpm" \;
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
printf "下载 appimagetool...\n"
|
||||
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
|
||||
printf "建立 AppDir 目录结构...\n"
|
||||
APPDIR="PiliPlus.AppDir"
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
mkdir -p "$APPDIR/usr/lib"
|
||||
mkdir -p "$APPDIR/usr/share/applications"
|
||||
mkdir -p "$APPDIR/usr/share/icons/hicolor/512x512/apps"
|
||||
|
||||
printf "复制应用文件...\n"
|
||||
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
|
||||
|
||||
printf "复制桌面文件和图标...\n"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$APPDIR/piliplus.png"
|
||||
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
|
||||
|
||||
printf "创建 AppRun 启动脚本...\n"
|
||||
cat > "$APPDIR/AppRun" <<'APPRUN_EOF'
|
||||
#!/bin/bash
|
||||
SELF=$(readlink -f "$0")
|
||||
HERE=${SELF%/*}
|
||||
export PATH="${HERE}/usr/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
|
||||
exec "${HERE}/usr/bin/piliplus" "$@"
|
||||
APPRUN_EOF
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
printf "修改桌面文件中的 Exec 路径...\n"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
|
||||
printf "打包 AppImage...\n"
|
||||
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_linux_*.tar.gz
|
||||
PiliPlus_linux_*.deb
|
||||
PiliPlus_linux_*.rpm
|
||||
PiliPlus_linux_*.AppImage
|
||||
|
||||
- name: Upload linux targz package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_targz_amd64_packege
|
||||
path: PiliPlus_linux_*.tar.gz
|
||||
|
||||
- name: Upload linux deb package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_deb_amd64_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
|
||||
- name: Upload linux rpm package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_rpm_amd64_package
|
||||
path: PiliPlus_linux_*.rpm
|
||||
|
||||
- name: Upload linux AppImage package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux_AppImage_amd64_package
|
||||
path: PiliPlus_linux_*.AppImage
|
||||
41
.github/workflows/mac.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: Build for Mac
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: "main"
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-mac-app:
|
||||
@@ -23,7 +18,6 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
@@ -32,12 +26,12 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Build Mac
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
VERSION=$(cat pubspec.yaml | grep 'version:' | sed 's/version: //g' | tr -d '[:space:]')
|
||||
echo "version=$VERSION" >> $GITHUB_ENV
|
||||
flutter build macos --release
|
||||
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
@@ -48,8 +42,17 @@ jobs:
|
||||
- name: Rename DMG
|
||||
run: mv PiliPlus*.dmg PiliPlus_macos_${{ env.version }}.dmg
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_macos_*.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-release
|
||||
path: PiliPlus*.dmg
|
||||
name: macOS-release
|
||||
path: PiliPlus_macos_*.dmg
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
name: Build for Windows
|
||||
name: Build for Windows x64
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: "main"
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-windows-app:
|
||||
name: Release Windows
|
||||
name: Release Windows x64
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
@@ -40,29 +34,47 @@ jobs:
|
||||
- name: Add Chinese language file for Inno Setup
|
||||
run: |
|
||||
Copy-Item "windows/packaging/exe/ChineseSimplified.isl" "C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl"
|
||||
shell: powershell
|
||||
shell: pwsh
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Build Windows
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
flutter build windows --release
|
||||
fastforge package --platform windows --targets exe
|
||||
fastforge package --platform windows --targets exe --flutter-build-args="dart-define-from-file=pili_release.json"
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
mkdir -p Release/PiliPlus-Win
|
||||
mkdir -p PiliPlus-Win-Setup
|
||||
mv build/windows/x64/runner/Release/* Release/PiliPlus-Win/
|
||||
mv dist/**/*.exe PiliPlus-Win-Setup/
|
||||
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
|
||||
|
||||
- name: Compress
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
run: |
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
|
||||
shell: pwsh
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_windows_*.zip
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-release
|
||||
name: Windows-file-x64-release
|
||||
path: Release
|
||||
|
||||
- name: Upload windows setup release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-setup-release
|
||||
name: Windows-setup-x64-release
|
||||
path: PiliPlus-Win-Setup
|
||||
10
.gitignore
vendored
@@ -137,9 +137,13 @@ app.*.symbols
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
/lib/build_config.dart
|
||||
|
||||
devtools_options.yaml
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
.fvm/
|
||||
|
||||
pili_release.json
|
||||
|
||||
dist
|
||||
|
||||
test.dart
|
||||
37
.vscode/launch.json
vendored
@@ -1,48 +1,25 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug",
|
||||
"name": "PiliPlus",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "Update build_config"
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "Profile",
|
||||
"name": "PiliPlus (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile",
|
||||
"preLaunchTask": "Update build_config"
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "Release",
|
||||
"name": "PiliPlus (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release",
|
||||
"preLaunchTask": "Update build_config"
|
||||
},
|
||||
{
|
||||
"name": "Debug (FVM)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "Update build_config (FVM)"
|
||||
},
|
||||
{
|
||||
"name": "Profile (FVM)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile",
|
||||
"preLaunchTask": "Update build_config (FVM)"
|
||||
},
|
||||
{
|
||||
"name": "Release (FVM)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release",
|
||||
"preLaunchTask": "Update build_config (FVM)"
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
.vscode/tasks.json
vendored
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Update build_config",
|
||||
"command": "dart lib/scripts/build.dart dev",
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Update build_config (FVM)",
|
||||
"command": "fvm dart lib/scripts/build.dart dev",
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
README.md
@@ -43,8 +43,14 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] DLNA 投屏
|
||||
- [x] 离线缓存/播放
|
||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 播放音频
|
||||
- [x] 跳过番剧片头/片尾
|
||||
- [x] 安卓端 `loudnorm` 适配 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] Win/Mac 支持极验、短信登录 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 视频截取 GIF by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 视频截取动图 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] AI 原声翻译
|
||||
- [x] SuperChat
|
||||
- [x] 播放课堂视频
|
||||
@@ -147,7 +153,7 @@
|
||||
- [x] 粉丝、关注用户、拉黑用户查看
|
||||
- [x] 用户主页查看
|
||||
- [x] 关注/取关用户
|
||||
- [ ] 离线缓存
|
||||
- [x] 离线缓存
|
||||
- [x] 稍后再看
|
||||
- [x] 观看记录
|
||||
- [x] 我的收藏
|
||||
|
||||
@@ -13,6 +13,7 @@ analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
- lib/grpc/google/**
|
||||
- lib/common/widgets/flutter/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
|
||||
@@ -210,4 +210,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.piliplus
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.SearchManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
@@ -42,7 +43,10 @@ class MainActivity : AudioServiceActivity() {
|
||||
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
|
||||
|
||||
val intent = Intent().apply {
|
||||
component = ComponentName("icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity")
|
||||
component = ComponentName(
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud",
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
|
||||
)
|
||||
putExtra("action", action)
|
||||
putExtra("oid", oid.toLong())
|
||||
putExtra("type", type)
|
||||
@@ -51,23 +55,27 @@ class MainActivity : AudioServiceActivity() {
|
||||
putExtra("parent", parent.toLong())
|
||||
putExtra("ctime", ctime.toLong())
|
||||
putExtra("comment_text", commentText)
|
||||
if(pictures != null)
|
||||
if (pictures != null)
|
||||
putExtra("pictures", pictures)
|
||||
putExtra("source_id", sourceId)
|
||||
putExtra("uid", uid.toLong())
|
||||
putStringArrayListExtra("cookies", ArrayList(cookies))
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (_: Exception) {}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
"linkVerifySettings" -> {
|
||||
val uri = ("package:" + context.packageName).toUri()
|
||||
try {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
||||
} else {
|
||||
Intent("android.intent.action.MAIN", uri).setClassName("com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity")
|
||||
Intent("android.intent.action.MAIN", uri).setClassName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (_: Throwable) {
|
||||
@@ -75,33 +83,56 @@ class MainActivity : AudioServiceActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
"music" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, title)
|
||||
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
|
||||
call.argument<String?>("artist")?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||
call.argument<String?>("album")?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||
call.argument<String?>("artist")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||
call.argument<String?>("album")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
}
|
||||
try {
|
||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(false)
|
||||
}
|
||||
|
||||
"setPipAutoEnterEnabled" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(call.argument<Boolean>("autoEnable") ?: false)
|
||||
.build()
|
||||
setPictureInPictureParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@@ -124,6 +155,7 @@ class MainActivity : AudioServiceActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
|
||||
super.onDestroy()
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
exitProcess(0)
|
||||
@@ -134,7 +166,10 @@ class MainActivity : AudioServiceActivity() {
|
||||
methodChannel.invokeMethod("onUserLeaveHint", null)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel(
|
||||
flutterEngine!!.dartExecutor.binaryMessenger,
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1.0.0
|
||||
|
||||
### 初始版本
|
||||
+ 直播、推荐、动态功能
|
||||
+ 投稿、番剧播放功能
|
||||
+ 播放器手势支持
|
||||
+ 画质、音质、解码格式支持
|
||||
+ 点赞、投币、收藏功能
|
||||
+ 关注/取关、用户主页功能
|
||||
+ 评论功能
|
||||
+ 历史记录、稍后再看功能
|
||||
@@ -1,7 +0,0 @@
|
||||
## 1.0.1
|
||||
|
||||
### 修复
|
||||
+ 升级播放器依赖
|
||||
+ android平台 AV1格式视频支持
|
||||
+ 视频全屏功能
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
## 1.0.10
|
||||
|
||||
### 修复
|
||||
+ 长按倍速抬起后未恢复默认倍速
|
||||
@@ -1,26 +0,0 @@
|
||||
## 1.0.11
|
||||
|
||||
### 新功能
|
||||
+ 适配了原生媒体通知栏 @Daydreamer-riri
|
||||
+ 视频主题图标 @Daydreamer-riri
|
||||
+ 关闭软件后自动画中画播放
|
||||
+ UP主分组管理
|
||||
+ md2样式底栏
|
||||
+
|
||||
|
||||
|
||||
### 修复
|
||||
+ 历史记录记忆播放
|
||||
+ 部分类型视频连播
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 视频播放速度总是显示1.0X
|
||||
+ 评论页面计数错误
|
||||
+ 退出视频还有声音
|
||||
|
||||
|
||||
### 优化
|
||||
+ 视频加载速度
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1.0.12
|
||||
|
||||
|
||||
### 修复
|
||||
+ iOS端视频播放时没有声音
|
||||
+ 超过6分钟弹幕不显示
|
||||
+ 视频详情页网络异常
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.13
|
||||
|
||||
|
||||
### 新功能
|
||||
+ 视频详情页稍后再看
|
||||
+ 发送弹幕 感谢@orz12
|
||||
+ 消息展示
|
||||
+ up主页显示获赞数
|
||||
+ up主页显示合集
|
||||
+ 视频详情页「ai总结」增加开关
|
||||
|
||||
### 修复
|
||||
+ 首页推荐问题(需要重新登录)
|
||||
+ 长按倍速逻辑
|
||||
+ 视频详情页网络异常
|
||||
|
||||
### 优化
|
||||
+ 设置面板样式 感谢@GuMengYu @KoolShow
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,28 +0,0 @@
|
||||
## 1.0.14
|
||||
|
||||
圣诞节快乐~ 🎉
|
||||
|
||||
大部分内容由@orz12提供,感谢👏
|
||||
|
||||
### 修复
|
||||
+ 全屏弹幕消失
|
||||
+ iOS全屏/退出全屏视频暂停
|
||||
+ 个人主页关注状态
|
||||
+ 视频合集向下滑动UI问题
|
||||
+ 媒体库滑动底栏不隐藏
|
||||
+ 个人主页动态加载问题 * 2
|
||||
+ 未登录状态访问个人主页异常
|
||||
+ 视频搜索标题特殊字符转义
|
||||
+ iOS闪退
|
||||
+ 消息页面夜间模式异常
|
||||
+ 消息页面含有撤回消息时异常
|
||||
+ 弹幕速度
|
||||
|
||||
### 优化
|
||||
+ 全屏播放方案优化
|
||||
+ 弹幕加载逻辑优化
|
||||
+ 点赞、投币逻辑优化
|
||||
+ 进度条及播放时间渲染优化
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.15
|
||||
|
||||
元旦快乐~ 🎉
|
||||
|
||||
### 功能
|
||||
+ 转发动态评论展示
|
||||
+ 推荐、最热、收藏视频增肌日期显示
|
||||
|
||||
### 修复
|
||||
+ 全屏播放相关问题
|
||||
+ 评论区@用户展示问题
|
||||
+ 登录状态闪退问题
|
||||
+ pip意外触发问题
|
||||
+ 动态页tab切换样式问题
|
||||
|
||||
### 优化
|
||||
+ 首页默认使用web端推荐
|
||||
+ 取消iOS路由切换效果
|
||||
+ 视频分享中添加Up主
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,15 +0,0 @@
|
||||
## 1.0.16
|
||||
|
||||
|
||||
### 功能
|
||||
+ toast 背景支持透明度调节
|
||||
|
||||
### 修复
|
||||
+ web端推荐未展示【已关注】
|
||||
+ up主动态页异常
|
||||
+ 未打开自动播放时,视频详情页异常
|
||||
+ 视频暂停状态取消自动ip
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,39 +0,0 @@
|
||||
## 1.0.17
|
||||
|
||||
|
||||
### 功能
|
||||
+ 视频全屏时隐藏进度条
|
||||
+ 动态内容增加投稿跳转
|
||||
+ 未开启自动播放时点击封面播放
|
||||
+ 弹幕发送标识
|
||||
+ 定时关闭
|
||||
+ 推荐视频卡片拉黑up功能
|
||||
+ 首页tabbar编辑排序
|
||||
|
||||
### 修复
|
||||
+ 连续跳转搜索页未刷新
|
||||
+ 搜索结果为空时页面异常
|
||||
+ 评论区链接解析
|
||||
+ 视频全屏状态栏背景色
|
||||
+ 私信对话气泡位置
|
||||
+ 设置up关注分组样式
|
||||
+ 每次推荐请求数据相同
|
||||
+ iOS代理网络异常
|
||||
+ 双击切换播放状态无声
|
||||
+ 设置自定义倍速白屏
|
||||
+ 免登录查看1080p
|
||||
|
||||
### 优化
|
||||
+ 首页web端推荐观看数展示
|
||||
+ 首页web端推荐接口更新
|
||||
+ 首页样式
|
||||
+ 搜索页跳转
|
||||
+ 弹幕资源优化
|
||||
+ 图片渲染占用内存优化(部分)
|
||||
+ 两次返回退出应用
|
||||
+ schame 补充
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,16 +0,0 @@
|
||||
## 1.0.18
|
||||
|
||||
|
||||
### 功能
|
||||
|
||||
|
||||
### 修复
|
||||
|
||||
|
||||
### 优化
|
||||
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,15 +0,0 @@
|
||||
## 1.0.19
|
||||
|
||||
|
||||
### 修复
|
||||
+ 视频404、评论加载错误
|
||||
+ bvav转换
|
||||
|
||||
### 优化
|
||||
+ 视频详情页内存占用
|
||||
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1.0.2
|
||||
|
||||
### 新功能
|
||||
+ 自动检查更新
|
||||
+ 封面图片保存
|
||||
+ 动态跳转番剧
|
||||
+ 历史记录番剧记忆播放
|
||||
+ 一键清空稍后再看
|
||||
|
||||
### 修复
|
||||
+ 切换分P cid未切换
|
||||
+ cookie存储问题
|
||||
+ 登录/退出登录问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 退出登录提示
|
||||
+ 请求节流
|
||||
+ 全屏播放
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1.0.3
|
||||
|
||||
建议卸载1.0.2版本,重新安装
|
||||
### 新功能
|
||||
+ 底部播放进度条设置
|
||||
+ 复制图片链接
|
||||
|
||||
|
||||
### 修复
|
||||
+ 用户数据格式修改
|
||||
+ video Fit
|
||||
+ 没有audio 资源的视频异常
|
||||
+ 评论区域图片无法点击
|
||||
+ 视频进度条拖动问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 部分页面样式
|
||||
+ 图片预览页面样式
|
||||
@@ -1,21 +0,0 @@
|
||||
## 1.0.4
|
||||
|
||||
### 新功能
|
||||
+ 热搜刷新
|
||||
+ 视频搜索排序、筛选
|
||||
+ app字体大小自定义
|
||||
+ app主题色自定义
|
||||
+ 「课堂」类动态渲染
|
||||
|
||||
|
||||
### 修复
|
||||
+ 搜索词联想richText渲染异常
|
||||
+ 部分动态点赞异常
|
||||
+ 默认视频解码格式
|
||||
+ 搜索页面返回搜索词未清空
|
||||
+ 动态详情评论加载异常
|
||||
+ 动态页面下拉刷新数据异常
|
||||
|
||||
### 优化
|
||||
+ 一些样式修改
|
||||
+ 取消热搜词缓存
|
||||
@@ -1,30 +0,0 @@
|
||||
## 1.0.5
|
||||
|
||||
主要是bug修复跟一部分小功能,弹幕功能需要下一版。
|
||||
问题反馈请前往QQ频道或提交issues。
|
||||
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
|
||||
|
||||
### 新功能
|
||||
+ 高帧率支持
|
||||
+ 默认评论排序设置
|
||||
+ 默认动态类别设置
|
||||
+ 动态合集查看
|
||||
+ 同时观看人数
|
||||
+ iOS路由切换效果
|
||||
|
||||
|
||||
### 修复
|
||||
+ 收藏夹翻页
|
||||
+ 首页搜索框频繁点击消失
|
||||
+ 评论排序切换空白
|
||||
+ 快速返回首页
|
||||
+ 重复进入个人中心页面数据未刷新
|
||||
+ 动态goods数据异常
|
||||
+ 大会员切换番剧
|
||||
+ 高画质codes匹配
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择
|
||||
+ 播放器亮度记忆
|
||||
+ 下载对应版本apk
|
||||
@@ -1,34 +0,0 @@
|
||||
## 1.0.6
|
||||
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
|
||||
### 新功能
|
||||
+ 首页单列布局
|
||||
+ 首页推荐展示播放量、弹幕数
|
||||
+ 简单弹幕功能实现(持续开发中...)
|
||||
+ 评论区搜索关键词开关 issues#46
|
||||
+ 热搜榜隐藏功能 issues#35
|
||||
+ 自动全屏 issues#37
|
||||
+ 快速收藏功能
|
||||
+ 双击快进/快退开关
|
||||
+ 评论链接跳转视频
|
||||
+ 支持移除单个稍后再看
|
||||
+ app scheme外链跳转
|
||||
|
||||
|
||||
### 修复
|
||||
+ 杜比、无损音频切换
|
||||
+ 收藏夹展示 issues#42
|
||||
+ 搜索建议次 issues#47
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择优化
|
||||
+ 导航条沉浸
|
||||
+ 取消Hero动画
|
||||
+ 视频锁定逻辑
|
||||
+ 登录逻辑优化
|
||||
+ 图片预览样式
|
||||
+ +评论区用户点击范围
|
||||
+ 关注、粉丝页面优化
|
||||
+ 关闭自动播放时播放器初始化逻辑
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.7
|
||||
|
||||
默认倍速、直播弹幕、专栏等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 弹幕设置、屏蔽功能
|
||||
+ 不是很完美的后台播放功能
|
||||
+ 不是很完美的画中画(pip)功能(Android端)
|
||||
|
||||
### 修复
|
||||
+ 动态页面加载异常
|
||||
+ 网络异常时页面空白
|
||||
+ 竖屏全屏状态栏问题
|
||||
+ iOS端代理请求异常
|
||||
|
||||
### 优化
|
||||
+ 图片预览
|
||||
+ 全屏播放时自动旋转
|
||||
+ 转发内容增加视频标题
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,24 +0,0 @@
|
||||
## 1.0.8
|
||||
|
||||
直播弹幕、循环播放等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 用户拉黑功能
|
||||
+ gif图片保存
|
||||
+ 删除已看历史记录
|
||||
|
||||
### 修复
|
||||
+ 弹幕数量较少
|
||||
+ 弹幕屏蔽设置自动记忆
|
||||
+ 动态页面渲染
|
||||
+ 用户主页数据错乱
|
||||
+ 大家都在搜空白
|
||||
+ 默认自动全屏,顶部操作栏丢失
|
||||
|
||||
|
||||
### 优化
|
||||
+ 全屏状态栏区域显示优化
|
||||
+ 图片保存至PiliPala文件夹
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,28 +0,0 @@
|
||||
## 1.0.9
|
||||
|
||||
|
||||
### 新功能
|
||||
+ 自定义倍速、默认倍速
|
||||
+ 历史记录搜索
|
||||
+ 收藏夹搜索
|
||||
+ 历史记录多选删除
|
||||
+ 视频循环播放
|
||||
+ 免登录看1080P
|
||||
+ 评论区视频链接跳转
|
||||
+ up主分组
|
||||
+ up主投稿搜索
|
||||
|
||||
### 修复
|
||||
+ 搜索视频标题乱码
|
||||
+ 屏幕帧率
|
||||
+ 动态页面渲染
|
||||
|
||||
|
||||
|
||||
### 优化
|
||||
+ 快进手势
|
||||
+ 视频简介链接匹配
|
||||
+ 视频全屏时安全区域
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,10 +0,0 @@
|
||||
PiliPlus is a third-party Bilibili client developed in Flutter,
|
||||
fork from PiliPalaX (https://github.com/orz12/PiliPalaX).
|
||||
|
||||
Top Features:
|
||||
|
||||
* List of recommended videos
|
||||
* List of hottest videos
|
||||
* Popular live streams
|
||||
* List of bangumis
|
||||
* Block videos from blacklisted users
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
||||
A third-party Bilibili client developed in Flutter
|
||||
@@ -1 +0,0 @@
|
||||
PiliPlus
|
||||
@@ -1,10 +0,0 @@
|
||||
PiliPlus 是使用 Flutter 开发的 BiliBili 第三方客户端,
|
||||
是由PiliPalaX仓库fork并进行了差异化开发的版本
|
||||
|
||||
主要功能:
|
||||
|
||||
* 推荐视频列表
|
||||
* 最热视频列表
|
||||
* 热门直播
|
||||
* 番剧列表
|
||||
* 屏蔽黑名单内用户视频
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
||||
使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
@@ -1 +0,0 @@
|
||||
PiliPlus
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,7 +4,4 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
16
lib/build_config.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
class BuildConfig {
|
||||
static const int versionCode = int.fromEnvironment(
|
||||
'pili.code',
|
||||
defaultValue: 1,
|
||||
);
|
||||
static const String versionName = String.fromEnvironment(
|
||||
'pili.name',
|
||||
defaultValue: 'SNAPSHOT',
|
||||
);
|
||||
|
||||
static const int buildTime = int.fromEnvironment('pili.time');
|
||||
static const String commitHash = String.fromEnvironment(
|
||||
'pili.hash',
|
||||
defaultValue: 'N/A',
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StyleString {
|
||||
@@ -43,7 +42,7 @@ class Constants {
|
||||
static const baseHeaders = {
|
||||
'connection': 'keep-alive',
|
||||
'accept-encoding': 'br,gzip',
|
||||
'referer': HttpString.baseUrl,
|
||||
// 'referer': HttpString.baseUrl,
|
||||
'env': 'prod',
|
||||
'app-key': 'android64',
|
||||
'x-bili-aurora-zone': 'sh001',
|
||||
|
||||
@@ -1,50 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget iconButton({
|
||||
required BuildContext context,
|
||||
BuildContext? context,
|
||||
String? tooltip,
|
||||
required IconData icon,
|
||||
required Widget icon,
|
||||
required VoidCallback? onPressed,
|
||||
double size = 36,
|
||||
double? iconSize,
|
||||
Color? bgColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
late final theme = Theme.of(context);
|
||||
Color? backgroundColor = bgColor;
|
||||
Color? foregroundColor = iconColor;
|
||||
if (context != null) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
backgroundColor = colorScheme.secondaryContainer;
|
||||
foregroundColor = colorScheme.onSecondaryContainer;
|
||||
}
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: IconButton(
|
||||
icon: icon,
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: iconSize ?? size / 2,
|
||||
color: iconColor ?? theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: bgColor ?? theme.colorScheme.secondaryContainer,
|
||||
iconSize: iconSize ?? size / 2,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget mediumButton({
|
||||
String? tooltip,
|
||||
IconData? icon,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(icon),
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
34
lib/common/widgets/button/more_btn.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget moreTextButton({
|
||||
String text = '查看更多',
|
||||
required VoidCallback onTap,
|
||||
EdgeInsets? padding,
|
||||
Color? color,
|
||||
}) {
|
||||
Widget child = Text.rich(
|
||||
style: TextStyle(color: color, height: 1),
|
||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: text),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
size: 22,
|
||||
color: color,
|
||||
Icons.keyboard_arrow_right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (padding != null) {
|
||||
child = Padding(padding: padding, child: child);
|
||||
}
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -10,19 +10,24 @@ class CustomIcons {
|
||||
static const IconData dyn = _CustomIconData(0xe804);
|
||||
static const IconData fav = _CustomIconData(0xe805);
|
||||
static const IconData live_reserve = _CustomIconData(0xe806);
|
||||
static const IconData share = _CustomIconData(0xe807);
|
||||
static const IconData share_line = _CustomIconData(0xe808);
|
||||
static const IconData share_node = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80a);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe811);
|
||||
static const IconData topic_tag = _CustomIconData(0xe812);
|
||||
static const IconData watch_later = _CustomIconData(0xe813);
|
||||
static const IconData player_dm_tip_back = _CustomIconData(0xe807);
|
||||
static const IconData player_dm_tip_copy = _CustomIconData(0xe808);
|
||||
static const IconData player_dm_tip_like = _CustomIconData(0xe809);
|
||||
static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a);
|
||||
static const IconData player_dm_tip_recall = _CustomIconData(0xe80b);
|
||||
static const IconData share = _CustomIconData(0xe80c);
|
||||
static const IconData share_line = _CustomIconData(0xe80d);
|
||||
static const IconData share_node = _CustomIconData(0xe80e);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80f);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe811);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe812);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe813);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe814);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe815);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe816);
|
||||
static const IconData topic_tag = _CustomIconData(0xe817);
|
||||
static const IconData watch_later = _CustomIconData(0xe818);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverPersistentHeaderDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
CustomSliverPersistentHeaderDelegate({
|
||||
const CustomSliverPersistentHeaderDelegate({
|
||||
required this.child,
|
||||
required this.bgColor,
|
||||
double extent = 45,
|
||||
|
||||
@@ -7,6 +7,7 @@ Future<void> showConfirmDialog({
|
||||
dynamic content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
assert(content is String? || content is Widget);
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/utils/extension.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';
|
||||
@@ -124,11 +125,12 @@ Future<void> autoWrapReportDialog(
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
SmartDialog.showToast(data['message']);
|
||||
SmartDialog.showToast(data['message'].toString());
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
@@ -231,4 +233,34 @@ class ReportOptions {
|
||||
0: '其他',
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get danmakuReport => const {
|
||||
'': {
|
||||
1: '违法违禁',
|
||||
2: '色情低俗',
|
||||
3: '赌博诈骗',
|
||||
4: '人身攻击',
|
||||
5: '侵犯隐私',
|
||||
6: '垃圾广告',
|
||||
7: '引战',
|
||||
8: '剧透',
|
||||
9: '恶意刷屏',
|
||||
10: '视频无关',
|
||||
12: '青少年不良信息',
|
||||
13: '违法信息外链',
|
||||
0: '其它', // 11
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get liveDanmakuReport => const {
|
||||
'': {
|
||||
1: '违法违规',
|
||||
2: '低俗色情',
|
||||
3: '垃圾广告',
|
||||
4: '辱骂引战',
|
||||
5: '政治敏感',
|
||||
6: '青少年不良信息',
|
||||
7: '其他', // avoid show form
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/ink_well.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -12,7 +12,7 @@ library;
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/button.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
@@ -316,6 +316,7 @@ class ListTile extends StatelessWidget {
|
||||
/// Requires one of its ancestors to be a [Material] widget.
|
||||
const ListTile({
|
||||
super.key,
|
||||
this.safeArea = false,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
@@ -335,6 +336,7 @@ class ListTile extends StatelessWidget {
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onSecondaryTap,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected = false,
|
||||
@@ -355,6 +357,8 @@ class ListTile extends StatelessWidget {
|
||||
this.statesController,
|
||||
}) : assert(isThreeLine != true || subtitle != null);
|
||||
|
||||
final bool safeArea;
|
||||
|
||||
/// A widget to display before the title.
|
||||
///
|
||||
/// Typically an [Icon] or a [CircleAvatar] widget.
|
||||
@@ -563,6 +567,8 @@ class ListTile extends StatelessWidget {
|
||||
/// Inoperative if [enabled] is false.
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
@@ -922,10 +928,64 @@ class ListTile extends StatelessWidget {
|
||||
? ListTileTitleAlignment.threeLine
|
||||
: ListTileTitleAlignment.titleHeight);
|
||||
|
||||
Widget child = IconTheme.merge(
|
||||
data: iconThemeData,
|
||||
child: IconButtonTheme(
|
||||
data: iconButtonThemeData,
|
||||
child: _ListTile(
|
||||
leading: leadingIcon,
|
||||
title: titleText,
|
||||
subtitle: subtitleText,
|
||||
trailing: trailingIcon,
|
||||
isDense: _isDenseLayout(theme, tileTheme),
|
||||
visualDensity:
|
||||
visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
|
||||
isThreeLine:
|
||||
isThreeLine ??
|
||||
tileTheme.isThreeLine ??
|
||||
theme.listTileTheme.isThreeLine ??
|
||||
false,
|
||||
textDirection: textDirection,
|
||||
titleBaselineType:
|
||||
titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!,
|
||||
subtitleBaselineType:
|
||||
subtitleStyle?.textBaseline ??
|
||||
defaults.subtitleTextStyle!.textBaseline!,
|
||||
horizontalTitleGap:
|
||||
horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
|
||||
minVerticalPadding:
|
||||
minVerticalPadding ??
|
||||
tileTheme.minVerticalPadding ??
|
||||
defaults.minVerticalPadding!,
|
||||
minLeadingWidth:
|
||||
minLeadingWidth ??
|
||||
tileTheme.minLeadingWidth ??
|
||||
defaults.minLeadingWidth!,
|
||||
minTileHeight: minTileHeight ?? tileTheme.minTileHeight,
|
||||
titleAlignment: effectiveTitleAlignment,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (safeArea) {
|
||||
child = SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: resolvedContentPadding,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
child = Padding(
|
||||
padding: resolvedContentPadding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
customBorder: shape ?? tileTheme.shape,
|
||||
onTap: enabled ? onTap : null,
|
||||
onLongPress: enabled ? onLongPress : null,
|
||||
onSecondaryTap: enabled ? onSecondaryTap : null,
|
||||
onFocusChange: onFocusChange,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
canRequestFocus: enabled,
|
||||
@@ -947,50 +1007,7 @@ class ListTile extends StatelessWidget {
|
||||
shape: shape ?? tileTheme.shape ?? const Border(),
|
||||
color: _tileBackgroundColor(theme, tileTheme, defaults),
|
||||
),
|
||||
child: Padding(
|
||||
padding: resolvedContentPadding,
|
||||
child: IconTheme.merge(
|
||||
data: iconThemeData,
|
||||
child: IconButtonTheme(
|
||||
data: iconButtonThemeData,
|
||||
child: _ListTile(
|
||||
leading: leadingIcon,
|
||||
title: titleText,
|
||||
subtitle: subtitleText,
|
||||
trailing: trailingIcon,
|
||||
isDense: _isDenseLayout(theme, tileTheme),
|
||||
visualDensity:
|
||||
visualDensity ??
|
||||
tileTheme.visualDensity ??
|
||||
theme.visualDensity,
|
||||
isThreeLine:
|
||||
isThreeLine ??
|
||||
tileTheme.isThreeLine ??
|
||||
theme.listTileTheme.isThreeLine ??
|
||||
false,
|
||||
textDirection: textDirection,
|
||||
titleBaselineType:
|
||||
titleStyle.textBaseline ??
|
||||
defaults.titleTextStyle!.textBaseline!,
|
||||
subtitleBaselineType:
|
||||
subtitleStyle?.textBaseline ??
|
||||
defaults.subtitleTextStyle!.textBaseline!,
|
||||
horizontalTitleGap:
|
||||
horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
|
||||
minVerticalPadding:
|
||||
minVerticalPadding ??
|
||||
tileTheme.minVerticalPadding ??
|
||||
defaults.minVerticalPadding!,
|
||||
minLeadingWidth:
|
||||
minLeadingWidth ??
|
||||
tileTheme.minLeadingWidth ??
|
||||
defaults.minLeadingWidth!,
|
||||
minTileHeight: minTileHeight ?? tileTheme.minTileHeight,
|
||||
titleAlignment: effectiveTitleAlignment,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -10,7 +10,7 @@
|
||||
/// @docImport 'text.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/scrollable.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -664,10 +664,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
vsync: this,
|
||||
reverseDuration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_anim = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0, 1),
|
||||
).animate(_animController);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -786,7 +782,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
bool? _isSliding;
|
||||
|
||||
late AnimationController _animController;
|
||||
late Animation<Offset> _anim;
|
||||
|
||||
@override
|
||||
@protected
|
||||
@@ -1185,8 +1180,15 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_maxWidth = constraints.maxWidth;
|
||||
return SlideTransition(
|
||||
position: _anim,
|
||||
return AnimatedBuilder(
|
||||
animation: _animController,
|
||||
builder: (context, child) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
heightFactor: 1 - _animController.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
color: widget.bgColor,
|
||||
child: widget.header != null
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/page_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
@@ -78,6 +78,7 @@ class RenderParagraph extends RenderBox
|
||||
Color? selectionColor,
|
||||
SelectionRegistrar? registrar,
|
||||
required Color primary,
|
||||
VoidCallback? onShowMore,
|
||||
}) : assert(text.debugAssertIsValid()),
|
||||
assert(
|
||||
maxLines == null ||
|
||||
@@ -93,6 +94,7 @@ class RenderParagraph extends RenderBox
|
||||
_softWrap = softWrap,
|
||||
_overflow = overflow,
|
||||
_selectionColor = selectionColor,
|
||||
_onShowMore = onShowMore,
|
||||
_textPainter = TextPainter(
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
@@ -294,6 +296,8 @@ class RenderParagraph extends RenderBox
|
||||
_disposeSelectableFragments();
|
||||
_textPainter.dispose();
|
||||
_textIntrinsicsCache?.dispose();
|
||||
_tapGestureRecognizer?.dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
_morePainter?.dispose();
|
||||
_morePainter = null;
|
||||
super.dispose();
|
||||
@@ -553,6 +557,18 @@ class RenderParagraph extends RenderBox
|
||||
@override
|
||||
@protected
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
if (_tapGestureRecognizer != null) {
|
||||
if (_morePainter case final textPainter?) {
|
||||
late final height = _textPainter.height;
|
||||
if (position.dx < textPainter.width &&
|
||||
position.dy > height &&
|
||||
position.dy < height + textPainter.height) {
|
||||
result.add(HitTestEntry(_moreTextSpan));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position);
|
||||
// The hit-test can't fall through the horizontal gaps between visually
|
||||
// adjacent characters on the same line, even with a large letter-spacing or
|
||||
@@ -680,9 +696,20 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
}
|
||||
|
||||
VoidCallback? _onShowMore;
|
||||
set onShowMore(VoidCallback? onShowMore) {
|
||||
if (_onShowMore != onShowMore) {
|
||||
_onShowMore = onShowMore;
|
||||
_tapGestureRecognizer?.onTap = onShowMore;
|
||||
}
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
TextSpan get _moreTextSpan => TextSpan(
|
||||
style: text.style!.copyWith(color: _primary),
|
||||
text: '查看更多',
|
||||
recognizer: _tapGestureRecognizer,
|
||||
);
|
||||
TextPainter? _morePainter;
|
||||
|
||||
@@ -709,6 +736,9 @@ class RenderParagraph extends RenderBox
|
||||
size.height < textSize.height || _textPainter.didExceedMaxLines;
|
||||
|
||||
if (didOverflowHeight) {
|
||||
if (_onShowMore != null) {
|
||||
_tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore;
|
||||
}
|
||||
_morePainter ??= TextPainter(
|
||||
text: _moreTextSpan,
|
||||
textDirection: textDirection,
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
|
||||
@@ -114,6 +114,8 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
this.textHeightBehavior,
|
||||
this.selectionRegistrar,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
}) : assert(maxLines == null || maxLines > 0),
|
||||
assert(selectionRegistrar == null || selectionColor != null),
|
||||
assert(
|
||||
@@ -228,6 +230,10 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
/// widgets.
|
||||
final Color? selectionColor;
|
||||
|
||||
final Color primary;
|
||||
|
||||
final VoidCallback? onShowMore;
|
||||
|
||||
@override
|
||||
RenderParagraph createRenderObject(BuildContext context) {
|
||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||
@@ -245,7 +251,8 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
locale: locale ?? Localizations.maybeLocaleOf(context),
|
||||
registrar: selectionRegistrar,
|
||||
selectionColor: selectionColor,
|
||||
primary: Theme.of(context).colorScheme.primary,
|
||||
primary: primary,
|
||||
onShowMore: onShowMore,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,7 +273,8 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||
..registrar = selectionRegistrar
|
||||
..selectionColor = selectionColor
|
||||
..primary = Theme.of(context).colorScheme.primary;
|
||||
..primary = primary
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -17,8 +17,8 @@ library;
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/text/rich_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide RichText;
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
@@ -174,6 +174,8 @@ class Text extends StatelessWidget {
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
}) : textSpan = null,
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
@@ -211,6 +213,8 @@ class Text extends StatelessWidget {
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
}) : data = null,
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
@@ -349,6 +353,10 @@ class Text extends StatelessWidget {
|
||||
/// (semi-transparent grey).
|
||||
final Color? selectionColor;
|
||||
|
||||
final Color primary;
|
||||
|
||||
final VoidCallback? onShowMore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||
@@ -404,6 +412,7 @@ class Text extends StatelessWidget {
|
||||
text: data,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
primary: primary,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -435,6 +444,8 @@ class Text extends StatelessWidget {
|
||||
text: data,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
onShowMore: onShowMore,
|
||||
primary: primary,
|
||||
);
|
||||
}
|
||||
if (semanticsLabel != null || semanticsIdentifier != null) {
|
||||
@@ -532,6 +543,7 @@ class _SelectableTextContainer extends StatefulWidget {
|
||||
required this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
required this.selectionColor,
|
||||
required this.primary,
|
||||
});
|
||||
|
||||
final TextSpan text;
|
||||
@@ -546,6 +558,7 @@ class _SelectableTextContainer extends StatefulWidget {
|
||||
final TextWidthBasis textWidthBasis;
|
||||
final ui.TextHeightBehavior? textHeightBehavior;
|
||||
final Color selectionColor;
|
||||
final Color primary;
|
||||
|
||||
@override
|
||||
State<_SelectableTextContainer> createState() =>
|
||||
@@ -588,6 +601,7 @@ class _SelectableTextContainerState extends State<_SelectableTextContainer> {
|
||||
textHeightBehavior: widget.textHeightBehavior,
|
||||
selectionColor: widget.selectionColor,
|
||||
text: widget.text,
|
||||
primary: widget.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -608,6 +622,7 @@ class _RichText extends StatelessWidget {
|
||||
required this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
required this.selectionColor,
|
||||
required this.primary,
|
||||
});
|
||||
|
||||
final GlobalKey? textKey;
|
||||
@@ -623,6 +638,7 @@ class _RichText extends StatelessWidget {
|
||||
final TextWidthBasis textWidthBasis;
|
||||
final ui.TextHeightBehavior? textHeightBehavior;
|
||||
final Color selectionColor;
|
||||
final Color primary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -642,6 +658,7 @@ class _RichText extends StatelessWidget {
|
||||
selectionRegistrar: registrar,
|
||||
selectionColor: selectionColor,
|
||||
text: text,
|
||||
primary: primary,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
/// @docImport 'text_field.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
@@ -7,13 +7,13 @@ library;
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart';
|
||||
import 'package:flutter/cupertino.dart'
|
||||
hide
|
||||
SpellCheckConfiguration,
|
||||
@@ -16,7 +16,7 @@ import 'dart:ui'
|
||||
SemanticsInputType,
|
||||
TextBox;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -22,10 +22,10 @@ import 'dart:math' as math;
|
||||
import 'dart:ui' as ui hide TextStyle;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
@@ -12,7 +12,7 @@ import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart'
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'
|
||||
show EditableTextContextMenuBuilder;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -13,15 +13,15 @@ library;
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart';
|
||||
import 'package:flutter/cupertino.dart'
|
||||
hide
|
||||
EditableText,
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' show kMinInteractiveDimension;
|
||||
174
lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
ImmediateTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
this.onTapDown,
|
||||
required this.onTapUp,
|
||||
this.onTapCancel,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
GestureTapDownCallback? onTapDown;
|
||||
|
||||
final GestureTapUpCallback onTapUp;
|
||||
|
||||
final GestureTapCancelCallback? onTapCancel;
|
||||
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
PointerUpEvent? _up;
|
||||
int _activePointer = 0;
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArena = false;
|
||||
|
||||
@override
|
||||
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => false;
|
||||
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) =>
|
||||
_activePointer == 0 && super.isPointerAllowed(event);
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
_reset(event.pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (event.pointer != _activePointer) {
|
||||
stopTrackingPointer(event.pointer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event is PointerDownEvent) {
|
||||
_handleTapDown(event);
|
||||
} else if (event is PointerMoveEvent) {
|
||||
_handlePointerMove(event);
|
||||
} else if (event is PointerUpEvent) {
|
||||
_up = event;
|
||||
_handlePointerUp(event);
|
||||
}
|
||||
|
||||
stopTrackingIfPointerNoLongerDown(event);
|
||||
}
|
||||
|
||||
void _handleTapDown(PointerDownEvent event) {
|
||||
if (_sentTapDown) return;
|
||||
|
||||
if (onTapDown != null) {
|
||||
_sentTapDown = true;
|
||||
final details = TapDownDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: event.kind,
|
||||
);
|
||||
invokeCallback<void>('onTapDown', () => onTapDown!(details));
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePointerMove(PointerMoveEvent event) {
|
||||
if (event.delta.distanceSquared > 2.0) {
|
||||
_cancelGesture('pointer moved');
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePointerUp(PointerUpEvent event) {
|
||||
if (_wonArena) {
|
||||
_handleTapUp(event);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTapUp(PointerUpEvent event) {
|
||||
final details = TapUpDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: event.kind,
|
||||
);
|
||||
invokeCallback<void>('onTapUp', () => onTapUp(details));
|
||||
|
||||
if (onTap != null) {
|
||||
invokeCallback<void>('onTap', onTap!);
|
||||
}
|
||||
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _cancelGesture(String reason) {
|
||||
if (_sentTapDown && onTapCancel != null) {
|
||||
invokeCallback<void>('onTapCancel: $reason', onTapCancel!);
|
||||
}
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _reset([int pointer = 0]) {
|
||||
_activePointer = pointer;
|
||||
_up = null;
|
||||
_sentTapDown = false;
|
||||
_wonArena = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
super.acceptGesture(pointer);
|
||||
|
||||
if (pointer == _activePointer) {
|
||||
_wonArena = true;
|
||||
|
||||
if (_up != null) {
|
||||
_handleTapUp(_up!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
|
||||
if (pointer == _activePointer) {
|
||||
_cancelGesture('gesture rejected by arena');
|
||||
stopTrackingPointer(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didStopTrackingLastPointer(int pointer) {
|
||||
// wait for arena
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelGesture('disposed');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'immediate tap';
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IntProperty('activePointer', _activePointer))
|
||||
..add(
|
||||
FlagProperty(
|
||||
'sentTapDown',
|
||||
value: _sentTapDown,
|
||||
ifTrue: 'has sentTapDown',
|
||||
),
|
||||
)
|
||||
..add(FlagProperty('wonArena', value: _wonArena, ifTrue: 'wonArena'))
|
||||
..add(
|
||||
DiagnosticsProperty<PointerUpEvent>(
|
||||
'pointerUpEvent',
|
||||
_up,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
905
lib/common/widgets/gesture/mouse_interactive_viewer.dart
Normal file
@@ -0,0 +1,905 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
|
||||
|
||||
class MouseInteractiveViewer extends StatefulWidget {
|
||||
const MouseInteractiveViewer({
|
||||
super.key,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.panAxis = PanAxis.free,
|
||||
this.boundaryMargin = EdgeInsets.zero,
|
||||
this.constrained = true,
|
||||
this.maxScale = 2.5,
|
||||
this.minScale = 0.8,
|
||||
this.interactionEndFrictionCoefficient = _kDrag,
|
||||
this.pointerSignalFallback,
|
||||
this.onPointerPanZoomUpdate,
|
||||
this.onPointerPanZoomEnd,
|
||||
this.onPointerDown,
|
||||
this.onInteractionEnd,
|
||||
this.onInteractionStart,
|
||||
this.onInteractionUpdate,
|
||||
this.panEnabled = true,
|
||||
this.scaleEnabled = true,
|
||||
this.scaleFactor = kDefaultMouseScrollToScaleFactor,
|
||||
this.transformationController,
|
||||
this.alignment,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
|
||||
required this.childKey,
|
||||
required this.child,
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(maxScale > 0),
|
||||
assert(maxScale >= minScale);
|
||||
|
||||
final Alignment? alignment;
|
||||
final Clip clipBehavior;
|
||||
final PanAxis panAxis;
|
||||
final EdgeInsets boundaryMargin;
|
||||
final Widget child;
|
||||
final bool constrained;
|
||||
final bool panEnabled;
|
||||
final bool scaleEnabled;
|
||||
final bool trackpadScrollCausesScale;
|
||||
final double scaleFactor;
|
||||
final double maxScale;
|
||||
final double minScale;
|
||||
final double interactionEndFrictionCoefficient;
|
||||
final PointerSignalEventListener? pointerSignalFallback;
|
||||
final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
|
||||
final PointerPanZoomEndEventListener? onPointerPanZoomEnd;
|
||||
final PointerDownEventListener? onPointerDown;
|
||||
final GestureScaleEndCallback? onInteractionEnd;
|
||||
final GestureScaleStartCallback? onInteractionStart;
|
||||
final GestureScaleUpdateCallback? onInteractionUpdate;
|
||||
final TransformationController? transformationController;
|
||||
final GlobalKey childKey;
|
||||
|
||||
static const double _kDrag = 0.0000135;
|
||||
|
||||
@override
|
||||
State<MouseInteractiveViewer> createState() => _MouseInteractiveViewerState();
|
||||
}
|
||||
|
||||
class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
with TickerProviderStateMixin {
|
||||
late TransformationController _transformer =
|
||||
widget.transformationController ?? TransformationController();
|
||||
|
||||
final GlobalKey _parentKey = GlobalKey();
|
||||
Animation<Offset>? _animation;
|
||||
Animation<double>? _scaleAnimation;
|
||||
late Offset _scaleAnimationFocalPoint;
|
||||
late AnimationController _controller;
|
||||
late AnimationController _scaleController;
|
||||
Axis? _currentAxis;
|
||||
Offset? _referenceFocalPoint;
|
||||
double? _scaleStart;
|
||||
double? _rotationStart = 0.0;
|
||||
double _currentRotation = 0.0;
|
||||
_GestureType? _gestureType;
|
||||
|
||||
static final gestureSettings = DeviceGestureSettings(
|
||||
touchSlop: Platform.isIOS ? 9 : 4,
|
||||
);
|
||||
|
||||
late final _scaleGestureRecognizer =
|
||||
ScaleGestureRecognizer(
|
||||
debugOwner: this,
|
||||
allowedButtonsFilter: (buttons) => buttons == kPrimaryButton,
|
||||
trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
|
||||
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
|
||||
)
|
||||
..gestureSettings = gestureSettings
|
||||
..onStart = _onScaleStart
|
||||
..onUpdate = _onScaleUpdate
|
||||
..onEnd = _onScaleEnd;
|
||||
|
||||
final bool _rotateEnabled = false;
|
||||
|
||||
Rect get _boundaryRect {
|
||||
assert(widget.childKey.currentContext != null);
|
||||
final RenderBox childRenderBox =
|
||||
widget.childKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
final Size childSize = childRenderBox.size;
|
||||
final Rect boundaryRect = widget.boundaryMargin.inflateRect(
|
||||
Offset.zero & childSize,
|
||||
);
|
||||
assert(
|
||||
!boundaryRect.isEmpty,
|
||||
"InteractiveViewer's child must have nonzero dimensions.",
|
||||
);
|
||||
assert(
|
||||
boundaryRect.isFinite ||
|
||||
(boundaryRect.left.isInfinite &&
|
||||
boundaryRect.top.isInfinite &&
|
||||
boundaryRect.right.isInfinite &&
|
||||
boundaryRect.bottom.isInfinite),
|
||||
'boundaryRect must either be infinite in all directions or finite in all directions.',
|
||||
);
|
||||
return boundaryRect;
|
||||
}
|
||||
|
||||
Rect get _viewport {
|
||||
assert(_parentKey.currentContext != null);
|
||||
final RenderBox parentRenderBox =
|
||||
_parentKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
return Offset.zero & parentRenderBox.size;
|
||||
}
|
||||
|
||||
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
|
||||
if (translation == Offset.zero) {
|
||||
return matrix.clone();
|
||||
}
|
||||
|
||||
final Offset alignedTranslation;
|
||||
|
||||
if (_currentAxis != null) {
|
||||
alignedTranslation = switch (widget.panAxis) {
|
||||
PanAxis.horizontal => _alignAxis(translation, Axis.horizontal),
|
||||
PanAxis.vertical => _alignAxis(translation, Axis.vertical),
|
||||
PanAxis.aligned => _alignAxis(translation, _currentAxis!),
|
||||
PanAxis.free => translation,
|
||||
};
|
||||
} else {
|
||||
alignedTranslation = translation;
|
||||
}
|
||||
|
||||
final Matrix4 nextMatrix = matrix.clone()
|
||||
..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1);
|
||||
|
||||
final Quad nextViewport = _transformViewport(nextMatrix, _viewport);
|
||||
|
||||
if (_boundaryRect.isInfinite) {
|
||||
return nextMatrix;
|
||||
}
|
||||
|
||||
final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(
|
||||
_boundaryRect,
|
||||
_currentRotation,
|
||||
);
|
||||
|
||||
final Offset offendingDistance = _exceedsBy(
|
||||
boundariesAabbQuad,
|
||||
nextViewport,
|
||||
);
|
||||
if (offendingDistance == Offset.zero) {
|
||||
return nextMatrix;
|
||||
}
|
||||
|
||||
final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix);
|
||||
final double currentScale = matrix.getMaxScaleOnAxis();
|
||||
final Offset correctedTotalTranslation = Offset(
|
||||
nextTotalTranslation.dx - offendingDistance.dx * currentScale,
|
||||
nextTotalTranslation.dy - offendingDistance.dy * currentScale,
|
||||
);
|
||||
final Matrix4 correctedMatrix = matrix.clone()
|
||||
..setTranslation(
|
||||
Vector3(
|
||||
correctedTotalTranslation.dx,
|
||||
correctedTotalTranslation.dy,
|
||||
0.0,
|
||||
),
|
||||
);
|
||||
|
||||
final Quad correctedViewport = _transformViewport(
|
||||
correctedMatrix,
|
||||
_viewport,
|
||||
);
|
||||
final Offset offendingCorrectedDistance = _exceedsBy(
|
||||
boundariesAabbQuad,
|
||||
correctedViewport,
|
||||
);
|
||||
if (offendingCorrectedDistance == Offset.zero) {
|
||||
return correctedMatrix;
|
||||
}
|
||||
|
||||
if (offendingCorrectedDistance.dx != 0.0 &&
|
||||
offendingCorrectedDistance.dy != 0.0) {
|
||||
return matrix.clone();
|
||||
}
|
||||
|
||||
final Offset unidirectionalCorrectedTotalTranslation = Offset(
|
||||
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
|
||||
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
|
||||
);
|
||||
return matrix.clone()..setTranslation(
|
||||
Vector3(
|
||||
unidirectionalCorrectedTotalTranslation.dx,
|
||||
unidirectionalCorrectedTotalTranslation.dy,
|
||||
0.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Matrix4 _matrixScale(Matrix4 matrix, double scale) {
|
||||
if (scale == 1.0) {
|
||||
return matrix.clone();
|
||||
}
|
||||
assert(scale != 0.0);
|
||||
|
||||
final double currentScale = _transformer.value.getMaxScaleOnAxis();
|
||||
final double totalScale = math.max(
|
||||
currentScale * scale,
|
||||
math.max(
|
||||
_viewport.width / _boundaryRect.width,
|
||||
_viewport.height / _boundaryRect.height,
|
||||
),
|
||||
);
|
||||
final double clampedTotalScale = clampDouble(
|
||||
totalScale,
|
||||
widget.minScale,
|
||||
widget.maxScale,
|
||||
);
|
||||
final double clampedScale = clampedTotalScale / currentScale;
|
||||
return matrix.clone()
|
||||
..scaleByDouble(clampedScale, clampedScale, clampedScale, 1);
|
||||
}
|
||||
|
||||
Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
|
||||
if (rotation == 0) {
|
||||
return matrix.clone();
|
||||
}
|
||||
final Offset focalPointScene = _transformer.toScene(focalPoint);
|
||||
return matrix.clone()
|
||||
..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1)
|
||||
..rotateZ(-rotation)
|
||||
..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1);
|
||||
}
|
||||
|
||||
bool _gestureIsSupported(_GestureType? gestureType) {
|
||||
return switch (gestureType) {
|
||||
_GestureType.rotate => _rotateEnabled,
|
||||
_GestureType.scale => widget.scaleEnabled,
|
||||
_GestureType.pan || null => widget.panEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
_GestureType _getGestureType(ScaleUpdateDetails details) {
|
||||
final double scale = !widget.scaleEnabled ? 1.0 : details.scale;
|
||||
final double rotation = !_rotateEnabled ? 0.0 : details.rotation;
|
||||
if ((scale - 1).abs() > rotation.abs()) {
|
||||
return _GestureType.scale;
|
||||
} else if (rotation != 0.0) {
|
||||
return _GestureType.rotate;
|
||||
} else {
|
||||
return _GestureType.pan;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the start of a gesture. All of pan, scale, and rotate are handled
|
||||
// with GestureDetector's scale gesture.
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
widget.onInteractionStart?.call(details);
|
||||
|
||||
if (_controller.isAnimating) {
|
||||
_controller
|
||||
..stop()
|
||||
..reset();
|
||||
_animation?.removeListener(_handleInertiaAnimation);
|
||||
_animation = null;
|
||||
}
|
||||
if (_scaleController.isAnimating) {
|
||||
_scaleController
|
||||
..stop()
|
||||
..reset();
|
||||
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
||||
_scaleAnimation = null;
|
||||
}
|
||||
|
||||
_gestureType = null;
|
||||
_currentAxis = null;
|
||||
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
_rotationStart = _currentRotation;
|
||||
}
|
||||
|
||||
// Handle an update to an ongoing gesture. All of pan, scale, and rotate are
|
||||
// handled with GestureDetector's scale gesture.
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
final double scale = _transformer.value.getMaxScaleOnAxis();
|
||||
_scaleAnimationFocalPoint = details.localFocalPoint;
|
||||
final Offset focalPointScene = _transformer.toScene(
|
||||
details.localFocalPoint,
|
||||
);
|
||||
|
||||
if (_gestureType == _GestureType.pan) {
|
||||
// When a gesture first starts, it sometimes has no change in scale and
|
||||
// rotation despite being a two-finger gesture. Here the gesture is
|
||||
// allowed to be reinterpreted as its correct type after originally
|
||||
// being marked as a pan.
|
||||
_gestureType = _getGestureType(details);
|
||||
} else {
|
||||
_gestureType ??= _getGestureType(details);
|
||||
}
|
||||
if (!_gestureIsSupported(_gestureType)) {
|
||||
widget.onInteractionUpdate?.call(details);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_gestureType!) {
|
||||
case _GestureType.scale:
|
||||
assert(_scaleStart != null);
|
||||
// details.scale gives us the amount to change the scale as of the
|
||||
// start of this gesture, so calculate the amount to scale as of the
|
||||
// previous call to _onScaleUpdate.
|
||||
final double desiredScale = _scaleStart! * details.scale;
|
||||
final double scaleChange = desiredScale / scale;
|
||||
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
||||
|
||||
// While scaling, translate such that the user's two fingers stay on
|
||||
// the same places in the scene. That means that the focal point of
|
||||
// the scale should be on the same place in the scene before and after
|
||||
// the scale.
|
||||
final Offset focalPointSceneScaled = _transformer.toScene(
|
||||
details.localFocalPoint,
|
||||
);
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
focalPointSceneScaled - _referenceFocalPoint!,
|
||||
);
|
||||
|
||||
// details.localFocalPoint should now be at the same location as the
|
||||
// original _referenceFocalPoint point. If it's not, that's because
|
||||
// the translate came in contact with a boundary. In that case, update
|
||||
// _referenceFocalPoint so subsequent updates happen in relation to
|
||||
// the new effective focal point.
|
||||
final Offset focalPointSceneCheck = _transformer.toScene(
|
||||
details.localFocalPoint,
|
||||
);
|
||||
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
|
||||
_referenceFocalPoint = focalPointSceneCheck;
|
||||
}
|
||||
|
||||
case _GestureType.rotate:
|
||||
if (details.rotation == 0.0) {
|
||||
widget.onInteractionUpdate?.call(details);
|
||||
return;
|
||||
}
|
||||
final double desiredRotation = _rotationStart! + details.rotation;
|
||||
_transformer.value = _matrixRotate(
|
||||
_transformer.value,
|
||||
_currentRotation - desiredRotation,
|
||||
details.localFocalPoint,
|
||||
);
|
||||
_currentRotation = desiredRotation;
|
||||
|
||||
case _GestureType.pan:
|
||||
assert(_referenceFocalPoint != null);
|
||||
// details may have a change in scale here when scaleEnabled is false.
|
||||
// In an effort to keep the behavior similar whether or not scaleEnabled
|
||||
// is true, these gestures are thrown away.
|
||||
if (details.scale != 1.0) {
|
||||
widget.onInteractionUpdate?.call(details);
|
||||
return;
|
||||
}
|
||||
_currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
|
||||
// Translate so that the same point in the scene is underneath the
|
||||
// focal point before and after the movement.
|
||||
final Offset translationChange =
|
||||
focalPointScene - _referenceFocalPoint!;
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
translationChange,
|
||||
);
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
}
|
||||
widget.onInteractionUpdate?.call(details);
|
||||
}
|
||||
|
||||
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
|
||||
// are handled with GestureDetector's scale gesture.
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
widget.onInteractionEnd?.call(details);
|
||||
_scaleStart = null;
|
||||
_rotationStart = null;
|
||||
_referenceFocalPoint = null;
|
||||
|
||||
_animation?.removeListener(_handleInertiaAnimation);
|
||||
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
||||
_controller.reset();
|
||||
_scaleController.reset();
|
||||
|
||||
if (!_gestureIsSupported(_gestureType)) {
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_gestureType) {
|
||||
case _GestureType.pan:
|
||||
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
final Vector3 translationVector = _transformer.value.getTranslation();
|
||||
final Offset translation = Offset(
|
||||
translationVector.x,
|
||||
translationVector.y,
|
||||
);
|
||||
final FrictionSimulation frictionSimulationX = FrictionSimulation(
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
translation.dx,
|
||||
details.velocity.pixelsPerSecond.dx,
|
||||
);
|
||||
final FrictionSimulation frictionSimulationY = FrictionSimulation(
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
translation.dy,
|
||||
details.velocity.pixelsPerSecond.dy,
|
||||
);
|
||||
final double tFinal = _getFinalTime(
|
||||
details.velocity.pixelsPerSecond.distance,
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
);
|
||||
_animation =
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
|
||||
)
|
||||
..addListener(_handleInertiaAnimation);
|
||||
_controller
|
||||
..duration = Duration(milliseconds: (tFinal * 1000).round())
|
||||
..forward();
|
||||
case _GestureType.scale:
|
||||
if (details.scaleVelocity.abs() < 0.1) {
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
final double scale = _transformer.value.getMaxScaleOnAxis();
|
||||
final FrictionSimulation frictionSimulation = FrictionSimulation(
|
||||
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
|
||||
scale,
|
||||
details.scaleVelocity / 10,
|
||||
);
|
||||
final double tFinal = _getFinalTime(
|
||||
details.scaleVelocity.abs(),
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
effectivelyMotionless: 0.1,
|
||||
);
|
||||
_scaleAnimation =
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.decelerate,
|
||||
),
|
||||
)
|
||||
..addListener(_handleScaleAnimation);
|
||||
_scaleController
|
||||
..duration = Duration(milliseconds: (tFinal * 1000).round())
|
||||
..forward();
|
||||
case _GestureType.rotate || null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _receivedPointerSignal(PointerSignalEvent event) {
|
||||
final Offset local = event.localPosition;
|
||||
final Offset global = event.position;
|
||||
final double scaleChange;
|
||||
if (event is PointerScrollEvent) {
|
||||
if (event.kind == PointerDeviceKind.trackpad) {
|
||||
widget.onInteractionStart?.call(
|
||||
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
|
||||
);
|
||||
|
||||
final Offset localDelta = PointerEvent.transformDeltaViaPositions(
|
||||
untransformedEndPosition: global + event.scrollDelta,
|
||||
untransformedDelta: event.scrollDelta,
|
||||
transform: event.transform,
|
||||
);
|
||||
|
||||
final Offset focalPointScene = _transformer.toScene(local);
|
||||
final Offset newFocalPointScene = _transformer.toScene(
|
||||
local - localDelta,
|
||||
);
|
||||
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
newFocalPointScene - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: global - event.scrollDelta,
|
||||
localFocalPoint: local - localDelta,
|
||||
focalPointDelta: -localDelta,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
return;
|
||||
}
|
||||
_handlePointerScrollEvent(event);
|
||||
return;
|
||||
} else if (event is PointerScaleEvent) {
|
||||
scaleChange = event.scale;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
widget.onInteractionStart?.call(
|
||||
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
|
||||
);
|
||||
|
||||
if (!_gestureIsSupported(_GestureType.scale)) {
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: global,
|
||||
localFocalPoint: local,
|
||||
scale: scaleChange,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
return;
|
||||
}
|
||||
|
||||
final Offset focalPointScene = _transformer.toScene(local);
|
||||
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
||||
|
||||
// After scaling, translate such that the event's position is at the
|
||||
// same scene point before and after the scale.
|
||||
final Offset focalPointSceneScaled = _transformer.toScene(local);
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
focalPointSceneScaled - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: global,
|
||||
localFocalPoint: local,
|
||||
scale: scaleChange,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
}
|
||||
|
||||
void _handlePointerScrollEvent(PointerScrollEvent event) {
|
||||
final Offset local = event.localPosition;
|
||||
final Offset global = event.position;
|
||||
|
||||
if (_gestureIsSupported(_GestureType.scale)) {
|
||||
if (HardwareKeyboard.instance.isControlPressed) {
|
||||
_handleMouseWheelScale(event, local, global);
|
||||
return;
|
||||
}
|
||||
final shift = HardwareKeyboard.instance.isShiftPressed;
|
||||
if (shift || HardwareKeyboard.instance.isAltPressed) {
|
||||
_handleMouseWheelPanAsScale(event, local, global, shift);
|
||||
return;
|
||||
}
|
||||
widget.pointerSignalFallback?.call(event);
|
||||
}
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: global,
|
||||
localFocalPoint: local,
|
||||
scale: math.exp(-event.scrollDelta.dy / widget.scaleFactor),
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
}
|
||||
|
||||
void _handleMouseWheelScale(
|
||||
PointerScrollEvent event,
|
||||
Offset local,
|
||||
Offset global,
|
||||
) {
|
||||
final double scaleChange = math.exp(
|
||||
-event.scrollDelta.dy / widget.scaleFactor,
|
||||
);
|
||||
final Offset focalPointScene = _transformer.toScene(local);
|
||||
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
||||
|
||||
final Offset focalPointSceneScaled = _transformer.toScene(local);
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
focalPointSceneScaled - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: global,
|
||||
localFocalPoint: local,
|
||||
scale: scaleChange,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
}
|
||||
|
||||
void _handleMouseWheelPanAsScale(
|
||||
PointerScrollEvent event,
|
||||
Offset local,
|
||||
Offset global,
|
||||
bool flip,
|
||||
) {
|
||||
final Offset translation = flip
|
||||
? event.scrollDelta.flip
|
||||
: event.scrollDelta;
|
||||
|
||||
final Offset focalPointScene = _transformer.toScene(local);
|
||||
final Offset newFocalPointScene = _transformer.toScene(local - translation);
|
||||
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
newFocalPointScene - focalPointScene,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleInertiaAnimation() {
|
||||
if (!_controller.isAnimating) {
|
||||
_currentAxis = null;
|
||||
_animation?.removeListener(_handleInertiaAnimation);
|
||||
_animation = null;
|
||||
_controller.reset();
|
||||
return;
|
||||
}
|
||||
final Vector3 translationVector = _transformer.value.getTranslation();
|
||||
final Offset translation = Offset(translationVector.x, translationVector.y);
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
_transformer.toScene(_animation!.value) -
|
||||
_transformer.toScene(translation),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleScaleAnimation() {
|
||||
if (!_scaleController.isAnimating) {
|
||||
_currentAxis = null;
|
||||
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
||||
_scaleAnimation = null;
|
||||
_scaleController.reset();
|
||||
return;
|
||||
}
|
||||
final double desiredScale = _scaleAnimation!.value;
|
||||
final double scaleChange =
|
||||
desiredScale / _transformer.value.getMaxScaleOnAxis();
|
||||
final Offset referenceFocalPoint = _transformer.toScene(
|
||||
_scaleAnimationFocalPoint,
|
||||
);
|
||||
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
||||
|
||||
final Offset focalPointSceneScaled = _transformer.toScene(
|
||||
_scaleAnimationFocalPoint,
|
||||
);
|
||||
_transformer.value = _matrixTranslate(
|
||||
_transformer.value,
|
||||
focalPointSceneScaled - referenceFocalPoint,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTransformation() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent event) {
|
||||
widget.onPointerDown?.call(event);
|
||||
_scaleGestureRecognizer.addPointer(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
|
||||
_transformer.addListener(_handleTransformation);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MouseInteractiveViewer oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final TransformationController? newController =
|
||||
widget.transformationController;
|
||||
if (newController == oldWidget.transformationController) {
|
||||
return;
|
||||
}
|
||||
_transformer.removeListener(_handleTransformation);
|
||||
if (oldWidget.transformationController == null) {
|
||||
_transformer.dispose();
|
||||
}
|
||||
_transformer = newController ?? TransformationController();
|
||||
_transformer.addListener(_handleTransformation);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleGestureRecognizer.dispose();
|
||||
_controller.dispose();
|
||||
_scaleController.dispose();
|
||||
_transformer.removeListener(_handleTransformation);
|
||||
if (widget.transformationController == null) {
|
||||
_transformer.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.child.key == widget.childKey);
|
||||
|
||||
return Listener(
|
||||
key: _parentKey,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPointerSignal: _receivedPointerSignal,
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerPanZoomStart: _scaleGestureRecognizer.addPointerPanZoom,
|
||||
onPointerPanZoomUpdate: widget.onPointerPanZoomUpdate,
|
||||
onPointerPanZoomEnd: widget.onPointerPanZoomEnd,
|
||||
child: _InteractiveViewerBuilt(
|
||||
childKey: widget.childKey,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
constrained: widget.constrained,
|
||||
matrix: _transformer.value,
|
||||
alignment: widget.alignment,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InteractiveViewerBuilt extends StatelessWidget {
|
||||
const _InteractiveViewerBuilt({
|
||||
required this.child,
|
||||
required this.childKey,
|
||||
required this.clipBehavior,
|
||||
required this.constrained,
|
||||
required this.matrix,
|
||||
required this.alignment,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final GlobalKey childKey;
|
||||
final Clip clipBehavior;
|
||||
final bool constrained;
|
||||
final Matrix4 matrix;
|
||||
final Alignment? alignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Transform(
|
||||
transform: matrix,
|
||||
alignment: alignment,
|
||||
child: this.child,
|
||||
);
|
||||
|
||||
if (!constrained) {
|
||||
child = OverflowBox(
|
||||
alignment: Alignment.topLeft,
|
||||
minWidth: 0.0,
|
||||
minHeight: 0.0,
|
||||
maxWidth: double.infinity,
|
||||
maxHeight: double.infinity,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (clipBehavior != Clip.none) {
|
||||
child = ClipRect(clipBehavior: clipBehavior, child: child);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
enum _GestureType { pan, scale, rotate }
|
||||
|
||||
double _getFinalTime(
|
||||
double velocity,
|
||||
double drag, {
|
||||
double effectivelyMotionless = 10,
|
||||
}) {
|
||||
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
|
||||
}
|
||||
|
||||
Offset _getMatrixTranslation(Matrix4 matrix) {
|
||||
final Vector3 nextTranslation = matrix.getTranslation();
|
||||
return Offset(nextTranslation.x, nextTranslation.y);
|
||||
}
|
||||
|
||||
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
|
||||
final Matrix4 inverseMatrix = matrix.clone()..invert();
|
||||
return Quad.points(
|
||||
inverseMatrix.transform3(
|
||||
Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
|
||||
final Matrix4 rotationMatrix = Matrix4.identity()
|
||||
..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1)
|
||||
..rotateZ(rotation)
|
||||
..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1);
|
||||
final Quad boundariesRotated = Quad.points(
|
||||
rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
|
||||
rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
|
||||
rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)),
|
||||
rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)),
|
||||
);
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated);
|
||||
}
|
||||
|
||||
Offset _exceedsBy(Quad boundary, Quad viewport) {
|
||||
final List<Vector3> viewportPoints = <Vector3>[
|
||||
viewport.point0,
|
||||
viewport.point1,
|
||||
viewport.point2,
|
||||
viewport.point3,
|
||||
];
|
||||
Offset largestExcess = Offset.zero;
|
||||
for (final Vector3 point in viewportPoints) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
final Vector3 pointInside = InteractiveViewer.getNearestPointInside(
|
||||
point,
|
||||
boundary,
|
||||
);
|
||||
final Offset excess = Offset(
|
||||
pointInside.x - point.x,
|
||||
pointInside.y - point.y,
|
||||
);
|
||||
if (excess.dx.abs() > largestExcess.dx.abs()) {
|
||||
largestExcess = Offset(excess.dx, largestExcess.dy);
|
||||
}
|
||||
if (excess.dy.abs() > largestExcess.dy.abs()) {
|
||||
largestExcess = Offset(largestExcess.dx, excess.dy);
|
||||
}
|
||||
}
|
||||
|
||||
return _round(largestExcess);
|
||||
}
|
||||
|
||||
Offset _round(Offset offset) {
|
||||
return Offset(
|
||||
double.parse(offset.dx.toStringAsFixed(9)),
|
||||
double.parse(offset.dy.toStringAsFixed(9)),
|
||||
);
|
||||
}
|
||||
|
||||
Offset _alignAxis(Offset offset, Axis axis) {
|
||||
return switch (axis) {
|
||||
Axis.horizontal => Offset(offset.dx, 0.0),
|
||||
Axis.vertical => Offset(0.0, offset.dy),
|
||||
};
|
||||
}
|
||||
|
||||
Axis? _getPanAxis(Offset point1, Offset point2) {
|
||||
if (point1 == point2) {
|
||||
return null;
|
||||
}
|
||||
final double x = point2.dx - point1.dx;
|
||||
final double y = point2.dy - point1.dy;
|
||||
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
|
||||
}
|
||||
|
||||
extension on Offset {
|
||||
Offset get flip => Offset(dy, dx);
|
||||
}
|
||||
@@ -124,9 +124,7 @@ class _CachedNetworkSVGImageState extends State<CachedNetworkSVGImage> {
|
||||
|
||||
Future<void> _loadImage() async {
|
||||
try {
|
||||
var file = (await widget._cacheManager.getFileFromMemory(
|
||||
_cacheKey,
|
||||
))?.file;
|
||||
var file = (await widget._cacheManager.getFileFromCache(_cacheKey))?.file;
|
||||
|
||||
file ??= await widget._cacheManager.getSingleFile(
|
||||
widget._url,
|
||||
@@ -173,15 +171,14 @@ class _CachedNetworkSVGImageState extends State<CachedNetworkSVGImage> {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
} else {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {}));
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
@@ -19,9 +19,8 @@ import 'dart:math' show min;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_layout.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/custom_layout.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/marquee.dart' show ContextSingleTicker;
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/utils/context_ext.dart';
|
||||
@@ -65,7 +64,6 @@ class CustomGridView extends StatelessWidget {
|
||||
required this.maxWidth,
|
||||
required this.picArr,
|
||||
this.onViewImage,
|
||||
this.onDismissed,
|
||||
this.fullScreen = false,
|
||||
});
|
||||
|
||||
@@ -73,7 +71,6 @@ class CustomGridView extends StatelessWidget {
|
||||
final double space;
|
||||
final List<ImageModel> picArr;
|
||||
final VoidCallback? onViewImage;
|
||||
final ValueChanged<int>? onDismissed;
|
||||
final bool fullScreen;
|
||||
|
||||
static bool horizontalPreview = Pref.horizontalPreview;
|
||||
@@ -96,20 +93,18 @@ class CustomGridView extends StatelessWidget {
|
||||
!context.mediaQuerySize.isPortrait) {
|
||||
final scaffoldState = Scaffold.maybeOf(context);
|
||||
if (scaffoldState != null) {
|
||||
onViewImage?.call();
|
||||
PageUtils.onHorizontalPreviewState(
|
||||
scaffoldState,
|
||||
ContextSingleTicker(scaffoldState.context),
|
||||
imgList,
|
||||
index,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
onViewImage?.call();
|
||||
PageUtils.imageView(
|
||||
initialPage: index,
|
||||
imgList: imgList,
|
||||
onDismissed: onDismissed,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,20 +19,17 @@ void imageSaveDialog({
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
late final iconColor = theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
Widget iconBtn({
|
||||
String? tooltip,
|
||||
required IconData icon,
|
||||
required Icon icon,
|
||||
required VoidCallback? onPressed,
|
||||
}) {
|
||||
return iconButton(
|
||||
context: context,
|
||||
onPressed: onPressed,
|
||||
iconSize: 20,
|
||||
icon: icon,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: iconColor,
|
||||
iconSize: 20,
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,19 +58,19 @@ void imageSaveDialog({
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const IconButton(
|
||||
child: IconButton(
|
||||
tooltip: '关闭',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Colors.black.withValues(alpha: 0.3),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: Icon(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
@@ -105,7 +102,7 @@ void imageSaveDialog({
|
||||
(res) => SmartDialog.showToast(res['msg']),
|
||||
),
|
||||
},
|
||||
icon: Icons.watch_later_outlined,
|
||||
icon: const Icon(Icons.watch_later_outlined),
|
||||
),
|
||||
if (cover?.isNotEmpty == true) ...[
|
||||
if (Utils.isMobile)
|
||||
@@ -115,7 +112,7 @@ void imageSaveDialog({
|
||||
SmartDialog.dismiss();
|
||||
ImageUtils.onShareImg(cover!);
|
||||
},
|
||||
icon: Icons.share,
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
iconBtn(
|
||||
tooltip: '保存封面图',
|
||||
@@ -128,7 +125,7 @@ void imageSaveDialog({
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
icon: Icons.download,
|
||||
icon: const Icon(Icons.download),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -80,7 +80,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
if (height == null || forceUseCacheWidth || width <= height!) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
memCacheHeight = height?.cacheSize(context);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl: ImageUtils.thumbnailUrl(src, quality),
|
||||
|
||||
@@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
|
||||
/// show a [Hero] animation.
|
||||
class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
HeroDialogRoute({
|
||||
required this.builder,
|
||||
required this.pageBuilder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
final RoutePageBuilder pageBuilder;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
@@ -50,12 +50,10 @@ class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget child = builder(context);
|
||||
final Widget result = Semantics(
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: child,
|
||||
child: pageBuilder(context, animation, secondaryAnimation),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.boundaryWidth,
|
||||
this.controller,
|
||||
required this.controller,
|
||||
this.onScaleChanged,
|
||||
this.onLeftBoundaryHit,
|
||||
this.onRightBoundaryHit,
|
||||
@@ -43,7 +43,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
||||
final double boundaryWidth;
|
||||
|
||||
/// The [TransformationController] for the [InteractiveViewer].
|
||||
final TransformationController? controller;
|
||||
final TransformationController controller;
|
||||
|
||||
/// Called when the scale changed after an interaction ended.
|
||||
final ScaleChanged? onScaleChanged;
|
||||
@@ -68,7 +68,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
|
||||
|
||||
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
with SingleTickerProviderStateMixin {
|
||||
TransformationController? _controller;
|
||||
late TransformationController _controller;
|
||||
|
||||
double? _scale;
|
||||
|
||||
@@ -85,8 +85,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = widget.controller ?? TransformationController();
|
||||
_controller = widget.controller;
|
||||
|
||||
_animateController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -98,9 +97,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller!.dispose();
|
||||
_animateController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -183,7 +180,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
}
|
||||
|
||||
void _updateBoundaryDetection() {
|
||||
final double scale = _controller!.value.row0[0];
|
||||
final double scale = _controller.value.row0[0];
|
||||
|
||||
if (_scale != scale) {
|
||||
// the scale changed
|
||||
@@ -196,7 +193,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
return;
|
||||
}
|
||||
|
||||
final double xOffset = _controller!.value.row0[3];
|
||||
final double xOffset = _controller.value.row0[3];
|
||||
final double boundaryWidth = widget.boundaryWidth;
|
||||
final double boundaryEnd = boundaryWidth * scale;
|
||||
final double xPos = boundaryEnd + xOffset;
|
||||
|
||||