mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
50 Commits
17b8b9ea8f
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7fa48cec29 | ||
e549a18dbf | |||
c17c4abb5b | |||
af57bc31b1 | |||
16449a1440 | |||
a7c1983f35 | |||
4c257d7178 | |||
3a9d634edf | |||
![]() |
e179c8f67f | ||
![]() |
c4b85471c1 | ||
![]() |
a898b57d96 | ||
50c6bec4cd | |||
![]() |
8c44f83d6c | ||
![]() |
103b6b2832 | ||
4129349c70 | |||
77a9aa5457 | |||
97940b9492 | |||
7945c0e54f | |||
dfee65c3af | |||
fa2dbd79f6 | |||
9a9f539906 | |||
d7331f36e9 | |||
![]() |
d0b76de465 | ||
894a922b8f | |||
a91d7fff2d | |||
![]() |
926a3a530e | ||
d308c2ac60 | |||
ac13807ef4 | |||
38a5b2b8cf | |||
3a7c8d5e38 | |||
![]() |
ce0d10aeb2 | ||
![]() |
0ac857ef9a | ||
3928f5afe7 | |||
8a61a4750b | |||
![]() |
1bc3fef47b | ||
![]() |
4dac132bee | ||
![]() |
7c60c00962 | ||
9d8ade6fe0 | |||
6245399810 | |||
c074e7f9d1 | |||
f822e198ea | |||
7035f11eb5 | |||
f2f5a4f573 | |||
2acf234f7d | |||
9ed8f351c7 | |||
7c35dc7cf7 | |||
ccb03343f4 | |||
![]() |
b9817ec030 | ||
![]() |
5ebb554e54 | ||
23ee79fe9d |
90
.github/workflows/main.yml
vendored
90
.github/workflows/main.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version-file: pubspec.yaml
|
flutter-version-file: pubspec.yaml
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
|
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
# Step 1: Decode and install the certificate
|
# Step 1: Decode and install the certificate
|
||||||
- name: Decode and install certificate
|
- name: Decode and install certificate
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version-file: pubspec.yaml
|
flutter-version-file: pubspec.yaml
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
|
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build ios --release --no-codesign
|
- run: flutter build ios --release --no-codesign
|
||||||
- run: |
|
- run: |
|
||||||
@@ -149,45 +149,6 @@ jobs:
|
|||||||
sudo rm -rf build/linux/arch/pkg
|
sudo rm -rf build/linux/arch/pkg
|
||||||
sudo rm -rf build/linux/arch/src
|
sudo rm -rf build/linux/arch/src
|
||||||
sudo rm -rf build/linux/arch/PKGBUILD
|
sudo rm -rf build/linux/arch/PKGBUILD
|
||||||
- name: Build AppImage
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y libfuse2
|
|
||||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
|
||||||
chmod +x appimagetool
|
|
||||||
|
|
||||||
mkdir -p Venera.AppDir
|
|
||||||
cp -r build/linux/x64/release/bundle/* Venera.AppDir/
|
|
||||||
|
|
||||||
cat > Venera.AppDir/venera.desktop << EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Venera
|
|
||||||
Exec=venera
|
|
||||||
Icon=venera
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cp assets/app_icon.png Venera.AppDir/venera.png
|
|
||||||
|
|
||||||
cat > Venera.AppDir/AppRun << EOF
|
|
||||||
#!/bin/sh
|
|
||||||
HERE=\$(dirname \$(readlink -f "\${0}"))
|
|
||||||
export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH}
|
|
||||||
export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH}
|
|
||||||
export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS}
|
|
||||||
exec "\${HERE}"/venera "\$@"
|
|
||||||
EOF
|
|
||||||
chmod +x Venera.AppDir/AppRun
|
|
||||||
|
|
||||||
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
|
||||||
./appimagetool Venera.AppDir Venera-${APP_VERSION}-x86_64.AppImage
|
|
||||||
|
|
||||||
mkdir -p build/linux/appimage
|
|
||||||
mv Venera-${APP_VERSION}-x86_64.AppImage build/linux/appimage/
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage_build
|
|
||||||
path: build/linux/appimage
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deb_build
|
name: deb_build
|
||||||
@@ -210,45 +171,6 @@ jobs:
|
|||||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate flutter_to_debian
|
||||||
- run: python3 debian/build.py arm64
|
- run: python3 debian/build.py arm64
|
||||||
- name: Build AppImage
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y libfuse2
|
|
||||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage"
|
|
||||||
chmod +x appimagetool
|
|
||||||
|
|
||||||
mkdir -p Venera.AppDir
|
|
||||||
cp -r build/linux/arm64/release/bundle/* Venera.AppDir/
|
|
||||||
|
|
||||||
cat > Venera.AppDir/venera.desktop << EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Venera
|
|
||||||
Exec=venera
|
|
||||||
Icon=venera
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cp assets/app_icon.png Venera.AppDir/venera.png
|
|
||||||
|
|
||||||
cat > Venera.AppDir/AppRun << EOF
|
|
||||||
#!/bin/sh
|
|
||||||
HERE=\$(dirname \$(readlink -f "\${0}"))
|
|
||||||
export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH}
|
|
||||||
export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH}
|
|
||||||
export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS}
|
|
||||||
exec "\${HERE}"/venera "\$@"
|
|
||||||
EOF
|
|
||||||
chmod +x Venera.AppDir/AppRun
|
|
||||||
|
|
||||||
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
|
||||||
./appimagetool Venera.AppDir Venera-${APP_VERSION}-aarch64.AppImage
|
|
||||||
|
|
||||||
mkdir -p build/linux/appimage
|
|
||||||
mv Venera-${APP_VERSION}-aarch64.AppImage build/linux/appimage/
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage_arm64_build
|
|
||||||
path: build/linux/appimage
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deb_arm64_build
|
name: deb_arm64_build
|
||||||
@@ -287,14 +209,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: deb_arm64_build
|
name: deb_arm64_build
|
||||||
path: outputs
|
path: outputs
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage_build
|
|
||||||
path: outputs
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage_arm64_build
|
|
||||||
path: outputs
|
|
||||||
- uses: softprops/action-gh-release@v2
|
- uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
|
12
README.md
12
README.md
@@ -1,15 +1,14 @@
|
|||||||
# venera
|
# venera
|
||||||
[](https://flutter.dev/)
|
[](https://flutter.dev/)
|
||||||
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
||||||
[](https://github.com/venera-app/venera/releases)
|
|
||||||
[](https://github.com/venera-app/venera/stargazers)
|
[](https://github.com/venera-app/venera/stargazers)
|
||||||
[](https://t.me/venera_release)
|
[](https://t.me/venera_release)
|
||||||
|
|
||||||
A comic reader that support reading local and network comics.
|
[](https://github.com/venera-app/venera/releases)
|
||||||
|
[](https://aur.archlinux.org/packages/venera-bin)
|
||||||
|
[](https://f-droid.org/packages/com.github.wgh136.venera/)
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
A comic reader that support reading local and network comics.
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Read local comics
|
- Read local comics
|
||||||
@@ -34,4 +33,7 @@ See [Comic Source](doc/comic_source.md)
|
|||||||
### Tags Translation
|
### Tags Translation
|
||||||
[](https://github.com/EhTagTranslation/Database)
|
[](https://github.com/EhTagTranslation/Database)
|
||||||
|
|
||||||
|
## Headless Mode
|
||||||
|
See [Headless Doc](doc/headless_doc.md)
|
||||||
|
|
||||||
The Chinese translation of the manga tags is from this project.
|
The Chinese translation of the manga tags is from this project.
|
||||||
|
@@ -34,6 +34,12 @@ android {
|
|||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion "28.0.13004108"
|
ndkVersion "28.0.13004108"
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
splits{
|
splits{
|
||||||
abi {
|
abi {
|
||||||
reset()
|
reset()
|
||||||
@@ -78,6 +84,9 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
// Temporarily solution to fix crash
|
||||||
|
minifyEnabled false
|
||||||
|
shrinkResources false
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
@@ -58,8 +59,6 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<!-- [flutter 3.27.1] Impeller is still worse than skia, disable it -->
|
|
||||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.9.0' apply false
|
id "com.android.application" version '8.9.0' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
@@ -4,6 +4,18 @@ Venera JavaScript Library
|
|||||||
This library provides a set of APIs for interacting with the Venera app.
|
This library provides a set of APIs for interacting with the Venera app.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function sendMessage
|
||||||
|
* @global
|
||||||
|
* @param {Object} message
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a timeout to execute a callback function after a specified delay.
|
||||||
|
* @param callback {Function}
|
||||||
|
* @param delay {number} - delay in milliseconds
|
||||||
|
*/
|
||||||
function setTimeout(callback, delay) {
|
function setTimeout(callback, delay) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
method: 'delay',
|
method: 'delay',
|
||||||
@@ -1322,13 +1334,15 @@ let UI = {
|
|||||||
* Show an input dialog
|
* Show an input dialog
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
||||||
|
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha.
|
||||||
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
||||||
*/
|
*/
|
||||||
showInputDialog: (title, validator) => {
|
showInputDialog: (title, validator, image) => {
|
||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: 'UI',
|
method: 'UI',
|
||||||
function: 'showInputDialog',
|
function: 'showInputDialog',
|
||||||
title: title,
|
title: title,
|
||||||
|
image: image,
|
||||||
validator: validator
|
validator: validator
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -1410,3 +1424,18 @@ function getClipboard() {
|
|||||||
method: 'getClipboard'
|
method: 'getClipboard'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
|
||||||
|
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
|
||||||
|
* @param args {any[]} - The arguments to pass to the function.
|
||||||
|
* @returns {Promise<any>} - The result of the function.
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
function compute(func, ...args) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'compute',
|
||||||
|
function: func,
|
||||||
|
args: args
|
||||||
|
})
|
||||||
|
}
|
3982
assets/opencc.txt
Normal file
3982
assets/opencc.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -400,7 +400,18 @@
|
|||||||
"Delete Chapters": "删除章节",
|
"Delete Chapters": "删除章节",
|
||||||
"Open Folder": "打开文件夹",
|
"Open Folder": "打开文件夹",
|
||||||
"Path copied to clipboard": "路径已复制到剪贴板",
|
"Path copied to clipboard": "路径已复制到剪贴板",
|
||||||
"Reverse default chapter order": "反转默认章节顺序"
|
"Reverse default chapter order": "反转默认章节顺序",
|
||||||
|
"Reload Configs": "重新加载配置文件",
|
||||||
|
"Reload": "重载",
|
||||||
|
"Disable Length Limitation": "禁用长度限制",
|
||||||
|
"Only valid for this run": "仅对本次运行有效",
|
||||||
|
"Logs": "日志",
|
||||||
|
"Export logs": "导出日志",
|
||||||
|
"Clear specific reader settings for all comics": "清除所有漫画的特殊阅读设置",
|
||||||
|
"Clear specific reader settings for this comic": "清除该漫画的特殊阅读设置",
|
||||||
|
"Enable comic specific settings": "启用此漫画特定设置",
|
||||||
|
"Ignore Certificate Errors": "忽略证书错误",
|
||||||
|
"Mouse scroll speed": "鼠标滚动速度"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -803,6 +814,17 @@
|
|||||||
"Delete Chapters": "刪除章節",
|
"Delete Chapters": "刪除章節",
|
||||||
"Open Folder": "打開資料夾",
|
"Open Folder": "打開資料夾",
|
||||||
"Path copied to clipboard": "路徑已複製到剪貼簿",
|
"Path copied to clipboard": "路徑已複製到剪貼簿",
|
||||||
"Reverse default chapter order": "反轉預設章節順序"
|
"Reverse default chapter order": "反轉預設章節順序",
|
||||||
|
"Reload Configs": "重新載入設定檔",
|
||||||
|
"Reload": "重載",
|
||||||
|
"Disable Length Limitation": "禁用長度限制",
|
||||||
|
"Only valid for this run": "僅對本次運行有效",
|
||||||
|
"Logs": "日誌",
|
||||||
|
"Export logs": "匯出日誌",
|
||||||
|
"Clear specific reader settings for all comics": "清除所有漫畫的特殊閱讀設定",
|
||||||
|
"Clear specific reader settings for this comic": "清除該漫畫的特殊閱讀設定",
|
||||||
|
"Enable comic specific settings": "啟用此漫畫特定設定",
|
||||||
|
"Ignore Certificate Errors": "忽略證書錯誤",
|
||||||
|
"Mouse scroll speed": "滑鼠滾動速度"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -13,6 +13,14 @@ This document will describe how to write a comic source for Venera.
|
|||||||
|
|
||||||
Venera can display a list of comic sources in the app.
|
Venera can display a list of comic sources in the app.
|
||||||
|
|
||||||
|
You can use the following repo url:
|
||||||
|
```
|
||||||
|
https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json
|
||||||
|
```
|
||||||
|
The repo is maintained by the Venera team.
|
||||||
|
|
||||||
|
> The link is a mirror of the original repo. To contribute your comic source, please visit the [original repo](https://github.com/venera-app/venera-configs)
|
||||||
|
|
||||||
You should provide a repository url to let the app load the comic source list.
|
You should provide a repository url to let the app load the comic source list.
|
||||||
The url should point to a JSON file that contains the list of comic sources.
|
The url should point to a JSON file that contains the list of comic sources.
|
||||||
|
|
||||||
@@ -33,12 +41,6 @@ The JSON file should have the following format:
|
|||||||
Only one of `url` and `filename` should be provided.
|
Only one of `url` and `filename` should be provided.
|
||||||
The description field is optional.
|
The description field is optional.
|
||||||
|
|
||||||
Currently, you can use the following repo url:
|
|
||||||
```
|
|
||||||
https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/index.json
|
|
||||||
```
|
|
||||||
The repo is maintained by the Venera team, and you can submit a pull request to add your comic source.
|
|
||||||
|
|
||||||
## Create a Comic Source
|
## Create a Comic Source
|
||||||
|
|
||||||
### Preparation
|
### Preparation
|
||||||
|
180
doc/headless_doc.md
Normal file
180
doc/headless_doc.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Venera Headless Mode
|
||||||
|
|
||||||
|
Venera's headless mode allows you to run key features from the command line, making it easy to automate tasks and integrate with other tools. This document outlines the available commands and their usage.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
To activate headless mode, use the `--headless` flag when running the Venera executable, followed by the desired command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
venera --headless <command> [subcommand] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Options
|
||||||
|
|
||||||
|
- **`--ignore-disheadless-log`**: Suppresses log output, providing a cleaner output for scripting.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `webdav`
|
||||||
|
|
||||||
|
Manage WebDAV data synchronization.
|
||||||
|
|
||||||
|
- **`webdav up`**: Uploads your local configuration to the WebDAV server.
|
||||||
|
- **`webdav down`**: Downloads and applies the remote configuration from the WebDAV server.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
venera --headless webdav up
|
||||||
|
```
|
||||||
|
|
||||||
|
### `updatescript`
|
||||||
|
|
||||||
|
Update comic source scripts.
|
||||||
|
|
||||||
|
- **`updatescript all`**: Checks for and applies all available updates for your comic source scripts.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
venera --headless updatescript all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Format:**
|
||||||
|
|
||||||
|
The `updatescript` command provides detailed progress and a final summary.
|
||||||
|
|
||||||
|
**Progress Logs:**
|
||||||
|
|
||||||
|
- **`Progress`**: Indicates a successful update for a single script.
|
||||||
|
- **`ProgressError`**: Indicates a failure during a script update.
|
||||||
|
|
||||||
|
**Example `Progress` Log:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"message": "Progress",
|
||||||
|
"data": {
|
||||||
|
"current": 1,
|
||||||
|
"total": 5,
|
||||||
|
"source": {
|
||||||
|
"key": "source-key",
|
||||||
|
"name": "Source Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"url": "https://example.com/source.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Final Summary:**
|
||||||
|
|
||||||
|
A summary is provided at the end, detailing the total number of scripts, how many were updated, and how many failed.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "All scripts updated.",
|
||||||
|
"data": {
|
||||||
|
"total": 5,
|
||||||
|
"updated": 4,
|
||||||
|
"errors": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `updatesubscribe`
|
||||||
|
|
||||||
|
Update your subscribed comics and retrieve a list of updated comics.
|
||||||
|
|
||||||
|
- **`updatesubscribe`**: Checks all subscribed comics for updates.
|
||||||
|
- **`updatesubscribe --update-comic-by-id-type <id> <type>`**: Updates a single comic specified by its `id` and `type`.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update all subscriptions
|
||||||
|
venera --headless updatesubscribe
|
||||||
|
|
||||||
|
# Update a single comic
|
||||||
|
venera --headless updatesubscribe --update-comic-by-id-type "comic-id" "source-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
All headless commands output JSON objects prefixed with `[CLI PRINT]`. This structured format allows for easy parsing in automated scripts. The JSON object always contains a `status` and a `message`. For commands that return data, a `data` field will also be present.
|
||||||
|
|
||||||
|
### `updatesubscribe` Output
|
||||||
|
|
||||||
|
The `updatesubscribe` command provides detailed progress and final results in JSON format.
|
||||||
|
|
||||||
|
**Progress Logs:**
|
||||||
|
|
||||||
|
During an update, you will receive `Progress` or `ProgressError` messages.
|
||||||
|
|
||||||
|
- **`Progress`**: Indicates a successful step in the update process.
|
||||||
|
- **`ProgressError`**: Indicates an error occurred while updating a specific comic.
|
||||||
|
|
||||||
|
**Example `Progress` Log:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"message": "Progress",
|
||||||
|
"data": {
|
||||||
|
"current": 1,
|
||||||
|
"total": 10,
|
||||||
|
"comic": {
|
||||||
|
"id": "some-comic-id",
|
||||||
|
"name": "Some Comic Name",
|
||||||
|
"coverUrl": "https://example.com/cover.jpg",
|
||||||
|
"author": "Author Name",
|
||||||
|
"type": "source-key",
|
||||||
|
"updateTime": "2023-10-27T12:00:00Z",
|
||||||
|
"tags": ["tag1", "tag2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example `ProgressError` Log:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"message": "ProgressError",
|
||||||
|
"data": {
|
||||||
|
"current": 2,
|
||||||
|
"total": 10,
|
||||||
|
"comic": {
|
||||||
|
"id": "another-comic-id",
|
||||||
|
"name": "Another Comic Name",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"error": "Error message here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Final Output:**
|
||||||
|
|
||||||
|
Once the update process is complete, a final JSON object is returned with a list of all comics that have been updated.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Updated comics list.",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "some-comic-id",
|
||||||
|
"name": "Some Comic Name",
|
||||||
|
"coverUrl": "https://example.com/cover.jpg",
|
||||||
|
"author": "Author Name",
|
||||||
|
"type": "source-key",
|
||||||
|
"updateTime": "2023-10-27T12:00:00Z",
|
||||||
|
"tags": ["tag1", "tag2"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -17,6 +17,7 @@ ImageProvider? _findImageProvider(Comic comic) {
|
|||||||
comic.cover,
|
comic.cover,
|
||||||
sourceKey: comic.sourceKey,
|
sourceKey: comic.sourceKey,
|
||||||
cid: comic.id,
|
cid: comic.id,
|
||||||
|
fallbackToLocalCover: comic is FavoriteItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
@@ -21,11 +22,13 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
|
|||||||
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
||||||
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/cloudflare.dart';
|
import 'package:venera/network/cloudflare.dart';
|
||||||
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
||||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
@@ -37,9 +37,11 @@ mixin class JsUiApi {
|
|||||||
case 'showInputDialog':
|
case 'showInputDialog':
|
||||||
var title = message['title'];
|
var title = message['title'];
|
||||||
var validator = message['validator'];
|
var validator = message['validator'];
|
||||||
|
var image = message['image'];
|
||||||
if (title is! String) return;
|
if (title is! String) return;
|
||||||
if (validator != null && validator is! JSInvokable) return;
|
if (validator != null && validator is! JSInvokable) return;
|
||||||
return _showInputDialog(title, validator);
|
if (image != null && image is! String) return;
|
||||||
|
return _showInputDialog(title, validator, image);
|
||||||
case 'showSelectDialog':
|
case 'showSelectDialog':
|
||||||
var title = message['title'];
|
var title = message['title'];
|
||||||
var options = message['options'];
|
var options = message['options'];
|
||||||
@@ -124,12 +126,13 @@ mixin class JsUiApi {
|
|||||||
controller?.close();
|
controller?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _showInputDialog(String title, JSInvokable? validator) async {
|
Future<String?> _showInputDialog(String title, JSInvokable? validator, String? image) async {
|
||||||
String? result;
|
String? result;
|
||||||
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
||||||
await showInputDialog(
|
await showInputDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
title: title,
|
title: title,
|
||||||
|
image: image,
|
||||||
onConfirm: (v) {
|
onConfirm: (v) {
|
||||||
if (func != null) {
|
if (func != null) {
|
||||||
var res = func.call([v]);
|
var res = func.call([v]);
|
||||||
|
@@ -7,6 +7,7 @@ class NetworkError extends StatelessWidget {
|
|||||||
this.retry,
|
this.retry,
|
||||||
this.withAppbar = true,
|
this.withAppbar = true,
|
||||||
this.buttonText,
|
this.buttonText,
|
||||||
|
this.action,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
@@ -17,6 +18,8 @@ class NetworkError extends StatelessWidget {
|
|||||||
|
|
||||||
final String? buttonText;
|
final String? buttonText;
|
||||||
|
|
||||||
|
final Widget? action;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var cfe = CloudflareException.fromString(message);
|
var cfe = CloudflareException.fromString(message);
|
||||||
@@ -41,18 +44,22 @@ class NetworkError extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
cfe == null ? message : "Cloudflare verification required".tl,
|
cfe == null ? message : "Cloudflare verification required".tl,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
if (retry != null)
|
TextButton(
|
||||||
const SizedBox(
|
onPressed: () {
|
||||||
height: 12,
|
saveFile(
|
||||||
|
data: utf8.encode(Log().toString()),
|
||||||
|
filename: 'log.txt',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text("Export logs".tl),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
if (retry != null)
|
if (retry != null)
|
||||||
if (cfe != null)
|
if (cfe != null)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
@@ -63,26 +70,29 @@ class NetworkError extends StatelessWidget {
|
|||||||
child: Text('Verify'.tl),
|
child: Text('Verify'.tl),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (action != null)
|
||||||
|
action!.paddingRight(8),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: retry,
|
onPressed: retry,
|
||||||
child: Text(buttonText ?? 'Retry'.tl),
|
child: Text(buttonText ?? 'Retry'.tl),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (withAppbar) {
|
if (withAppbar) {
|
||||||
body = Column(
|
body = Column(
|
||||||
children: [
|
children: [
|
||||||
const Appbar(title: Text("")),
|
const Appbar(title: Text("")),
|
||||||
Expanded(
|
Expanded(child: body),
|
||||||
child: body,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Material(
|
return Material(child: body);
|
||||||
child: body,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +104,7 @@ class ListLoadingIndicator extends StatelessWidget {
|
|||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 80,
|
height: 80,
|
||||||
child: Center(
|
child: Center(child: FiveDotLoadingAnimation()),
|
||||||
child: FiveDotLoadingAnimation(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +116,9 @@ class SliverListLoadingIndicator extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// SliverToBoxAdapter can not been lazy loaded.
|
// SliverToBoxAdapter can not been lazy loaded.
|
||||||
// Use SliverList to make sure the animation can be lazy loaded.
|
// Use SliverList to make sure the animation can be lazy loaded.
|
||||||
return SliverList.list(children: const [
|
return SliverList.list(
|
||||||
SizedBox(),
|
children: const [SizedBox(), ListLoadingIndicator()],
|
||||||
ListLoadingIndicator(),
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +185,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildError() {
|
Widget buildError() {
|
||||||
return NetworkError(
|
return NetworkError(message: error!, retry: retry);
|
||||||
message: error!,
|
|
||||||
retry: retry,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -323,11 +327,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildError(BuildContext context, String error) {
|
Widget buildError(BuildContext context, String error) {
|
||||||
return NetworkError(
|
return NetworkError(withAppbar: false, message: error, retry: reset);
|
||||||
withAppbar: false,
|
|
||||||
message: error,
|
|
||||||
retry: reset,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -388,7 +388,7 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
|
|||||||
Colors.green,
|
Colors.green,
|
||||||
Colors.blue,
|
Colors.blue,
|
||||||
Colors.yellow,
|
Colors.yellow,
|
||||||
Colors.purple
|
Colors.purple,
|
||||||
];
|
];
|
||||||
|
|
||||||
static const _padding = 12.0;
|
static const _padding = 12.0;
|
||||||
@@ -405,11 +405,10 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _dotSize * 5 + _padding * 6,
|
width: _dotSize * 5 + _padding * 6,
|
||||||
height: _height,
|
height: _height,
|
||||||
child: Stack(
|
child: Stack(children: List.generate(5, (index) => buildDot(index))),
|
||||||
children: List.generate(5, (index) => buildDot(index)),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildDot(int index) {
|
Widget buildDot(int index) {
|
||||||
@@ -417,7 +416,8 @@ class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
|
|||||||
var startValue = index * 0.8;
|
var startValue = index * 0.8;
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: index * _dotSize + (index + 1) * _padding,
|
left: index * _dotSize + (index + 1) * _padding,
|
||||||
bottom: (math.sin(math.pi / 2 * (value - startValue).clamp(0, 2))) *
|
bottom:
|
||||||
|
(math.sin(math.pi / 2 * (value - startValue).clamp(0, 2))) *
|
||||||
(_height - _dotSize),
|
(_height - _dotSize),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: _dotSize,
|
width: _dotSize,
|
||||||
|
@@ -359,6 +359,7 @@ Future<void> showInputDialog({
|
|||||||
String confirmText = "Confirm",
|
String confirmText = "Confirm",
|
||||||
String cancelText = "Cancel",
|
String cancelText = "Cancel",
|
||||||
RegExp? inputValidator,
|
RegExp? inputValidator,
|
||||||
|
String? image,
|
||||||
}) {
|
}) {
|
||||||
var controller = TextEditingController(text: initialValue);
|
var controller = TextEditingController(text: initialValue);
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
@@ -371,7 +372,14 @@ Future<void> showInputDialog({
|
|||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: title,
|
title: title,
|
||||||
content: TextField(
|
content: Column(
|
||||||
|
children: [
|
||||||
|
if (image != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 108,
|
||||||
|
child: Image.network(image, fit: BoxFit.none),
|
||||||
|
).paddingBottom(8),
|
||||||
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
@@ -379,6 +387,8 @@ Future<void> showInputDialog({
|
|||||||
errorText: error,
|
errorText: error,
|
||||||
),
|
),
|
||||||
).paddingHorizontal(12),
|
).paddingHorizontal(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
|
@@ -7,8 +7,11 @@ class PaneItemEntry {
|
|||||||
|
|
||||||
IconData activeIcon;
|
IconData activeIcon;
|
||||||
|
|
||||||
PaneItemEntry(
|
PaneItemEntry({
|
||||||
{required this.label, required this.icon, required this.activeIcon});
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.activeIcon,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class PaneActionEntry {
|
class PaneActionEntry {
|
||||||
@@ -18,20 +21,24 @@ class PaneActionEntry {
|
|||||||
|
|
||||||
VoidCallback onTap;
|
VoidCallback onTap;
|
||||||
|
|
||||||
PaneActionEntry(
|
PaneActionEntry({
|
||||||
{required this.label, required this.icon, required this.onTap});
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class NaviPane extends StatefulWidget {
|
class NaviPane extends StatefulWidget {
|
||||||
const NaviPane(
|
const NaviPane({
|
||||||
{required this.paneItems,
|
required this.paneItems,
|
||||||
required this.paneActions,
|
required this.paneActions,
|
||||||
required this.pageBuilder,
|
required this.pageBuilder,
|
||||||
this.initialPage = 0,
|
this.initialPage = 0,
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
required this.observer,
|
required this.observer,
|
||||||
required this.navigatorKey,
|
required this.navigatorKey,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final List<PaneItemEntry> paneItems;
|
final List<PaneItemEntry> paneItems;
|
||||||
|
|
||||||
@@ -187,7 +194,8 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
child: buildLeft(),
|
child: buildLeft(),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
left: _kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
|
left:
|
||||||
|
_kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
|
||||||
(_kSideBarWidth - _kFoldedSideBarWidth) *
|
(_kSideBarWidth - _kFoldedSideBarWidth) *
|
||||||
((value - 2).clamp(0, 1)),
|
((value - 2).clamp(0, 1)),
|
||||||
child: buildMainView(),
|
child: buildMainView(),
|
||||||
@@ -202,6 +210,10 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
Widget buildMainView() {
|
Widget buildMainView() {
|
||||||
return HeroControllerScope(
|
return HeroControllerScope(
|
||||||
controller: MaterialApp.createMaterialHeroController(),
|
controller: MaterialApp.createMaterialHeroController(),
|
||||||
|
child: NavigatorPopHandler(
|
||||||
|
onPopWithResult: (result) {
|
||||||
|
widget.navigatorKey.currentState?.maybePop(result);
|
||||||
|
},
|
||||||
child: Navigator(
|
child: Navigator(
|
||||||
observers: [widget.observer],
|
observers: [widget.observer],
|
||||||
key: widget.navigatorKey,
|
key: widget.navigatorKey,
|
||||||
@@ -212,6 +224,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +252,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
icon: Icon(action.icon),
|
icon: Icon(action.icon),
|
||||||
onPressed: action.onTap,
|
onPressed: action.onTap,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -261,9 +274,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: List<Widget>.generate(
|
children: List<Widget>.generate(widget.paneItems.length, (index) {
|
||||||
widget.paneItems.length,
|
|
||||||
(index) {
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: _SingleBottomNaviWidget(
|
child: _SingleBottomNaviWidget(
|
||||||
enabled: currentPage == index,
|
enabled: currentPage == index,
|
||||||
@@ -274,8 +285,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
key: ValueKey(index),
|
key: ValueKey(index),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -286,7 +296,8 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
const paddingHorizontal = 12.0;
|
const paddingHorizontal = 12.0;
|
||||||
return Material(
|
return Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: _kFoldedSideBarWidth +
|
width:
|
||||||
|
_kFoldedSideBarWidth +
|
||||||
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
|
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
|
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
|
||||||
@@ -323,9 +334,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
key: ValueKey(index + widget.paneItems.length),
|
key: ValueKey(index + widget.paneItems.length),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -334,12 +343,13 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SideNaviWidget extends StatelessWidget {
|
class _SideNaviWidget extends StatelessWidget {
|
||||||
const _SideNaviWidget(
|
const _SideNaviWidget({
|
||||||
{required this.enabled,
|
required this.enabled,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.showTitle,
|
required this.showTitle,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
@@ -368,18 +378,18 @@ class _SideNaviWidget extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(alignment: Alignment.centerLeft, child: icon),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PaneActionWidget extends StatelessWidget {
|
class _PaneActionWidget extends StatelessWidget {
|
||||||
const _PaneActionWidget(
|
const _PaneActionWidget({
|
||||||
{required this.entry, required this.showTitle, super.key});
|
required this.entry,
|
||||||
|
required this.showTitle,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final PaneActionEntry entry;
|
final PaneActionEntry entry;
|
||||||
|
|
||||||
@@ -399,21 +409,19 @@ class _PaneActionWidget extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(alignment: Alignment.centerLeft, child: icon),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SingleBottomNaviWidget extends StatefulWidget {
|
class _SingleBottomNaviWidget extends StatefulWidget {
|
||||||
const _SingleBottomNaviWidget(
|
const _SingleBottomNaviWidget({
|
||||||
{required this.enabled,
|
required this.enabled,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
@@ -482,8 +490,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
|
|||||||
Widget buildContent() {
|
Widget buildContent() {
|
||||||
final value = controller.value;
|
final value = controller.value;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final icon =
|
final icon = Icon(
|
||||||
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
|
widget.enabled ? widget.entry.activeIcon : widget.entry.icon,
|
||||||
|
);
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 64,
|
width: 64,
|
||||||
@@ -570,8 +579,11 @@ class NaviObserver extends NavigatorObserver implements Listenable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NaviPopScope extends StatelessWidget {
|
class _NaviPopScope extends StatelessWidget {
|
||||||
const _NaviPopScope(
|
const _NaviPopScope({
|
||||||
{required this.child, this.popGesture = false, required this.action});
|
required this.child,
|
||||||
|
this.popGesture = false,
|
||||||
|
required this.action,
|
||||||
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool popGesture;
|
final bool popGesture;
|
||||||
@@ -581,15 +593,7 @@ class _NaviPopScope extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget res = App.isIOS
|
Widget res = child;
|
||||||
? child
|
|
||||||
: PopScope(
|
|
||||||
canPop: App.isAndroid ? false : true,
|
|
||||||
onPopInvokedWithResult: (value, result) {
|
|
||||||
action();
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (popGesture) {
|
if (popGesture) {
|
||||||
res = GestureDetector(
|
res = GestureDetector(
|
||||||
onPanStart: (details) {
|
onPanStart: (details) {
|
||||||
@@ -606,7 +610,8 @@ class _NaviPopScope extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
panStartAtEdge = false;
|
panStartAtEdge = false;
|
||||||
},
|
},
|
||||||
child: res);
|
child: res,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@@ -117,16 +117,25 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
_futurePosition ??= currentLocation;
|
_futurePosition ??= currentLocation;
|
||||||
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
||||||
_futurePosition = _futurePosition! + pointerSignal.scrollDelta.dy * k;
|
_futurePosition = _futurePosition! + pointerSignal.scrollDelta.dy * k;
|
||||||
|
var beforeOffset = (_futurePosition! - currentLocation).abs();
|
||||||
_futurePosition = _futurePosition!.clamp(
|
_futurePosition = _futurePosition!.clamp(
|
||||||
_controller.position.minScrollExtent,
|
_controller.position.minScrollExtent,
|
||||||
_controller.position.maxScrollExtent,
|
_controller.position.maxScrollExtent,
|
||||||
);
|
);
|
||||||
|
var afterOffset = (_futurePosition! - currentLocation).abs();
|
||||||
if (_futurePosition == old) return;
|
if (_futurePosition == old) return;
|
||||||
var target = _futurePosition!;
|
var target = _futurePosition!;
|
||||||
|
var duration = _fastAnimationDuration;
|
||||||
|
if (afterOffset < beforeOffset) {
|
||||||
|
duration = duration * (afterOffset / beforeOffset);
|
||||||
|
if (duration < Duration(milliseconds: 10)) {
|
||||||
|
duration = Duration(milliseconds: 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
_controller
|
_controller
|
||||||
.animateTo(
|
.animateTo(
|
||||||
_futurePosition!,
|
_futurePosition!,
|
||||||
duration: _fastAnimationDuration,
|
duration: duration,
|
||||||
curve: Curves.linear,
|
curve: Curves.linear,
|
||||||
)
|
)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
@@ -228,7 +237,7 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
|
|
||||||
double viewHeight = 0;
|
double viewHeight = 0;
|
||||||
|
|
||||||
final _scrollIndicatorSize = App.isDesktop ? 42.0 : 64.0;
|
final _scrollIndicatorSize = App.isDesktop ? 36.0 : 54.0;
|
||||||
|
|
||||||
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
||||||
|
|
||||||
@@ -345,7 +354,7 @@ class _ScrollIndicatorPainter extends CustomPainter {
|
|||||||
Offset(size.width, 0),
|
Offset(size.width, 0),
|
||||||
radius: Radius.circular(size.width),
|
radius: Radius.circular(size.width),
|
||||||
);
|
);
|
||||||
canvas.drawShadow(path, shadowColor, 4, true);
|
canvas.drawShadow(path, shadowColor, 2, true);
|
||||||
var backgroundPaint = Paint()
|
var backgroundPaint = Paint()
|
||||||
..color = backgroundColor
|
..color = backgroundColor
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.4.5";
|
final version = "1.5.1";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
@@ -30,6 +30,10 @@ class _App {
|
|||||||
|
|
||||||
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
|
// Whether the app has been initialized.
|
||||||
|
// If current Isolate is main Isolate, this value is always true.
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
Locale get locale {
|
Locale get locale {
|
||||||
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
||||||
if (deviceLocale.languageCode == "zh" &&
|
if (deviceLocale.languageCode == "zh" &&
|
||||||
@@ -81,6 +85,7 @@ class _App {
|
|||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
||||||
}
|
}
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initComponents() async {
|
Future<void> initComponents() async {
|
||||||
|
@@ -2,6 +2,7 @@ import 'dart:math';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
|
||||||
const double _kBackGestureWidth = 20.0;
|
const double _kBackGestureWidth = 20.0;
|
||||||
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
||||||
@@ -115,7 +116,14 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
return SlidePageTransitionBuilder().buildTransitions(
|
PageTransitionsBuilder builder;
|
||||||
|
if (App.isAndroid) {
|
||||||
|
builder = PredictiveBackPageTransitionsBuilder();
|
||||||
|
} else {
|
||||||
|
builder = SlidePageTransitionBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.buildTransitions(
|
||||||
this,
|
this,
|
||||||
context,
|
context,
|
||||||
animation,
|
animation,
|
||||||
|
@@ -26,8 +26,7 @@ class Appdata with Init {
|
|||||||
var data = jsonEncode(toJson());
|
var data = jsonEncode(toJson());
|
||||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||||
await file.writeAsString(data);
|
await file.writeAsString(data);
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
_isSavingData = false;
|
_isSavingData = false;
|
||||||
}
|
}
|
||||||
if (sync) {
|
if (sync) {
|
||||||
@@ -57,10 +56,7 @@ class Appdata with Init {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'settings': settings._data, 'searchHistory': searchHistory};
|
||||||
'settings': settings._data,
|
|
||||||
'searchHistory': searchHistory,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Following fields are related to device-specific data and should not be synced.
|
/// Following fields are related to device-specific data and should not be synced.
|
||||||
@@ -95,8 +91,7 @@ class Appdata with Init {
|
|||||||
try {
|
try {
|
||||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||||
await file.writeAsString(jsonEncode(implicitData));
|
await file.writeAsString(jsonEncode(implicitData));
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
_isSavingData = false;
|
_isSavingData = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,10 +99,7 @@ class Appdata with Init {
|
|||||||
@override
|
@override
|
||||||
Future<void> doInit() async {
|
Future<void> doInit() async {
|
||||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
var file = File(FilePath.join(
|
var file = File(FilePath.join(dataPath, 'appdata.json'));
|
||||||
dataPath,
|
|
||||||
'appdata.json',
|
|
||||||
));
|
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,8 +111,7 @@ class Appdata with Init {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
searchHistory = List.from(json['searchHistory']);
|
searchHistory = List.from(json['searchHistory']);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
Log.error("Appdata", "Failed to load appdata", e);
|
Log.error("Appdata", "Failed to load appdata", e);
|
||||||
Log.info("Appdata", "Resetting appdata");
|
Log.info("Appdata", "Resetting appdata");
|
||||||
file.deleteIgnoreError();
|
file.deleteIgnoreError();
|
||||||
@@ -130,8 +121,7 @@ class Appdata with Init {
|
|||||||
if (await implicitDataFile.exists()) {
|
if (await implicitDataFile.exists()) {
|
||||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
Log.error("Appdata", "Failed to load implicit data", e);
|
Log.error("Appdata", "Failed to load implicit data", e);
|
||||||
Log.info("Appdata", "Resetting implicit data");
|
Log.info("Appdata", "Resetting implicit data");
|
||||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||||
@@ -189,7 +179,7 @@ class Settings with ChangeNotifier {
|
|||||||
'customImageProcessing': defaultCustomImageProcessing,
|
'customImageProcessing': defaultCustomImageProcessing,
|
||||||
'sni': true,
|
'sni': true,
|
||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
'comicSourceListUrl': '',
|
'comicSourceListUrl': _defaultSourceListUrl,
|
||||||
'preloadImageCount': 4,
|
'preloadImageCount': 4,
|
||||||
'followUpdatesFolder': null,
|
'followUpdatesFolder': null,
|
||||||
'initialPage': '0',
|
'initialPage': '0',
|
||||||
@@ -199,6 +189,9 @@ class Settings with ChangeNotifier {
|
|||||||
'enableDoubleTapToZoom': true,
|
'enableDoubleTapToZoom': true,
|
||||||
'reverseChapterOrder': false,
|
'reverseChapterOrder': false,
|
||||||
'showSystemStatusBar': false,
|
'showSystemStatusBar': false,
|
||||||
|
'comicSpecificSettings': <String, Map<String, dynamic>>{},
|
||||||
|
'ignoreBadCertificate': false,
|
||||||
|
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
@@ -212,6 +205,43 @@ class Settings with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setEnabledComicSpecificSettings(String comicId, String sourceKey, bool enabled) {
|
||||||
|
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isComicSpecificSettingsEnabled(String? comicId, String? sourceKey) {
|
||||||
|
if (comicId == null || sourceKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
||||||
|
if (!isComicSpecificSettingsEnabled(comicId, sourceKey)) {
|
||||||
|
return _data[key];
|
||||||
|
}
|
||||||
|
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?[key] ??
|
||||||
|
_data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
void setReaderSetting(
|
||||||
|
String comicId,
|
||||||
|
String sourceKey,
|
||||||
|
String key,
|
||||||
|
dynamic value,
|
||||||
|
) {
|
||||||
|
(_data['comicSpecificSettings'] as Map<String, dynamic>).putIfAbsent(
|
||||||
|
"$comicId@$sourceKey",
|
||||||
|
() => <String, dynamic>{},
|
||||||
|
)[key] = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetComicReaderSettings(String key) {
|
||||||
|
(_data['comicSpecificSettings'] as Map).remove(key);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return _data.toString();
|
return _data.toString();
|
||||||
@@ -235,3 +265,6 @@ function processImage(image, cid, eid, page, sourceKey) {
|
|||||||
return futureImage;
|
return futureImage;
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
const _defaultSourceListUrl =
|
||||||
|
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json";
|
||||||
|
@@ -401,9 +401,14 @@ class SearchOptions {
|
|||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
String category, String? param, List<String> options, int page);
|
String category, String? param, List<String> options, int page);
|
||||||
|
|
||||||
|
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
|
||||||
|
String category, String? param);
|
||||||
|
|
||||||
class CategoryComicsData {
|
class CategoryComicsData {
|
||||||
/// options
|
/// options
|
||||||
final List<CategoryComicsOptions> options;
|
final List<CategoryComicsOptions>? options;
|
||||||
|
|
||||||
|
final CategoryOptionsLoader? optionsLoader;
|
||||||
|
|
||||||
/// [category] is the one clicked by the user on the category page.
|
/// [category] is the one clicked by the user on the category page.
|
||||||
///
|
///
|
||||||
@@ -414,7 +419,7 @@ class CategoryComicsData {
|
|||||||
|
|
||||||
final RankingData? rankingData;
|
final RankingData? rankingData;
|
||||||
|
|
||||||
const CategoryComicsData(this.options, this.load, {this.rankingData});
|
const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
|
||||||
}
|
}
|
||||||
|
|
||||||
class RankingData {
|
class RankingData {
|
||||||
@@ -429,6 +434,9 @@ class RankingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CategoryComicsOptions {
|
class CategoryComicsOptions {
|
||||||
|
// The label will not be displayed if it is empty.
|
||||||
|
final String label;
|
||||||
|
|
||||||
/// Use a [LinkedHashMap] to describe an option list.
|
/// Use a [LinkedHashMap] to describe an option list.
|
||||||
/// key is for loading comics, value is the name displayed on screen.
|
/// key is for loading comics, value is the name displayed on screen.
|
||||||
/// Default value will be the first of the Map.
|
/// Default value will be the first of the Map.
|
||||||
@@ -439,7 +447,7 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
final List<String>? showWhen;
|
final List<String>? showWhen;
|
||||||
|
|
||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
|
@@ -64,8 +64,13 @@ class ComicSourceParser {
|
|||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (file.existsSync()) {
|
while (file.existsSync()) {
|
||||||
file = File(FilePath.join(App.dataPath, "comic_source",
|
file = File(
|
||||||
"${fileName.split('.').first}($i).js"));
|
FilePath.join(
|
||||||
|
App.dataPath,
|
||||||
|
"comic_source",
|
||||||
|
"${fileName.split('.').first}($i).js",
|
||||||
|
),
|
||||||
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,8 +85,9 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
Future<ComicSource> parse(String js, String filePath) async {
|
Future<ComicSource> parse(String js, String filePath) async {
|
||||||
js = js.replaceAll("\r\n", "\n");
|
js = js.replaceAll("\r\n", "\n");
|
||||||
var line1 =
|
var line1 = js
|
||||||
js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class "));
|
.split('\n')
|
||||||
|
.firstWhereOrNull((e) => e.trim().startsWith("class "));
|
||||||
if (line1 == null ||
|
if (line1 == null ||
|
||||||
!line1.startsWith("class ") ||
|
!line1.startsWith("class ") ||
|
||||||
!line1.contains("extends ComicSource")) {
|
!line1.contains("extends ComicSource")) {
|
||||||
@@ -89,24 +95,27 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
var className = line1.split("class")[1].split("extends ComicSource").first;
|
var className = line1.split("class")[1].split("extends ComicSource").first;
|
||||||
className = className.trim();
|
className = className.trim();
|
||||||
JsEngine().runCode("""
|
JsEngine().runCode("""(() => { $js
|
||||||
(() => { $js
|
|
||||||
this['temp'] = new $className()
|
this['temp'] = new $className()
|
||||||
}).call()
|
}).call()
|
||||||
""", className);
|
""", className);
|
||||||
_name = JsEngine().runCode("this['temp'].name") ??
|
_name =
|
||||||
|
JsEngine().runCode("this['temp'].name") ??
|
||||||
(throw ComicSourceParseException('name is required'));
|
(throw ComicSourceParseException('name is required'));
|
||||||
var key = JsEngine().runCode("this['temp'].key") ??
|
var key =
|
||||||
|
JsEngine().runCode("this['temp'].key") ??
|
||||||
(throw ComicSourceParseException('key is required'));
|
(throw ComicSourceParseException('key is required'));
|
||||||
var version = JsEngine().runCode("this['temp'].version") ??
|
var version =
|
||||||
|
JsEngine().runCode("this['temp'].version") ??
|
||||||
(throw ComicSourceParseException('version is required'));
|
(throw ComicSourceParseException('version is required'));
|
||||||
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
||||||
var url = JsEngine().runCode("this['temp'].url");
|
var url = JsEngine().runCode("this['temp'].url");
|
||||||
if (minAppVersion != null) {
|
if (minAppVersion != null) {
|
||||||
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
||||||
throw ComicSourceParseException(
|
throw ComicSourceParseException(
|
||||||
"minAppVersion @version is required"
|
"minAppVersion @version is required".tlParams({
|
||||||
.tlParams({"version": minAppVersion}),
|
"version": minAppVersion,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,8 +184,10 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _checkExists(String index) {
|
bool _checkExists(String index) {
|
||||||
return JsEngine().runCode("ComicSource.sources.$_key.$index !== null "
|
return JsEngine().runCode(
|
||||||
"&& ComicSource.sources.$_key.$index !== undefined");
|
"ComicSource.sources.$_key.$index !== null "
|
||||||
|
"&& ComicSource.sources.$_key.$index !== undefined",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic _getValue(String index) {
|
dynamic _getValue(String index) {
|
||||||
@@ -277,16 +288,24 @@ class ComicSourceParser {
|
|||||||
if (type == "singlePageWithMultiPart") {
|
if (type == "singlePageWithMultiPart") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine()
|
var res = await JsEngine().runCode(
|
||||||
.runCode("ComicSource.sources.$_key.explore[$i].load()");
|
"ComicSource.sources.$_key.explore[$i].load()",
|
||||||
return Res(List.from(res.keys
|
);
|
||||||
.map((e) => ExplorePagePart(
|
return Res(
|
||||||
|
List.from(
|
||||||
|
res.keys
|
||||||
|
.map(
|
||||||
|
(e) => ExplorePagePart(
|
||||||
e,
|
e,
|
||||||
(res[e] as List)
|
(res[e] as List)
|
||||||
.map<Comic>((e) => Comic.fromJson(e, _key!))
|
.map<Comic>((e) => Comic.fromJson(e, _key!))
|
||||||
.toList(),
|
.toList(),
|
||||||
null))
|
null,
|
||||||
.toList()));
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Data Analysis", "$e\n$s");
|
Log.error("Data Analysis", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -297,11 +316,15 @@ class ComicSourceParser {
|
|||||||
loadPage = (int page) async {
|
loadPage = (int page) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})");
|
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -311,10 +334,13 @@ class ComicSourceParser {
|
|||||||
loadNext = (next) async {
|
loadNext = (next) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})");
|
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -326,8 +352,9 @@ class ComicSourceParser {
|
|||||||
} else if (type == "multiPartPage") {
|
} else if (type == "multiPartPage") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine()
|
var res = await JsEngine().runCode(
|
||||||
.runCode("ComicSource.sources.$_key.explore[$i].load()");
|
"ComicSource.sources.$_key.explore[$i].load()",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.from(
|
List.from(
|
||||||
(res as List).map((e) {
|
(res as List).map((e) {
|
||||||
@@ -350,19 +377,22 @@ class ComicSourceParser {
|
|||||||
loadMixed = (index) async {
|
loadMixed = (index) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})");
|
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})",
|
||||||
|
);
|
||||||
var list = <Object>[];
|
var list = <Object>[];
|
||||||
for (var data in (res['data'] as List)) {
|
for (var data in (res['data'] as List)) {
|
||||||
if (data is List) {
|
if (data is List) {
|
||||||
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
|
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
|
||||||
} else if (data is Map) {
|
} else if (data is Map) {
|
||||||
list.add(ExplorePagePart(
|
list.add(
|
||||||
|
ExplorePagePart(
|
||||||
data['title'],
|
data['title'],
|
||||||
(data['comics'] as List).map((e) {
|
(data['comics'] as List).map((e) {
|
||||||
return Comic.fromJson(e, _key!);
|
return Comic.fromJson(e, _key!);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
data['viewMore'],
|
data['viewMore'],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Res(list, subData: res['maxPage']);
|
return Res(list, subData: res['maxPage']);
|
||||||
@@ -372,21 +402,25 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pages.add(ExplorePageData(
|
pages.add(
|
||||||
|
ExplorePageData(
|
||||||
title,
|
title,
|
||||||
switch (type) {
|
switch (type) {
|
||||||
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart,
|
"singlePageWithMultiPart" =>
|
||||||
|
ExplorePageType.singlePageWithMultiPart,
|
||||||
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
||||||
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
||||||
"mixed" => ExplorePageType.mixed,
|
"mixed" => ExplorePageType.mixed,
|
||||||
_ =>
|
_ => throw ComicSourceParseException(
|
||||||
throw ComicSourceParseException("Unknown explore page type $type")
|
"Unknown explore page type $type",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
loadPage,
|
loadPage,
|
||||||
loadNext,
|
loadNext,
|
||||||
loadMultiPart,
|
loadMultiPart,
|
||||||
loadMixed,
|
loadMixed,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
@@ -426,18 +460,17 @@ class ComicSourceParser {
|
|||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts.add(FixedCategoryPart(name, cs!));
|
categoryParts.add(FixedCategoryPart(name, cs!));
|
||||||
} else if (type == "random") {
|
} else if (type == "random") {
|
||||||
categoryParts
|
categoryParts.add(
|
||||||
.add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1));
|
RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1),
|
||||||
|
);
|
||||||
} else if (type == "dynamic" && categories == null) {
|
} else if (type == "dynamic" && categories == null) {
|
||||||
var loader = c["loader"];
|
var loader = c["loader"];
|
||||||
if (loader is! JSInvokable) {
|
if (loader is! JSInvokable) {
|
||||||
throw "DynamicCategoryPart loader must be a function";
|
throw "DynamicCategoryPart loader must be a function";
|
||||||
}
|
}
|
||||||
categoryParts.add(DynamicCategoryPart(
|
categoryParts.add(
|
||||||
name,
|
DynamicCategoryPart(name, JSAutoFreeFunction(loader), _key!),
|
||||||
JSAutoFreeFunction(loader),
|
);
|
||||||
_key!,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// old format
|
// old format
|
||||||
@@ -454,30 +487,16 @@ class ComicSourceParser {
|
|||||||
for (int i = 0; i < tags.length; i++) {
|
for (int i = 0; i < tags.length; i++) {
|
||||||
PageJumpTarget target;
|
PageJumpTarget target;
|
||||||
if (itemType == 'category') {
|
if (itemType == 'category') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'category', {
|
||||||
_key!,
|
|
||||||
'category',
|
|
||||||
{
|
|
||||||
"category": tags[i],
|
"category": tags[i],
|
||||||
"param": categoryParams?.elementAtOrNull(i),
|
"param": categoryParams?.elementAtOrNull(i),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else if (itemType == 'search') {
|
} else if (itemType == 'search') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'search', {"keyword": tags[i]});
|
||||||
_key!,
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
"keyword": tags[i],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (itemType == 'search_with_namespace') {
|
} else if (itemType == 'search_with_namespace') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'search', {
|
||||||
_key!,
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
"keyword": "$name:$tags[i]",
|
"keyword": "$name:$tags[i]",
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
target = PageJumpTarget(_key!, itemType, null);
|
target = PageJumpTarget(_key!, itemType, null);
|
||||||
}
|
}
|
||||||
@@ -486,8 +505,9 @@ class ComicSourceParser {
|
|||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts.add(FixedCategoryPart(name, cs));
|
categoryParts.add(FixedCategoryPart(name, cs));
|
||||||
} else if (type == "random") {
|
} else if (type == "random") {
|
||||||
categoryParts
|
categoryParts.add(
|
||||||
.add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1));
|
RandomCategoryPart(name, cs, c["randomNumber"] ?? 1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,12 +516,16 @@ class ComicSourceParser {
|
|||||||
title: title,
|
title: title,
|
||||||
categories: categoryParts,
|
categories: categoryParts,
|
||||||
enableRankingPage: enableRankingPage ?? false,
|
enableRankingPage: enableRankingPage ?? false,
|
||||||
key: title);
|
key: title,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryComicsData? _loadCategoryComicsData() {
|
CategoryComicsData? _loadCategoryComicsData() {
|
||||||
if (!_checkExists("categoryComics")) return null;
|
if (!_checkExists("categoryComics")) return null;
|
||||||
var options = <CategoryComicsOptions>[];
|
|
||||||
|
List<CategoryComicsOptions>? options;
|
||||||
|
if (_checkExists("categoryComics.optionList")) {
|
||||||
|
options = <CategoryComicsOptions>[];
|
||||||
for (var element in _getValue("categoryComics.optionList") ?? []) {
|
for (var element in _getValue("categoryComics.optionList") ?? []) {
|
||||||
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
for (var option in element["options"]) {
|
for (var option in element["options"]) {
|
||||||
@@ -513,11 +537,64 @@ class ComicSourceParser {
|
|||||||
var value = split.join("-");
|
var value = split.join("-");
|
||||||
map[key] = value;
|
map[key] = value;
|
||||||
}
|
}
|
||||||
options.add(CategoryComicsOptions(
|
options.add(
|
||||||
|
CategoryComicsOptions(
|
||||||
|
element["label"] ?? "",
|
||||||
map,
|
map,
|
||||||
List.from(element["notShowWhen"] ?? []),
|
List.from(element["notShowWhen"] ?? []),
|
||||||
element["showWhen"] == null ? null : List.from(element["showWhen"])));
|
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryOptionsLoader? optionLoader;
|
||||||
|
if (_checkExists("categoryComics.optionLoader")) {
|
||||||
|
optionLoader = (category, param) async {
|
||||||
|
try {
|
||||||
|
dynamic res = JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.categoryComics.optionLoader(
|
||||||
|
${jsonEncode(category)}, ${jsonEncode(param)})
|
||||||
|
""");
|
||||||
|
if (res is Future) {
|
||||||
|
res = await res;
|
||||||
|
}
|
||||||
|
if (res is! List) {
|
||||||
|
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
|
||||||
|
}
|
||||||
|
var options = <CategoryComicsOptions>[];
|
||||||
|
for (var element in res) {
|
||||||
|
if (element is! Map) {
|
||||||
|
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
|
||||||
|
}
|
||||||
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
|
for (var option in element["options"] ?? []) {
|
||||||
|
if (option.isEmpty || !option.contains("-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var split = option.split("-");
|
||||||
|
var key = split.removeAt(0);
|
||||||
|
var value = split.join("-");
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
options.add(
|
||||||
|
CategoryComicsOptions(
|
||||||
|
element["label"] ?? "",
|
||||||
|
map,
|
||||||
|
List.from(element["notShowWhen"] ?? []),
|
||||||
|
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Res(options);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
RankingData? rankingData;
|
RankingData? rankingData;
|
||||||
if (_checkExists("categoryComics.ranking")) {
|
if (_checkExists("categoryComics.ranking")) {
|
||||||
var options = <String, String>{};
|
var options = <String, String>{};
|
||||||
@@ -541,9 +618,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(option)}, ${jsonEncode(page)})
|
${jsonEncode(option)}, ${jsonEncode(page)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -557,8 +637,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(option)}, ${jsonEncode(next)})
|
${jsonEncode(option)}, ${jsonEncode(next)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -569,7 +651,15 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
rankingData = RankingData(options, load, loadWithNext);
|
rankingData = RankingData(options, load, loadWithNext);
|
||||||
}
|
}
|
||||||
return CategoryComicsData(options, (category, param, options, page) async {
|
|
||||||
|
if (options == null && optionLoader == null) {
|
||||||
|
options = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return CategoryComicsData(
|
||||||
|
options: options,
|
||||||
|
optionsLoader: optionLoader,
|
||||||
|
load: (category, param, options, page) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode("""
|
var res = await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.categoryComics.load(
|
ComicSource.sources.$_key.categoryComics.load(
|
||||||
@@ -580,14 +670,19 @@ class ComicSourceParser {
|
|||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
}, rankingData: rankingData);
|
},
|
||||||
|
rankingData: rankingData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchPageData? _loadSearchData() {
|
SearchPageData? _loadSearchData() {
|
||||||
@@ -604,12 +699,14 @@ class ComicSourceParser {
|
|||||||
var value = split.join("-");
|
var value = split.join("-");
|
||||||
map[key] = value;
|
map[key] = value;
|
||||||
}
|
}
|
||||||
options.add(SearchOptions(
|
options.add(
|
||||||
|
SearchOptions(
|
||||||
map,
|
map,
|
||||||
element["label"],
|
element["label"],
|
||||||
element['type'] ?? 'select',
|
element['type'] ?? 'select',
|
||||||
element['default'] == null ? null : jsonEncode(element['default']),
|
element['default'] == null ? null : jsonEncode(element['default']),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchFunction? loadPage;
|
SearchFunction? loadPage;
|
||||||
@@ -624,9 +721,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
|
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -640,8 +740,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
|
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -690,8 +792,9 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
final bool multiFolder = _getValue("favorites.multiFolder");
|
final bool multiFolder = _getValue("favorites.multiFolder");
|
||||||
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
||||||
final bool? singleFolderForSingleComic =
|
final bool? singleFolderForSingleComic = _getValue(
|
||||||
_getValue("favorites.singleFolderForSingleComic");
|
"favorites.singleFolderForSingleComic",
|
||||||
|
);
|
||||||
|
|
||||||
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
||||||
if (!ComicSource.find(_key!)!.isLogged) {
|
if (!ComicSource.find(_key!)!.isLogged) {
|
||||||
@@ -744,9 +847,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(page)}, ${jsonEncode(folder)})
|
${jsonEncode(page)}, ${jsonEncode(folder)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -766,8 +872,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(next)}, ${jsonEncode(folder)})
|
${jsonEncode(next)}, ${jsonEncode(folder)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -859,7 +967,8 @@ class ComicSourceParser {
|
|||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
subData: res["maxPage"]);
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -1114,7 +1223,8 @@ class ComicSourceParser {
|
|||||||
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
|
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList());
|
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList(),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
|
162
lib/foundation/follow_updates.dart
Normal file
162
lib/foundation/follow_updates.dart
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
|
||||||
|
class ComicUpdateResult {
|
||||||
|
final bool updated;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
ComicUpdateResult(this.updated, this.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ComicUpdateResult> updateComic(
|
||||||
|
FavoriteItemWithUpdateInfo c, String folder) async {
|
||||||
|
int retries = 3;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
var comicSource = c.type.comicSource;
|
||||||
|
if (comicSource == null) {
|
||||||
|
return ComicUpdateResult(false, "Comic source not found");
|
||||||
|
}
|
||||||
|
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||||
|
|
||||||
|
var newTags = <String>[];
|
||||||
|
for (var entry in newInfo.tags.entries) {
|
||||||
|
const shouldIgnore = ['author', 'artist', 'time'];
|
||||||
|
var namespace = entry.key;
|
||||||
|
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (var tag in entry.value) {
|
||||||
|
newTags.add("$namespace:$tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = FavoriteItem(
|
||||||
|
id: c.id,
|
||||||
|
name: newInfo.title,
|
||||||
|
coverPath: newInfo.cover,
|
||||||
|
author: newInfo.subTitle ??
|
||||||
|
newInfo.tags['author']?.firstOrNull ??
|
||||||
|
c.author,
|
||||||
|
type: c.type,
|
||||||
|
tags: newTags,
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalFavoritesManager().updateInfo(folder, item, false);
|
||||||
|
|
||||||
|
var updated = false;
|
||||||
|
var updateTime = newInfo.findUpdateTime();
|
||||||
|
if (updateTime != null && updateTime != c.updateTime) {
|
||||||
|
LocalFavoritesManager().updateUpdateTime(
|
||||||
|
folder,
|
||||||
|
c.id,
|
||||||
|
c.type,
|
||||||
|
updateTime,
|
||||||
|
);
|
||||||
|
updated = true;
|
||||||
|
} else {
|
||||||
|
LocalFavoritesManager().updateCheckTime(folder, c.id, c.type);
|
||||||
|
}
|
||||||
|
return ComicUpdateResult(updated, null);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Check Updates", e, s);
|
||||||
|
retries--;
|
||||||
|
if (retries == 0) {
|
||||||
|
return ComicUpdateResult(false, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateProgress {
|
||||||
|
final int total;
|
||||||
|
final int current;
|
||||||
|
final int errors;
|
||||||
|
final int updated;
|
||||||
|
final FavoriteItemWithUpdateInfo? comic;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
UpdateProgress(this.total, this.current, this.errors, this.updated,
|
||||||
|
[this.comic, this.errorMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFolderBase(
|
||||||
|
String folder,
|
||||||
|
StreamController<UpdateProgress> stream,
|
||||||
|
bool ignoreCheckTime,
|
||||||
|
) async {
|
||||||
|
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||||
|
int total = comics.length;
|
||||||
|
int current = 0;
|
||||||
|
int errors = 0;
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
stream.add(UpdateProgress(total, current, errors, updated));
|
||||||
|
|
||||||
|
var comicsToUpdate = <FavoriteItemWithUpdateInfo>[];
|
||||||
|
|
||||||
|
for (var comic in comics) {
|
||||||
|
if (!ignoreCheckTime) {
|
||||||
|
var lastCheckTime = comic.lastCheckTime;
|
||||||
|
if (lastCheckTime != null &&
|
||||||
|
DateTime.now().difference(lastCheckTime).inDays < 1) {
|
||||||
|
current++;
|
||||||
|
stream.add(UpdateProgress(total, current, errors, updated));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comicsToUpdate.add(comic);
|
||||||
|
}
|
||||||
|
|
||||||
|
total = comicsToUpdate.length;
|
||||||
|
current = 0;
|
||||||
|
stream.add(UpdateProgress(total, current, errors, updated));
|
||||||
|
|
||||||
|
var futures = <Future>[];
|
||||||
|
for (var comic in comicsToUpdate) {
|
||||||
|
var future = updateComic(comic, folder).then((result) {
|
||||||
|
current++;
|
||||||
|
if (result.updated) {
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
if (result.errorMessage != null) {
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
stream.add(
|
||||||
|
UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
|
||||||
|
});
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait(futures);
|
||||||
|
|
||||||
|
if (updated > 0) {
|
||||||
|
LocalFavoritesManager().notifyChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Stream<UpdateProgress> updateFolder(String folder, bool ignoreCheckTime) {
|
||||||
|
var stream = StreamController<UpdateProgress>();
|
||||||
|
updateFolderBase(folder, stream, ignoreCheckTime);
|
||||||
|
return stream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getUpdatedComicsAsJson(String folder) async {
|
||||||
|
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||||
|
var updatedComics = comics.where((c) => c.hasNewUpdate).toList();
|
||||||
|
var jsonList = updatedComics.map((c) => {
|
||||||
|
'id': c.id,
|
||||||
|
'name': c.name,
|
||||||
|
'coverUrl': c.coverPath,
|
||||||
|
'author': c.author,
|
||||||
|
'type': c.type.sourceKey,
|
||||||
|
'updateTime': c.updateTime,
|
||||||
|
'tags': c.tags,
|
||||||
|
}).toList();
|
||||||
|
return jsonEncode(jsonList);
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async' show Future;
|
import 'dart:async' show Future;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/network/images.dart';
|
import 'package:venera/network/images.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'base_image_provider.dart';
|
import 'base_image_provider.dart';
|
||||||
@@ -11,7 +13,12 @@ class CachedImageProvider
|
|||||||
/// Image provider for normal image.
|
/// Image provider for normal image.
|
||||||
///
|
///
|
||||||
/// [url] is the url of the image. Local file path is also supported.
|
/// [url] is the url of the image. Local file path is also supported.
|
||||||
const CachedImageProvider(this.url, {this.headers, this.sourceKey, this.cid});
|
const CachedImageProvider(this.url, {
|
||||||
|
this.headers,
|
||||||
|
this.sourceKey,
|
||||||
|
this.cid,
|
||||||
|
this.fallbackToLocalCover = false,
|
||||||
|
});
|
||||||
|
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
@@ -21,6 +28,9 @@ class CachedImageProvider
|
|||||||
|
|
||||||
final String? cid;
|
final String? cid;
|
||||||
|
|
||||||
|
// Use local cover if network image fails to load.
|
||||||
|
final bool fallbackToLocalCover;
|
||||||
|
|
||||||
static int loadingCount = 0;
|
static int loadingCount = 0;
|
||||||
|
|
||||||
static const _kMaxLoadingCount = 8;
|
static const _kMaxLoadingCount = 8;
|
||||||
@@ -49,6 +59,24 @@ class CachedImageProvider
|
|||||||
}
|
}
|
||||||
throw "Error: Empty response body.";
|
throw "Error: Empty response body.";
|
||||||
}
|
}
|
||||||
|
catch(e) {
|
||||||
|
if (fallbackToLocalCover && sourceKey != null && cid != null) {
|
||||||
|
final localComic = LocalManager().find(
|
||||||
|
cid!,
|
||||||
|
ComicType.fromKey(sourceKey!),
|
||||||
|
);
|
||||||
|
if (localComic != null) {
|
||||||
|
var file = localComic.coverFile;
|
||||||
|
if (await file.exists()) {
|
||||||
|
var data = await file.readAsBytes();
|
||||||
|
if (data.isNotEmpty) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
loadingCount--;
|
loadingCount--;
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import 'package:pointycastle/block/modes/ofb.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:venera/components/js_ui.dart';
|
import 'package:venera/components/js_ui.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/js_pool.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:venera/network/proxy.dart';
|
import 'package:venera/network/proxy.dart';
|
||||||
@@ -68,6 +69,12 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Uint8List? _jsInitCache;
|
||||||
|
|
||||||
|
static void cacheJsInit(Uint8List jsInit) {
|
||||||
|
_jsInitCache = jsInit;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@protected
|
@protected
|
||||||
Future<void> doInit() async {
|
Future<void> doInit() async {
|
||||||
@@ -75,9 +82,11 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (App.isInitialized) {
|
||||||
|
_cookieJar ??= await SingleInstanceCookieJar.createInstance();
|
||||||
|
}
|
||||||
_dio ??= AppDio(BaseOptions(
|
_dio ??= AppDio(BaseOptions(
|
||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
_cookieJar ??= SingleInstanceCookieJar.instance!;
|
|
||||||
_closed = false;
|
_closed = false;
|
||||||
_engine = FlutterQjs();
|
_engine = FlutterQjs();
|
||||||
_engine!.dispatch();
|
_engine!.dispatch();
|
||||||
@@ -86,9 +95,15 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
||||||
setGlobalFunc(["appVersion", App.version]);
|
setGlobalFunc(["appVersion", App.version]);
|
||||||
setGlobalFunc.free();
|
setGlobalFunc.free();
|
||||||
var jsInit = await rootBundle.load("assets/init.js");
|
Uint8List jsInit;
|
||||||
|
if (_jsInitCache != null) {
|
||||||
|
jsInit = _jsInitCache!;
|
||||||
|
} else {
|
||||||
|
var buffer = await rootBundle.load("assets/init.js");
|
||||||
|
jsInit = buffer.buffer.asUint8List();
|
||||||
|
}
|
||||||
_engine!
|
_engine!
|
||||||
.evaluate(utf8.decode(jsInit.buffer.asUint8List()), name: "<init>");
|
.evaluate(utf8.decode(jsInit), name: "<init>");
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
||||||
}
|
}
|
||||||
@@ -97,6 +112,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
Object? _messageReceiver(dynamic message) {
|
Object? _messageReceiver(dynamic message) {
|
||||||
try {
|
try {
|
||||||
if (message is Map<dynamic, dynamic>) {
|
if (message is Map<dynamic, dynamic>) {
|
||||||
|
if (message["method"] == null) return null;
|
||||||
String method = message["method"] as String;
|
String method = message["method"] as String;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "log":
|
case "log":
|
||||||
@@ -172,6 +188,20 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
return res?.text;
|
return res?.text;
|
||||||
});
|
});
|
||||||
|
case "compute":
|
||||||
|
final func = message["function"];
|
||||||
|
final args = message["args"];
|
||||||
|
if (func is JSInvokable) {
|
||||||
|
func.free();
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (func is! String) {
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (args != null && args is! List) {
|
||||||
|
throw "Args must be a list";
|
||||||
|
}
|
||||||
|
return JSPool().execute(func, args ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
163
lib/foundation/js_pool.dart
Normal file
163
lib/foundation/js_pool.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
|
||||||
|
class JSPool {
|
||||||
|
static final int _maxInstances = 4;
|
||||||
|
final List<IsolateJsEngine> _instances = [];
|
||||||
|
bool _isInitializing = false;
|
||||||
|
|
||||||
|
static final JSPool _singleton = JSPool._internal();
|
||||||
|
factory JSPool() {
|
||||||
|
return _singleton;
|
||||||
|
}
|
||||||
|
JSPool._internal();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_isInitializing) return;
|
||||||
|
_isInitializing = true;
|
||||||
|
var jsInitBuffer = await rootBundle.load("assets/init.js");
|
||||||
|
var jsInit = jsInitBuffer.buffer.asUint8List();
|
||||||
|
for (int i = 0; i < _maxInstances; i++) {
|
||||||
|
_instances.add(IsolateJsEngine(jsInit));
|
||||||
|
}
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
await init();
|
||||||
|
var selectedInstance = _instances[0];
|
||||||
|
for (var instance in _instances) {
|
||||||
|
if (instance.pendingTasks < selectedInstance.pendingTasks) {
|
||||||
|
selectedInstance = instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedInstance.execute(jsFunction, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsolateJsEngineInitParam {
|
||||||
|
final SendPort sendPort;
|
||||||
|
|
||||||
|
final Uint8List jsInit;
|
||||||
|
|
||||||
|
_IsolateJsEngineInitParam(this.sendPort, this.jsInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsolateJsEngine {
|
||||||
|
Isolate? _isolate;
|
||||||
|
|
||||||
|
SendPort? _sendPort;
|
||||||
|
ReceivePort? _receivePort;
|
||||||
|
|
||||||
|
int _counter = 0;
|
||||||
|
final Map<int, Completer<dynamic>> _tasks = {};
|
||||||
|
|
||||||
|
bool _isClosed = false;
|
||||||
|
|
||||||
|
int get pendingTasks => _tasks.length;
|
||||||
|
|
||||||
|
IsolateJsEngine(Uint8List jsInit) {
|
||||||
|
_receivePort = ReceivePort();
|
||||||
|
_receivePort!.listen(_onMessage);
|
||||||
|
Isolate.spawn(_run, _IsolateJsEngineInitParam(_receivePort!.sendPort, jsInit));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessage(dynamic message) {
|
||||||
|
if (message is SendPort) {
|
||||||
|
_sendPort = message;
|
||||||
|
} else if (message is TaskResult) {
|
||||||
|
final completer = _tasks.remove(message.id);
|
||||||
|
if (completer != null) {
|
||||||
|
if (message.error != null) {
|
||||||
|
completer.completeError(message.error!);
|
||||||
|
} else {
|
||||||
|
completer.complete(message.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message is Exception) {
|
||||||
|
Log.error("IsolateJsEngine", message.toString());
|
||||||
|
for (var completer in _tasks.values) {
|
||||||
|
completer.completeError(message);
|
||||||
|
}
|
||||||
|
_tasks.clear();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _run(_IsolateJsEngineInitParam params) async {
|
||||||
|
var sendPort = params.sendPort;
|
||||||
|
final port = ReceivePort();
|
||||||
|
sendPort.send(port.sendPort);
|
||||||
|
final engine = JsEngine();
|
||||||
|
try {
|
||||||
|
JsEngine.cacheJsInit(params.jsInit);
|
||||||
|
await engine.init();
|
||||||
|
}
|
||||||
|
catch(e, s) {
|
||||||
|
sendPort.send(Exception("Failed to initialize JS engine: $e\n$s"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await for (final message in port) {
|
||||||
|
if (message is Task) {
|
||||||
|
try {
|
||||||
|
final jsFunc = engine.runCode(message.jsFunction);
|
||||||
|
if (jsFunc is! JSInvokable) {
|
||||||
|
throw Exception("The provided code does not evaluate to a function.");
|
||||||
|
}
|
||||||
|
final result = jsFunc.invoke(message.args);
|
||||||
|
jsFunc.free();
|
||||||
|
sendPort.send(TaskResult(message.id, result, null));
|
||||||
|
} catch (e) {
|
||||||
|
sendPort.send(TaskResult(message.id, null, e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
if (_isClosed) {
|
||||||
|
throw Exception("IsolateJsEngine is closed.");
|
||||||
|
}
|
||||||
|
while (_sendPort == null) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
final taskId = _counter++;
|
||||||
|
_tasks[taskId] = completer;
|
||||||
|
final task = Task(taskId, jsFunction, args);
|
||||||
|
_sendPort?.send(task);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() async {
|
||||||
|
if (!_isClosed) {
|
||||||
|
_isClosed = true;
|
||||||
|
while (_tasks.isNotEmpty) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
_receivePort?.close();
|
||||||
|
_isolate?.kill(priority: Isolate.immediate);
|
||||||
|
_isolate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Task {
|
||||||
|
final int id;
|
||||||
|
final String jsFunction;
|
||||||
|
final List<dynamic> args;
|
||||||
|
|
||||||
|
const Task(this.id, this.jsFunction, this.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskResult {
|
||||||
|
final int id;
|
||||||
|
final Object? result;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const TaskResult(this.id, this.result, this.error);
|
||||||
|
}
|
@@ -28,6 +28,8 @@ class Log {
|
|||||||
|
|
||||||
static bool ignoreLimitation = false;
|
static bool ignoreLimitation = false;
|
||||||
|
|
||||||
|
static bool isMuted = false;
|
||||||
|
|
||||||
static void printWarning(String text) {
|
static void printWarning(String text) {
|
||||||
debugPrint('\x1B[33m$text\x1B[0m');
|
debugPrint('\x1B[33m$text\x1B[0m');
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,8 @@ class Log {
|
|||||||
static IOSink? _file;
|
static IOSink? _file;
|
||||||
|
|
||||||
static void addLog(LogLevel level, String title, String content) {
|
static void addLog(LogLevel level, String title, String content) {
|
||||||
if (_file == null) {
|
if (isMuted) return;
|
||||||
|
if (_file == null && App.isInitialized) {
|
||||||
Directory dir;
|
Directory dir;
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
dir = Directory(App.externalStoragePath!);
|
dir = Directory(App.externalStoragePath!);
|
||||||
|
244
lib/headless.dart
Normal file
244
lib/headless.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:venera/utils/data_sync.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
|
import 'package:venera/init.dart';
|
||||||
|
import 'package:venera/foundation/follow_updates.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
|
||||||
|
void cliPrint(Map<String, dynamic> data) {
|
||||||
|
print('[CLI PRINT] ${jsonEncode(data)}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> runHeadlessMode(List<String> args) async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
if (args.contains('--ignore-disheadless-log')) {
|
||||||
|
Log.isMuted = true;
|
||||||
|
}
|
||||||
|
if(Platform.isLinux || Platform.isMacOS){
|
||||||
|
Directory.current = Platform.environment['HOME']!;
|
||||||
|
}
|
||||||
|
// The first arg is '--headless', so we look at the next ones.
|
||||||
|
var commandIndex = args.indexOf('--headless') + 1;
|
||||||
|
if (commandIndex >= args.length) {
|
||||||
|
cliPrint({'status': 'error', 'message': 'No command provided for headless mode.'});
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to initialize the app for some features to work
|
||||||
|
await init();
|
||||||
|
|
||||||
|
var command = args[commandIndex];
|
||||||
|
var subCommand = (commandIndex + 1 < args.length) ? args[commandIndex + 1] : null;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'webdav':
|
||||||
|
if (subCommand == 'up') {
|
||||||
|
cliPrint({'status': 'running', 'message': 'Uploading WebDAV data...'});
|
||||||
|
await DataSync().uploadData();
|
||||||
|
cliPrint({'status': 'success', 'message': 'Upload complete.'});
|
||||||
|
} else if (subCommand == 'down') {
|
||||||
|
cliPrint({'status': 'running', 'message': 'Downloading WebDAV data...'});
|
||||||
|
await DataSync().downloadData();
|
||||||
|
cliPrint({'status': 'success', 'message': 'Download complete.'});
|
||||||
|
} else {
|
||||||
|
cliPrint({'status': 'error', 'message': 'Invalid webdav command. Use "up" or "down".'});
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'updatescript':
|
||||||
|
if (subCommand == 'all') {
|
||||||
|
cliPrint({'status': 'running', 'message': 'Checking for comic source script updates...'});
|
||||||
|
await ComicSourcePage.checkComicSourceUpdate();
|
||||||
|
var updates = ComicSourceManager().availableUpdates;
|
||||||
|
if (updates.isEmpty) {
|
||||||
|
cliPrint({'status': 'success', 'message': 'No updates found.'});
|
||||||
|
} else {
|
||||||
|
var total = updates.length;
|
||||||
|
var current = 0;
|
||||||
|
var errors = 0;
|
||||||
|
var updated = 0;
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': 'Updating all comic source scripts...',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'current': 0,
|
||||||
|
'updated': 0,
|
||||||
|
'errors': 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (var key in updates.keys) {
|
||||||
|
var source = ComicSource.find(key);
|
||||||
|
if (source != null) {
|
||||||
|
current++;
|
||||||
|
var data = {
|
||||||
|
'current': current,
|
||||||
|
'total': total,
|
||||||
|
'source': {
|
||||||
|
'key': source.key,
|
||||||
|
'name': source.name,
|
||||||
|
'version': source.version,
|
||||||
|
'url': source.url,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await ComicSourcePage.update(source, false);
|
||||||
|
updated++;
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': 'Progress',
|
||||||
|
'data': data,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
errors++;
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': 'ProgressError',
|
||||||
|
'data': {
|
||||||
|
...data,
|
||||||
|
'error': e.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cliPrint({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'All scripts updated.',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'updated': updated,
|
||||||
|
'errors': errors,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cliPrint({'status': 'error', 'message': 'Invalid updatescript command. Use "all".'});
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'updatesubscribe':
|
||||||
|
cliPrint({'status': 'running', 'message': 'Updating subscribed comics...'});
|
||||||
|
var folder = appdata.settings["followUpdatesFolder"];
|
||||||
|
if (folder == null) {
|
||||||
|
cliPrint({'status': 'error', 'message': 'Follow updates folder is not configured.'});
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateIndex = args.indexOf('--update-comic-by-id-type');
|
||||||
|
if (updateIndex != -1) {
|
||||||
|
var id = args[updateIndex + 1];
|
||||||
|
var type = args[updateIndex + 2];
|
||||||
|
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
||||||
|
var comic = comics.firstWhere((c) => c.id == id && c.type.sourceKey == type);
|
||||||
|
|
||||||
|
var result = await updateComic(comic, folder);
|
||||||
|
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'current': 1,
|
||||||
|
'total': 1,
|
||||||
|
'comic': {
|
||||||
|
'id': comic.id,
|
||||||
|
'name': comic.name,
|
||||||
|
'coverUrl': comic.coverPath,
|
||||||
|
'author': comic.author,
|
||||||
|
'type': comic.type.sourceKey,
|
||||||
|
'updateTime': comic.updateTime,
|
||||||
|
'tags': comic.tags,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var message = 'Progress';
|
||||||
|
if (result.errorMessage != null) {
|
||||||
|
message = 'ProgressError';
|
||||||
|
data['error'] = result.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': message,
|
||||||
|
'data': data,
|
||||||
|
});
|
||||||
|
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': 'Update check complete.',
|
||||||
|
'data': {
|
||||||
|
'total': 1,
|
||||||
|
'updated': result.updated ? 1 : 0,
|
||||||
|
'errors': result.errorMessage != null ? 1 : 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
var json = await getUpdatedComicsAsJson(folder);
|
||||||
|
cliPrint({
|
||||||
|
'status': result.errorMessage != null ? 'error' : 'success',
|
||||||
|
'message': 'Updated comics list.',
|
||||||
|
'data': jsonDecode(json),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
int total = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int errors = 0;
|
||||||
|
await for (var progress in updateFolder(folder, true)) {
|
||||||
|
total = progress.total;
|
||||||
|
updated = progress.updated;
|
||||||
|
errors = progress.errors;
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'current': progress.current,
|
||||||
|
'total': progress.total,
|
||||||
|
};
|
||||||
|
if (progress.comic != null) {
|
||||||
|
data['comic'] = {
|
||||||
|
'id': progress.comic!.id,
|
||||||
|
'name': progress.comic!.name,
|
||||||
|
'coverUrl': progress.comic!.coverPath,
|
||||||
|
'author': progress.comic!.author,
|
||||||
|
'type': progress.comic!.type.sourceKey,
|
||||||
|
'updateTime': progress.comic!.updateTime,
|
||||||
|
'tags': progress.comic!.tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var message = 'Progress';
|
||||||
|
if (progress.errorMessage != null) {
|
||||||
|
message = 'ProgressError';
|
||||||
|
data['error'] = progress.errorMessage;
|
||||||
|
}
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': message,
|
||||||
|
'data': data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cliPrint({
|
||||||
|
'status': 'running',
|
||||||
|
'message': 'Update check complete.',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'updated': updated,
|
||||||
|
'errors': errors,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
var json = await getUpdatedComicsAsJson(folder);
|
||||||
|
cliPrint({
|
||||||
|
'status': errors > 0 ? 'error' : 'success',
|
||||||
|
'message': 'Updated comics list.',
|
||||||
|
'data': jsonDecode(json),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cliPrint({'status': 'error', 'message': 'Unknown command: $command'});
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit after command execution
|
||||||
|
exit(0);
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:display_mode/display_mode.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_saf/flutter_saf.dart';
|
import 'package:flutter_saf/flutter_saf.dart';
|
||||||
@@ -15,6 +16,7 @@ import 'package:venera/pages/follow_updates_page.dart';
|
|||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/handle_text_share.dart';
|
import 'package:venera/utils/handle_text_share.dart';
|
||||||
|
import 'package:venera/utils/opencc.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'foundation/appdata.dart';
|
import 'foundation/appdata.dart';
|
||||||
@@ -35,6 +37,7 @@ extension _FutureInit<T> on Future<T> {
|
|||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await App.init().wait();
|
await App.init().wait();
|
||||||
await SingleInstanceCookieJar.createInstance();
|
await SingleInstanceCookieJar.createInstance();
|
||||||
|
try {
|
||||||
var futures = [
|
var futures = [
|
||||||
Rhttp.init(),
|
Rhttp.init(),
|
||||||
App.initComponents(),
|
App.initComponents(),
|
||||||
@@ -43,13 +46,22 @@ Future<void> init() async {
|
|||||||
TagsTranslation.readData().wait(),
|
TagsTranslation.readData().wait(),
|
||||||
JsEngine().init().wait(),
|
JsEngine().init().wait(),
|
||||||
ComicSourceManager().init().wait(),
|
ComicSourceManager().init().wait(),
|
||||||
|
OpenCC.init(),
|
||||||
];
|
];
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("init", "$e\n$s");
|
||||||
|
}
|
||||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||||
_checkOldConfigs();
|
_checkOldConfigs();
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
handleLinks();
|
handleLinks();
|
||||||
handleTextShare();
|
handleTextShare();
|
||||||
|
try {
|
||||||
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
|
} catch(e) {
|
||||||
|
Log.error("Display Mode", "Failed to set high refresh rate: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
|
@@ -14,9 +14,14 @@ import 'components/components.dart';
|
|||||||
import 'components/window_frame.dart';
|
import 'components/window_frame.dart';
|
||||||
import 'foundation/app.dart';
|
import 'foundation/app.dart';
|
||||||
import 'foundation/appdata.dart';
|
import 'foundation/appdata.dart';
|
||||||
|
import 'headless.dart';
|
||||||
import 'init.dart';
|
import 'init.dart';
|
||||||
|
|
||||||
void main(List<String> args) {
|
void main(List<String> args) {
|
||||||
|
if (args.contains('--headless')) {
|
||||||
|
runHeadlessMode(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (runWebViewTitleBarWidget(args)) return;
|
if (runWebViewTitleBarWidget(args)) return;
|
||||||
overrideIO(() {
|
overrideIO(() {
|
||||||
runZonedGuarded(() async {
|
runZonedGuarded(() async {
|
||||||
@@ -237,6 +242,27 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
if (widget != null) {
|
if (widget != null) {
|
||||||
|
/// 如果无法检测到状态栏高度设定指定高度
|
||||||
|
/// https://github.com/flutter/flutter/issues/161086
|
||||||
|
var isPaddingCheckError =
|
||||||
|
MediaQuery.of(context).viewPadding.top <= 0 ||
|
||||||
|
MediaQuery.of(context).viewPadding.top > 50;
|
||||||
|
|
||||||
|
if (isPaddingCheckError && Platform.isAndroid) {
|
||||||
|
widget = MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
viewPadding: const EdgeInsets.only(
|
||||||
|
top: 15,
|
||||||
|
bottom: 15,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 15,
|
||||||
|
bottom: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget);
|
||||||
|
}
|
||||||
|
|
||||||
widget = OverlayWidget(widget);
|
widget = OverlayWidget(widget);
|
||||||
if (App.isDesktop) {
|
if (App.isDesktop) {
|
||||||
widget = Shortcuts(
|
widget = Shortcuts(
|
||||||
|
@@ -112,11 +112,13 @@ class AppDio with DioMixin {
|
|||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
httpClientAdapter = RHttpAdapter();
|
httpClientAdapter = RHttpAdapter();
|
||||||
|
if (App.isInitialized) {
|
||||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(NetworkCacheManager());
|
||||||
interceptors.add(CloudflareInterceptor());
|
interceptors.add(CloudflareInterceptor());
|
||||||
interceptors.add(MyLogInterceptor());
|
interceptors.add(MyLogInterceptor());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final Map<String, bool> _requests = {};
|
static final Map<String, bool> _requests = {};
|
||||||
|
|
||||||
@@ -173,6 +175,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
||||||
tlsSettings: rhttp.TlsSettings(
|
tlsSettings: rhttp.TlsSettings(
|
||||||
sni: appdata.settings['sni'] != false,
|
sni: appdata.settings['sni'] != false,
|
||||||
|
verifyCertificates: appdata.settings['ignoreBadCertificate'] != true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -202,9 +202,13 @@ class SingleInstanceCookieJar extends CookieJarSql {
|
|||||||
|
|
||||||
static SingleInstanceCookieJar? instance;
|
static SingleInstanceCookieJar? instance;
|
||||||
|
|
||||||
static Future<void> createInstance() async {
|
static Future<SingleInstanceCookieJar> createInstance() async {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance!;
|
||||||
|
}
|
||||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
||||||
|
return instance!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -71,7 +71,8 @@ abstract class ImageDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configs['onResponse'] is JSInvokable) {
|
if (configs['onResponse'] is JSInvokable) {
|
||||||
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
|
final uint8List = Uint8List.fromList(buffer);
|
||||||
|
buffer = (configs['onResponse'] as JSInvokable)([uint8List]);
|
||||||
(configs['onResponse'] as JSInvokable).free();
|
(configs['onResponse'] as JSInvokable).free();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,12 +181,17 @@ abstract class ImageDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configs['onResponse'] is JSInvokable) {
|
if (configs['onResponse'] is JSInvokable) {
|
||||||
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
|
buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
|
||||||
(configs['onResponse'] as JSInvokable).free();
|
(configs['onResponse'] as JSInvokable).free();
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = Uint8List.fromList(buffer);
|
Uint8List data;
|
||||||
|
if (buffer is Uint8List) {
|
||||||
|
data = buffer;
|
||||||
|
} else {
|
||||||
|
data = Uint8List.fromList(buffer);
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (configs['modifyImage'] != null) {
|
if (configs['modifyImage'] != null) {
|
||||||
var newData = await modifyImageWithScript(
|
var newData = await modifyImageWithScript(
|
||||||
|
@@ -27,9 +27,11 @@ class CategoryComicsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||||
late final CategoryComicsData data;
|
late final CategoryComicsData data;
|
||||||
late final List<CategoryComicsOptions> options;
|
late List<CategoryComicsOptions>? options;
|
||||||
|
late final CategoryOptionsLoader? optionsLoader;
|
||||||
late List<String> optionsValue;
|
late List<String> optionsValue;
|
||||||
late String sourceKey;
|
late String sourceKey;
|
||||||
|
String? error;
|
||||||
|
|
||||||
void findData() {
|
void findData() {
|
||||||
for (final source in ComicSource.all()) {
|
for (final source in ComicSource.all()) {
|
||||||
@@ -38,7 +40,8 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
throw "The comic source ${source.name} does not support category comics";
|
throw "The comic source ${source.name} does not support category comics";
|
||||||
}
|
}
|
||||||
data = source.categoryComicsData!;
|
data = source.categoryComicsData!;
|
||||||
options = data.options.where((element) {
|
if (data.options != null) {
|
||||||
|
options = data.options!.where((element) {
|
||||||
if (element.notShowWhen.contains(widget.category)) {
|
if (element.notShowWhen.contains(widget.category)) {
|
||||||
return false;
|
return false;
|
||||||
} else if (element.showWhen != null) {
|
} else if (element.showWhen != null) {
|
||||||
@@ -46,16 +49,14 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
var defaultOptionsValue =
|
} else {
|
||||||
options.map((e) => e.options.keys.first).toList();
|
options = null;
|
||||||
if (optionsValue.length != options.length) {
|
|
||||||
var newOptionsValue = List<String>.filled(options.length, "");
|
|
||||||
for (var i = 0; i < options.length; i++) {
|
|
||||||
newOptionsValue[i] =
|
|
||||||
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
|
|
||||||
}
|
}
|
||||||
optionsValue = newOptionsValue;
|
if (data.optionsLoader != null) {
|
||||||
|
optionsLoader = data.optionsLoader;
|
||||||
|
loadOptions();
|
||||||
}
|
}
|
||||||
|
resetOptionsValue();
|
||||||
sourceKey = source.key;
|
sourceKey = source.key;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,36 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
throw "${widget.categoryKey} Not found";
|
throw "${widget.categoryKey} Not found";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetOptionsValue() {
|
||||||
|
if (options == null) return;
|
||||||
|
var defaultOptionsValue = options!
|
||||||
|
.map((e) => e.options.keys.first)
|
||||||
|
.toList();
|
||||||
|
if (optionsValue.length != options!.length) {
|
||||||
|
var newOptionsValue = List<String>.filled(options!.length, "");
|
||||||
|
for (var i = 0; i < options!.length; i++) {
|
||||||
|
newOptionsValue[i] =
|
||||||
|
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
|
||||||
|
}
|
||||||
|
optionsValue = newOptionsValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadOptions() async {
|
||||||
|
final res = await optionsLoader!(widget.category, widget.param);
|
||||||
|
if (res.error) {
|
||||||
|
setState(() {
|
||||||
|
error = res.errorMessage;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
options = res.data;
|
||||||
|
resetOptionsValue();
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if (widget.options != null) {
|
if (widget.options != null) {
|
||||||
@@ -77,27 +108,44 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var topPadding = context.padding.top + 56.0;
|
var topPadding = context.padding.top + 56.0;
|
||||||
|
|
||||||
|
Widget body;
|
||||||
|
|
||||||
|
if (options == null) {
|
||||||
|
body = Center(child: CircularProgressIndicator());
|
||||||
|
} else if (error != null) {
|
||||||
|
body = NetworkError(
|
||||||
|
message: error!,
|
||||||
|
retry: () {
|
||||||
|
setState(() {
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
loadOptions();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = ComicList(
|
||||||
|
key: Key(widget.category + optionsValue.toString()),
|
||||||
|
errorLeading: buildOptions().paddingTop(topPadding),
|
||||||
|
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
|
||||||
|
loadPage: (i) =>
|
||||||
|
data.load(widget.category, widget.param, optionsValue, i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
appBar: Appbar(
|
appBar: Appbar(title: Text(widget.category)),
|
||||||
title: Text(widget.category),
|
body: body,
|
||||||
),
|
|
||||||
body: ComicList(
|
|
||||||
key: Key(widget.category + optionsValue.toString()),
|
|
||||||
errorLeading: SizedBox(height: topPadding),
|
|
||||||
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
|
|
||||||
loadPage: (i) => data.load(
|
|
||||||
widget.category,
|
|
||||||
widget.param,
|
|
||||||
optionsValue,
|
|
||||||
i,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildOptionItem(
|
Widget buildOptionItem(
|
||||||
String text, String value, int group, BuildContext context) {
|
String text,
|
||||||
|
String value,
|
||||||
|
int group,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
return OptionChip(
|
return OptionChip(
|
||||||
text: text.ts(sourceKey),
|
text: text.ts(sourceKey),
|
||||||
isSelected: value == optionsValue[group],
|
isSelected: value == optionsValue[group],
|
||||||
@@ -112,8 +160,26 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
|
|
||||||
Widget buildOptions() {
|
Widget buildOptions() {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
for (var optionList in options) {
|
var group = 0;
|
||||||
children.add(Wrap(
|
for (var optionList in options!) {
|
||||||
|
if (optionList.label.isNotEmpty) {
|
||||||
|
children.add(Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 8.0,
|
||||||
|
left: 4.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
optionList.label.ts(sourceKey),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (optionList.options.length <= 8) {
|
||||||
|
children.add(
|
||||||
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
@@ -121,14 +187,30 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
buildOptionItem(
|
buildOptionItem(
|
||||||
option.value.tl,
|
option.value.tl,
|
||||||
option.key,
|
option.key,
|
||||||
options.indexOf(optionList),
|
group,
|
||||||
context,
|
context,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var g = group;
|
||||||
|
children.add(Select(
|
||||||
|
current: optionList.options[optionsValue[g]],
|
||||||
|
values: optionList.options.values.toList(),
|
||||||
|
onTap: (i) {
|
||||||
|
var key = optionList.options.keys.elementAt(i);
|
||||||
|
if (key == optionsValue[g]) return;
|
||||||
|
setState(() {
|
||||||
|
optionsValue[g] = key;
|
||||||
|
});
|
||||||
|
},
|
||||||
));
|
));
|
||||||
if (options.last != optionList) {
|
}
|
||||||
|
if (options!.last != optionList) {
|
||||||
children.add(const SizedBox(height: 8));
|
children.add(const SizedBox(height: 8));
|
||||||
}
|
}
|
||||||
|
group++;
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@@ -77,8 +77,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onReadEnd() {
|
void onReadEnd() {
|
||||||
history ??=
|
history ??= HistoryManager().find(
|
||||||
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
|
widget.id,
|
||||||
|
ComicType(widget.sourceKey.hashCode),
|
||||||
|
);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +95,32 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildError() {
|
||||||
|
final isDownloaded = LocalManager().isDownloaded(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
Widget? action;
|
||||||
|
if (isDownloaded) {
|
||||||
|
action = FilledButton.tonal(
|
||||||
|
child: Text("Read".tl),
|
||||||
|
onPressed: () {
|
||||||
|
final localComic = LocalManager().find(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
if (localComic == null) {
|
||||||
|
context.showMessage(message: "Local comic not found".tl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localComic.read();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NetworkError(message: error!, retry: retry, action: action);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
scrollController.addListener(onScroll);
|
scrollController.addListener(onScroll);
|
||||||
@@ -114,7 +142,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
ComicDetails get comic => data!;
|
ComicDetails get comic => data!;
|
||||||
|
|
||||||
void onScroll() {
|
void onScroll() {
|
||||||
var offset = scrollController.position.pixels -
|
var offset =
|
||||||
|
scrollController.position.pixels -
|
||||||
scrollController.position.minScrollExtent;
|
scrollController.position.minScrollExtent;
|
||||||
var showFAB = offset > 0;
|
var showFAB = offset > 0;
|
||||||
if (showFAB != this.showFAB) {
|
if (showFAB != this.showFAB) {
|
||||||
@@ -145,9 +174,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
floatingActionButton: showFAB
|
floatingActionButton: showFAB
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
scrollController.animateTo(0,
|
scrollController.animateTo(
|
||||||
|
0,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.ease);
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.arrow_upward),
|
child: const Icon(Icons.arrow_upward),
|
||||||
)
|
)
|
||||||
@@ -164,7 +195,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildThumbnails(),
|
buildThumbnails(),
|
||||||
buildRecommend(),
|
buildRecommend(),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
|
padding: EdgeInsets.only(
|
||||||
|
bottom: context.padding.bottom + 80,
|
||||||
|
), // Add additional padding for FAB
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -190,12 +223,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
initialPage: history?.page,
|
initialPage: history?.page,
|
||||||
initialChapter: history?.ep,
|
initialChapter: history?.ep,
|
||||||
initialChapterGroup: history?.group,
|
initialChapterGroup: history?.group,
|
||||||
history: history ??
|
history:
|
||||||
History.fromModel(
|
history ??
|
||||||
model: localComic,
|
History.fromModel(model: localComic, ep: 0, page: 0),
|
||||||
ep: 0,
|
|
||||||
page: 0,
|
|
||||||
),
|
|
||||||
author: localComic.subTitle ?? '',
|
author: localComic.subTitle ?? '',
|
||||||
tags: localComic.tags,
|
tags: localComic.tags,
|
||||||
);
|
);
|
||||||
@@ -215,8 +245,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
widget.id,
|
widget.id,
|
||||||
ComicType(widget.sourceKey.hashCode),
|
ComicType(widget.sourceKey.hashCode),
|
||||||
);
|
);
|
||||||
history =
|
history = HistoryManager().find(
|
||||||
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
|
widget.id,
|
||||||
|
ComicType(widget.sourceKey.hashCode),
|
||||||
|
);
|
||||||
return comicSource.loadComicInfo!(widget.id);
|
return comicSource.loadComicInfo!(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +257,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
isLiked = comic.isLiked ?? false;
|
isLiked = comic.isLiked ?? false;
|
||||||
isFavorite = comic.isFavorite ?? false;
|
isFavorite = comic.isFavorite ?? false;
|
||||||
if (comic.chapters == null) {
|
if (comic.chapters == null) {
|
||||||
isDownloaded = LocalManager().isDownloaded(
|
isDownloaded = LocalManager().isDownloaded(comic.id, comic.comicType, 0);
|
||||||
comic.id,
|
|
||||||
comic.comicType,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +270,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: showMoreActions, icon: const Icon(Icons.more_horiz))
|
onPressed: showMoreActions,
|
||||||
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -288,8 +318,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
children: [
|
children: [
|
||||||
SelectableText(comic.title, style: ts.s18),
|
SelectableText(comic.title, style: ts.s18),
|
||||||
if (comic.subTitle != null)
|
if (comic.subTitle != null)
|
||||||
SelectableText(comic.subTitle!, style: ts.s14)
|
SelectableText(
|
||||||
.paddingVertical(4),
|
comic.subTitle!,
|
||||||
|
style: ts.s14,
|
||||||
|
).paddingVertical(4),
|
||||||
Text(
|
Text(
|
||||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||||
style: ts.s12,
|
style: ts.s12,
|
||||||
@@ -338,7 +370,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
icon: const Icon(Icons.favorite_border),
|
icon: const Icon(Icons.favorite_border),
|
||||||
activeIcon: const Icon(Icons.favorite),
|
activeIcon: const Icon(Icons.favorite),
|
||||||
isActive: isLiked,
|
isActive: isLiked,
|
||||||
text: ((data!.likesCount != null)
|
text:
|
||||||
|
((data!.likesCount != null)
|
||||||
? (data!.likesCount! + (isLiked ? 1 : 0))
|
? (data!.likesCount! + (isLiked ? 1 : 0))
|
||||||
: (isLiked ? 'Liked'.tl : 'Like'.tl))
|
: (isLiked ? 'Liked'.tl : 'Like'.tl))
|
||||||
.toString(),
|
.toString(),
|
||||||
@@ -383,9 +416,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: hasHistory
|
child: hasHistory
|
||||||
? FilledButton(
|
? FilledButton(
|
||||||
onPressed: continueRead, child: Text("Continue".tl))
|
onPressed: continueRead,
|
||||||
: FilledButton(onPressed: read, child: Text("Read".tl)),
|
child: Text("Continue".tl),
|
||||||
)
|
)
|
||||||
|
: FilledButton(onPressed: read, child: Text("Read".tl)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).paddingHorizontal(16).paddingVertical(8),
|
).paddingHorizontal(16).paddingVertical(8),
|
||||||
if (history != null)
|
if (history != null)
|
||||||
@@ -412,19 +447,20 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
var epName = "E$ep";
|
var epName = "E$ep";
|
||||||
String? groupName;
|
String? groupName;
|
||||||
try {
|
try {
|
||||||
if (group == null){
|
if (group == null) {
|
||||||
epName = comic.chapters!.titles.elementAt(
|
epName = comic.chapters!.titles.elementAt(
|
||||||
math.min(ep - 1, comic.chapters!.length - 1),
|
math.min(ep - 1, comic.chapters!.length - 1),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
groupName = comic.chapters!.groups.elementAt(group - 1);
|
groupName = comic.chapters!.groups.elementAt(
|
||||||
|
group - 1,
|
||||||
|
);
|
||||||
epName = comic.chapters!
|
epName = comic.chapters!
|
||||||
.getGroupByIndex(group - 1)
|
.getGroupByIndex(group - 1)
|
||||||
.values
|
.values
|
||||||
.elementAt(ep - 1);
|
.elementAt(ep - 1);
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
text = groupName == null
|
text = groupName == null
|
||||||
@@ -453,9 +489,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
return SliverLazyToBoxAdapter(
|
return SliverLazyToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(title: Text("Description".tl)),
|
||||||
title: Text("Description".tl),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
||||||
@@ -539,10 +573,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: color, borderRadius: borderRadius),
|
||||||
color: color,
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
),
|
|
||||||
child: Text(text).padding(padding),
|
child: Text(text).padding(padding),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -552,13 +583,13 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (int.tryParse(time) != null) {
|
if (int.tryParse(time) != null) {
|
||||||
var t = int.tryParse(time);
|
var t = int.tryParse(time);
|
||||||
if (t! > 1000000000000) {
|
if (t! > 1000000000000) {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(t)
|
return DateTime.fromMillisecondsSinceEpoch(
|
||||||
.toString()
|
t,
|
||||||
.substring(0, 19);
|
).toString().substring(0, 19);
|
||||||
} else {
|
} else {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(t * 1000)
|
return DateTime.fromMillisecondsSinceEpoch(
|
||||||
.toString()
|
t * 1000,
|
||||||
.substring(0, 19);
|
).toString().substring(0, 19);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (time.contains('T') || time.contains('Z')) {
|
if (time.contains('T') || time.contains('Z')) {
|
||||||
@@ -583,17 +614,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(title: Text("Information".tl)),
|
||||||
title: Text("Information".tl),
|
|
||||||
),
|
|
||||||
if (comic.stars != null)
|
if (comic.stars != null)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StarRating(
|
StarRating(value: comic.stars!, size: 24, onTap: starRating),
|
||||||
value: comic.stars!,
|
|
||||||
size: 24,
|
|
||||||
onTap: starRating,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(comic.stars!.toStringAsFixed(2)),
|
Text(comic.stars!.toStringAsFixed(2)),
|
||||||
],
|
],
|
||||||
@@ -671,24 +696,19 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (comic.recommend == null || comic.recommend!.isEmpty) {
|
if (comic.recommend == null || comic.recommend!.isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return SliverMainAxisGroup(slivers: [
|
return SliverMainAxisGroup(
|
||||||
SliverToBoxAdapter(
|
slivers: [
|
||||||
child: ListTile(
|
SliverToBoxAdapter(child: ListTile(title: Text("Related".tl))),
|
||||||
title: Text("Related".tl),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverGridComics(comics: comic.recommend!),
|
SliverGridComics(comics: comic.recommend!),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildComments() {
|
Widget buildComments() {
|
||||||
if (comic.comments == null || comic.comments!.isEmpty) {
|
if (comic.comments == null || comic.comments!.isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return _CommentsPart(
|
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
||||||
comments: comic.comments!,
|
|
||||||
showMore: showComments,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,8 +813,8 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
title: Text(widget.eps[i]),
|
title: Text(widget.eps[i]),
|
||||||
value: selected.contains(i) ||
|
value:
|
||||||
widget.downloadedEps.contains(i),
|
selected.contains(i) || widget.downloadedEps.contains(i),
|
||||||
onChanged: widget.downloadedEps.contains(i)
|
onChanged: widget.downloadedEps.contains(i)
|
||||||
? null
|
? null
|
||||||
: (v) {
|
: (v) {
|
||||||
@@ -805,7 +825,8 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
selected.add(i);
|
selected.add(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -813,9 +834,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
height: 50,
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(color: context.colorScheme.outlineVariant),
|
||||||
color: context.colorScheme.outlineVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -880,8 +899,12 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget buildContainer(double? width, double? height,
|
Widget buildContainer(
|
||||||
{Color? color, double? radius}) {
|
double? width,
|
||||||
|
double? height, {
|
||||||
|
Color? color,
|
||||||
|
double? radius,
|
||||||
|
}) {
|
||||||
return Container(
|
return Container(
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -923,13 +946,9 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
if (context.width < changePoint)
|
if (context.width < changePoint)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: buildContainer(null, 36, radius: 18)),
|
||||||
child: buildContainer(null, 36, radius: 18),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(child: buildContainer(null, 36, radius: 18)),
|
||||||
child: buildContainer(null, 36, radius: 18),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).paddingHorizontal(16),
|
).paddingHorizontal(16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -938,7 +957,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2.4,
|
strokeWidth: 2.4,
|
||||||
).fixHeight(24).fixWidth(24),
|
).fixHeight(24).fixWidth(24),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -948,11 +967,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
Widget child;
|
Widget child;
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
child = AnimatedImage(
|
child = AnimatedImage(
|
||||||
image: CachedImageProvider(
|
image: CachedImageProvider(cover!, sourceKey: sourceKey, cid: cid),
|
||||||
cover!,
|
|
||||||
sourceKey: sourceKey,
|
|
||||||
cid: cid,
|
|
||||||
),
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
@@ -18,6 +18,57 @@ import 'package:venera/utils/translations.dart';
|
|||||||
class ComicSourcePage extends StatelessWidget {
|
class ComicSourcePage extends StatelessWidget {
|
||||||
const ComicSourcePage({super.key});
|
const ComicSourcePage({super.key});
|
||||||
|
|
||||||
|
static Future<void> update(
|
||||||
|
ComicSource source, [
|
||||||
|
bool showLoading = true,
|
||||||
|
]) async {
|
||||||
|
if (!source.url.isURL) {
|
||||||
|
if (showLoading) {
|
||||||
|
App.rootContext.showMessage(message: "Invalid url config");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw Exception("Invalid url config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ComicSourceManager().remove(source.key);
|
||||||
|
bool cancel = false;
|
||||||
|
LoadingDialogController? controller;
|
||||||
|
if (showLoading) {
|
||||||
|
controller = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
onCancel: () => cancel = true,
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var res = await AppDio().get<String>(
|
||||||
|
source.url,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.plain,
|
||||||
|
headers: {"cache-time": "no"},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (cancel) return;
|
||||||
|
controller?.close();
|
||||||
|
await ComicSourceParser().parse(res.data!, source.filePath);
|
||||||
|
await io.File(source.filePath).writeAsString(res.data!);
|
||||||
|
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
||||||
|
ComicSourceManager().availableUpdates.remove(source.key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (cancel) return;
|
||||||
|
if (showLoading) {
|
||||||
|
App.rootContext.showMessage(message: e.toString());
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ComicSourceManager().reload();
|
||||||
|
if (showLoading) {
|
||||||
|
App.forceRebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<int> checkComicSourceUpdate() async {
|
static Future<int> checkComicSourceUpdate() async {
|
||||||
if (ComicSource.all().isEmpty) {
|
if (ComicSource.all().isEmpty) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -152,52 +203,11 @@ class _BodyState extends State<_Body> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> update(
|
void update(ComicSource source, [bool showLoading = true]) {
|
||||||
ComicSource source, [
|
ComicSourcePage.update(source, showLoading);
|
||||||
bool showLoading = true,
|
|
||||||
]) async {
|
|
||||||
if (!source.url.isURL) {
|
|
||||||
App.rootContext.showMessage(message: "Invalid url config");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ComicSourceManager().remove(source.key);
|
|
||||||
bool cancel = false;
|
|
||||||
LoadingDialogController? controller;
|
|
||||||
if (showLoading) {
|
|
||||||
controller = showLoadingDialog(
|
|
||||||
App.rootContext,
|
|
||||||
onCancel: () => cancel = true,
|
|
||||||
barrierDismissible: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var res = await AppDio().get<String>(
|
|
||||||
source.url,
|
|
||||||
options: Options(responseType: ResponseType.plain),
|
|
||||||
);
|
|
||||||
if (cancel) return;
|
|
||||||
controller?.close();
|
|
||||||
await ComicSourceParser().parse(res.data!, source.filePath);
|
|
||||||
await File(source.filePath).writeAsString(res.data!);
|
|
||||||
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
|
||||||
ComicSourceManager().availableUpdates.remove(source.key);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (cancel) return;
|
|
||||||
App.rootContext.showMessage(message: e.toString());
|
|
||||||
}
|
|
||||||
await ComicSourceManager().reload();
|
|
||||||
App.forceRebuild();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCard(BuildContext context) {
|
Widget buildCard(BuildContext context) {
|
||||||
Widget buildButton({
|
|
||||||
required Widget child,
|
|
||||||
required VoidCallback onPressed,
|
|
||||||
}) {
|
|
||||||
return Button.normal(onPressed: onPressed, child: child).fixHeight(32);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -224,10 +234,13 @@ class _BodyState extends State<_Body> {
|
|||||||
},
|
},
|
||||||
onSubmitted: handleAddSource,
|
onSubmitted: handleAddSource,
|
||||||
).paddingHorizontal(16).paddingBottom(8),
|
).paddingHorizontal(16).paddingBottom(8),
|
||||||
ListTile(
|
Wrap(
|
||||||
title: Text("Comic Source list".tl),
|
spacing: 8,
|
||||||
trailing: buildButton(
|
runSpacing: 8,
|
||||||
child: Text("View".tl),
|
children: [
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
icon: Icon(Icons.article_outlined),
|
||||||
|
label: Text("Comic Source list".tl),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showPopUpWidget(
|
showPopUpWidget(
|
||||||
App.rootContext,
|
App.rootContext,
|
||||||
@@ -235,22 +248,19 @@ class _BodyState extends State<_Body> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
FilledButton.tonalIcon(
|
||||||
ListTile(
|
icon: Icon(Icons.file_open_outlined),
|
||||||
title: Text("Use a config file".tl),
|
label: Text("Use a config file".tl),
|
||||||
trailing: buildButton(
|
|
||||||
onPressed: _selectFile,
|
onPressed: _selectFile,
|
||||||
child: Text("Select".tl),
|
|
||||||
),
|
),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
icon: Icon(Icons.help_outline),
|
||||||
|
label: Text("Help".tl),
|
||||||
|
onPressed: help,
|
||||||
),
|
),
|
||||||
ListTile(
|
_CheckUpdatesButton(),
|
||||||
title: Text("Help".tl),
|
],
|
||||||
trailing: buildButton(onPressed: help, child: Text("Open".tl)),
|
).paddingHorizontal(12).paddingVertical(8),
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text("Check updates".tl),
|
|
||||||
trailing: _CheckUpdatesButton(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -294,7 +304,10 @@ class _BodyState extends State<_Body> {
|
|||||||
try {
|
try {
|
||||||
var res = await AppDio().get<String>(
|
var res = await AppDio().get<String>(
|
||||||
url,
|
url,
|
||||||
options: Options(responseType: ResponseType.plain),
|
options: Options(
|
||||||
|
responseType: ResponseType.plain,
|
||||||
|
headers: {"cache-time": "no"},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
controller.close();
|
controller.close();
|
||||||
@@ -686,7 +699,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
|
var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
|
||||||
for (var key in shouldUpdate) {
|
for (var key in shouldUpdate) {
|
||||||
var source = ComicSource.find(key)!;
|
var source = ComicSource.find(key)!;
|
||||||
await _BodyState.update(source, false);
|
await ComicSourcePage.update(source, false);
|
||||||
current++;
|
current++;
|
||||||
loadingController.setProgress(current / total);
|
loadingController.setProgress(current / total);
|
||||||
}
|
}
|
||||||
@@ -699,11 +712,17 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Button.normal(
|
return FilledButton.tonalIcon(
|
||||||
|
icon: isLoading
|
||||||
|
? SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Icon(Icons.update),
|
||||||
|
label: Text("Check updates".tl),
|
||||||
onPressed: check,
|
onPressed: check,
|
||||||
isLoading: isLoading,
|
);
|
||||||
child: Text("Check".tl),
|
|
||||||
).fixHeight(32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@ import 'package:venera/pages/reader/reader.dart';
|
|||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
import 'package:venera/utils/opencc.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
@@ -52,7 +52,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
} else {
|
} else {
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
for (var comic in comics) {
|
for (var comic in comics) {
|
||||||
if (matchKeyword(keyword, comic)) {
|
if (matchKeyword(keyword, comic) ||
|
||||||
|
matchKeywordT(keyword, comic) ||
|
||||||
|
matchKeywordS(keyword, comic)) {
|
||||||
searchResults.add(comic);
|
searchResults.add(comic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +132,24 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert keyword to traditional Chinese to match comics
|
||||||
|
bool matchKeywordT(String keyword, FavoriteItem comic) {
|
||||||
|
if (!OpenCC.hasChineseSimplified(keyword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
keyword = OpenCC.simplifiedToTraditional(keyword);
|
||||||
|
return matchKeyword(keyword, comic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert keyword to simplified Chinese to match comics
|
||||||
|
bool matchKeywordS(String keyword, FavoriteItem comic) {
|
||||||
|
if (!OpenCC.hasChineseTraditional(keyword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
keyword = OpenCC.traditionalToSimplified(keyword);
|
||||||
|
return matchKeyword(keyword, comic);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
@@ -492,6 +512,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
|
text: "Read".tl,
|
||||||
|
onClick: () {
|
||||||
|
final c = selectedComics.keys.first as FavoriteItem;
|
||||||
|
App.rootContext.to(() => ReaderWithLoading(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -5,10 +5,10 @@ import 'package:venera/components/components.dart';
|
|||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import '../foundation/global_state.dart';
|
import '../foundation/global_state.dart';
|
||||||
|
import 'package:venera/foundation/follow_updates.dart';
|
||||||
|
|
||||||
class FollowUpdatesWidget extends StatefulWidget {
|
class FollowUpdatesWidget extends StatefulWidget {
|
||||||
const FollowUpdatesWidget({super.key});
|
const FollowUpdatesWidget({super.key});
|
||||||
@@ -460,7 +460,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
|||||||
message: "Updating comics...".tl,
|
message: "Updating comics...".tl,
|
||||||
);
|
);
|
||||||
|
|
||||||
await for (var progress in _updateFolder(folder, true)) {
|
await for (var progress in updateFolder(folder, true)) {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
|||||||
|
|
||||||
int updated = 0;
|
int updated = 0;
|
||||||
|
|
||||||
await for (var progress in _updateFolder(folder!, true)) {
|
await for (var progress in updateFolder(folder!, true)) {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -532,128 +532,6 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
|||||||
Object? get key => 'FollowUpdatesPage';
|
Object? get key => 'FollowUpdatesPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateProgress {
|
|
||||||
final int total;
|
|
||||||
final int current;
|
|
||||||
final int errors;
|
|
||||||
final int updated;
|
|
||||||
|
|
||||||
_UpdateProgress(this.total, this.current, this.errors, this.updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateFolderBase(
|
|
||||||
String folder,
|
|
||||||
StreamController<_UpdateProgress> stream,
|
|
||||||
bool ignoreCheckTime,
|
|
||||||
) async {
|
|
||||||
var comics = LocalFavoritesManager().getComicsWithUpdatesInfo(folder);
|
|
||||||
int current = 0;
|
|
||||||
int errors = 0;
|
|
||||||
int updated = 0;
|
|
||||||
var futures = <Future>[];
|
|
||||||
const maxConcurrent = 5;
|
|
||||||
|
|
||||||
for (int i = 0; i < comics.length; i++) {
|
|
||||||
if (stream.isClosed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ignoreCheckTime) {
|
|
||||||
var lastCheckTime = comics[i].lastCheckTime;
|
|
||||||
if (lastCheckTime != null &&
|
|
||||||
DateTime.now().difference(lastCheckTime).inDays < 1) {
|
|
||||||
current++;
|
|
||||||
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (futures.length >= maxConcurrent) {
|
|
||||||
await Future.any(futures);
|
|
||||||
}
|
|
||||||
|
|
||||||
var future = () async {
|
|
||||||
int retries = 3;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
var c = comics[i];
|
|
||||||
var comicSource = c.type.comicSource;
|
|
||||||
if (comicSource == null) return;
|
|
||||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
|
||||||
|
|
||||||
var newTags = <String>[];
|
|
||||||
for (var entry in newInfo.tags.entries) {
|
|
||||||
const shouldIgnore = ['author', 'artist', 'time'];
|
|
||||||
var namespace = entry.key;
|
|
||||||
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (var tag in entry.value) {
|
|
||||||
newTags.add("$namespace:$tag");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = FavoriteItem(
|
|
||||||
id: c.id,
|
|
||||||
name: newInfo.title,
|
|
||||||
coverPath: newInfo.cover,
|
|
||||||
author: newInfo.subTitle ??
|
|
||||||
newInfo.tags['author']?.firstOrNull ??
|
|
||||||
c.author,
|
|
||||||
type: c.type,
|
|
||||||
tags: newTags,
|
|
||||||
);
|
|
||||||
|
|
||||||
LocalFavoritesManager().updateInfo(folder, item, false);
|
|
||||||
|
|
||||||
var updateTime = newInfo.findUpdateTime();
|
|
||||||
if (updateTime != null && updateTime != c.updateTime) {
|
|
||||||
LocalFavoritesManager().updateUpdateTime(
|
|
||||||
folder,
|
|
||||||
c.id,
|
|
||||||
c.type,
|
|
||||||
updateTime,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
LocalFavoritesManager().updateCheckTime(folder, c.id, c.type);
|
|
||||||
}
|
|
||||||
updated++;
|
|
||||||
return;
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("Check Updates", e, s);
|
|
||||||
retries--;
|
|
||||||
if (retries == 0) {
|
|
||||||
errors++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
current++;
|
|
||||||
stream.add(_UpdateProgress(comics.length, current, errors, updated));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
future.then((_) {
|
|
||||||
futures.remove(future);
|
|
||||||
});
|
|
||||||
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.wait(futures);
|
|
||||||
|
|
||||||
if (updated > 0) {
|
|
||||||
LocalFavoritesManager().notifyChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) {
|
|
||||||
var stream = StreamController<_UpdateProgress>();
|
|
||||||
_updateFolderBase(folder, stream, ignoreCheckTime);
|
|
||||||
return stream.stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Background service for checking updates
|
/// Background service for checking updates
|
||||||
abstract class FollowUpdatesService {
|
abstract class FollowUpdatesService {
|
||||||
static bool _isChecking = false;
|
static bool _isChecking = false;
|
||||||
@@ -683,7 +561,7 @@ abstract class FollowUpdatesService {
|
|||||||
|
|
||||||
int updated = 0;
|
int updated = 0;
|
||||||
try {
|
try {
|
||||||
await for (var progress in _updateFolder(folder, false)) {
|
await for (var progress in updateFolder(folder, false)) {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -152,7 +152,8 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
|
|
||||||
bool _dragInProgress = false;
|
bool _dragInProgress = false;
|
||||||
|
|
||||||
bool get _enableDoubleTapToZoom => appdata.settings["enableDoubleTapToZoom"];
|
bool get _enableDoubleTapToZoom =>
|
||||||
|
appdata.settings.getReaderSetting(reader.cid, reader.type.sourceKey, 'enableDoubleTapToZoom');
|
||||||
|
|
||||||
void onTapUp(TapUpDetails event) {
|
void onTapUp(TapUpDetails event) {
|
||||||
if (_longPressInProgress) {
|
if (_longPressInProgress) {
|
||||||
@@ -190,7 +191,8 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
} else if (context.readerScaffold.isOpen) {
|
} else if (context.readerScaffold.isOpen) {
|
||||||
context.readerScaffold.openOrClose();
|
context.readerScaffold.openOrClose();
|
||||||
} else {
|
} else {
|
||||||
if (appdata.settings['enableTapToTurnPages']) {
|
if (appdata.settings.getReaderSetting(
|
||||||
|
reader.cid, reader.type.sourceKey, 'enableTapToTurnPages')) {
|
||||||
bool isLeft = false, isRight = false, isTop = false, isBottom = false;
|
bool isLeft = false, isRight = false, isTop = false, isBottom = false;
|
||||||
final width = context.width;
|
final width = context.width;
|
||||||
final height = context.height;
|
final height = context.height;
|
||||||
@@ -207,11 +209,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
isBottom = true;
|
isBottom = true;
|
||||||
}
|
}
|
||||||
bool isCenter = false;
|
bool isCenter = false;
|
||||||
var prev = context.reader.toPrevPage;
|
var prev = () => context.reader.toPrevPage();
|
||||||
var next = context.reader.toNextPage;
|
var next = () => context.reader.toNextPage();
|
||||||
if (appdata.settings['reverseTapToTurnPages']) {
|
if (appdata.settings.getReaderSetting(
|
||||||
prev = context.reader.toNextPage;
|
reader.cid, reader.type.sourceKey, 'reverseTapToTurnPages')) {
|
||||||
next = context.reader.toPrevPage;
|
prev = () => context.reader.toNextPage();
|
||||||
|
next = () => context.reader.toPrevPage();
|
||||||
}
|
}
|
||||||
switch (context.reader.mode) {
|
switch (context.reader.mode) {
|
||||||
case ReaderMode.galleryLeftToRight:
|
case ReaderMode.galleryLeftToRight:
|
||||||
|
@@ -32,10 +32,17 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
inProgress = true;
|
inProgress = true;
|
||||||
if (reader.type == ComicType.local ||
|
if (reader.type == ComicType.local ||
|
||||||
(LocalManager().isDownloaded(
|
(LocalManager().isDownloaded(
|
||||||
reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
reader.cid,
|
||||||
|
reader.type,
|
||||||
|
reader.chapter,
|
||||||
|
reader.widget.chapters,
|
||||||
|
))) {
|
||||||
try {
|
try {
|
||||||
var images = await LocalManager()
|
var images = await LocalManager().getImages(
|
||||||
.getImages(reader.cid, reader.type, reader.chapter);
|
reader.cid,
|
||||||
|
reader.type,
|
||||||
|
reader.chapter,
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
reader.images = images;
|
reader.images = images;
|
||||||
reader.isLoading = false;
|
reader.isLoading = false;
|
||||||
@@ -81,11 +88,14 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (reader.isLoading) {
|
if (reader.isLoading) {
|
||||||
load();
|
load();
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
} else if (error != null) {
|
} else if (error != null) {
|
||||||
return NetworkError(
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.readerScaffold.openOrClose();
|
||||||
|
},
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: NetworkError(
|
||||||
message: error!,
|
message: error!,
|
||||||
retry: () {
|
retry: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -93,11 +103,14 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
error = null;
|
error = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (reader.mode.isGallery) {
|
if (reader.mode.isGallery) {
|
||||||
return _GalleryMode(
|
return _GalleryMode(
|
||||||
key: Key('${reader.mode.key}_${reader.imagesPerPage}'));
|
key: Key('${reader.mode.key}_${reader.imagesPerPage}'),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return _ContinuousMode(key: Key(reader.mode.key));
|
return _ContinuousMode(key: Key(reader.mode.key));
|
||||||
}
|
}
|
||||||
@@ -125,11 +138,15 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
/// [totalPages] is the total number of pages in the current chapter.
|
/// [totalPages] is the total number of pages in the current chapter.
|
||||||
/// More than one images can be displayed on one page.
|
/// More than one images can be displayed on one page.
|
||||||
int get totalPages {
|
int get totalPages {
|
||||||
if (!reader.showSingleImageOnFirstPage) {
|
if (!reader.showSingleImageOnFirstPage()) {
|
||||||
return (reader.images!.length / reader.imagesPerPage).ceil();
|
return (reader.images!.length /
|
||||||
|
reader.imagesPerPage())
|
||||||
|
.ceil();
|
||||||
} else {
|
} else {
|
||||||
return 1 +
|
return 1 +
|
||||||
((reader.images!.length - 1) / reader.imagesPerPage).ceil();
|
((reader.images!.length - 1) /
|
||||||
|
reader.imagesPerPage())
|
||||||
|
.ceil();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,19 +169,24 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
/// Get the range of images for the given page. [page] is 1-based.
|
/// Get the range of images for the given page. [page] is 1-based.
|
||||||
(int start, int end) getPageImagesRange(int page) {
|
(int start, int end) getPageImagesRange(int page) {
|
||||||
if (reader.showSingleImageOnFirstPage) {
|
var imagesPerPage = reader.imagesPerPage();
|
||||||
|
if (reader.showSingleImageOnFirstPage()) {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
return (0, 1);
|
return (0, 1);
|
||||||
} else {
|
} else {
|
||||||
int startIndex = (page - 2) * reader.imagesPerPage + 1;
|
int startIndex = (page - 2) * imagesPerPage + 1;
|
||||||
int endIndex = math.min(
|
int endIndex = math.min(
|
||||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
startIndex + imagesPerPage,
|
||||||
|
reader.images!.length,
|
||||||
|
);
|
||||||
return (startIndex, endIndex);
|
return (startIndex, endIndex);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int startIndex = (page - 1) * reader.imagesPerPage;
|
int startIndex = (page - 1) * imagesPerPage;
|
||||||
int endIndex = math.min(
|
int endIndex = math.min(
|
||||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
startIndex + imagesPerPage,
|
||||||
|
reader.images!.length,
|
||||||
|
);
|
||||||
return (startIndex, endIndex);
|
return (startIndex, endIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,9 +208,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
var (startIndex, endIndex) = getPageImagesRange(page);
|
var (startIndex, endIndex) = getPageImagesRange(page);
|
||||||
for (int i = startIndex; i < endIndex; i++) {
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
if (shouldPreCache) {
|
if (shouldPreCache) {
|
||||||
_precacheImage(i+1, context);
|
_precacheImage(i + 1, context);
|
||||||
} else {
|
} else {
|
||||||
_preDownloadImage(i+1, context);
|
_preDownloadImage(i + 1, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,16 +232,12 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
var controller = photoViewControllers[reader.page]!;
|
var controller = photoViewControllers[reader.page]!;
|
||||||
Offset value = event.delta;
|
Offset value = event.delta;
|
||||||
if (isLongPressing) {
|
if (isLongPressing) {
|
||||||
controller.updateMultiple(
|
controller.updateMultiple(position: controller.position + value);
|
||||||
position: controller.position + value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: PhotoViewGallery.builder(
|
child: PhotoViewGallery.builder(
|
||||||
backgroundDecoration: BoxDecoration(
|
backgroundDecoration: BoxDecoration(color: context.colorScheme.surface),
|
||||||
color: context.colorScheme.surface,
|
|
||||||
),
|
|
||||||
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
@@ -232,14 +250,17 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var (startIndex, endIndex) = getPageImagesRange(index);
|
var (startIndex, endIndex) = getPageImagesRange(index);
|
||||||
List<String> pageImages =
|
List<String> pageImages = reader.images!.sublist(
|
||||||
reader.images!.sublist(startIndex, endIndex);
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
);
|
||||||
|
|
||||||
cache(index);
|
cache(index);
|
||||||
|
|
||||||
photoViewControllers[index] ??= PhotoViewController();
|
photoViewControllers[index] ??= PhotoViewController();
|
||||||
|
|
||||||
if (reader.imagesPerPage == 1 || pageImages.length == 1) {
|
if (reader.imagesPerPage() == 1 ||
|
||||||
|
pageImages.length == 1) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
controller: photoViewControllers[index],
|
controller: photoViewControllers[index],
|
||||||
@@ -349,13 +370,16 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
onInit: (state) => imageStates.add(state),
|
onInit: (state) => imageStates.add(state),
|
||||||
onDispose: (state) => imageStates.remove(state),
|
onDispose: (state) => imageStates.remove(state),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
imageWidgets = images.map((imageKey) {
|
imageWidgets = images.map((imageKey) {
|
||||||
startIndex++;
|
startIndex++;
|
||||||
ImageProvider imageProvider =
|
ImageProvider imageProvider = _createImageProviderFromKey(
|
||||||
_createImageProviderFromKey(imageKey, context, startIndex);
|
imageKey,
|
||||||
|
context,
|
||||||
|
startIndex,
|
||||||
|
);
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: ComicImage(
|
child: ComicImage(
|
||||||
image: imageProvider,
|
image: imageProvider,
|
||||||
@@ -416,10 +440,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
} else {
|
} else {
|
||||||
zoomPosition = Offset(0, 0);
|
zoomPosition = Offset(0, 0);
|
||||||
}
|
}
|
||||||
photoViewController.animateScale?.call(
|
photoViewController.animateScale?.call(target, zoomPosition);
|
||||||
target,
|
|
||||||
zoomPosition,
|
|
||||||
);
|
|
||||||
isLongPressing = true;
|
isLongPressing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,14 +485,14 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
keyRepeatTimer = null;
|
keyRepeatTimer = null;
|
||||||
}
|
}
|
||||||
if (forward == true) {
|
if (forward == true) {
|
||||||
reader.toPage(reader.page+1);
|
reader.toPage(reader.page + 1);
|
||||||
} else if (forward == false) {
|
} else if (forward == false) {
|
||||||
reader.toPage(reader.page-1);
|
reader.toPage(reader.page - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
||||||
keyRepeatTimer = Timer.periodic(
|
keyRepeatTimer = Timer.periodic(
|
||||||
reader.enablePageAnimation
|
reader.enablePageAnimation(reader.cid, reader.type)
|
||||||
? const Duration(milliseconds: 200)
|
? const Duration(milliseconds: 200)
|
||||||
: const Duration(milliseconds: 50),
|
: const Duration(milliseconds: 50),
|
||||||
(timer) {
|
(timer) {
|
||||||
@@ -479,9 +500,9 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
} else if (forward == true) {
|
} else if (forward == true) {
|
||||||
reader.toPage(reader.page+1);
|
reader.toPage(reader.page + 1);
|
||||||
} else if (forward == false) {
|
} else if (forward == false) {
|
||||||
reader.toPage(reader.page-1);
|
reader.toPage(reader.page - 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -505,15 +526,15 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
return await File(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
} else {
|
} else {
|
||||||
return (await CacheManager().findCache(
|
return (await CacheManager().findCache(
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}",
|
||||||
.readAsBytes();
|
))!.readAsBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getImageKeyByOffset(Offset offset) {
|
String? getImageKeyByOffset(Offset offset) {
|
||||||
String? imageKey;
|
String? imageKey;
|
||||||
if (reader.imagesPerPage == 1) {
|
if (reader.imagesPerPage() == 1) {
|
||||||
imageKey = reader.images![reader.page - 1];
|
imageKey = reader.images![reader.page - 1];
|
||||||
} else {
|
} else {
|
||||||
for (var imageState in imageStates) {
|
for (var imageState in imageStates) {
|
||||||
@@ -531,7 +552,7 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
|||||||
PointerDeviceKind.mouse,
|
PointerDeviceKind.mouse,
|
||||||
PointerDeviceKind.stylus,
|
PointerDeviceKind.stylus,
|
||||||
PointerDeviceKind.invertedStylus,
|
PointerDeviceKind.invertedStylus,
|
||||||
PointerDeviceKind.unknown
|
PointerDeviceKind.unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
const double _kChangeChapterOffset = 160;
|
const double _kChangeChapterOffset = 160;
|
||||||
@@ -617,27 +638,52 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
cacheImages(page);
|
cacheImages(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
double? futurePosition;
|
double? _futurePosition;
|
||||||
|
|
||||||
void smoothTo(double offset) {
|
void smoothTo(double offset) {
|
||||||
futurePosition ??= scrollController.offset;
|
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||||
if (futurePosition! > scrollController.position.maxScrollExtent &&
|
|
||||||
offset > 0) {
|
|
||||||
return;
|
|
||||||
} else if (futurePosition! < scrollController.position.minScrollExtent &&
|
|
||||||
offset < 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
futurePosition = futurePosition! + offset * 1.2;
|
var currentLocation = scrollController.position.pixels;
|
||||||
futurePosition = futurePosition!.clamp(
|
var old = _futurePosition;
|
||||||
|
_futurePosition ??= currentLocation;
|
||||||
|
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
||||||
|
final customSpeed = appdata.settings.getReaderSetting(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type.sourceKey,
|
||||||
|
"readerScrollSpeed",
|
||||||
|
);
|
||||||
|
if (customSpeed is num) {
|
||||||
|
k *= customSpeed;
|
||||||
|
}
|
||||||
|
_futurePosition = _futurePosition! + offset * k;
|
||||||
|
var beforeOffset = (_futurePosition! - currentLocation).abs();
|
||||||
|
_futurePosition = _futurePosition!.clamp(
|
||||||
scrollController.position.minScrollExtent,
|
scrollController.position.minScrollExtent,
|
||||||
scrollController.position.maxScrollExtent,
|
scrollController.position.maxScrollExtent,
|
||||||
);
|
);
|
||||||
scrollController.animateTo(
|
var afterOffset = (_futurePosition! - currentLocation).abs();
|
||||||
futurePosition!,
|
if (_futurePosition == old) return;
|
||||||
duration: const Duration(milliseconds: 200),
|
var target = _futurePosition!;
|
||||||
|
var duration = const Duration(milliseconds: 160);
|
||||||
|
if (afterOffset < beforeOffset) {
|
||||||
|
duration = duration * (afterOffset / beforeOffset);
|
||||||
|
if (duration < Duration(milliseconds: 10)) {
|
||||||
|
duration = Duration(milliseconds: 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollController
|
||||||
|
.animateTo(
|
||||||
|
_futurePosition!,
|
||||||
|
duration: duration,
|
||||||
curve: Curves.linear,
|
curve: Curves.linear,
|
||||||
);
|
)
|
||||||
|
.then((_) {
|
||||||
|
var current = scrollController.position.pixels;
|
||||||
|
if (current == target && current == _futurePosition) {
|
||||||
|
_futurePosition = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPointerSignal(PointerSignalEvent event) {
|
void onPointerSignal(PointerSignalEvent event) {
|
||||||
@@ -666,10 +712,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
void onScroll() {
|
void onScroll() {
|
||||||
if (prepareToPrevChapter) {
|
if (prepareToPrevChapter) {
|
||||||
jumpToNextChapter = false;
|
jumpToNextChapter = false;
|
||||||
jumpToPrevChapter = scrollController.offset <
|
jumpToPrevChapter =
|
||||||
|
scrollController.offset <
|
||||||
scrollController.position.minScrollExtent - _kChangeChapterOffset;
|
scrollController.position.minScrollExtent - _kChangeChapterOffset;
|
||||||
} else if (prepareToNextChapter) {
|
} else if (prepareToNextChapter) {
|
||||||
jumpToNextChapter = scrollController.offset >
|
jumpToNextChapter =
|
||||||
|
scrollController.offset >
|
||||||
scrollController.position.maxScrollExtent + _kChangeChapterOffset;
|
scrollController.position.maxScrollExtent + _kChangeChapterOffset;
|
||||||
jumpToPrevChapter = false;
|
jumpToPrevChapter = false;
|
||||||
}
|
}
|
||||||
@@ -743,8 +791,10 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
scrollBehavior: const MaterialScrollBehavior()
|
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
||||||
.copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes),
|
scrollbars: false,
|
||||||
|
dragDevices: _kTouchLikeDeviceTypes,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
widget = Stack(
|
widget = Stack(
|
||||||
@@ -762,7 +812,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
disableScroll = true;
|
disableScroll = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
futurePosition = null;
|
_futurePosition = null;
|
||||||
if (_isMouseScrolling) {
|
if (_isMouseScrolling) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isMouseScrolling = false;
|
_isMouseScrolling = false;
|
||||||
@@ -888,20 +938,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PhotoView.customChild(
|
return PhotoView.customChild(
|
||||||
backgroundDecoration: BoxDecoration(
|
backgroundDecoration: BoxDecoration(color: context.colorScheme.surface),
|
||||||
color: context.colorScheme.surface,
|
|
||||||
),
|
|
||||||
childSize: Size(width, height),
|
childSize: Size(width, height),
|
||||||
minScale: 1.0,
|
minScale: 1.0,
|
||||||
maxScale: 2.5,
|
maxScale: 2.5,
|
||||||
strictScale: true,
|
strictScale: true,
|
||||||
controller: photoViewController,
|
controller: photoViewController,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleUpdate: onScaleUpdate,
|
||||||
child: SizedBox(
|
child: SizedBox(width: width, height: height, child: widget),
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
child: widget,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,10 +1015,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
} else {
|
} else {
|
||||||
zoomPosition = Offset(0, 0);
|
zoomPosition = Offset(0, 0);
|
||||||
}
|
}
|
||||||
photoViewController.animateScale?.call(
|
photoViewController.animateScale?.call(target, zoomPosition);
|
||||||
target,
|
|
||||||
zoomPosition,
|
|
||||||
);
|
|
||||||
onScaleUpdate(target);
|
onScaleUpdate(target);
|
||||||
isLongPressing = true;
|
isLongPressing = true;
|
||||||
}
|
}
|
||||||
@@ -993,7 +1034,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
@override
|
@override
|
||||||
void toPage(int page) {
|
void toPage(int page) {
|
||||||
itemScrollController.jumpTo(index: page);
|
itemScrollController.jumpTo(index: page);
|
||||||
futurePosition = null;
|
_futurePosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1062,8 +1103,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
return await File(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
} else {
|
} else {
|
||||||
return (await CacheManager().findCache(
|
return (await CacheManager().findCache(
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}",
|
||||||
.readAsBytes();
|
))!.readAsBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,10 +1148,7 @@ void _precacheImage(int page, BuildContext context) {
|
|||||||
if (page <= 0 || page > context.reader.images!.length) {
|
if (page <= 0 || page > context.reader.images!.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
precacheImage(
|
precacheImage(_createImageProvider(page, context), context);
|
||||||
_createImageProvider(page, context),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [_preDownloadImage] is used to download the image for the given page.
|
/// [_preDownloadImage] is used to download the image for the given page.
|
||||||
@@ -1131,10 +1169,7 @@ void _preDownloadImage(int page, BuildContext context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SwipeChangeChapterProgress extends StatefulWidget {
|
class _SwipeChangeChapterProgress extends StatefulWidget {
|
||||||
const _SwipeChangeChapterProgress({
|
const _SwipeChangeChapterProgress({this.controller, required this.isPrev});
|
||||||
this.controller,
|
|
||||||
required this.isPrev,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ScrollController? controller;
|
final ScrollController? controller;
|
||||||
|
|
||||||
@@ -1251,7 +1286,12 @@ class _ProgressPainter extends CustomPainter {
|
|||||||
paint.color = color;
|
paint.color = color;
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
RRect.fromLTRBR(
|
RRect.fromLTRBR(
|
||||||
0, 0, size.width * value, size.height, Radius.circular(16)),
|
0,
|
||||||
|
0,
|
||||||
|
size.width * value,
|
||||||
|
size.height,
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
paint,
|
paint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -115,15 +115,17 @@ class _ReaderState extends State<Reader>
|
|||||||
if (images == null) {
|
if (images == null) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (!showSingleImageOnFirstPage) {
|
if (!showSingleImageOnFirstPage()) {
|
||||||
return (images!.length / imagesPerPage).ceil();
|
return (images!.length / imagesPerPage()).ceil();
|
||||||
} else {
|
} else {
|
||||||
return 1 + ((images!.length - 1) / imagesPerPage).ceil();
|
return 1 + ((images!.length - 1) / imagesPerPage()).ceil();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
ComicType get type => widget.type;
|
ComicType get type => widget.type;
|
||||||
|
|
||||||
|
@override
|
||||||
String get cid => widget.cid;
|
String get cid => widget.cid;
|
||||||
|
|
||||||
String get eid => widget.chapters?.ids.elementAtOrNull(chapter - 1) ?? '0';
|
String get eid => widget.chapters?.ids.elementAtOrNull(chapter - 1) ?? '0';
|
||||||
@@ -162,12 +164,13 @@ class _ReaderState extends State<Reader>
|
|||||||
if (widget.initialPage != null) {
|
if (widget.initialPage != null) {
|
||||||
page = widget.initialPage!;
|
page = widget.initialPage!;
|
||||||
}
|
}
|
||||||
mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
// mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||||
|
mode = ReaderMode.fromKey(appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode'));
|
||||||
history = widget.history;
|
history = widget.history;
|
||||||
if (!appdata.settings['showSystemStatusBar']) {
|
if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
}
|
}
|
||||||
if (appdata.settings['enableTurnPageByVolumeKey']) {
|
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
|
||||||
handleVolumeEvent();
|
handleVolumeEvent();
|
||||||
}
|
}
|
||||||
setImageCacheSize();
|
setImageCacheSize();
|
||||||
@@ -177,10 +180,18 @@ class _ReaderState extends State<Reader>
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
if (!_isInitialized) {
|
||||||
initImagesPerPage(widget.initialPage ?? 1);
|
initImagesPerPage(widget.initialPage ?? 1);
|
||||||
|
_isInitialized = true;
|
||||||
|
} else {
|
||||||
|
// For orientation changed
|
||||||
|
_checkImagesPerPageChange();
|
||||||
|
}
|
||||||
initReaderWindow();
|
initReaderWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,13 +277,13 @@ class _ReaderState extends State<Reader>
|
|||||||
history!.page = images?.length ?? 1;
|
history!.page = images?.length ?? 1;
|
||||||
} else {
|
} else {
|
||||||
/// Record the first image of the page
|
/// Record the first image of the page
|
||||||
if (!showSingleImageOnFirstPage || imagesPerPage == 1) {
|
if (!showSingleImageOnFirstPage() || imagesPerPage() == 1) {
|
||||||
history!.page = (page - 1) * imagesPerPage + 1;
|
history!.page = (page - 1) * imagesPerPage() + 1;
|
||||||
} else {
|
} else {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
history!.page = 1;
|
history!.page = 1;
|
||||||
} else {
|
} else {
|
||||||
history!.page = (page - 2) * imagesPerPage + 2;
|
history!.page = (page - 2) * imagesPerPage() + 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,6 +356,8 @@ class _ReaderState extends State<Reader>
|
|||||||
abstract mixin class _ImagePerPageHandler {
|
abstract mixin class _ImagePerPageHandler {
|
||||||
late int _lastImagesPerPage;
|
late int _lastImagesPerPage;
|
||||||
|
|
||||||
|
late bool _lastOrientation;
|
||||||
|
|
||||||
bool get isPortrait;
|
bool get isPortrait;
|
||||||
|
|
||||||
int get page;
|
int get page;
|
||||||
@@ -353,46 +366,72 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
|
|
||||||
ReaderMode get mode;
|
ReaderMode get mode;
|
||||||
|
|
||||||
|
String get cid;
|
||||||
|
|
||||||
|
ComicType get type;
|
||||||
|
|
||||||
void initImagesPerPage(int initialPage) {
|
void initImagesPerPage(int initialPage) {
|
||||||
_lastImagesPerPage = imagesPerPage;
|
_lastImagesPerPage = imagesPerPage();
|
||||||
if (imagesPerPage != 1) {
|
_lastOrientation = isPortrait;
|
||||||
if (showSingleImageOnFirstPage) {
|
if (imagesPerPage() != 1) {
|
||||||
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
|
if (showSingleImageOnFirstPage()) {
|
||||||
|
page = ((initialPage - 1) / imagesPerPage()).ceil() + 1;
|
||||||
} else {
|
} else {
|
||||||
page = (initialPage / imagesPerPage).ceil();
|
page = (initialPage / imagesPerPage()).ceil();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get showSingleImageOnFirstPage =>
|
bool showSingleImageOnFirstPage() =>
|
||||||
appdata.settings["showSingleImageOnFirstPage"];
|
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
|
||||||
|
|
||||||
/// The number of images displayed on one screen
|
/// The number of images displayed on one screen
|
||||||
int get imagesPerPage {
|
int imagesPerPage() {
|
||||||
if (mode.isContinuous) return 1;
|
if (mode.isContinuous) return 1;
|
||||||
if (isPortrait) {
|
if (isPortrait) {
|
||||||
return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1;
|
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
|
||||||
} else {
|
} else {
|
||||||
return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1;
|
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForLandscape') ?? 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the number of images per page has changed
|
/// Check if the number of images per page has changed
|
||||||
void _checkImagesPerPageChange() {
|
void _checkImagesPerPageChange() {
|
||||||
int currentImagesPerPage = imagesPerPage;
|
int currentImagesPerPage = imagesPerPage();
|
||||||
if (_lastImagesPerPage != currentImagesPerPage) {
|
bool currentOrientation = isPortrait;
|
||||||
_adjustPageForImagesPerPageChange(
|
|
||||||
_lastImagesPerPage, currentImagesPerPage);
|
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
||||||
|
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
||||||
_lastImagesPerPage = currentImagesPerPage;
|
_lastImagesPerPage = currentImagesPerPage;
|
||||||
|
_lastOrientation = currentOrientation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adjust the page number when the number of images per page changes
|
/// Adjust the page number when the number of images per page changes
|
||||||
void _adjustPageForImagesPerPageChange(
|
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
||||||
int oldImagesPerPage, int newImagesPerPage) {
|
int previousImageIndex = 1;
|
||||||
int previousImageIndex = (page - 1) * oldImagesPerPage;
|
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
||||||
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
|
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
||||||
page = newPage;
|
} else {
|
||||||
|
if (page == 1) {
|
||||||
|
previousImageIndex = 1;
|
||||||
|
} else {
|
||||||
|
previousImageIndex = (page - 2) * oldImagesPerPage + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int newPage;
|
||||||
|
if (newImagesPerPage != 1) {
|
||||||
|
if (showSingleImageOnFirstPage()) {
|
||||||
|
newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1;
|
||||||
|
} else {
|
||||||
|
newPage = (previousImageIndex / newImagesPerPage).ceil();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPage = previousImageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
page = newPage>0 ? newPage : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,9 +498,13 @@ abstract mixin class _ReaderLocation {
|
|||||||
|
|
||||||
bool get isLoading;
|
bool get isLoading;
|
||||||
|
|
||||||
|
String get cid;
|
||||||
|
|
||||||
|
ComicType get type;
|
||||||
|
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
bool get enablePageAnimation => appdata.settings['enablePageAnimation'];
|
bool enablePageAnimation(String cid, ComicType type) => appdata.settings.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
||||||
|
|
||||||
_ImageViewController? _imageViewController;
|
_ImageViewController? _imageViewController;
|
||||||
|
|
||||||
@@ -498,7 +541,7 @@ abstract mixin class _ReaderLocation {
|
|||||||
}
|
}
|
||||||
this.page = page;
|
this.page = page;
|
||||||
update();
|
update();
|
||||||
if (enablePageAnimation) {
|
if (enablePageAnimation(cid, type)) {
|
||||||
_animationCount++;
|
_animationCount++;
|
||||||
_imageViewController!.animateToPage(page).then((_) {
|
_imageViewController!.animateToPage(page).then((_) {
|
||||||
_animationCount--;
|
_animationCount--;
|
||||||
@@ -537,12 +580,12 @@ abstract mixin class _ReaderLocation {
|
|||||||
|
|
||||||
Timer? autoPageTurningTimer;
|
Timer? autoPageTurningTimer;
|
||||||
|
|
||||||
void autoPageTurning() {
|
void autoPageTurning(String cid, ComicType type) {
|
||||||
if (autoPageTurningTimer != null) {
|
if (autoPageTurningTimer != null) {
|
||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
autoPageTurningTimer = null;
|
autoPageTurningTimer = null;
|
||||||
} else {
|
} else {
|
||||||
int interval = appdata.settings['autoPageTurningInterval'];
|
int interval = appdata.settings.getReaderSetting(cid, type.sourceKey, 'autoPageTurningInterval');
|
||||||
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
||||||
if (page == maxPage) {
|
if (page == maxPage) {
|
||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
|
@@ -128,9 +128,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(child: widget.child),
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
if (appdata.settings['showPageNumberInReader'] == true)
|
if (appdata.settings['showPageNumberInReader'] == true)
|
||||||
buildPageInfoText(),
|
buildPageInfoText(),
|
||||||
buildStatusInfo(),
|
buildStatusInfo(),
|
||||||
@@ -168,10 +166,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface.toOpacity(0.92),
|
color: context.colorScheme.surface.toOpacity(0.92),
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(color: Colors.grey.toOpacity(0.5), width: 0.5),
|
||||||
color: Colors.grey.toOpacity(0.5),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -217,7 +212,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
if (context.reader.images![0].contains('file://')) {
|
if (context.reader.images![0].contains('file://')) {
|
||||||
showToast(
|
showToast(
|
||||||
message: "Local comic collection is not supported at present".tl,
|
message: "Local comic collection is not supported at present".tl,
|
||||||
context: context);
|
context: context,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String id = context.reader.cid;
|
String id = context.reader.cid;
|
||||||
@@ -234,8 +230,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
List<String> tags = context.reader.widget.tags;
|
List<String> tags = context.reader.widget.tags;
|
||||||
String author = context.reader.widget.author;
|
String author = context.reader.widget.author;
|
||||||
|
|
||||||
var epName = context.reader.widget.chapters?.titles
|
var epName =
|
||||||
.elementAtOrNull(context.reader.chapter - 1) ??
|
context.reader.widget.chapters?.titles.elementAtOrNull(
|
||||||
|
context.reader.chapter - 1,
|
||||||
|
) ??
|
||||||
"E${context.reader.chapter}";
|
"E${context.reader.chapter}";
|
||||||
var translatedTags = tags.map((e) => e.translateTagsToCN).toList();
|
var translatedTags = tags.map((e) => e.translateTagsToCN).toList();
|
||||||
|
|
||||||
@@ -248,7 +246,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImageFavoriteManager().deleteImageFavorite([
|
ImageFavoriteManager().deleteImageFavorite([
|
||||||
ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName)
|
ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName),
|
||||||
]);
|
]);
|
||||||
showToast(
|
showToast(
|
||||||
message: "Uncollected the image".tl,
|
message: "Uncollected the image".tl,
|
||||||
@@ -256,7 +254,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
seconds: 1,
|
seconds: 1,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var imageFavoritesComic = ImageFavoriteManager().find(id, sourceKey) ??
|
var imageFavoritesComic =
|
||||||
|
ImageFavoriteManager().find(id, sourceKey) ??
|
||||||
ImageFavoritesComic(
|
ImageFavoritesComic(
|
||||||
id,
|
id,
|
||||||
[],
|
[],
|
||||||
@@ -270,10 +269,19 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
subTitle,
|
subTitle,
|
||||||
maxPage,
|
maxPage,
|
||||||
);
|
);
|
||||||
ImageFavorite imageFavorite =
|
ImageFavorite imageFavorite = ImageFavorite(
|
||||||
ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName);
|
page,
|
||||||
ImageFavoritesEp? imageFavoritesEp =
|
imageKey,
|
||||||
imageFavoritesComic.imageFavoritesEp.firstWhereOrNull((e) {
|
null,
|
||||||
|
eid,
|
||||||
|
id,
|
||||||
|
ep,
|
||||||
|
sourceKey,
|
||||||
|
epName,
|
||||||
|
);
|
||||||
|
ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic
|
||||||
|
.imageFavoritesEp
|
||||||
|
.firstWhereOrNull((e) {
|
||||||
return e.ep == ep;
|
return e.ep == ep;
|
||||||
});
|
});
|
||||||
if (imageFavoritesEp == null) {
|
if (imageFavoritesEp == null) {
|
||||||
@@ -285,10 +293,20 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
// 不是第一页的话, 自动塞一个封面进去
|
// 不是第一页的话, 自动塞一个封面进去
|
||||||
imageFavoritesEp = ImageFavoritesEp(
|
imageFavoritesEp = ImageFavoritesEp(
|
||||||
eid, ep, [copy, imageFavorite], epName, maxPage);
|
eid,
|
||||||
|
ep,
|
||||||
|
[copy, imageFavorite],
|
||||||
|
epName,
|
||||||
|
maxPage,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
imageFavoritesEp =
|
imageFavoritesEp = ImageFavoritesEp(
|
||||||
ImageFavoritesEp(eid, ep, [imageFavorite], epName, maxPage);
|
eid,
|
||||||
|
ep,
|
||||||
|
[imageFavorite],
|
||||||
|
epName,
|
||||||
|
maxPage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp);
|
imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp);
|
||||||
} else {
|
} else {
|
||||||
@@ -312,7 +330,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
|
|
||||||
ImageFavoriteManager().addOrUpdateOrDelete(imageFavoritesComic);
|
ImageFavoriteManager().addOrUpdateOrDelete(imageFavoritesComic);
|
||||||
showToast(
|
showToast(
|
||||||
message: "Successfully collected".tl, context: context, seconds: 1);
|
message: "Successfully collected".tl,
|
||||||
|
context: context,
|
||||||
|
seconds: 1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
@@ -327,65 +348,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
text = "P${context.reader.page}";
|
text = "P${context.reader.page}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget child = SizedBox(
|
final buttons = [
|
||||||
height: kBottomBarHeight,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: () => !isReversed
|
|
||||||
? context.reader.chapter > 1
|
|
||||||
? context.reader.toPrevChapter()
|
|
||||||
: context.reader.toPage(1)
|
|
||||||
: context.reader.chapter < context.reader.maxChapter
|
|
||||||
? context.reader.toNextChapter()
|
|
||||||
: context.reader.toPage(context.reader.maxPage),
|
|
||||||
icon: const Icon(Icons.first_page),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: buildSlider(),
|
|
||||||
),
|
|
||||||
IconButton.filledTonal(
|
|
||||||
onPressed: () => !isReversed
|
|
||||||
? context.reader.chapter < context.reader.maxChapter
|
|
||||||
? context.reader.toNextChapter()
|
|
||||||
: context.reader.toPage(context.reader.maxPage)
|
|
||||||
: context.reader.chapter > 1
|
|
||||||
? context.reader.toPrevChapter()
|
|
||||||
: context.reader.toPage(1),
|
|
||||||
icon: const Icon(Icons.last_page)),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 24,
|
|
||||||
padding: const EdgeInsets.fromLTRB(6, 2, 6, 0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(text),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Collect the image".tl,
|
message: "Collect the image".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon:
|
icon: Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
|
||||||
Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
|
|
||||||
onPressed: addImageFavorite,
|
onPressed: addImageFavorite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -427,14 +394,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
});
|
});
|
||||||
SystemChrome.setPreferredOrientations([
|
SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.landscapeLeft,
|
DeviceOrientation.landscapeLeft,
|
||||||
DeviceOrientation.landscapeRight
|
DeviceOrientation.landscapeRight,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
rotation = null;
|
rotation = null;
|
||||||
});
|
});
|
||||||
SystemChrome.setPreferredOrientations(
|
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||||
DeviceOrientation.values);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -446,7 +412,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
? const Icon(Icons.timer)
|
? const Icon(Icons.timer)
|
||||||
: const Icon(Icons.timer_sharp),
|
: const Icon(Icons.timer_sharp),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.reader.autoPageTurning();
|
context.reader.autoPageTurning(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type,
|
||||||
|
);
|
||||||
update();
|
update();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -468,14 +437,63 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Share".tl,
|
message: "Share".tl,
|
||||||
child: IconButton(
|
child: IconButton(icon: const Icon(Icons.share), onPressed: share),
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: share,
|
|
||||||
),
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget child = SizedBox(
|
||||||
|
height: kBottomBarHeight,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => !isReversed
|
||||||
|
? context.reader.chapter > 1
|
||||||
|
? context.reader.toPrevChapter()
|
||||||
|
: context.reader.toPage(1)
|
||||||
|
: context.reader.chapter < context.reader.maxChapter
|
||||||
|
? context.reader.toNextChapter()
|
||||||
|
: context.reader.toPage(context.reader.maxPage),
|
||||||
|
icon: const Icon(Icons.first_page),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4)
|
Expanded(child: buildSlider()),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => !isReversed
|
||||||
|
? context.reader.chapter < context.reader.maxChapter
|
||||||
|
? context.reader.toNextChapter()
|
||||||
|
: context.reader.toPage(context.reader.maxPage)
|
||||||
|
: context.reader.chapter > 1
|
||||||
|
? context.reader.toPrevChapter()
|
||||||
|
: context.reader.toPage(1),
|
||||||
|
icon: const Icon(Icons.last_page),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if ((constrains.maxWidth - buttons.length * 42) > 80)
|
||||||
|
Container(
|
||||||
|
height: 24,
|
||||||
|
padding: const EdgeInsets.fromLTRB(6, 2, 6, 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(child: Text(text)),
|
||||||
|
).paddingLeft(16),
|
||||||
|
const Spacer(),
|
||||||
|
...buttons,
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -506,8 +524,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
focusNode: sliderFocus,
|
focusNode: sliderFocus,
|
||||||
value: context.reader.page.toDouble(),
|
value: context.reader.page.toDouble(),
|
||||||
min: 1,
|
min: 1,
|
||||||
max:
|
max: context.reader.maxPage
|
||||||
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
.clamp(context.reader.page, 1 << 16)
|
||||||
|
.toDouble(),
|
||||||
reversed: isReversed,
|
reversed: isReversed,
|
||||||
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
||||||
onChanged: (i) {
|
onChanged: (i) {
|
||||||
@@ -517,8 +536,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildPageInfoText() {
|
Widget buildPageInfoText() {
|
||||||
var epName = context.reader.widget.chapters?.titles
|
var epName =
|
||||||
.elementAtOrNull(context.reader.chapter - 1) ??
|
context.reader.widget.chapters?.titles.elementAtOrNull(
|
||||||
|
context.reader.chapter - 1,
|
||||||
|
) ??
|
||||||
"E${context.reader.chapter}";
|
"E${context.reader.chapter}";
|
||||||
if (epName.length > 8) {
|
if (epName.length > 8) {
|
||||||
epName = "${epName.substring(0, 8)}...";
|
epName = "${epName.substring(0, 8)}...";
|
||||||
@@ -594,23 +615,31 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var filename = "${context.reader.page}${fileType.ext}";
|
var filename = "${context.reader.page}${fileType.ext}";
|
||||||
Share.shareFile(
|
Share.shareFile(data: data, filename: filename, mime: fileType.mime);
|
||||||
data: data,
|
|
||||||
filename: filename,
|
|
||||||
mime: fileType.mime,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void openSetting() {
|
void openSetting() {
|
||||||
showSideBar(
|
showSideBar(
|
||||||
context,
|
context,
|
||||||
ReaderSettings(
|
ReaderSettings(
|
||||||
|
comicId: context.reader.cid,
|
||||||
|
comicSource: context.reader.type.sourceKey,
|
||||||
onChanged: (key) {
|
onChanged: (key) {
|
||||||
if (key == "readerMode") {
|
if (key == "readerMode") {
|
||||||
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
context.reader.mode = ReaderMode.fromKey(
|
||||||
|
appdata.settings.getReaderSetting(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type.sourceKey,
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (key == "enableTurnPageByVolumeKey") {
|
if (key == "enableTurnPageByVolumeKey") {
|
||||||
if (appdata.settings[key]) {
|
if (appdata.settings.getReaderSetting(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type.sourceKey,
|
||||||
|
key,
|
||||||
|
)) {
|
||||||
context.reader.handleVolumeEvent();
|
context.reader.handleVolumeEvent();
|
||||||
} else {
|
} else {
|
||||||
context.reader.stopVolumeEvent();
|
context.reader.stopVolumeEvent();
|
||||||
@@ -716,8 +745,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
return await File(imageKey.substring(7)).readAsBytes();
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
} else {
|
} else {
|
||||||
return (await CacheManager().findCache(
|
return (await CacheManager().findCache(
|
||||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}",
|
||||||
.readAsBytes();
|
))!.readAsBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,14 +762,17 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
entry = OverlayEntry(
|
entry = OverlayEntry(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Positioned.fill(
|
return Positioned.fill(
|
||||||
child: _SelectImageOverlayContent(onTap: (offset) {
|
child: _SelectImageOverlayContent(
|
||||||
|
onTap: (offset) {
|
||||||
completer.complete(offset);
|
completer.complete(offset);
|
||||||
entry!.remove();
|
entry!.remove();
|
||||||
}, onDispose: () {
|
},
|
||||||
|
onDispose: () {
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted) {
|
||||||
completer.complete(null);
|
completer.complete(null);
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -840,9 +872,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
|
|||||||
size: 16,
|
size: 16,
|
||||||
color: batteryColor,
|
color: batteryColor,
|
||||||
// Stroke
|
// Stroke
|
||||||
shadows: List.generate(
|
shadows: List.generate(9, (index) {
|
||||||
9,
|
|
||||||
(index) {
|
|
||||||
if (index == 4) {
|
if (index == 4) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -852,8 +882,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
|
|||||||
color: context.colorScheme.onInverseSurface,
|
color: context.colorScheme.onInverseSurface,
|
||||||
offset: Offset(offsetX, offsetY),
|
offset: Offset(offsetX, offsetY),
|
||||||
);
|
);
|
||||||
},
|
}).whereType<Shadow>().toList(),
|
||||||
).whereType<Shadow>().toList(),
|
|
||||||
),
|
),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -940,10 +969,12 @@ class _SelectImageOverlayContent extends StatefulWidget {
|
|||||||
final void Function() onDispose;
|
final void Function() onDispose;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_SelectImageOverlayContent> createState() => _SelectImageOverlayContentState();
|
State<_SelectImageOverlayContent> createState() =>
|
||||||
|
_SelectImageOverlayContentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent> {
|
class _SelectImageOverlayContentState
|
||||||
|
extends State<_SelectImageOverlayContent> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.onDispose();
|
widget.onDispose();
|
||||||
@@ -960,19 +991,14 @@ class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent>
|
|||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black.withAlpha(50),
|
color: Colors.black.withAlpha(50),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment(
|
alignment: Alignment(0, -0.8),
|
||||||
0,
|
|
||||||
-0.8,
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 232,
|
width: 232,
|
||||||
height: 42,
|
height: 42,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface,
|
color: context.colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(color: context.colorScheme.outlineVariant),
|
||||||
color: context.colorScheme.outlineVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@@ -193,12 +193,46 @@ class LogsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LogsPageState extends State<LogsPage> {
|
class _LogsPageState extends State<LogsPage> {
|
||||||
|
String logLevelToShow = "all";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var logToShow = logLevelToShow == "all"
|
||||||
|
? Log.logs
|
||||||
|
: Log.logs.where((log) => log.level.name == logLevelToShow).toList();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(
|
appBar: Appbar(
|
||||||
title: const Text("Logs"),
|
title: Text("Logs".tl),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
final RelativeRect position = RelativeRect.fromLTRB(
|
||||||
|
MediaQuery.of(context).size.width,
|
||||||
|
MediaQuery.of(context).padding.top + kToolbarHeight,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
showMenu(context: context, position: position, items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("all"),
|
||||||
|
onTap: () => setState(() => logLevelToShow = "all")
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("info"),
|
||||||
|
onTap: () => setState(() => logLevelToShow = "info")
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("warning"),
|
||||||
|
onTap: () => setState(() => logLevelToShow = "warning")
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("error"),
|
||||||
|
onTap: () => setState(() => logLevelToShow = "error")
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
icon: const Icon(Icons.filter_list_outlined)
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
final RelativeRect position = RelativeRect.fromLTRB(
|
final RelativeRect position = RelativeRect.fromLTRB(
|
||||||
@@ -217,7 +251,7 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Log.ignoreLimitation = true;
|
Log.ignoreLimitation = true;
|
||||||
context.showMessage(
|
context.showMessage(
|
||||||
message: "Only valid for this run");
|
message: "Only valid for this run".tl);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -232,9 +266,9 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: Log.logs.length,
|
itemCount: logToShow.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
index = Log.logs.length - index - 1;
|
index = logToShow.length - index - 1;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
child: SelectionArea(
|
child: SelectionArea(
|
||||||
@@ -253,7 +287,7 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||||
child: Text(Log.logs[index].title),
|
child: Text(logToShow[index].title),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -265,16 +299,16 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
Theme.of(context).colorScheme.error,
|
Theme.of(context).colorScheme.error,
|
||||||
Theme.of(context).colorScheme.errorContainer,
|
Theme.of(context).colorScheme.errorContainer,
|
||||||
Theme.of(context).colorScheme.primaryContainer
|
Theme.of(context).colorScheme.primaryContainer
|
||||||
][Log.logs[index].level.index],
|
][logToShow[index].level.index],
|
||||||
borderRadius:
|
borderRadius:
|
||||||
const BorderRadius.all(Radius.circular(16)),
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||||
child: Text(
|
child: Text(
|
||||||
Log.logs[index].level.name,
|
logToShow[index].level.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Log.logs[index].level.index == 0
|
color: logToShow[index].level.index == 0
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: Colors.black),
|
: Colors.black),
|
||||||
),
|
),
|
||||||
@@ -282,14 +316,14 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(Log.logs[index].content),
|
Text(logToShow[index].content),
|
||||||
Text(Log.logs[index].time
|
Text(logToShow[index].time
|
||||||
.toString()
|
.toString()
|
||||||
.replaceAll(RegExp(r"\.\w+"), "")),
|
.replaceAll(RegExp(r"\.\w+"), "")),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: Log.logs[index].content));
|
ClipboardData(text: logToShow[index].content));
|
||||||
},
|
},
|
||||||
child: Text("Copy".tl),
|
child: Text("Copy".tl),
|
||||||
),
|
),
|
||||||
|
@@ -18,8 +18,8 @@ class DebugPageState extends State<DebugPage> {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverAppbar(title: Text("Debug".tl)),
|
SliverAppbar(title: Text("Debug".tl)),
|
||||||
_CallbackSetting(
|
_CallbackSetting(
|
||||||
title: "Reload Configs",
|
title: "Reload Configs".tl,
|
||||||
actionTitle: "Reload",
|
actionTitle: "Reload".tl,
|
||||||
callback: () {
|
callback: () {
|
||||||
ComicSourceManager().reload();
|
ComicSourceManager().reload();
|
||||||
},
|
},
|
||||||
@@ -31,6 +31,10 @@ class DebugPageState extends State<DebugPage> {
|
|||||||
},
|
},
|
||||||
actionTitle: 'Open'.tl,
|
actionTitle: 'Open'.tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Ignore Certificate Errors".tl,
|
||||||
|
settingKey: "ignoreBadCertificate",
|
||||||
|
).toSliver(),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -58,7 +62,7 @@ class DebugPageState extends State<DebugPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
try {
|
try {
|
||||||
var res = JsEngine().runCode(controller.text);
|
var res = JsEngine().runCode(controller.text, "<debug>");
|
||||||
setState(() {
|
setState(() {
|
||||||
result = res.toString();
|
result = res.toString();
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
part of 'settings_page.dart';
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
class ReaderSettings extends StatefulWidget {
|
class ReaderSettings extends StatefulWidget {
|
||||||
const ReaderSettings({super.key, this.onChanged});
|
const ReaderSettings({
|
||||||
|
super.key,
|
||||||
|
this.onChanged,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
|
});
|
||||||
|
|
||||||
final void Function(String key)? onChanged;
|
final void Function(String key)? onChanged;
|
||||||
|
final String? comicId;
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ReaderSettings> createState() => _ReaderSettingsState();
|
State<ReaderSettings> createState() => _ReaderSettingsState();
|
||||||
@@ -12,15 +19,57 @@ class ReaderSettings extends StatefulWidget {
|
|||||||
class _ReaderSettingsState extends State<ReaderSettings> {
|
class _ReaderSettingsState extends State<ReaderSettings> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final comicId = widget.comicId;
|
||||||
|
final sourceKey = widget.comicSource;
|
||||||
|
final key = "$comicId@$sourceKey";
|
||||||
|
|
||||||
|
bool isEnabledSpecificSettings =
|
||||||
|
comicId != null &&
|
||||||
|
appdata.settings.isComicSpecificSettingsEnabled(comicId, sourceKey);
|
||||||
|
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppbar(title: Text("Reading".tl)),
|
SliverAppbar(title: Text("Reading".tl)),
|
||||||
|
if (comicId != null && sourceKey != null)
|
||||||
|
SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text("Enable comic specific settings".tl),
|
||||||
|
value: isEnabledSpecificSettings,
|
||||||
|
onChanged: (b) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings.setEnabledComicSpecificSettings(
|
||||||
|
comicId,
|
||||||
|
sourceKey,
|
||||||
|
b,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
if (isEnabledSpecificSettings)
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings.resetComicReaderSettings(key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Clear specific reader settings for this comic".tl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toSliver(),
|
||||||
|
Divider().toSliver(),
|
||||||
|
],
|
||||||
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Tap to turn Pages".tl,
|
title: "Tap to turn Pages".tl,
|
||||||
settingKey: "enableTapToTurnPages",
|
settingKey: "enableTapToTurnPages",
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("enableTapToTurnPages");
|
widget.onChanged?.call("enableTapToTurnPages");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Reverse tap to turn Pages".tl,
|
title: "Reverse tap to turn Pages".tl,
|
||||||
@@ -28,6 +77,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("reverseTapToTurnPages");
|
widget.onChanged?.call("reverseTapToTurnPages");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Page animation".tl,
|
title: "Page animation".tl,
|
||||||
@@ -35,6 +86,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("enablePageAnimation");
|
widget.onChanged?.call("enablePageAnimation");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Reading mode".tl,
|
title: "Reading mode".tl,
|
||||||
@@ -58,6 +111,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
}
|
}
|
||||||
widget.onChanged?.call("readerMode");
|
widget.onChanged?.call("readerMode");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SliderSetting(
|
_SliderSetting(
|
||||||
title: "Auto page turning interval".tl,
|
title: "Auto page turning interval".tl,
|
||||||
@@ -69,6 +124,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
widget.onChanged?.call("autoPageTurningInterval");
|
widget.onChanged?.call("autoPageTurningInterval");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SliverAnimatedVisibility(
|
SliverAnimatedVisibility(
|
||||||
visible: appdata.settings['readerMode']!.startsWith('gallery'),
|
visible: appdata.settings['readerMode']!.startsWith('gallery'),
|
||||||
@@ -84,6 +141,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
widget.onChanged?.call("readerScreenPicNumberForLandscape");
|
widget.onChanged?.call("readerScreenPicNumberForLandscape");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverAnimatedVisibility(
|
SliverAnimatedVisibility(
|
||||||
@@ -99,10 +158,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("readerScreenPicNumberForPortrait");
|
widget.onChanged?.call("readerScreenPicNumberForPortrait");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverAnimatedVisibility(
|
SliverAnimatedVisibility(
|
||||||
visible: appdata.settings['readerMode']!.startsWith('gallery') &&
|
visible:
|
||||||
|
appdata.settings['readerMode']!.startsWith('gallery') &&
|
||||||
(appdata.settings['readerScreenPicNumberForLandscape'] > 1 ||
|
(appdata.settings['readerScreenPicNumberForLandscape'] > 1 ||
|
||||||
appdata.settings['readerScreenPicNumberForPortrait'] > 1),
|
appdata.settings['readerScreenPicNumberForPortrait'] > 1),
|
||||||
child: _SwitchSetting(
|
child: _SwitchSetting(
|
||||||
@@ -111,6 +173,23 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("showSingleImageOnFirstPage");
|
widget.onChanged?.call("showSingleImageOnFirstPage");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverAnimatedVisibility(
|
||||||
|
visible: appdata.settings['readerMode']!.startsWith('continuous'),
|
||||||
|
child: _SliderSetting(
|
||||||
|
title: "Mouse scroll speed".tl,
|
||||||
|
settingsIndex: "readerScrollSpeed",
|
||||||
|
interval: 0.1,
|
||||||
|
min: 0.5,
|
||||||
|
max: 3,
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("readerScrollSpeed");
|
||||||
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
@@ -120,6 +199,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
widget.onChanged?.call('enableDoubleTapToZoom');
|
widget.onChanged?.call('enableDoubleTapToZoom');
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Long press to zoom'.tl,
|
title: 'Long press to zoom'.tl,
|
||||||
@@ -128,6 +209,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
widget.onChanged?.call('enableLongPressToZoom');
|
widget.onChanged?.call('enableLongPressToZoom');
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SliverAnimatedVisibility(
|
SliverAnimatedVisibility(
|
||||||
visible: appdata.settings['enableLongPressToZoom'] == true,
|
visible: appdata.settings['enableLongPressToZoom'] == true,
|
||||||
@@ -138,6 +221,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
"press": "Press position".tl,
|
"press": "Press position".tl,
|
||||||
"center": "Screen center".tl,
|
"center": "Screen center".tl,
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
@@ -147,6 +232,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call('limitImageWidth');
|
widget.onChanged?.call('limitImageWidth');
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
if (App.isAndroid)
|
if (App.isAndroid)
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
@@ -155,6 +242,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call('enableTurnPageByVolumeKey');
|
widget.onChanged?.call('enableTurnPageByVolumeKey');
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Display time & battery info in reader".tl,
|
title: "Display time & battery info in reader".tl,
|
||||||
@@ -162,6 +251,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Show system status bar".tl,
|
title: "Show system status bar".tl,
|
||||||
@@ -169,6 +260,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("showSystemStatusBar");
|
widget.onChanged?.call("showSystemStatusBar");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Quick collect image".tl,
|
title: "Quick collect image".tl,
|
||||||
@@ -184,6 +277,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
help:
|
help:
|
||||||
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode"
|
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode"
|
||||||
.tl,
|
.tl,
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_CallbackSetting(
|
_CallbackSetting(
|
||||||
title: "Custom Image Processing".tl,
|
title: "Custom Image Processing".tl,
|
||||||
@@ -196,6 +291,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
interval: 1,
|
interval: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 16,
|
max: 16,
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Show Page Number".tl,
|
title: "Show Page Number".tl,
|
||||||
@@ -203,6 +300,8 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
onChanged: () {
|
onChanged: () {
|
||||||
widget.onChanged?.call("showPageNumberInReader");
|
widget.onChanged?.call("showPageNumberInReader");
|
||||||
},
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -248,7 +347,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Text("Reset".tl),
|
child: Text("Reset".tl),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -274,7 +373,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -6,6 +6,8 @@ class _SwitchSetting extends StatefulWidget {
|
|||||||
required this.settingKey,
|
required this.settingKey,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -16,6 +18,10 @@ class _SwitchSetting extends StatefulWidget {
|
|||||||
|
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
|
final String? comicId;
|
||||||
|
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_SwitchSetting> createState() => _SwitchSettingState();
|
State<_SwitchSetting> createState() => _SwitchSettingState();
|
||||||
}
|
}
|
||||||
@@ -23,16 +29,33 @@ class _SwitchSetting extends StatefulWidget {
|
|||||||
class _SwitchSettingState extends State<_SwitchSetting> {
|
class _SwitchSettingState extends State<_SwitchSetting> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(appdata.settings[widget.settingKey] is bool);
|
var value = widget.comicId == null
|
||||||
|
? appdata.settings[widget.settingKey]
|
||||||
|
: appdata.settings.getReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(value is bool);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),
|
subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: appdata.settings[widget.settingKey],
|
value: value,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (widget.comicId == null) {
|
||||||
appdata.settings[widget.settingKey] = value;
|
appdata.settings[widget.settingKey] = value;
|
||||||
|
} else {
|
||||||
|
appdata.settings.setReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
appdata.saveData().then((_) {
|
appdata.saveData().then((_) {
|
||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
@@ -51,6 +74,8 @@ class SelectSetting extends StatelessWidget {
|
|||||||
required this.optionTranslation,
|
required this.optionTranslation,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.help,
|
this.help,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -63,6 +88,10 @@ class SelectSetting extends StatelessWidget {
|
|||||||
|
|
||||||
final String? help;
|
final String? help;
|
||||||
|
|
||||||
|
final String? comicId;
|
||||||
|
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@@ -76,6 +105,8 @@ class SelectSetting extends StatelessWidget {
|
|||||||
optionTranslation: optionTranslation,
|
optionTranslation: optionTranslation,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
help: help,
|
help: help,
|
||||||
|
comicId: comicId,
|
||||||
|
comicSource: comicSource,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return _EndSelectorSelectSetting(
|
return _EndSelectorSelectSetting(
|
||||||
@@ -84,6 +115,8 @@ class SelectSetting extends StatelessWidget {
|
|||||||
optionTranslation: optionTranslation,
|
optionTranslation: optionTranslation,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
help: help,
|
help: help,
|
||||||
|
comicId: comicId,
|
||||||
|
comicSource: comicSource,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,6 +132,8 @@ class _DoubleLineSelectSettings extends StatefulWidget {
|
|||||||
required this.optionTranslation,
|
required this.optionTranslation,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.help,
|
this.help,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -111,6 +146,10 @@ class _DoubleLineSelectSettings extends StatefulWidget {
|
|||||||
|
|
||||||
final String? help;
|
final String? help;
|
||||||
|
|
||||||
|
final String? comicId;
|
||||||
|
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DoubleLineSelectSettings> createState() =>
|
State<_DoubleLineSelectSettings> createState() =>
|
||||||
_DoubleLineSelectSettingsState();
|
_DoubleLineSelectSettingsState();
|
||||||
@@ -119,6 +158,14 @@ class _DoubleLineSelectSettings extends StatefulWidget {
|
|||||||
class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var value = widget.comicId == null
|
||||||
|
? appdata.settings[widget.settingKey]
|
||||||
|
: appdata.settings.getReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -134,9 +181,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(widget.help!)
|
content: Text(
|
||||||
.paddingHorizontal(16)
|
widget.help!,
|
||||||
.fixWidth(double.infinity),
|
).paddingHorizontal(16).fixWidth(double.infinity),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: context.pop,
|
onPressed: context.pop,
|
||||||
@@ -150,9 +197,7 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(widget.optionTranslation[value] ?? "None"),
|
||||||
widget.optionTranslation[appdata.settings[widget.settingKey]] ??
|
|
||||||
"None"),
|
|
||||||
trailing: const Icon(Icons.arrow_drop_down),
|
trailing: const Icon(Icons.arrow_drop_down),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
var renderBox = context.findRenderObject() as RenderBox;
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
@@ -170,16 +215,27 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
|||||||
Offset.zero & MediaQuery.of(context).size,
|
Offset.zero & MediaQuery.of(context).size,
|
||||||
),
|
),
|
||||||
items: widget.optionTranslation.keys
|
items: widget.optionTranslation.keys
|
||||||
.map((key) => PopupMenuItem(
|
.map(
|
||||||
|
(key) => PopupMenuItem(
|
||||||
value: key,
|
value: key,
|
||||||
height: App.isMobile ? 46 : 40,
|
height: App.isMobile ? 46 : 40,
|
||||||
child: Text(widget.optionTranslation[key]!),
|
child: Text(widget.optionTranslation[key]!),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (widget.comicId == null) {
|
||||||
appdata.settings[widget.settingKey] = value;
|
appdata.settings[widget.settingKey] = value;
|
||||||
|
} else {
|
||||||
|
appdata.settings.setReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
@@ -197,6 +253,8 @@ class _EndSelectorSelectSetting extends StatefulWidget {
|
|||||||
required this.optionTranslation,
|
required this.optionTranslation,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.help,
|
this.help,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -209,6 +267,10 @@ class _EndSelectorSelectSetting extends StatefulWidget {
|
|||||||
|
|
||||||
final String? help;
|
final String? help;
|
||||||
|
|
||||||
|
final String? comicId;
|
||||||
|
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_EndSelectorSelectSetting> createState() =>
|
State<_EndSelectorSelectSetting> createState() =>
|
||||||
_EndSelectorSelectSettingState();
|
_EndSelectorSelectSettingState();
|
||||||
@@ -218,6 +280,13 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var options = widget.optionTranslation;
|
var options = widget.optionTranslation;
|
||||||
|
var value = widget.comicId == null
|
||||||
|
? appdata.settings[widget.settingKey]
|
||||||
|
: appdata.settings.getReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -233,9 +302,9 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(widget.help!)
|
content: Text(
|
||||||
.paddingHorizontal(16)
|
widget.help!,
|
||||||
.fixWidth(double.infinity),
|
).paddingHorizontal(16).fixWidth(double.infinity),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: context.pop,
|
onPressed: context.pop,
|
||||||
@@ -250,12 +319,22 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Select(
|
trailing: Select(
|
||||||
current: options[appdata.settings[widget.settingKey]],
|
current: options[value],
|
||||||
values: options.values.toList(),
|
values: options.values.toList(),
|
||||||
minWidth: 64,
|
minWidth: 64,
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
appdata.settings[widget.settingKey] = options.keys.elementAt(index);
|
var value = options.keys.elementAt(index);
|
||||||
|
if (widget.comicId == null) {
|
||||||
|
appdata.settings[widget.settingKey] = value;
|
||||||
|
} else {
|
||||||
|
appdata.settings.setReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingKey,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
@@ -273,6 +352,8 @@ class _SliderSetting extends StatefulWidget {
|
|||||||
required this.min,
|
required this.min,
|
||||||
required this.max,
|
required this.max,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
|
this.comicId,
|
||||||
|
this.comicSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -287,6 +368,10 @@ class _SliderSetting extends StatefulWidget {
|
|||||||
|
|
||||||
final VoidCallback? onChanged;
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
final String? comicId;
|
||||||
|
|
||||||
|
final String? comicSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_SliderSetting> createState() => _SliderSettingState();
|
State<_SliderSetting> createState() => _SliderSettingState();
|
||||||
}
|
}
|
||||||
@@ -294,28 +379,52 @@ class _SliderSetting extends StatefulWidget {
|
|||||||
class _SliderSettingState extends State<_SliderSetting> {
|
class _SliderSettingState extends State<_SliderSetting> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var value =
|
||||||
|
(widget.comicId == null
|
||||||
|
? appdata.settings[widget.settingsIndex]
|
||||||
|
: appdata.settings.getReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingsIndex,
|
||||||
|
))
|
||||||
|
.toDouble();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(widget.title),
|
Text(widget.title),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(value.toString(), style: ts.s12),
|
||||||
appdata.settings[widget.settingsIndex].toString(),
|
|
||||||
style: ts.s12,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Slider(
|
subtitle: Slider(
|
||||||
value: appdata.settings[widget.settingsIndex].toDouble(),
|
value: value,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value.toInt() == value) {
|
if (value.toInt() == value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (widget.comicId == null) {
|
||||||
appdata.settings[widget.settingsIndex] = value.toInt();
|
appdata.settings[widget.settingsIndex] = value.toInt();
|
||||||
|
} else {
|
||||||
|
appdata.settings.setReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingsIndex,
|
||||||
|
value.toInt(),
|
||||||
|
);
|
||||||
|
}
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (widget.comicId == null) {
|
||||||
appdata.settings[widget.settingsIndex] = value;
|
appdata.settings[widget.settingsIndex] = value;
|
||||||
|
} else {
|
||||||
|
appdata.settings.setReaderSetting(
|
||||||
|
widget.comicId!,
|
||||||
|
widget.comicSource!,
|
||||||
|
widget.settingsIndex,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -405,7 +514,8 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
offset: Offset(0, 2),
|
offset: Offset(0, 2),
|
||||||
spreadRadius: 2)
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onReorder: (reorderFunc) {
|
onReorder: (reorderFunc) {
|
||||||
@@ -435,7 +545,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
label: Text("Add".tl),
|
label: Text("Add".tl),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
onPressed: showAddDialog,
|
onPressed: showAddDialog,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
body: view,
|
body: view,
|
||||||
);
|
);
|
||||||
@@ -450,7 +560,8 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
keys.remove(key);
|
keys.remove(key);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete_outline)),
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -458,10 +569,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
key: Key(key),
|
key: Key(key),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [removeButton, const Icon(Icons.drag_handle)],
|
||||||
removeButton,
|
|
||||||
const Icon(Icons.drag_handle),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -477,7 +585,8 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Add".tl,
|
title: "Add".tl,
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -534,7 +643,8 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,8 @@ final _resolver = MimeTypeResolver()
|
|||||||
..addMagicNumber([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 'application/x-7z-compressed')
|
..addMagicNumber([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 'application/x-7z-compressed')
|
||||||
// rar
|
// rar
|
||||||
..addMagicNumber([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], 'application/vnd.rar')
|
..addMagicNumber([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], 'application/vnd.rar')
|
||||||
|
// avif
|
||||||
|
..addMagicNumber([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66], 'image/avif')
|
||||||
;
|
;
|
||||||
|
|
||||||
FileType detectFileType(List<int> data) {
|
FileType detectFileType(List<int> data) {
|
||||||
|
67
lib/utils/opencc.dart
Normal file
67
lib/utils/opencc.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
abstract class OpenCC {
|
||||||
|
static late final Map<int, int> _s2t;
|
||||||
|
static late final Map<int, int> _t2s;
|
||||||
|
|
||||||
|
static Future<void> init() async {
|
||||||
|
var data = await rootBundle.load("assets/opencc.txt");
|
||||||
|
var txt = utf8.decode(data.buffer.asUint8List());
|
||||||
|
_s2t = <int, int>{};
|
||||||
|
_t2s = <int, int>{};
|
||||||
|
for (var line in txt.split('\n')) {
|
||||||
|
if (line.isEmpty || line.startsWith('#') || line.length != 2) continue;
|
||||||
|
var s = line.runes.elementAt(0);
|
||||||
|
var t = line.runes.elementAt(1);
|
||||||
|
_s2t[s] = t;
|
||||||
|
_t2s[t] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasChineseSimplified(String text) {
|
||||||
|
if (text != "监禁") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var rune in text.runes) {
|
||||||
|
if (_s2t.containsKey(rune)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasChineseTraditional(String text) {
|
||||||
|
for (var rune in text.runes) {
|
||||||
|
if (_t2s.containsKey(rune)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String simplifiedToTraditional(String text) {
|
||||||
|
var sb = StringBuffer();
|
||||||
|
for (var rune in text.runes) {
|
||||||
|
if (_s2t.containsKey(rune)) {
|
||||||
|
sb.write(String.fromCharCodes([_s2t[rune]!]));
|
||||||
|
} else {
|
||||||
|
sb.write(String.fromCharCodes([rune]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String traditionalToSimplified(String text) {
|
||||||
|
var sb = StringBuffer();
|
||||||
|
for (var rune in text.runes) {
|
||||||
|
if (_t2s.containsKey(rune)) {
|
||||||
|
sb.write(String.fromCharCodes([_t2s[rune]!]));
|
||||||
|
} else {
|
||||||
|
sb.write(String.fromCharCodes([rune]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
30
pubspec.lock
30
pubspec.lock
@@ -170,6 +170,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
display_mode:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: display_mode
|
||||||
|
sha256: "8a381f3602a09dc4e96140a0df30808631468d6d0dfff7722f67b1f83757a7cc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -548,26 +556,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "11.0.1"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -933,10 +941,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.6"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1029,10 +1037,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1108,4 +1116,4 @@ packages:
|
|||||||
version: "0.0.12"
|
version: "0.0.12"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.32.4"
|
flutter: ">=3.35.2"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.4.5+145
|
version: 1.5.1+151
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
flutter: 3.32.4
|
flutter: 3.35.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -86,6 +86,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
yaml: ^3.1.3
|
yaml: ^3.1.3
|
||||||
enough_convert: ^1.6.0
|
enough_convert: ^1.6.0
|
||||||
|
display_mode: ^0.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -102,6 +103,7 @@ flutter:
|
|||||||
- assets/app_icon.png
|
- assets/app_icon.png
|
||||||
- assets/tags.json
|
- assets/tags.json
|
||||||
- assets/tags_tw.json
|
- assets/tags_tw.json
|
||||||
|
- assets/opencc.txt
|
||||||
|
|
||||||
flutter_to_arch:
|
flutter_to_arch:
|
||||||
name: Venera
|
name: Venera
|
||||||
|
@@ -98,14 +98,20 @@ bool FlutterWindow::OnCreate() {
|
|||||||
else
|
else
|
||||||
result->Success(flutter::EncodableValue("No Proxy"));
|
result->Success(flutter::EncodableValue("No Proxy"));
|
||||||
delete(res);
|
delete(res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
#ifdef NDEBUG
|
||||||
else if (call.method_name() == "heartBeat") {
|
else if (call.method_name() == "heartBeat") {
|
||||||
|
|
||||||
if (monitorThread == nullptr) {
|
if (monitorThread == nullptr) {
|
||||||
monitorThread = new std::thread{ monitorUIThread };
|
monitorThread = new std::thread{ monitorUIThread };
|
||||||
}
|
}
|
||||||
lastHeartbeat = std::chrono::steady_clock::now();
|
lastHeartbeat = std::chrono::steady_clock::now();
|
||||||
result->Success();
|
result->Success();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
result->Success(); // Default response for unhandled method calls
|
||||||
});
|
});
|
||||||
|
|
||||||
flutter::EventChannel<> channel2(
|
flutter::EventChannel<> channel2(
|
||||||
|
Reference in New Issue
Block a user