17 Commits

Author SHA1 Message Date
d308c2ac60 Add mouse scroll speed setting. Close #471 2025-08-24 19:52:24 +08:00
ac13807ef4 Add option to ignore certificate errors. Close #485 2025-08-24 19:19:40 +08:00
38a5b2b8cf refactor: comic specific settings 2025-08-24 19:04:42 +08:00
3a7c8d5e38 Fix toolbar overflow. 2025-08-24 17:57:35 +08:00
enximi
ce0d10aeb2 Add a feature to allow saving custom reader settings for each comic. (#459)
* Add a feature to allow saving custom reader settings for each  comic.

* Comic-specific settings disabled by default
2025-08-10 16:02:44 +08:00
角砂糖
0ac857ef9a Temp solution for hyper os multi window display issue (#467)
Temp solution for hyper os multi window display
2025-08-10 16:02:00 +08:00
3928f5afe7 Improve smooth scroll. Close #462 2025-08-03 17:05:31 +08:00
8a61a4750b Add avif format. 2025-08-03 16:40:25 +08:00
nyne
1bc3fef47b Fix workflow 2025-07-23 15:36:12 +08:00
nyne
4dac132bee Remove appimage. 2025-07-23 15:07:28 +08:00
nyne
7c60c00962 Merge pull request #454 from venera-app/v1.4.6-dev
V1.4.6
2025-07-23 14:38:42 +08:00
nyne
17b8b9ea8f Update README.md 2025-07-16 14:10:00 +08:00
Selene29
951bcae603 Local Comic: Add "Open Folder" button (#443) 2025-07-13 18:52:23 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
0b9de68c86 fastlane workflow: path condition (#442) 2025-07-11 14:11:59 +08:00
boa
81b27fd941 update iOS privacy permission descriptions in AltStore config (#432) 2025-07-01 22:14:44 +08:00
Gandum2077
d5d72911ed Add custom tag suggestion handler (#424) 2025-06-24 19:47:14 +08:00
boa
838d5c9c3e Add AltStore Source Support (#416)
* add altstore source

* rename altstore source
2025-06-24 19:46:22 +08:00
26 changed files with 1205 additions and 541 deletions

View File

@@ -4,8 +4,12 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ "master" ] branches: [ "master" ]
paths:
- 'fastlane/**'
pull_request: pull_request:
branches: [ "master" ] branches: [ "master" ]
paths:
- 'fastlane/**'
jobs: jobs:
go: go:

View File

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

76
.github/workflows/update_alt_store.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Update AltStore Source
on:
workflow_run:
workflows: ["Build ALL"]
types: [completed]
workflow_dispatch:
jobs:
update-source:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Record job start time
id: job_start_time
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
- name: Update AltStore source
id: update_source
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python update_alt_store.py
git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com'
git add alt_store.json
if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT
else
git commit -m "Updated source with latest release"
git push
echo "changes=true" >> $GITHUB_OUTPUT
fi
- name: Calculate job duration
id: duration
if: always()
run: |
end_time=$(date +%s)
duration=$((end_time - ${{ steps.job_start_time.outputs.start_time }}))
echo "duration=$duration seconds" >> $GITHUB_OUTPUT
- name: Create job summary
run: |
if [[ "${{ steps.update_source.outputs.changes }}" == "true" ]]; then
echo "## Update Altstore Source Summary 🚀" >> $GITHUB_STEP_SUMMARY
echo "✅ Changes Detected and Applied" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The alt_store.json file has been updated with the latest release information." >> $GITHUB_STEP_SUMMARY
else
echo "## Update Altstore Source Summary 🚀" >> $GITHUB_STEP_SUMMARY
echo "🔍 No Changes Detected" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The alt_store.json file is up to date. No changes were necessary." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "🕐 Execution Time" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This job took ${{ steps.duration.outputs.duration }} to complete." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📆 Next Scheduled Run" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The next scheduled run will be tomorrow at midnight UTC." >> $GITHUB_STEP_SUMMARY

View File

@@ -3,7 +3,7 @@
[![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE) [![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases) [![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![stars](https://img.shields.io/github/stars/venera-app/venera?style=flat)](https://github.com/venera-app/venera/stargazers) [![stars](https://img.shields.io/github/stars/venera-app/venera?style=flat)](https://github.com/venera-app/venera/stargazers)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/+Ws-IpmUutzkxMjhl) [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/venera_release)
A comic reader that support reading local and network comics. A comic reader that support reading local and network comics.

64
alt_store.json Normal file
View File

@@ -0,0 +1,64 @@
{
"name": "Venera",
"identifier": "com.github.wgh136.venera.source",
"website": "https://github.com/venera-app/venera",
"subtitle": "Venera official AltStore Source.",
"description": "This is the official AltStore Source for Venera.\n\n A comic reader that supports reading local and network comics",
"tintColor": "#0784FC",
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
"apps": [
{
"beta": false,
"name": "Venera",
"bundleIdentifier": "com.github.wgh136.venera",
"developerName": "wgh136",
"subtitle": "A comic reader that supports reading local and network comics",
"version": "1.4.5",
"versionDate": "2025-06-18",
"versionDescription": "1. Fixed an abnormal single image height issue when \"imagesPerPage > 1\". 379 \r\n2. Fixed an invalid page calculation issue when \"showSingleImageOnFirstPage\" is enabled. \r\n3. Fixed an issue with incorrect reading history when displaying a single image on the first page. \r\n4. Fixed abnormal history recording when pages are not flipped. 392 \r\n5. Fixed an issue where the download task would stop after exiting the reader. 387 \r\n6. Fixed a \"RangeError\" when translating tags. 356 \r\n7. Reset the current folder to null on the favorites page if the folder is invalid. 389 \r\n8. Fixed various issues when using a custom download path on Android. 400 \r\n9. Set the initial chapter to the first downloaded chapter if no history exists when starting to read a local comic. 405 \r\n10. Removed the config file repository URL from the app.",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.4.5/venera-ios-1.4.5%2B145.ipa",
"localizedDescription": "A comic reader that supports reading local and network comics",
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
"tintColor": "#0784FC",
"category": "utilities",
"size": 14960268,
"appPermissions": {
"entitlements": [
"application-identifier",
"com.apple.security.application-groups",
"get-task-allow",
"keychain-access-groups",
"com.apple.developer.kernel.extended-virtual-addressing",
"com.apple.developer.kernel.increased-memory-limit",
"com.apple.developer.healthkit.background-delivery"
],
"privacy": {
"NSFaceIDUsageDescription": "Face ID or Touch ID is used to protect your privacy when opening the app, ensuring secure access to your reading content.",
"NSPhotoLibraryAddUsageDescription": "Used to save comic images you've favorited or downloaded to your photo library for easy access and sharing.",
"NSPhotoLibraryUsageDescription": "Used to select images from your photo library when needed, and to save comic images you've collected to your device."
}
},
"versions": [
{
"version": "1.4.5",
"date": "2025-06-18",
"localizedDescription": "1. Fixed an abnormal single image height issue when \"imagesPerPage > 1\". 379 \r\n2. Fixed an invalid page calculation issue when \"showSingleImageOnFirstPage\" is enabled. \r\n3. Fixed an issue with incorrect reading history when displaying a single image on the first page. \r\n4. Fixed abnormal history recording when pages are not flipped. 392 \r\n5. Fixed an issue where the download task would stop after exiting the reader. 387 \r\n6. Fixed a \"RangeError\" when translating tags. 356 \r\n7. Reset the current folder to null on the favorites page if the folder is invalid. 389 \r\n8. Fixed various issues when using a custom download path on Android. 400 \r\n9. Set the initial chapter to the first downloaded chapter if no history exists when starting to read a local comic. 405 \r\n10. Removed the config file repository URL from the app.",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.4.5/venera-ios-1.4.5%2B145.ipa",
"size": 14960268
}
]
}
],
"news": [
{
"appID": "com.github.wgh136.venera",
"caption": "Update of Venera just got released!",
"date": "2025-06-18T09:02:01Z",
"identifier": "release-v1.4.5",
"notify": true,
"tintColor": "#0784FC",
"title": "v1.4.5 - Venera 18/06/25",
"url": "https://github.com/venera-app/venera/releases/tag/v1.4.5"
}
]
}

View File

@@ -398,6 +398,7 @@
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",
"Reverse": "反转", "Reverse": "反转",
"Delete Chapters": "删除章节", "Delete Chapters": "删除章节",
"Open Folder": "打开文件夹",
"Path copied to clipboard": "路径已复制到剪贴板", "Path copied to clipboard": "路径已复制到剪贴板",
"Reverse default chapter order": "反转默认章节顺序", "Reverse default chapter order": "反转默认章节顺序",
"Reload Configs": "重新加载配置文件", "Reload Configs": "重新加载配置文件",
@@ -405,7 +406,12 @@
"Disable Length Limitation": "禁用长度限制", "Disable Length Limitation": "禁用长度限制",
"Only valid for this run": "仅对本次运行有效", "Only valid for this run": "仅对本次运行有效",
"Logs": "日志", "Logs": "日志",
"Export 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": "首頁",
@@ -806,6 +812,7 @@
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",
"Reverse": "反轉", "Reverse": "反轉",
"Delete Chapters": "刪除章節", "Delete Chapters": "刪除章節",
"Open Folder": "打開資料夾",
"Path copied to clipboard": "路徑已複製到剪貼簿", "Path copied to clipboard": "路徑已複製到剪貼簿",
"Reverse default chapter order": "反轉預設章節順序", "Reverse default chapter order": "反轉預設章節順序",
"Reload Configs": "重新載入設定檔", "Reload Configs": "重新載入設定檔",
@@ -813,6 +820,11 @@
"Disable Length Limitation": "禁用長度限制", "Disable Length Limitation": "禁用長度限制",
"Only valid for this run": "僅對本次運行有效", "Only valid for this run": "僅對本次運行有效",
"Logs": "日誌", "Logs": "日誌",
"Export 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": "滑鼠滾動速度"
} }
} }

View File

@@ -365,6 +365,11 @@ This part is used to load comics of a category.
// enable tags suggestions // enable tags suggestions
enableTagsSuggestions: false, enableTagsSuggestions: false,
// [Optional] handle tag suggestion click
onTagSuggestionSelected: (namespace, tag) => {
// return the text to insert into search box
return `${namespace}:${tag}`
},
} }
``` ```

View File

@@ -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((_) {

View File

@@ -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'));
@@ -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();
@@ -236,4 +266,5 @@ function processImage(image, cid, eid, page, sourceKey) {
} }
'''; ''';
const _defaultSourceListUrl = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json"; const _defaultSourceListUrl =
"https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json";

View File

@@ -184,6 +184,9 @@ class ComicSource {
final HandleClickTagEvent? handleClickTagEvent; final HandleClickTagEvent? handleClickTagEvent;
/// Callback when a tag suggestion is selected in search.
final TagSuggestionSelectFunc? onTagSuggestionSelected;
final LinkHandler? linkHandler; final LinkHandler? linkHandler;
final bool enableTagsSuggestions; final bool enableTagsSuggestions;
@@ -259,6 +262,7 @@ class ComicSource {
this.idMatcher, this.idMatcher,
this.translations, this.translations,
this.handleClickTagEvent, this.handleClickTagEvent,
this.onTagSuggestionSelected,
this.linkHandler, this.linkHandler,
this.enableTagsSuggestions, this.enableTagsSuggestions,
this.enableTagsTranslate, this.enableTagsTranslate,

View File

@@ -148,6 +148,7 @@ class ComicSourceParser {
_parseIdMatch(), _parseIdMatch(),
_parseTranslation(), _parseTranslation(),
_parseClickTagEvent(), _parseClickTagEvent(),
_parseTagSuggestionSelectFunc(),
_parseLinkHandler(), _parseLinkHandler(),
_getValue("search.enableTagsSuggestions") ?? false, _getValue("search.enableTagsSuggestions") ?? false,
_getValue("comic.enableTagsTranslate") ?? false, _getValue("comic.enableTagsTranslate") ?? false,
@@ -1057,6 +1058,19 @@ class ComicSourceParser {
}; };
} }
TagSuggestionSelectFunc? _parseTagSuggestionSelectFunc() {
if (!_checkExists("search.onTagSuggestionSelected")) {
return null;
}
return (namespace, tag) {
var res = JsEngine().runCode("""
ComicSource.sources.$_key.search.onTagSuggestionSelected(
${jsonEncode(namespace)}, ${jsonEncode(tag)})
""");
return res is String ? res : "$namespace:$tag";
};
}
LinkHandler? _parseLinkHandler() { LinkHandler? _parseLinkHandler() {
if (!_checkExists("comic.link")) { if (!_checkExists("comic.link")) {
return null; return null;

View File

@@ -44,5 +44,10 @@ typedef VoteCommentFunc = Future<Res<int?>> Function(
typedef HandleClickTagEvent = PageJumpTarget? Function( typedef HandleClickTagEvent = PageJumpTarget? Function(
String namespace, String tag); String namespace, String tag);
/// Handle tag suggestion selection event. Should return the text to insert
/// into the search field.
typedef TagSuggestionSelectFunc = String Function(
String namespace, String tag);
/// [rating] is the rating value, 0-10. 1 represents 0.5 star. /// [rating] is the rating value, 0-10. 1 represents 0.5 star.
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating); typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);

View File

@@ -237,6 +237,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) {
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(

View File

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

View File

@@ -14,6 +14,7 @@ import 'package:venera/utils/io.dart';
import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/pdf.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
import 'package:zip_flutter/zip_flutter.dart'; import 'package:zip_flutter/zip_flutter.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LocalComicsPage extends StatefulWidget { class LocalComicsPage extends StatefulWidget {
const LocalComicsPage({super.key}); const LocalComicsPage({super.key});
@@ -143,6 +144,14 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
addFavorite(selectedComics.keys.toList()); addFavorite(selectedComics.keys.toList());
}, },
), ),
if (selectedComics.length == 1)
MenuEntry(
icon: Icons.folder_open,
text: "Open Folder".tl,
onClick: () {
openComicFolder(selectedComics.keys.first);
},
),
if (selectedComics.length == 1) if (selectedComics.length == 1)
MenuEntry( MenuEntry(
icon: Icons.chrome_reader_mode_outlined, icon: Icons.chrome_reader_mode_outlined,
@@ -313,6 +322,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}, },
menuBuilder: (c) { menuBuilder: (c) {
return [ return [
MenuEntry(
icon: Icons.folder_open,
text: "Open Folder".tl,
onClick: () {
openComicFolder(c as LocalComic);
},
),
MenuEntry( MenuEntry(
icon: Icons.delete, icon: Icons.delete,
text: "Delete".tl, text: "Delete".tl,
@@ -519,6 +535,49 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
typedef ExportComicFunc = Future<File> Function( typedef ExportComicFunc = Future<File> Function(
LocalComic comic, String outFilePath); LocalComic comic, String outFilePath);
/// Opens the folder containing the comic in the system file explorer
Future<void> openComicFolder(LocalComic comic) async {
try {
final folderPath = comic.baseDir;
if (App.isWindows) {
await Process.run('explorer', [folderPath]);
} else if (App.isMacOS) {
await Process.run('open', [folderPath]);
} else if (App.isLinux) {
// Try different file managers commonly found on Linux
try {
await Process.run('xdg-open', [folderPath]);
} catch (e) {
// Fallback to other common file managers
try {
await Process.run('nautilus', [folderPath]);
} catch (e) {
try {
await Process.run('dolphin', [folderPath]);
} catch (e) {
try {
await Process.run('thunar', [folderPath]);
} catch (e) {
// Last resort: use the URL launcher with file:// protocol
await launchUrlString('file://$folderPath');
}
}
}
}
} else {
// For mobile platforms, use the URL launcher with file:// protocol
await launchUrlString('file://$folderPath');
}
} catch (e, s) {
Log.error("Open Folder", "Failed to open comic folder: $e", s);
// Show error message to user
if (App.rootContext.mounted) {
App.rootContext.showMessage(message: "Failed to open folder: $e");
}
}
}
void showDeleteChaptersPopWindow(BuildContext context, LocalComic comic) { void showDeleteChaptersPopWindow(BuildContext context, LocalComic comic) {
var chapters = <String>[]; var chapters = <String>[];

View File

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

View File

@@ -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,9 +88,7 @@ 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 GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -104,7 +109,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
} 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));
} }
@@ -132,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();
} }
} }
@@ -159,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);
} }
} }
@@ -193,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);
} }
} }
} }
@@ -217,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
@@ -239,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],
@@ -356,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,
@@ -423,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;
} }
@@ -471,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) {
@@ -486,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);
} }
}, },
); );
@@ -512,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) {
@@ -538,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;
@@ -624,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) {
@@ -673,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;
} }
@@ -721,8 +762,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
physics: isCTRLPressed || _isMouseScrolling || disableScroll physics: isCTRLPressed || _isMouseScrolling || disableScroll
? const NeverScrollableScrollPhysics() ? const NeverScrollableScrollPhysics()
: isZoomedIn : isZoomedIn
? const ClampingScrollPhysics() ? const ClampingScrollPhysics()
: const BouncingScrollPhysics(), : const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0 || index == reader.maxPage + 1) { if (index == 0 || index == reader.maxPage + 1) {
return const SizedBox(); return const SizedBox();
@@ -750,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(
@@ -769,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;
@@ -895,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,
),
); );
} }
@@ -978,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;
} }
@@ -1000,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
@@ -1069,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();
} }
} }
@@ -1114,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.
@@ -1138,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;
@@ -1258,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,
); );
} }

