32 Commits

Author SHA1 Message Date
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
nyne
85baac657a Merge pull request #421 from lings03/local
Allow user to keep favorite and history when delete local comic. Close #420
2025-06-23 19:06:34 +08:00
nyne
cceca6b96f Merge branch 'master' into local 2025-06-23 19:04:20 +08:00
角砂糖
b5b0dc85e3 Show group in last read and history when group existing. (#419) 2025-06-23 19:03:24 +08:00
nyne
50044c4372 Merge pull request #418 from lings03/reverse
Add a option to reverse the default chapter order. Close #414
2025-06-23 19:02:52 +08:00
nyne
5fd7f1b880 Merge branch 'master' into reverse 2025-06-23 19:00:48 +08:00
角砂糖
058fde3f5a Add a button to show system status bar (#417) 2025-06-23 19:00:04 +08:00
角砂糖
a2d46123dd Add missing translation
debug时发现之前少了这一句翻译,也不至于提个pr,顺便加在这里吧
2025-06-22 20:07:15 +08:00
角砂糖
01acc4f9de Allow user to keep favorite and history when delete local comic. Close #420 2025-06-22 19:50:38 +08:00
角砂糖
856aae0769 Add a option to reverse the default chapter order. Close #414 2025-06-22 00:29:49 +08:00
nyne
8eda8adcc8 Merge pull request #410 from venera-app/v1.4.5-dev
V1.4.5
2025-06-18 16:52:59 +08:00
defd4b8624 Update version code. 2025-06-18 16:39:02 +08:00
b2a164e066 Remove the config file repository url from app. 2025-06-18 16:34:49 +08:00
a46ceebf19 Fixed the issue where the update dialog was not showed on startup. 2025-06-18 16:07:36 +08:00
cc08445f13 Set initial chapter to first downloaded chapter if there is no history when starting to read a local comic. Close #405 2025-06-17 17:36:13 +08:00
93f7f72d07 Fixed some issues when using custom download path on Android. Close #400 2025-06-17 17:15:35 +08:00
20f7ab4866 Clear folder value if it does not exist in local favorites. Close #389 2025-06-15 15:02:45 +08:00
54363919cd Fixed RangeError when translating tags. Close #356 2025-06-15 14:58:15 +08:00
182a821fc5 Fixed the issue where the download task would stop after exiting the reader. Close #387 2025-06-15 14:58:15 +08:00
8868c6edb3 Update Flutter SDK version to 3.32.4 2025-06-15 14:58:15 +08:00
角砂糖
fffbb4ed23 Only add closeListener when app is desktop (#397) 2025-06-04 12:11:45 +08:00
角砂糖
b057be0311 Fix abnormal history recording when not flipping pages. Close #392 (#395) 2025-06-03 17:36:20 +08:00
角砂糖
fc5fed1707 Fix history of page when show single image on first page (#393) 2025-06-03 17:35:45 +08:00
角砂糖
8525f5318f Fix page calculate when showSingleImageOnFirstPage is enabled (#391) 2025-06-03 17:35:17 +08:00
角砂糖
d58cafc4a0 Fix abnormal single image height when imagesPerPage > 1. Close #379 2025-05-31 10:50:17 +08:00
23afafd1d6 Update rhttp 2025-05-26 19:05:15 +08:00
nyne
3b6e0adbbb Merge pull request #377 from venera-app/v1.4.4
V1.4.4
2025-05-26 18:18:43 +08:00
35 changed files with 754 additions and 208 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:

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

@@ -234,8 +234,10 @@
"Please add some sources": "请添加一些源", "Please add some sources": "请添加一些源",
"Please check your settings": "请检查您的设置", "Please check your settings": "请检查您的设置",
"No Category Pages": "没有分类页面", "No Category Pages": "没有分类页面",
"Group @group": "第 @group 组",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
"Page @page": "第 @page 页", "Page @page": "第 @page 页",
"Remove local favorite and history": "删除本地收藏和历史记录",
"Also remove files on disk": "同时删除磁盘上的文件", "Also remove files on disk": "同时删除磁盘上的文件",
"Copy to app local path": "将漫画复制到本地存储目录中", "Copy to app local path": "将漫画复制到本地存储目录中",
"Delete all unavailable local favorite items": "删除所有无效的本地收藏", "Delete all unavailable local favorite items": "删除所有无效的本地收藏",
@@ -388,13 +390,17 @@
"Suggestions": "建议", "Suggestions": "建议",
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题", "Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
"Show single image on first page": "在首页显示单张图片", "Show single image on first page": "在首页显示单张图片",
"Show system status bar": "显示系统状态栏",
"Click to select an image": "点击选择一张图片", "Click to select an image": "点击选择一张图片",
"Source URL": "地址", "Repo URL": "仓库地址",
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
"Double tap to zoom": "双击缩放", "Double tap to zoom": "双击缩放",
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",
"Reverse": "反转", "Reverse": "反转",
"Delete Chapters": "删除章节" "Delete Chapters": "删除章节",
"Open Folder": "打开文件夹",
"Path copied to clipboard": "路径已复制到剪贴板",
"Reverse default chapter order": "反转默认章节顺序"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -631,8 +637,10 @@
"Please add some sources": "請添加一些源", "Please add some sources": "請添加一些源",
"Please check your settings": "請檢查您的設定", "Please check your settings": "請檢查您的設定",
"No Category Pages": "沒有分類頁面", "No Category Pages": "沒有分類頁面",
"Group @group": "第 @group 組",
"Chapter @ep": "第 @ep 章", "Chapter @ep": "第 @ep 章",
"Page @page": "第 @page 頁", "Page @page": "第 @page 頁",
"Remove local favorite and history": "刪除本機收藏和歷史記錄",
"Also remove files on disk": "同時刪除磁碟上的文件", "Also remove files on disk": "同時刪除磁碟上的文件",
"Copy to app local path": "將漫畫複製到本機儲存目錄中", "Copy to app local path": "將漫畫複製到本機儲存目錄中",
"Delete all unavailable local favorite items": "刪除所有無效的本機收藏", "Delete all unavailable local favorite items": "刪除所有無效的本機收藏",
@@ -785,12 +793,16 @@
"Suggestions": "建議", "Suggestions": "建議",
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題", "Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
"Show single image on first page": "在首頁顯示單張圖片", "Show single image on first page": "在首頁顯示單張圖片",
"Show system status bar": "顯示系統狀態欄",
"Click to select an image": "點擊選擇一張圖片", "Click to select an image": "點擊選擇一張圖片",
"Source URL": "地址", "Repo URL": "倉庫地址",
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
"Double tap to zoom": "雙擊縮放", "Double tap to zoom": "雙擊縮放",
"Clear Unfavorited": "清除未收藏", "Clear Unfavorited": "清除未收藏",
"Reverse": "反轉", "Reverse": "反轉",
"Delete Chapters": "刪除章節" "Delete Chapters": "刪除章節",
"Open Folder": "打開資料夾",
"Path copied to clipboard": "路徑已複製到剪貼簿",
"Reverse default chapter order": "反轉預設章節順序"
} }
} }

View File

@@ -9,13 +9,45 @@ Venera uses [flutter_qjs](https://github.com/wgh136/flutter_qjs) as js engine wh
This document will describe how to write a comic source for Venera. This document will describe how to write a comic source for Venera.
## Preparation ## Comic Source List
Venera can display a list of comic sources in the app.
You should provide a repository url to let the app load the comic source list.
The url should point to a JSON file that contains the list of comic sources.
The JSON file should have the following format:
```json
[
{
"name": "Source Name",
"url": "https://example.com/source.js",
"filename": "Relative path to the source file",
"version": "1.0.0",
"description": "A brief description of the source"
}
]
```
Only one of `url` and `filename` should be provided.
The description field is optional.
Currently, you can use the following repo url:
```
https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/index.json
```
The repo is maintained by the Venera team, and you can submit a pull request to add your comic source.
## Create a Comic Source
### Preparation
- Install Venera. Using flutter to run the project is recommended since it's easier to debug. - Install Venera. Using flutter to run the project is recommended since it's easier to debug.
- An editor that supports javascript. - An editor that supports javascript.
- Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs). - Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs).
## Start Writing ### Start Writing
The template contains detailed comments and examples. You can refer to it when writing your own comic source. The template contains detailed comments and examples. You can refer to it when writing your own comic source.
@@ -23,7 +55,7 @@ Here is a brief introduction to the template:
> Note: Javascript api document is [here](js_api.md). > Note: Javascript api document is [here](js_api.md).
### Write basic information #### Write basic information
```javascript ```javascript
class NewComicSource extends ComicSource { class NewComicSource extends ComicSource {
@@ -49,7 +81,7 @@ In this part, you need to do the following:
- Change the class name to your source name. - Change the class name to your source name.
- Fill in the name, key, version, minAppVersion, and url fields. - Fill in the name, key, version, minAppVersion, and url fields.
### init function #### init function
```javascript ```javascript
/** /**
@@ -64,7 +96,7 @@ The function will be called when the source is initialized. You can do some init
Remove this function if not used. Remove this function if not used.
### Account #### Account
```javascript ```javascript
// [Optional] account related // [Optional] account related
@@ -140,7 +172,7 @@ In this part, you can implement login, logout, and register functions.
Remove this part if not used. Remove this part if not used.
### Explore page #### Explore page
```javascript ```javascript
// explore page list // explore page list
@@ -185,7 +217,7 @@ There are three types of explore pages:
- multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page. - multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page.
- mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button. - mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button.
### Category Page #### Category Page
```javascript ```javascript
// categories // categories
@@ -227,7 +259,7 @@ Category page is a static page that contains multiple parts, each part contains
A comic source can only have one category page. A comic source can only have one category page.
### Category Comics Page #### Category Comics Page
```javascript ```javascript
/// category comic loading related /// category comic loading related
@@ -280,7 +312,7 @@ When user clicks on a category, the category comics page will be displayed.
This part is used to load comics of a category. This part is used to load comics of a category.
### Search #### Search
```javascript ```javascript
/// search related /// search related
@@ -331,6 +363,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}`
},
} }
``` ```
@@ -339,7 +376,7 @@ This part is used to load search results.
`load` and `loadNext` functions are used to load search results. `load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored. If `load` function is implemented, `loadNext` function will be ignored.
### Favorites #### Favorites
```javascript ```javascript
// favorite related // favorite related
@@ -411,7 +448,7 @@ This part is used to manage network favorites of the source.
`load` and `loadNext` functions are used to load search results. `load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored. If `load` function is implemented, `loadNext` function will be ignored.
### Comic Details #### Comic Details
```javascript ```javascript
/// single comic related /// single comic related
@@ -576,7 +613,7 @@ If `load` function is implemented, `loadNext` function will be ignored.
This part is used to load comic details. This part is used to load comic details.
### Settings #### Settings
```javascript ```javascript
/* /*
@@ -635,7 +672,7 @@ This part is used to load comic details.
This part is used to provide settings for the source. This part is used to provide settings for the source.
### Translations #### Translations
```javascript ```javascript
// [Optional] translations for the strings in this config // [Optional] translations for the strings in this config

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.4.4"; final version = "1.4.5";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -189,7 +189,7 @@ class Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing, 'customImageProcessing': defaultCustomImageProcessing,
'sni': true, 'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
'comicSourceListUrl': defaultComicSourceUrl, 'comicSourceListUrl': '',
'preloadImageCount': 4, 'preloadImageCount': 4,
'followUpdatesFolder': null, 'followUpdatesFolder': null,
'initialPage': '0', 'initialPage': '0',
@@ -197,6 +197,8 @@ class Settings with ChangeNotifier {
'showPageNumberInReader': true, 'showPageNumberInReader': true,
'showSingleImageOnFirstPage': false, 'showSingleImageOnFirstPage': false,
'enableDoubleTapToZoom': true, 'enableDoubleTapToZoom': true,
'reverseChapterOrder': false,
'showSystemStatusBar': false,
}; };
operator [](String key) { operator [](String key) {
@@ -233,5 +235,3 @@ function processImage(image, cid, eid, page, sourceKey) {
return futureImage; return futureImage;
} }
'''; ''';
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/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

@@ -133,6 +133,11 @@ class History implements Comic {
@override @override
String get description { String get description {
var res = ""; var res = "";
if (group != null){
res += "${"Group @group".tlParams({
"group": group!,
})} - ";
}
if (ep >= 1) { if (ep >= 1) {
res += "Chapter @ep".tlParams({ res += "Chapter @ep".tlParams({
"ep": ep, "ep": ep,

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:flutter_saf/flutter_saf.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -108,15 +109,42 @@ class LocalComic with HistoryMixin implements Comic {
void read() { void read() {
var history = HistoryManager().find(id, comicType); var history = HistoryManager().find(id, comicType);
int? firstDownloadedChapter;
int? firstDownloadedChapterGroup;
if (downloadedChapters.isNotEmpty && chapters != null) {
final chapters = this.chapters!;
if (chapters.isGrouped) {
for (int i=0; i<chapters.groupCount; i++) {
var group = chapters.getGroupByIndex(i);
var keys = group.keys.toList();
for (int j=0; j<keys.length; j++) {
var chapterId = keys[j];
if (downloadedChapters.contains(chapterId)) {
firstDownloadedChapter = j + 1;
firstDownloadedChapterGroup = i + 1;
break;
}
}
}
} else {
var keys = chapters.allChapters.keys;
for (int i = 0; i < keys.length; i++) {
if (downloadedChapters.contains(keys.elementAt(i))) {
firstDownloadedChapter = i + 1;
break;
}
}
}
}
App.rootContext.to( App.rootContext.to(
() => Reader( () => Reader(
type: comicType, type: comicType,
cid: id, cid: id,
name: title, name: title,
chapters: chapters, chapters: chapters,
initialChapter: history?.ep, initialChapter: history?.ep ?? firstDownloadedChapter,
initialPage: history?.page, initialPage: history?.page,
initialChapterGroup: history?.group, initialChapterGroup: history?.group ?? firstDownloadedChapterGroup,
history: history ?? history: history ??
History.fromModel( History.fromModel(
model: this, model: this,
@@ -583,7 +611,7 @@ class LocalManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true]) { void batchDeleteComics(List<LocalComic> comics, [bool removeFileOnDisk = true, bool removeFavoriteAndHistory = true]) {
if (comics.isEmpty) { if (comics.isEmpty) {
return; return;
} }
@@ -612,8 +640,11 @@ class LocalManager with ChangeNotifier {
_db.execute('COMMIT;'); _db.execute('COMMIT;');
var comicIDs = comics.map((e) => ComicID(e.comicType, e.id)).toList(); var comicIDs = comics.map((e) => ComicID(e.comicType, e.id)).toList();
LocalFavoritesManager().batchDeleteComicsInAllFolders(comicIDs);
HistoryManager().batchDeleteHistories(comicIDs); if (removeFavoriteAndHistory) {
LocalFavoritesManager().batchDeleteComicsInAllFolders(comicIDs);
HistoryManager().batchDeleteHistories(comicIDs);
}
notifyListeners(); notifyListeners();
@@ -625,6 +656,7 @@ class LocalManager with ChangeNotifier {
/// Deletes the directories in a separate isolate to avoid blocking the UI thread. /// Deletes the directories in a separate isolate to avoid blocking the UI thread.
static void _deleteDirectories(List<Directory> directories) { static void _deleteDirectories(List<Directory> directories) {
Isolate.run(() async { Isolate.run(() async {
await SAFTaskWorker().init();
for (var dir in directories) { for (var dir in directories) {
try { try {
if (dir.existsSync()) { if (dir.existsSync()) {

View File

@@ -95,8 +95,7 @@ Future<void> _checkAppUpdates() async {
appdata.writeImplicitData(); appdata.writeImplicitData();
ComicSourcePage.checkComicSourceUpdate(); ComicSourcePage.checkComicSourceUpdate();
if (appdata.settings['checkUpdateOnStart']) { if (appdata.settings['checkUpdateOnStart']) {
await Future.delayed(const Duration(milliseconds: 300)); await checkUpdateUi(false, true);
await checkUpdateUi(false);
} }
} }

View File

@@ -552,7 +552,7 @@ class _ImageDownloadWrapper {
void start() async { void start() async {
int lastBytes = 0; int lastBytes = 0;
try { try {
await for (var p in ImageDownloader.loadComicImage( await for (var p in ImageDownloader.loadComicImageUnwrapped(
image, task.source.key, task.comicId, chapter)) { image, task.source.key, task.comicId, chapter)) {
if (isCancelled) { if (isCancelled) {
return; return;

View File

@@ -111,6 +111,11 @@ abstract class ImageDownloader {
return stream.stream; return stream.stream;
} }
static Stream<ImageDownloadProgress> loadComicImageUnwrapped(
String imageKey, String? sourceKey, String cid, String eid) {
return _loadComicImage(imageKey, sourceKey, cid, eid);
}
static Stream<ImageDownloadProgress> _loadComicImage( static Stream<ImageDownloadProgress> _loadComicImage(
String imageKey, String? sourceKey, String cid, String eid) async* { String imageKey, String? sourceKey, String cid, String eid) async* {
final cacheKey = "$imageKey@$sourceKey@$cid@$eid"; final cacheKey = "$imageKey@$sourceKey@$cid@$eid";

View File

@@ -27,7 +27,7 @@ class _NormalComicChapters extends StatefulWidget {
class _NormalComicChaptersState extends State<_NormalComicChapters> { class _NormalComicChaptersState extends State<_NormalComicChapters> {
late _ComicPageState state; late _ComicPageState state;
bool reverse = false; late bool reverse;
bool showAll = false; bool showAll = false;
@@ -38,6 +38,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
reverse = appdata.settings["reverseChapterOrder"] ?? false;
history = widget.history; history = widget.history;
} }
@@ -176,7 +177,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late _ComicPageState state; late _ComicPageState state;
bool reverse = false; late bool reverse;
bool showAll = false; bool showAll = false;
@@ -191,6 +192,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
reverse = appdata.settings["reverseChapterOrder"] ?? false;
history = widget.history; history = widget.history;
if (history?.group != null) { if (history?.group != null) {
index = history!.group! - 1; index = history!.group! - 1;

View File

@@ -410,20 +410,26 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
String text; String text;
if (haveChapter) { if (haveChapter) {
var epName = "E$ep"; var epName = "E$ep";
String? groupName;
try { try {
epName = group == null if (group == null){
? comic.chapters!.titles.elementAt( epName = comic.chapters!.titles.elementAt(
math.min(ep - 1, comic.chapters!.length - 1), math.min(ep - 1, comic.chapters!.length - 1),
) );
: comic.chapters! } else {
.getGroupByIndex(group - 1) groupName = comic.chapters!.groups.elementAt(group - 1);
.values epName = comic.chapters!
.elementAt(ep - 1); .getGroupByIndex(group - 1)
.values
.elementAt(ep - 1);
}
} }
catch(e) { catch(e) {
// ignore // ignore
} }
text = "${"Last Reading".tl}: $epName P$page"; text = groupName == null
? "${"Last Reading".tl}: $epName P$page"
: "${"Last Reading".tl}: $groupName $epName P$page";
} else { } else {
text = "${"Last Reading".tl}: P$page"; text = "${"Last Reading".tl}: P$page";
} }

View File

@@ -51,9 +51,7 @@ class ComicSourcePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(body: const _Body());
body: const _Body(),
);
} }
} }
@@ -87,10 +85,7 @@ class _BodyState extends State<_Body> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SmoothCustomScrollView( return SmoothCustomScrollView(
slivers: [ slivers: [
SliverAppbar( SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow),
title: Text('Comic Source'.tl),
style: AppbarStyle.shadow,
),
buildCard(context), buildCard(context),
for (var source in ComicSource.all()) for (var source in ComicSource.all())
_SliverComicSource( _SliverComicSource(
@@ -109,9 +104,7 @@ class _BodyState extends State<_Body> {
showConfirmDialog( showConfirmDialog(
context: App.rootContext, context: App.rootContext,
title: "Delete".tl, title: "Delete".tl,
content: "Delete comic source '@n' ?".tlParams({ content: "Delete comic source '@n' ?".tlParams({"n": source.name}),
"n": source.name,
}),
btnColor: context.colorScheme.error, btnColor: context.colorScheme.error,
onConfirm: () { onConfirm: () {
var file = File(source.filePath); var file = File(source.filePath);
@@ -133,14 +126,16 @@ class _BodyState extends State<_Body> {
title: const Text("Reload Configs"), title: const Text("Reload Configs"),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("cancel")), child: const Text("cancel"),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await ComicSourceManager().reload(); await ComicSourceManager().reload();
App.forceRebuild(); App.forceRebuild();
}, },
child: const Text("continue")), child: const Text("continue"),
),
], ],
), ),
); );
@@ -157,8 +152,10 @@ class _BodyState extends State<_Body> {
); );
} }
static Future<void> update(ComicSource source, static Future<void> update(
[bool showLoading = true]) async { ComicSource source, [
bool showLoading = true,
]) async {
if (!source.url.isURL) { if (!source.url.isURL) {
App.rootContext.showMessage(message: "Invalid url config"); App.rootContext.showMessage(message: "Invalid url config");
return; return;
@@ -174,8 +171,10 @@ class _BodyState extends State<_Body> {
); );
} }
try { try {
var res = await AppDio().get<String>(source.url, var res = await AppDio().get<String>(
options: Options(responseType: ResponseType.plain)); source.url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return; if (cancel) return;
controller?.close(); controller?.close();
await ComicSourceParser().parse(res.data!, source.filePath); await ComicSourceParser().parse(res.data!, source.filePath);
@@ -192,12 +191,11 @@ class _BodyState extends State<_Body> {
} }
Widget buildCard(BuildContext context) { Widget buildCard(BuildContext context) {
Widget buildButton( Widget buildButton({
{required Widget child, required VoidCallback onPressed}) { required Widget child,
return Button.normal( required VoidCallback onPressed,
onPressed: onPressed, }) {
child: child, return Button.normal(onPressed: onPressed, child: child).fixHeight(32);
).fixHeight(32);
} }
return SliverToBoxAdapter( return SliverToBoxAdapter(
@@ -213,12 +211,14 @@ class _BodyState extends State<_Body> {
), ),
TextField( TextField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: "URL", hintText: "URL",
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
suffix: IconButton( suffix: IconButton(
onPressed: () => handleAddSource(url), onPressed: () => handleAddSource(url),
icon: const Icon(Icons.check))), icon: const Icon(Icons.check),
),
),
onChanged: (value) { onChanged: (value) {
url = value; url = value;
}, },
@@ -245,10 +245,7 @@ class _BodyState extends State<_Body> {
), ),
ListTile( ListTile(
title: Text("Help".tl), title: Text("Help".tl),
trailing: buildButton( trailing: buildButton(onPressed: help, child: Text("Open".tl)),
onPressed: help,
child: Text("Open".tl),
),
), ),
ListTile( ListTile(
title: Text("Check updates".tl), title: Text("Check updates".tl),
@@ -277,7 +274,8 @@ class _BodyState extends State<_Body> {
void help() { void help() {
launchUrlString( launchUrlString(
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); "https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
} }
Future<void> handleAddSource(String url) async { Future<void> handleAddSource(String url) async {
@@ -288,11 +286,16 @@ class _BodyState extends State<_Body> {
splits.removeWhere((element) => element == ""); splits.removeWhere((element) => element == "");
var fileName = splits.last; var fileName = splits.last;
bool cancel = false; bool cancel = false;
var controller = showLoadingDialog(App.rootContext, var controller = showLoadingDialog(
onCancel: () => cancel = true, barrierDismissible: false); App.rootContext,
onCancel: () => cancel = true,
barrierDismissible: false,
);
try { try {
var res = await AppDio() var res = await AppDio().get<String>(
.get<String>(url, options: Options(responseType: ResponseType.plain)); url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return; if (cancel) return;
controller.close(); controller.close();
await addSource(res.data!, fileName); await addSource(res.data!, fileName);
@@ -332,6 +335,12 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = null; json = null;
}); });
} }
if (controller.text.isEmpty) {
setState(() {
json = [];
});
return;
}
var dio = AppDio(); var dio = AppDio();
try { try {
var res = await dio.get<String>(controller.text); var res = await dio.get<String>(controller.text);
@@ -343,8 +352,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = jsonDecode(res.data!); json = jsonDecode(res.data!);
}); });
} }
} } catch (e) {
catch(e) {
context.showMessage(message: "Network error".tl); context.showMessage(message: "Network error".tl);
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -372,10 +380,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopUpWidgetScaffold( return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody());
title: "Comic Source".tl,
body: buildBody(),
);
} }
Widget buildBody() { Widget buildBody() {
@@ -399,32 +404,36 @@ class _ComicSourceListState extends State<_ComicSourceList> {
children: [ children: [
ListTile( ListTile(
leading: Icon(Icons.source_outlined), leading: Icon(Icons.source_outlined),
title: Text("Source URL".tl), title: Text("Repo URL".tl),
), ),
TextField( TextField(
controller: controller, controller: controller,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "URL", hintText: "URL",
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
contentPadding: contentPadding: const EdgeInsets.symmetric(horizontal: 12),
const EdgeInsets.symmetric(horizontal: 12),
), ),
onChanged: (value) { onChanged: (value) {
changed = true; changed = true;
}, },
).paddingHorizontal(16).paddingBottom(8), ).paddingHorizontal(16).paddingBottom(8),
Text("The URL should point to a 'index.json' file".tl).paddingLeft(16), Text(
Text("Do not report any issues related to sources to App repo.".tl).paddingLeft(16), "The URL should point to a 'index.json' file".tl,
).paddingLeft(16),
Text(
"Do not report any issues related to sources to App repo.".tl,
).paddingLeft(16),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: () { onPressed: () {
controller.text = defaultComicSourceUrl; launchUrlString(
changed = true; "https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
}, },
child: Text("Reset".tl), child: Text("Help".tl),
), ),
FilledButton.tonal( FilledButton.tonal(
onPressed: load, onPressed: load,
@@ -440,7 +449,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
} }
if (index == 1 && json == null) { if (index == 1 && json == null) {
return Center(child: CircularProgressIndicator()); return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(24).fixHeight(24),
);
} }
index--; index--;
@@ -449,28 +462,28 @@ class _ComicSourceListState extends State<_ComicSourceList> {
var action = currentKey.contains(key) var action = currentKey.contains(key)
? const Icon(Icons.check, size: 20).paddingRight(8) ? const Icon(Icons.check, size: 20).paddingRight(8)
: Button.filled( : Button.filled(
child: Text("Add".tl), child: Text("Add".tl),
onPressed: () async { onPressed: () async {
var fileName = json![index]["fileName"]; var fileName = json![index]["fileName"];
var url = json![index]["url"]; var url = json![index]["url"];
if (url == null || !(url.toString()).isURL) { if (url == null || !(url.toString()).isURL) {
var listUrl = var listUrl =
appdata.settings['comicSourceListUrl'] as String; appdata.settings['comicSourceListUrl'] as String;
if (listUrl if (listUrl
.replaceFirst("https://", "") .replaceFirst("https://", "")
.replaceFirst("http://", "") .replaceFirst("http://", "")
.contains("/")) { .contains("/")) {
url = url =
listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + listUrl.substring(0, listUrl.lastIndexOf("/") + 1) +
fileName; fileName;
} else { } else {
url = '$listUrl/$fileName'; url = '$listUrl/$fileName';
} }
} }
await widget.onAdd(url); await widget.onAdd(url);
setState(() {}); setState(() {});
}, },
).fixHeight(32); ).fixHeight(32);
var description = json![index]["version"]; var description = json![index]["version"];
if (json![index]["description"] != null) { if (json![index]["description"] != null) {
@@ -551,8 +564,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
!networkFavorites.contains(source.favoriteData!.key)) { !networkFavorites.contains(source.favoriteData!.key)) {
networkFavorites.add(source.favoriteData!.key); networkFavorites.add(source.favoriteData!.key);
} }
if (source.searchPageData != null && if (source.searchPageData != null && !searchPages.contains(source.key)) {
!searchPages.contains(source.key)) {
searchPages.add(source.key); searchPages.add(source.key);
} }
@@ -594,15 +606,10 @@ class __EditFilePageState extends State<_EditFilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: Appbar( appBar: Appbar(title: Text("Edit".tl)),
title: Text("Edit".tl),
),
body: Column( body: Column(
children: [ children: [
Container( Container(height: 0.6, color: context.colorScheme.outlineVariant),
height: 0.6,
color: context.colorScheme.outlineVariant,
),
Expanded( Expanded(
child: CodeEditor( child: CodeEditor(
initialValue: current, initialValue: current,
@@ -643,9 +650,11 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
} }
void showUpdateDialog() async { void showUpdateDialog() async {
var text = ComicSourceManager().availableUpdates.entries.map((e) { var text = ComicSourceManager().availableUpdates.entries
return "${ComicSource.find(e.key)!.name}: ${e.value}"; .map((e) {
}).join("\n"); return "${ComicSource.find(e.key)!.name}: ${e.value}";
})
.join("\n");
bool doUpdate = false; bool doUpdate = false;
await showDialog( await showDialog(
context: App.rootContext, context: App.rootContext,
@@ -783,10 +792,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
child: ListTile( child: ListTile(
title: Row( title: Row(
children: [ children: [
Text( Text(source.name, style: ts.s18),
source.name,
style: ts.s18,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -819,7 +825,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
), ),
), ),
).paddingLeft(4) ).paddingLeft(4),
], ],
), ),
trailing: Row( trailing: Row(
@@ -864,15 +870,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Column( child: Column(children: buildSourceSettings().toList()),
children: buildSourceSettings().toList(),
),
),
SliverToBoxAdapter(
child: Column(
children: _buildAccount().toList(),
),
), ),
SliverToBoxAdapter(child: Column(children: _buildAccount().toList())),
], ],
); );
} }
@@ -898,8 +898,10 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
} }
} }
} else { } else {
current = item.value['options'] current =
.firstWhere((e) => e['value'] == current)['text'] ?? item.value['options'].firstWhere(
(e) => e['value'] == current,
)['text'] ??
current; current;
} }
yield ListTile( yield ListTile(
@@ -907,8 +909,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: Select( trailing: Select(
current: (current as String).ts(source.key), current: (current as String).ts(source.key),
values: (item.value['options'] as List) values: (item.value['options'] as List)
.map<String>((e) => .map<String>(
((e['text'] ?? e['value']) as String).ts(source.key)) (e) => ((e['text'] ?? e['value']) as String).ts(source.key),
)
.toList(), .toList(),
onTap: (i) { onTap: (i) {
source.data['settings'][key] = source.data['settings'][key] =
@@ -936,8 +939,11 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
source.data['settings'][key] ?? item.value['default'] ?? ''; source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile( yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)), title: Text((item.value['title'] as String).ts(source.key)),
subtitle: subtitle: Text(
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis), current,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
onPressed: () { onPressed: () {
@@ -978,10 +984,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () async { onTap: () async {
await context.to( await context.to(
() => _LoginPage( () => _LoginPage(config: source.account!, source: source),
config: source.account!,
source: source,
),
); );
source.saveData(); source.saveData();
setState(() {}); setState(() {});
@@ -1027,9 +1030,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: loading trailing: loading
? const SizedBox.square( ? const SizedBox.square(
dimension: 24, dimension: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(strokeWidth: 2),
strokeWidth: 2,
),
) )
: const Icon(Icons.refresh), : const Icon(Icons.refresh),
); );
@@ -1070,9 +1071,7 @@ class _LoginPageState extends State<_LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const Appbar( appBar: const Appbar(title: Text('')),
title: Text(''),
),
body: Center( body: Center(
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -1200,8 +1199,9 @@ class _LoginPageState extends State<_LoginPage> {
setState(() { setState(() {
loading = true; loading = true;
}); });
var cookies = var cookies = widget.config.cookieFields!
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); .map((e) => _cookies[e] ?? '')
.toList();
widget.config.validateCookies!(cookies).then((value) { widget.config.validateCookies!(cookies).then((value) {
if (value) { if (value) {
widget.source.data['account'] = 'ok'; widget.source.data['account'] = 'ok';

View File

@@ -66,6 +66,11 @@ class _FavoritesPageState extends State<FavoritesPage> {
folder = data['name']; folder = data['name'];
isNetwork = data['isNetwork'] ?? false; isNetwork = data['isNetwork'] ?? false;
} }
if (folder != null
&& !isNetwork
&& !LocalFavoritesManager().existsFolder(folder!)) {
folder = null;
}
super.initState(); super.initState();
} }

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,
@@ -361,17 +377,31 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
context: App.rootContext, context: App.rootContext,
builder: (context) { builder: (context) {
bool removeComicFile = true; bool removeComicFile = true;
bool removeFavoriteAndHistory = true;
return StatefulBuilder(builder: (context, state) { return StatefulBuilder(builder: (context, state) {
return ContentDialog( return ContentDialog(
title: "Delete".tl, title: "Delete".tl,
content: CheckboxListTile( content: Column(
title: Text("Also remove files on disk".tl), children: [
value: removeComicFile, CheckboxListTile(
onChanged: (v) { title: Text("Remove local favorite and history".tl),
state(() { value: removeFavoriteAndHistory,
removeComicFile = !removeComicFile; onChanged: (v) {
}); state(() {
}, removeFavoriteAndHistory = !removeFavoriteAndHistory;
});
},
),
CheckboxListTile(
title: Text("Also remove files on disk".tl),
value: removeComicFile,
onChanged: (v) {
state(() {
removeComicFile = !removeComicFile;
});
},
)
],
), ),
actions: [ actions: [
if (comics.length == 1 && comics.first.hasChapters) if (comics.length == 1 && comics.first.hasChapters)
@@ -388,6 +418,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
LocalManager().batchDeleteComics( LocalManager().batchDeleteComics(
comics, comics,
removeComicFile, removeComicFile,
removeFavoriteAndHistory,
); );
isDeleted = true; isDeleted = true;
}, },
@@ -504,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

@@ -40,6 +40,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
reader.images = images; reader.images = images;
reader.isLoading = false; reader.isLoading = false;
inProgress = false; inProgress = false;
Future.microtask(() {
reader.updateHistory();
});
}); });
} catch (e) { } catch (e) {
setState(() { setState(() {
@@ -65,6 +68,9 @@ class _ReaderImagesState extends State<_ReaderImages> {
reader.images = res.data; reader.images = res.data;
reader.isLoading = false; reader.isLoading = false;
inProgress = false; inProgress = false;
Future.microtask(() {
reader.updateHistory();
});
}); });
} }
} }
@@ -233,7 +239,7 @@ class _GalleryModeState extends State<_GalleryMode>
photoViewControllers[index] ??= PhotoViewController(); photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage == 1) { if (reader.imagesPerPage == 1 || pageImages.length == 1) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
controller: photoViewControllers[index], controller: photoViewControllers[index],

View File

@@ -164,10 +164,9 @@ class _ReaderState extends State<Reader>
} }
mode = ReaderMode.fromKey(appdata.settings['readerMode']); mode = ReaderMode.fromKey(appdata.settings['readerMode']);
history = widget.history; history = widget.history;
Future.microtask(() { if (!appdata.settings['showSystemStatusBar']) {
updateHistory(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}); }
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
if (appdata.settings['enableTurnPageByVolumeKey']) { if (appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent(); handleVolumeEvent();
} }
@@ -267,7 +266,15 @@ 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
history!.page = (page - 1) * imagesPerPage + 1; if (!showSingleImageOnFirstPage || imagesPerPage == 1) {
history!.page = (page - 1) * imagesPerPage + 1;
} else {
if (page == 1) {
history!.page = 1;
} else {
history!.page = (page - 2) * imagesPerPage + 2;
}
}
} }
history!.maxPage = images?.length ?? 1; history!.maxPage = images?.length ?? 1;
if (widget.chapters?.isGrouped ?? false) { if (widget.chapters?.isGrouped ?? false) {
@@ -349,7 +356,11 @@ abstract mixin class _ImagePerPageHandler {
void initImagesPerPage(int initialPage) { void initImagesPerPage(int initialPage) {
_lastImagesPerPage = imagesPerPage; _lastImagesPerPage = imagesPerPage;
if (imagesPerPage != 1) { if (imagesPerPage != 1) {
page = (initialPage / imagesPerPage).ceil(); if (showSingleImageOnFirstPage) {
page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
} else {
page = (initialPage / imagesPerPage).ceil();
}
} }
} }

View File

@@ -107,7 +107,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (!_isOpen) { if (!_isOpen) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else { } else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); if (!appdata.settings['showSystemStatusBar']) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
} }
setState(() { setState(() {
_isOpen = !_isOpen; _isOpen = !_isOpen;

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

@@ -96,10 +96,13 @@ Future<bool> checkUpdate() async {
return false; return false;
} }
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async { Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true, bool delay = false]) async {
try { try {
var value = await checkUpdate(); var value = await checkUpdate();
if (value) { if (value) {
if (delay) {
await Future.delayed(const Duration(seconds: 2));
}
showDialog( showDialog(
context: App.rootContext, context: App.rootContext,
builder: (context) { builder: (context) {

View File

@@ -52,6 +52,10 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Show history on comic tile".tl, title: "Show history on comic tile".tl,
settingKey: "showHistoryStatusOnTile", settingKey: "showHistoryStatusOnTile",
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Reverse default chapter order".tl,
settingKey: "reverseChapterOrder",
).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Keyword blocking".tl, title: "Keyword blocking".tl,
builder: () => const _ManageBlockingWordView(), builder: () => const _ManageBlockingWordView(),

View File

@@ -163,6 +163,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableClockAndBatteryInfoInReader"); widget.onChanged?.call("enableClockAndBatteryInfoInReader");
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Show system status bar".tl,
settingKey: "showSystemStatusBar",
onChanged: () {
widget.onChanged?.call("showSystemStatusBar");
},
).toSliver(),
SelectSetting( SelectSetting(
title: "Quick collect image".tl, title: "Quick collect image".tl,
settingKey: "quickCollectImage", settingKey: "quickCollectImage",

View File

@@ -22,10 +22,12 @@ class DataSync with ChangeNotifier {
} }
LocalFavoritesManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged);
ComicSourceManager().addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged);
Future.delayed(const Duration(seconds: 1), () { if (App.isDesktop) {
var controller = WindowFrame.of(App.rootContext); Future.delayed(const Duration(seconds: 1), () {
controller.addCloseListener(_handleWindowClose); var controller = WindowFrame.of(App.rootContext);
}); controller.addCloseListener(_handleWindowClose);
});
}
} }
void onDataChanged() { void onDataChanged() {

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:flutter_saf/flutter_saf.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/utils/image.dart'; import 'package:venera/utils/image.dart';
@@ -74,6 +75,9 @@ Future<Isolate> _runIsolate(
return Isolate.spawn<SendPort>( return Isolate.spawn<SendPort>(
(sendPort) => overrideIO( (sendPort) => overrideIO(
() async { () async {
if (App.isAndroid) {
await SAFTaskWorker().init();
}
var receivePort = ReceivePort(); var receivePort = ReceivePort();
sendPort.send(receivePort.sendPort); sendPort.send(receivePort.sendPort);

View File

@@ -35,8 +35,10 @@ extension TagsTranslation on String{
/// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace. /// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace.
static String _translateTags(String tag){ static String _translateTags(String tag){
if (tag.contains('|')) { if (tag.contains('|')) {
var splits = tag.split(' | '); var splits = tag.split('|');
return enTagsTranslations[splits[0]]??enTagsTranslations[splits[1]]??tag; return enTagsTranslations[splits[0].trim()]
?? enTagsTranslations[splits[1].trim()]
?? tag;
} else if(tag.contains(':')) { } else if(tag.contains(':')) {
var splits = tag.split(':'); var splits = tag.split(':');
if(_haveNamespace(splits[0])) { if(_haveNamespace(splits[0])) {

View File

@@ -433,10 +433,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_rust_bridge name: flutter_rust_bridge
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" sha256: b416ff56002789e636244fb4cc449f587656eff995e5a7169457eb0593fcaddb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.0" version: "2.10.0"
flutter_saf: flutter_saf:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -766,11 +766,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: rhttp path: rhttp
ref: e7dca15ca543b5df49f3ada06016e874b79bce36 ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36 resolved-ref: "1f0ff50336062c5f809c256726dc55cd30b9ce59"
url: "https://github.com/wgh136/rhttp" url: "https://github.com/wgh136/rhttp"
source: git source: git
version: "0.11.0" version: "0.12.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -1108,4 +1108,4 @@ packages:
version: "0.0.12" version: "0.0.12"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.32.4"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.4.4+144 version: 1.4.5+145
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
flutter: 3.32.0 flutter: 3.32.4
dependencies: dependencies:
flutter: flutter:
@@ -58,10 +58,10 @@ dependencies:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1 ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
rhttp: rhttp:
git: git:
url: https://github.com/wgh136/rhttp url: https://github.com/wgh136/rhttp
ref: e7dca15ca543b5df49f3ada06016e874b79bce36 ref: 1f0ff50336062c5f809c256726dc55cd30b9ce59
path: rhttp path: rhttp
webdav_client: webdav_client:
git: git:

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()