View File

@@ -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();
@@ -274,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;
} }
} }
} }
@@ -363,49 +366,51 @@ 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();
_lastOrientation = isPortrait; _lastOrientation = isPortrait;
if (imagesPerPage != 1) { if (imagesPerPage() != 1) {
if (showSingleImageOnFirstPage) { if (showSingleImageOnFirstPage()) {
page = ((initialPage - 1) / imagesPerPage).ceil() + 1; 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();
bool currentOrientation = isPortrait; bool currentOrientation = isPortrait;
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) { if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
_adjustPageForImagesPerPageChange( _adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
_lastImagesPerPage, currentImagesPerPage);
_lastImagesPerPage = currentImagesPerPage; _lastImagesPerPage = currentImagesPerPage;
_lastOrientation = currentOrientation; _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 = 1;
if (!showSingleImageOnFirstPage || oldImagesPerPage == 1) { if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
previousImageIndex = (page - 1) * oldImagesPerPage + 1; previousImageIndex = (page - 1) * oldImagesPerPage + 1;
} else { } else {
if (page == 1) { if (page == 1) {
@@ -417,7 +422,7 @@ abstract mixin class _ImagePerPageHandler {
int newPage; int newPage;
if (newImagesPerPage != 1) { if (newImagesPerPage != 1) {
if (showSingleImageOnFirstPage) { if (showSingleImageOnFirstPage()) {
newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1; newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1;
} else { } else {
newPage = (previousImageIndex / newImagesPerPage).ceil(); newPage = (previousImageIndex / newImagesPerPage).ceil();
@@ -493,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;
@@ -532,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--;
@@ -571,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();

View File

@@ -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(
@@ -216,8 +211,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
try { try {
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,12 +269,21 @@ 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,
return e.ep == ep; eid,
}); id,
ep,
sourceKey,
epName,
);
ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic
.imageFavoritesEp
.firstWhereOrNull((e) {
return e.ep == ep;
});
if (imageFavoritesEp == null) { if (imageFavoritesEp == null) {
if (page != firstPage) { if (page != firstPage) {
var copy = imageFavorite.copyWith( var copy = imageFavorite.copyWith(
@@ -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,155 +348,152 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
text = "P${context.reader.page}"; text = "P${context.reader.page}";
} }
final buttons = [
Tooltip(
message: "Collect the image".tl,
child: IconButton(
icon: Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
onPressed: addImageFavorite,
),
),
if (App.isDesktop)
Tooltip(
message: "${"Full Screen".tl}(F12)",
child: IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
context.reader.fullscreen();
},
),
),
if (App.isAndroid)
Tooltip(
message: "Screen Rotation".tl,
child: IconButton(
icon: () {
if (rotation == null) {
return const Icon(Icons.screen_rotation);
} else if (rotation == false) {
return const Icon(Icons.screen_lock_portrait);
} else {
return const Icon(Icons.screen_lock_landscape);
}
}.call(),
onPressed: () {
if (rotation == null) {
setState(() {
rotation = false;
});
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else if (rotation == false) {
setState(() {
rotation = true;
});
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
} else {
setState(() {
rotation = null;
});
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
}
},
),
),
Tooltip(
message: "Auto Page Turning".tl,
child: IconButton(
icon: context.reader.autoPageTurningTimer != null
? const Icon(Icons.timer)
: const Icon(Icons.timer_sharp),
onPressed: () {
context.reader.autoPageTurning(
context.reader.cid,
context.reader.type,
);
update();
},
),
),
if (context.reader.widget.chapters != null)
Tooltip(
message: "Chapters".tl,
child: IconButton(
icon: const Icon(Icons.library_books),
onPressed: openChapterDrawer,
),
),
Tooltip(
message: "Save Image".tl,
child: IconButton(
icon: const Icon(Icons.download),
onPressed: saveCurrentImage,
),
),
Tooltip(
message: "Share".tl,
child: IconButton(icon: const Icon(Icons.share), onPressed: share),
),
];
Widget child = SizedBox( Widget child = SizedBox(
height: kBottomBarHeight, height: kBottomBarHeight,
child: Column( child: Column(
children: [ children: [
const SizedBox( const SizedBox(height: 8),
height: 8,
),
Row( Row(
children: [ children: [
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton.filledTonal( IconButton.filledTonal(
onPressed: () => !isReversed onPressed: () => !isReversed
? context.reader.chapter > 1 ? context.reader.chapter > 1
? context.reader.toPrevChapter() ? context.reader.toPrevChapter()
: context.reader.toPage(1) : context.reader.toPage(1)
: context.reader.chapter < context.reader.maxChapter : context.reader.chapter < context.reader.maxChapter
? context.reader.toNextChapter() ? context.reader.toNextChapter()
: context.reader.toPage(context.reader.maxPage), : context.reader.toPage(context.reader.maxPage),
icon: const Icon(Icons.first_page), icon: const Icon(Icons.first_page),
), ),
Expanded( Expanded(child: buildSlider()),
child: buildSlider(),
),
IconButton.filledTonal( IconButton.filledTonal(
onPressed: () => !isReversed onPressed: () => !isReversed
? context.reader.chapter < context.reader.maxChapter ? context.reader.chapter < context.reader.maxChapter
? context.reader.toNextChapter() ? context.reader.toNextChapter()
: context.reader.toPage(context.reader.maxPage) : context.reader.toPage(context.reader.maxPage)
: context.reader.chapter > 1 : context.reader.chapter > 1
? context.reader.toPrevChapter() ? context.reader.toPrevChapter()
: context.reader.toPage(1), : context.reader.toPage(1),
icon: const Icon(Icons.last_page)), icon: const Icon(Icons.last_page),
const SizedBox(
width: 8,
), ),
const SizedBox(width: 8),
], ],
), ),
Row( LayoutBuilder(
children: [ builder: (context, constrains) {
const SizedBox( return Row(
width: 16, children: [
), if ((constrains.maxWidth - buttons.length * 42) > 80)
Container( Container(
height: 24, height: 24,
padding: const EdgeInsets.fromLTRB(6, 2, 6, 0), padding: const EdgeInsets.fromLTRB(6, 2, 6, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiaryContainer, color: Theme.of(context).colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Center( child: Center(child: Text(text)),
child: Text(text), ).paddingLeft(16),
), const Spacer(),
), ...buttons,
const Spacer(), const SizedBox(width: 4),
Tooltip( ],
message: "Collect the image".tl, );
child: IconButton( },
icon: ),
Icon(isLiked() ? Icons.favorite : Icons.favorite_border),
onPressed: addImageFavorite,
),
),
if (App.isDesktop)
Tooltip(
message: "${"Full Screen".tl}(F12)",
child: IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
context.reader.fullscreen();
},
),
),
if (App.isAndroid)
Tooltip(
message: "Screen Rotation".tl,
child: IconButton(
icon: () {
if (rotation == null) {
return const Icon(Icons.screen_rotation);
} else if (rotation == false) {
return const Icon(Icons.screen_lock_portrait);
} else {
return const Icon(Icons.screen_lock_landscape);
}
}.call(),
onPressed: () {
if (rotation == null) {
setState(() {
rotation = false;
});
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else if (rotation == false) {
setState(() {
rotation = true;
});
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight
]);
} else {
setState(() {
rotation = null;
});
SystemChrome.setPreferredOrientations(
DeviceOrientation.values);
}
},
),
),
Tooltip(
message: "Auto Page Turning".tl,
child: IconButton(
icon: context.reader.autoPageTurningTimer != null
? const Icon(Icons.timer)
: const Icon(Icons.timer_sharp),
onPressed: () {
context.reader.autoPageTurning();
update();
},
),
),
if (context.reader.widget.chapters != null)
Tooltip(
message: "Chapters".tl,
child: IconButton(
icon: const Icon(Icons.library_books),
onPressed: openChapterDrawer,
),
),
Tooltip(
message: "Save Image".tl,
child: IconButton(
icon: const Icon(Icons.download),
onPressed: saveCurrentImage,
),
),
Tooltip(
message: "Share".tl,
child: IconButton(
icon: const Icon(Icons.share),
onPressed: share,
),
),
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(
completer.complete(offset); onTap: (offset) {
entry!.remove(); completer.complete(offset);
}, onDispose: () { entry!.remove();
if (!completer.isCompleted) { },
completer.complete(null); onDispose: () {
} if (!completer.isCompleted) {
}), completer.complete(null);
}
},
),
); );
}, },
); );
@@ -840,20 +872,17 @@ class _BatteryWidgetState extends State<_BatteryWidget> {
size: 16, size: 16,
color: batteryColor, color: batteryColor,
// Stroke // Stroke
shadows: List.generate( shadows: List.generate(9, (index) {
9, if (index == 4) {
(index) { return null;
if (index == 4) { }
return null; double offsetX = (index % 3 - 1) * 0.8;
} double offsetY = ((index / 3).floor() - 1) * 0.8;
double offsetX = (index % 3 - 1) * 0.8; return Shadow(
double offsetY = ((index / 3).floor() - 1) * 0.8; color: context.colorScheme.onInverseSurface,
return Shadow( offset: Offset(offsetX, offsetY),
color: context.colorScheme.onInverseSurface, );
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: [

View File

@@ -376,11 +376,16 @@ class _SearchPageState extends State<SearchPage> {
controller.text = controller.text =
controller.text.replaceLast(words[words.length - 1], ""); controller.text.replaceLast(words[words.length - 1], "");
} }
if (type != null) { final source = ComicSource.find(searchTarget);
controller.text += "${type.name}:$text "; String insert;
if (source?.onTagSuggestionSelected != null) {
insert = source!.onTagSuggestionSelected!(type?.name ?? '', text);
} else { } else {
controller.text += "$text "; var t = text;
if (t.contains(' ')) t = "'$t'";
insert = type != null ? "${type.name}:$t" : t;
} }
controller.text += "$insert ";
suggestions.clear(); suggestions.clear();
update(); update();
focusNode.requestFocus(); focusNode.requestFocus();

View File

@@ -124,7 +124,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
options = widget.options ?? const []; options = widget.options ?? const [];
validateOptions(); validateOptions();
appdata.addSearchHistory(text); appdata.addSearchHistory(text);
suggestionsController = _SuggestionsController(controller); suggestionsController = _SuggestionsController(controller, sourceKey);
super.initState(); super.initState();
} }
@@ -213,6 +213,8 @@ class _SuggestionsController {
final SearchBarController controller; final SearchBarController controller;
final String sourceKey;
OverlayEntry? entry; OverlayEntry? entry;
void updateWidget() { void updateWidget() {
@@ -270,7 +272,7 @@ class _SuggestionsController {
find(TagsTranslation.cosplayerTags, TranslationType.cosplayer); find(TagsTranslation.cosplayerTags, TranslationType.cosplayer);
} }
_SuggestionsController(this.controller); _SuggestionsController(this.controller, this.sourceKey);
} }
class _Suggestions extends StatefulWidget { class _Suggestions extends StatefulWidget {
@@ -400,14 +402,16 @@ class _SuggestionsState extends State<_Suggestions> {
controller.text = controller.text =
controller.text.replaceLast(words[words.length - 1], ""); controller.text.replaceLast(words[words.length - 1], "");
} }
if (text.contains(' ')) { final source = ComicSource.find(widget.controller.sourceKey);
text = "'$text'"; String insert;
} if (source?.onTagSuggestionSelected != null) {
if (type != null) { insert = source!.onTagSuggestionSelected!(type?.name ?? '', text);
controller.text += "${type.name}:$text ";
} else { } else {
controller.text += "$text "; var t = text;
if (t.contains(' ')) t = "'$t'";
insert = type != null ? "${type.name}:$t" : t;
} }
controller.text += "$insert ";
widget.controller.suggestions.clear(); widget.controller.suggestions.clear();
widget.controller.remove(); widget.controller.remove();
} }

View File

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

View File

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

View File

@@ -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(() {
appdata.settings[widget.settingKey] = value; if (widget.comicId == null) {
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(
value: key, (key) => PopupMenuItem(
height: App.isMobile ? 46 : 40, value: key,
child: Text(widget.optionTranslation[key]!), height: App.isMobile ? 46 : 40,
)) child: Text(widget.optionTranslation[key]!),
),
)
.toList(), .toList(),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
setState(() { setState(() {
appdata.settings[widget.settingKey] = value; 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();
@@ -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(() {
appdata.settings[widget.settingsIndex] = value.toInt(); if (widget.comicId == null) {
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(() {
appdata.settings[widget.settingsIndex] = value; if (widget.comicId == null) {
appdata.settings[widget.settingsIndex] = value;
} else {
appdata.settings.setReaderSetting(
widget.comicId!,
widget.comicSource!,
widget.settingsIndex,
value,
);
}
appdata.saveData(); appdata.saveData();
}); });
} }
@@ -402,10 +511,11 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
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,
); );
@@ -445,12 +555,13 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
Widget removeButton = Padding( Widget removeButton = Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
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,64 +585,66 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return StatefulBuilder(builder: (context, setState) { return StatefulBuilder(
return ContentDialog( builder: (context, setState) {
title: "Add".tl, return ContentDialog(
content: Column( title: "Add".tl,
mainAxisSize: MainAxisSize.min, content: Column(
children: canAdd.entries mainAxisSize: MainAxisSize.min,
.map( children: canAdd.entries
(e) => CheckboxListTile( .map(
value: selected.contains(e.key), (e) => CheckboxListTile(
title: Text(e.value), value: selected.contains(e.key),
key: Key(e.key), title: Text(e.value),
onChanged: (value) { key: Key(e.key),
setState(() { onChanged: (value) {
if (value!) { setState(() {
selected.add(e.key); if (value!) {
} else { selected.add(e.key);
selected.remove(e.key); } else {
} selected.remove(e.key);
}); }
}, });
), },
) ),
.toList(), )
), .toList(),
actions: [
if (selected.length < canAdd.length)
TextButton(
child: Text("Select All".tl),
onPressed: () {
setState(() {
selected = canAdd.keys.toList();
});
},
)
else
TextButton(
child: Text("Deselect All".tl),
onPressed: () {
setState(() {
selected.clear();
});
},
),
const SizedBox(width: 8),
FilledButton(
onPressed: selected.isNotEmpty
? () {
this.setState(() {
keys.addAll(selected);
});
Navigator.pop(context);
}
: null,
child: Text("Add".tl),
), ),
], actions: [
); if (selected.length < canAdd.length)
}); TextButton(
child: Text("Select All".tl),
onPressed: () {
setState(() {
selected = canAdd.keys.toList();
});
},
)
else
TextButton(
child: Text("Deselect All".tl),
onPressed: () {
setState(() {
selected.clear();
});
},
),
const SizedBox(width: 8),
FilledButton(
onPressed: selected.isNotEmpty
? () {
this.setState(() {
keys.addAll(selected);
});
Navigator.pop(context);
}
: null,
child: Text("Add".tl),
),
],
);
},
);
}, },
); );
} }

View File

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

150
update_alt_store.py Normal file
View File

@@ -0,0 +1,150 @@
import json
import plistlib
import re
import requests
import os
from datetime import datetime
def prepare_description(text):
text = re.sub('<[^<]+?>', '', text) # Remove HTML tags
text = re.sub(r'#{1,6}\s?', '', text) # Remove markdown header tags
text = re.sub(r'\*{2}', '', text) # Remove all occurrences of two consecutive asterisks
text = re.sub(r'(?<=\r|\n)-', '', text) # Only replace - with • if it is preceded by \r or \n
text = re.sub(r'`', '"', text) # Replace ` with "
text = re.sub(r'\r\n\r\n', '\r \n', text) # Replace \r\n\r\n with \r \n (avoid incorrect display of the description regarding paragraphs)
return text
def fetch_latest_release(repo_url):
api_url = f"https://api.github.com/repos/{repo_url}/releases"
headers = {
"Accept": "application/vnd.github+json",
}
try:
response = requests.get(api_url, headers=headers)
response.raise_for_status()
release = response.json()
return release
except requests.RequestException as e:
print(f"Error fetching releases: {e}")
raise
def get_file_size(url):
try:
response = requests.head(url)
response.raise_for_status()
return int(response.headers.get('Content-Length', 0))
except requests.RequestException as e:
print(f"Error getting file size: {e}")
return 194586
def update_json_file_release(json_file, latest_release):
if isinstance(latest_release, list) and latest_release:
latest_release = latest_release[0]
else:
print("Error getting latest release")
return
try:
with open(json_file, "r") as file:
data = json.load(file)
except json.JSONDecodeError as e:
print(f"Error reading JSON file: {e}")
data = {"apps": []}
raise
app = data["apps"][0]
full_version = latest_release["tag_name"]
tag = latest_release["tag_name"]
# Extract version like 1.4.5 from tag, which may be like 'v1.4.5'
version_match = re.search(r"(\d+\.\d+\.\d+)", full_version)
if version_match:
version = version_match.group(1)
else:
print("Error: Could not parse version from tag_name.")
return
version_date = latest_release["published_at"]
date_obj = datetime.strptime(version_date, "%Y-%m-%dT%H:%M:%SZ")
version_date = date_obj.strftime("%Y-%m-%d")
description = latest_release["body"]
description = prepare_description(description)
assets = latest_release.get("assets", [])
download_url = None
size = None
for asset in assets:
# venera-ios-1.4.5+145.ipa
if asset["name"] == f"venera-ios-{version}+{version.replace('.', '')}.ipa":
download_url = asset["browser_download_url"]
size = asset["size"]
break
if download_url is None or size is None:
print("Error: IPA file not found in release assets.")
return
version_entry = {
"version": version,
"date": version_date,
"localizedDescription": description,
"downloadURL": download_url,
"size": size
}
duplicate_entries = [item for item in app["versions"] if item["version"] == version]
if duplicate_entries:
app["versions"].remove(duplicate_entries[0])
app["versions"].insert(0, version_entry)
app.update({
"version": version,
"versionDate": version_date,
"versionDescription": description,
"downloadURL": download_url,
"size": size
})
if "news" not in data:
data["news"] = []
news_identifier = f"release-{full_version}"
date_string = date_obj.strftime("%d/%m/%y")
news_entry = {
"appID": "com.github.wgh136.venera",
"caption": f"Update of Venera just got released!",
"date": latest_release["published_at"],
"identifier": news_identifier,
"notify": True,
"tintColor": "#0784FC",
"title": f"{full_version} - Venera {date_string}",
"url": f"https://github.com/venera-app/venera/releases/tag/{tag}"
}
news_entry_exists = any(item["identifier"] == news_identifier for item in data["news"])
if not news_entry_exists:
data["news"].append(news_entry)
try:
with open(json_file, "w") as file:
json.dump(data, file, indent=2)
print("JSON file updated successfully.")
except IOError as e:
print(f"Error writing to JSON file: {e}")
raise
def main():
repo_url = "venera-app/venera"
is_nightly = "NIGHTLY_LINK" in os.environ
try:
fetched_data_latest = fetch_latest_release(repo_url)
json_file = "alt_store.json"
update_json_file_release(json_file, fetched_data_latest)
except Exception as e:
print(f"An error occurred: {e}")
raise
if __name__ == "__main__":
main()