112 Commits

Author SHA1 Message Date
nyne
bf634f8654 Merge pull request #104 from venera-app/dev
v1.1.2
2024-12-18 20:11:11 +08:00
nyne
bda215ebb7 Merge branch 'master' into dev 2024-12-18 20:10:41 +08:00
a70b690d3c Run dart fix 2024-12-18 20:07:35 +08:00
0b8ae2d377 Update version code 2024-12-18 20:05:59 +08:00
24c5a1bb01 Improve local comics page 2024-12-18 20:04:45 +08:00
ea973a2787 fix #92 2024-12-18 19:36:54 +08:00
nyne
17bce96143 Merge pull request #103 from UjuiUjuMandan/abivercode
Add abiVersionCode & Remove x86
2024-12-18 19:17:58 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
909c0014ac * 10 for universal 2024-12-18 19:15:45 +08:00
eb1abfc02a Fixed the issue where the images of multi-chapter comic are downloaded to invalid folder. 2024-12-18 19:13:35 +08:00
UjuiUjuMandan
788e41f584 Add abiVersionCode & Remove x86 2024-12-18 10:50:19 +00:00
929ec88e84 Fixed issue where deleting a download caused favourites to be deleted. 2024-12-18 17:58:18 +08:00
abaeaf4f77 improve mouse hover effects on click areas 2024-12-18 17:37:03 +08:00
nyne
a614e83470 Merge pull request #102 from UjuiUjuMandan/rb
update dependencies again
2024-12-18 17:22:30 +08:00
8b9fd0d03d improve pop_up_widget and side_bar in dark mode 2024-12-18 17:19:34 +08:00
UjuiUjuMandan
1964c4c0d5 update dependencies again 2024-12-18 09:16:28 +00:00
43d724dd27 fix #97 2024-12-18 17:08:03 +08:00
f9c42aef4b fix #98 2024-12-18 16:51:57 +08:00
06a6e5156a Fix minimum support platform version 2024-12-18 15:48:35 +08:00
deltamaya
be45a06981 update minimum support platform version 2024-12-18 15:25:32 +08:00
4763b9c7b4 test zip_flutter 2024-12-18 15:14:27 +08:00
7e608be70f test zip_flutter 2024-12-18 14:15:08 +08:00
211e6ab8c8 update dependencies 2024-12-18 13:29:43 +08:00
nyne
100dc6458b Merge pull request #100 from UjuiUjuMandan/master
F-Droid
2024-12-17 22:56:39 +08:00
UjuiUjuMandan
8dab5f9e88 test fastlane
add icon.png

add icon.png

scale to 512x512

metadata for zh-CN

Revert "metadata for zh-CN"

This reverts commit 77b30b9209dd1b082f050c55fa175fa96afbfcf6.
2024-12-17 14:18:41 +00:00
d08383e14b disable Impeller 2024-12-17 20:11:18 +08:00
a55e4eff67 Update to flutter 3.27.1 & Fix android build 2024-12-17 17:21:10 +08:00
ab3953292b fix https://github.com/venera-app/venera-configs/issues/28 2024-12-17 13:01:44 +08:00
b49e0974ab improve zip 2024-12-17 12:10:57 +08:00
nyne
b6cccb7749 update version code 2024-12-13 09:34:37 +08:00
nyne
dac07cfac4 Fix windows build script 2024-12-13 09:30:28 +08:00
nyne
da12b3bcca Fix favorites_page 2024-12-13 09:27:53 +08:00
nyne
017f964705 [Android] Disable Impeller 2024-12-13 09:25:09 +08:00
nyne
bed0f78e81 Merge pull request #96 from venera-app/dev
v1.1.0-patch
2024-12-12 23:30:44 +08:00
nyne
092eb59c10 fix #94 2024-12-12 23:28:54 +08:00
nyne
a5d3d160c8 fix #95 2024-12-12 23:22:19 +08:00
nyne
d3c3748ce5 Update app.dart 2024-12-12 22:07:42 +08:00
nyne
586874de15 Merge pull request #93 from venera-app/dev
v1.1.0
2024-12-12 21:22:26 +08:00
bda2c6c2e1 Merge remote-tracking branch 'origin/dev' into dev 2024-12-12 21:17:54 +08:00
e9aa6fcf30 download comics in local favorites page 2024-12-12 21:17:30 +08:00
60c6be08c5 fix #87: Add translated_tags field to all local favorite table. 2024-12-12 21:17:30 +08:00
e4e2d264f5 update flutter to 3.27.0 & update packages 2024-12-12 21:17:30 +08:00
c2cfd066f6 update flutter_memory_info 2024-12-12 21:17:30 +08:00
d7b91f6a50 fix #91 2024-12-12 21:17:30 +08:00
da025b16ff improve performance & ui 2024-12-12 21:17:30 +08:00
08e0082186 improve explore page loading 2024-12-12 21:17:30 +08:00
463805f5ed Improve TabBar 2024-12-12 21:17:30 +08:00
72b146a9bf Use PageStorage to store state 2024-12-12 21:17:30 +08:00
1104d28f14 improve ui 2024-12-12 21:17:30 +08:00
cf7be85f29 aggregated search 2024-12-12 21:17:30 +08:00
cab66619df fix #61 2024-12-12 21:17:30 +08:00
bdd0724788 delete cache 2024-12-12 21:17:30 +08:00
617c452e07 fix #90: export comic as epub 2024-12-12 21:17:30 +08:00
c8e6e1311c download comics in local favorites page 2024-12-12 18:00:58 +08:00
0bdb1299ca fix #87: Add translated_tags field to all local favorite table. 2024-12-12 17:14:36 +08:00
af9835eb8f update flutter to 3.27.0 & update packages 2024-12-12 16:41:42 +08:00
4801457e0e update flutter_memory_info 2024-12-12 14:24:11 +08:00
0c9f7126a2 fix #91 2024-12-11 13:41:34 +08:00
3cf9228e2a improve performance & ui 2024-12-10 16:01:06 +08:00
07f8cd2455 improve explore page loading 2024-12-10 14:45:48 +08:00
659b211038 Improve TabBar 2024-12-09 20:07:08 +08:00
4e121748cd Use PageStorage to store state 2024-12-09 19:56:43 +08:00
14fe901144 improve ui 2024-12-09 18:06:35 +08:00
835b40860d aggregated search 2024-12-09 17:56:44 +08:00
ef435dcaa5 fix #61 2024-12-08 17:56:30 +08:00
e999652a3e delete cache 2024-12-07 20:11:11 +08:00
425cbed8a1 fix #90: export comic as epub 2024-12-07 20:04:22 +08:00
nyne
488299bcfb Update main.yml 2024-12-02 21:50:16 +08:00
b8bdda16c6 fix cbz import 2024-12-02 21:00:06 +08:00
1a50b8bc27 fix TabBar 2024-12-02 21:00:06 +08:00
546f619063 comment button 2024-12-02 21:00:06 +08:00
Naomi
0e831468ee feat: 漫画列表页本地收藏自动选择默认收藏夹 (#84)
Signed-off-by: Naomi <33375791+Henvy-Mango@users.noreply.github.com>
2024-12-02 21:00:06 +08:00
a4cc0a3af2 Update saf 2024-12-02 21:00:06 +08:00
80811bf12d rollback android storage setting 2024-12-02 21:00:06 +08:00
21bf9d72c0 Add HistoryImageProvider 2024-12-02 21:00:06 +08:00
035a84380c fix copyDirectoryIsolate 2024-12-02 21:00:06 +08:00
5ddb6f47ca add telegram link 2024-12-02 21:00:06 +08:00
c1672d01f8 update reader 2024-12-02 21:00:06 +08:00
buste
66ebdb03b1 Feat 为画廊模式添加每页显示图片数量的配置 (#82)
* Feat: Add dynamic image-per-page configuration for gallery mode

- Implemented a slider to configure the number of images displayed per page (1-5) in gallery mode.
- Updated the reader to dynamically reflect changes in the `imagesPerPage` setting without requiring a mode switch or reopening.
- Ensured compatibility with existing continuous reading mode.

* fix currentImagesPerPage

* fix Continuous mode

* improve readerScreenPicNumber setting disable view

* improve PhotoViewController
2024-12-02 21:00:06 +08:00
df2ba6efd1 update version code 2024-12-02 21:00:06 +08:00
705c448cfe export comic as pdf 2024-12-02 21:00:06 +08:00
a711335012 import pica data 2024-12-02 21:00:06 +08:00
305ef9263d fix selecting file on Android 2024-12-02 21:00:06 +08:00
f8b8811aaa fix #52 2024-12-02 21:00:06 +08:00
a868fe3fff prevent too many image loading at save time 2024-12-02 21:00:06 +08:00
873cbd779e fix #73 2024-12-02 21:00:06 +08:00
d56e3fd59f fix #76 2024-12-02 21:00:06 +08:00
d96b36414d fix subtitle 2024-12-02 21:00:06 +08:00
b30bd11d1a fix #77 2024-12-02 21:00:06 +08:00
nyne
72507d907a Feat/saf (#81)
* [Android] Use SAF to change local path

* Use IOOverrides to replace openDirectoryPlatform and openFilePlatform

* fix io
2024-12-02 21:00:06 +08:00
9b821f1b46 fix cbz import 2024-12-02 20:55:47 +08:00
867b2a4b64 fix TabBar 2024-12-02 17:45:26 +08:00
8f07c8a2bb comment button 2024-12-02 16:47:13 +08:00
Naomi
7aed61a65e feat: 漫画列表页本地收藏自动选择默认收藏夹 (#84)
Signed-off-by: Naomi <33375791+Henvy-Mango@users.noreply.github.com>
2024-12-02 16:27:40 +08:00
674b5c9636 Update saf 2024-12-02 15:30:59 +08:00
153f1a9dfe rollback android storage setting 2024-12-02 11:39:28 +08:00
6c5df47663 Add HistoryImageProvider 2024-12-02 11:19:06 +08:00
24188b51c0 fix copyDirectoryIsolate 2024-12-01 21:10:51 +08:00
070c803f97 add telegram link 2024-12-01 20:25:32 +08:00
b425eec561 update reader 2024-12-01 20:22:33 +08:00
buste
95c98eeaed Feat 为画廊模式添加每页显示图片数量的配置 (#82)
* Feat: Add dynamic image-per-page configuration for gallery mode

- Implemented a slider to configure the number of images displayed per page (1-5) in gallery mode.
- Updated the reader to dynamically reflect changes in the `imagesPerPage` setting without requiring a mode switch or reopening.
- Ensured compatibility with existing continuous reading mode.

* fix currentImagesPerPage

* fix Continuous mode

* improve readerScreenPicNumber setting disable view

* improve PhotoViewController
2024-12-01 19:56:38 +08:00
60f7b4d3b0 update version code 2024-12-01 18:57:35 +08:00
2ee2a01550 export comic as pdf 2024-12-01 18:54:17 +08:00
a2f628001a import pica data 2024-12-01 18:06:19 +08:00
de4503a2de fix selecting file on Android 2024-12-01 18:05:59 +08:00
30b2aa2f99 fix #52 2024-11-30 21:36:23 +08:00
2f4927f719 prevent too many image loading at save time 2024-11-30 21:30:39 +08:00
9fb3482474 fix #73 2024-11-30 21:05:35 +08:00
2063eee82b fix #76 2024-11-30 20:52:55 +08:00
91b765ffba fix subtitle 2024-11-30 13:50:07 +08:00
bbfe87fff2 fix #77 2024-11-30 10:07:03 +08:00
nyne
430b6eeb3a Feat/saf (#81)
* [Android] Use SAF to change local path

* Use IOOverrides to replace openDirectoryPlatform and openFilePlatform

* fix io
2024-11-29 21:33:28 +08:00
onlytheworld
06094fc5fc 自动发布 (#80)
* change workflow

* Update main.yml
2024-11-29 17:08:01 +08:00
88 changed files with 3180 additions and 1392 deletions

16
.github/workflows/fastlane.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Validate Fastlane metadata
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0

View File

@@ -2,6 +2,9 @@ name: Build ALL
run-name: Build ALL run-name: Build ALL
on: on:
workflow_dispatch: {} workflow_dispatch: {}
release:
types: [published]
jobs: jobs:
Build_MacOS: Build_MacOS:
runs-on: macos-15 runs-on: macos-15
@@ -139,3 +142,45 @@ jobs:
name: arch_build name: arch_build
path: build/linux/arch/ path: build/linux/arch/
Release:
runs-on: ubuntu-latest
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux]
if: github.event_name == 'release' # 仅在 push 事件时执行
steps:
- uses: actions/download-artifact@v4
with:
name: venera.dmg
path: outputs
- uses: actions/download-artifact@v4
with:
name: app-ios.ipa
path: outputs
- uses: actions/download-artifact@v4
with:
name: apks
path: outputs
- uses: actions/download-artifact@v4
with:
name: windows_build
path: outputs
- uses: actions/download-artifact@v4
with:
name: deb_build
path: outputs
- uses: actions/download-artifact@v4
with:
name: arch_build
path: outputs
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
files: |
outputs/*.ipa
outputs/*.dmg
outputs/*.apk
outputs/*.zip
outputs/*.exe
outputs/*.deb
outputs/*.zst
env:
GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }}

View File

@@ -35,7 +35,7 @@ android {
splits{ splits{
abi { abi {
reset() reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
enable true enable true
universalApk true universalApk true
} }
@@ -78,16 +78,22 @@ android {
buildTypes { buildTypes {
release { release {
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
} }
signingConfig signingConfigs.release signingConfig signingConfigs.release
ext.abiCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86_64": 3]
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.all { output -> variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI) def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) { if (abi != null) {
outputFileName = "venera-${variant.versionName}-${abi}.apk" outputFileName = "venera-${variant.versionName}-${abi}.apk"
def abiVersionCode = project.ext.abiCodes.get(abi)
if (abiVersionCode != null) {
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
} else { } else {
outputFileName = "venera-${variant.versionName}.apk" outputFileName = "venera-${variant.versionName}.apk"
versionCodeOverride = variant.versionCode * 10
} }
} }
} }
@@ -102,4 +108,4 @@ flutter {
dependencies { dependencies {
implementation "androidx.activity:activity-ktx:1.9.2" implementation "androidx.activity:activity-ktx:1.9.2"
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
} }

View File

@@ -53,6 +53,8 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- [flutter 3.27.1] Impeller is still worse than skia, disable it -->
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

View File

@@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -324,8 +325,25 @@ class MainActivity : FlutterFragmentActivity() {
} }
} }
// use copy method // use copy method
val filePath = FileUtils.getPathFromCopyOfFileFromUri(this, uri) val tmp = File(cacheDir, fileName)
result.success(filePath) if(tmp.exists()) {
tmp.delete()
}
Log.i("Venera", "copy file (${fileName}) to ${tmp.absolutePath}")
Thread {
try {
contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(tmp).use { output ->
input.copyTo(output, bufferSize = DEFAULT_BUFFER_SIZE)
output.flush()
}
}
result.success(tmp.absolutePath)
}
catch (e: Exception) {
result.error("copy error", e.message, null)
}
}.start()
} }
} }
} }

View File

@@ -877,6 +877,8 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
/** /**
* Create a comic details object * Create a comic details object
* @param title {string} * @param title {string}
* @param subtitle {string}
* @param subTitle {string} - equal to subtitle
* @param cover {string} * @param cover {string}
* @param description {string?} * @param description {string?}
* @param tags {Map<string, string[]> | {} | null | undefined} * @param tags {Map<string, string[]> | {} | null | undefined}
@@ -897,8 +899,9 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
* @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page. * @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page.
* @constructor * @constructor
*/ */
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) { function ComicDetails({title, subtitle, subTitle, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) {
this.title = title; this.title = title;
this.subtitle = subtitle ?? subTitle;
this.cover = cover; this.cover = cover;
this.description = description; this.description = description;
this.tags = tags; this.tags = tags;

View File

@@ -105,6 +105,7 @@
"Continuous (Right to Left)": "连续(从右到左)", "Continuous (Right to Left)": "连续(从右到左)",
"Continuous (Top to Bottom)": "连续(从上到下)", "Continuous (Top to Bottom)": "连续(从上到下)",
"Auto page turning interval": "自动翻页间隔", "Auto page turning interval": "自动翻页间隔",
"The number of pic in screen (Only Gallery Mode)": "同屏幕图片数量(仅画廊模式)",
"Theme Mode": "主题模式", "Theme Mode": "主题模式",
"System": "系统", "System": "系统",
"Light": "浅色", "Light": "浅色",
@@ -152,7 +153,7 @@
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
"Export as cbz": "导出为cbz", "Export as cbz": "导出为cbz",
"Select a cbz file." : "选择一个cbz文件", "Select a cbz/zip file." : "选择一个cbz/zip文件",
"A cbz file" : "一个cbz文件", "A cbz file" : "一个cbz文件",
"Fullscreen": "全屏", "Fullscreen": "全屏",
"Exit": "退出", "Exit": "退出",
@@ -244,7 +245,20 @@
"Deleted @a favorite items.": "已删除 @a 条无效收藏", "Deleted @a favorite items.": "已删除 @a 条无效收藏",
"New version available": "有新版本可用", "New version available": "有新版本可用",
"A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?",
"No new version available": "没有新版本可用" "No new version available": "没有新版本可用",
"Export as pdf": "导出为pdf",
"Export as epub": "导出为epub",
"Aggregated Search": "聚合搜索",
"No search results found": "未找到搜索结果",
"Added @c comics to download queue." : "已添加 @c 本漫画到下载队列",
"Download started": "下载已开始",
"Click favorite": "点击收藏",
"End": "末尾",
"None": "无",
"View Detail": "查看详情",
"Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录",
"Multiple cbz files" : "多个cbz文件",
"No valid comics found" : "未找到有效的漫画"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -352,6 +366,7 @@
"Continuous (Right to Left)": "連續(從右到左)", "Continuous (Right to Left)": "連續(從右到左)",
"Continuous (Top to Bottom)": "連續(從上到下)", "Continuous (Top to Bottom)": "連續(從上到下)",
"Auto page turning interval": "自動翻頁間隔", "Auto page turning interval": "自動翻頁間隔",
"The number of pic in screen (Only Gallery Mode)": "同螢幕圖片數量(僅畫廊模式)",
"Theme Mode": "主題模式", "Theme Mode": "主題模式",
"System": "系統", "System": "系統",
"Light": "浅色", "Light": "浅色",
@@ -399,7 +414,7 @@
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
"Export as cbz": "匯出為cbz", "Export as cbz": "匯出為cbz",
"Select a cbz file." : "選擇一個cbz文件", "Select a cbz/zip file." : "選擇一個cbz/zip文件",
"A cbz file" : "一個cbz文件", "A cbz file" : "一個cbz文件",
"Fullscreen": "全螢幕", "Fullscreen": "全螢幕",
"Exit": "退出", "Exit": "退出",
@@ -491,6 +506,19 @@
"Deleted @a favorite items.": "已刪除 @a 條無效收藏", "Deleted @a favorite items.": "已刪除 @a 條無效收藏",
"New version available": "有新版本可用", "New version available": "有新版本可用",
"A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?",
"No new version available": "沒有新版本可用" "No new version available": "沒有新版本可用",
"Export as pdf": "匯出為pdf",
"Export as epub": "匯出為epub",
"Aggregated Search": "聚合搜索",
"No search results found": "未找到搜索結果",
"Added @c comics to download queue." : "已添加 @c 本漫畫到下載隊列",
"Download started": "下載已開始",
"Click favorite": "點擊收藏",
"End": "末尾",
"None": "無",
"View Detail": "查看詳情",
"Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄",
"Multiple cbz files" : "多個cbz文件",
"No valid comics found" : "未找到有效的漫畫"
} }
} }

View File

@@ -0,0 +1,40 @@
<p><a href="https://flutter.dev/"><img src="https://img.shields.io/badge/flutter-3.24.4-blue" alt="flutter"></a>
<a href="https://github.com/venera-app/venera/blob/master/LICENSE"><img src="https://img.shields.io/github/license/venera-app/venera" alt="License"></a>
<a href="https://github.com/venera-app/venera/releases"><img src="https://img.shields.io/github/v/release/venera-app/venera" alt="Download"></a>
<a href="https://github.com/venera-app/venera/stargazers"><img src="https://img.shields.io/github/stars/venera-app/venera" alt="stars"></a>
<a href="https://t.me/+Ws-IpmUutzkxMjhl"><img src="https://img.shields.io/badge/Telegram-2CA5E0?style=flat&amp;logo=telegram&amp;logoColor=white" alt="Telegram"></a></p>
<p>A comic reader that support reading local and network comics.</p>
<h2>Features</h2>
<ul>
<li>Read local comics</li>
<li>Use javascript to create comic sources</li>
<li>Read comics from network sources</li>
<li>Manage favorite comics</li>
<li>Download comics</li>
<li>View comments, tags, and other information of comics if the source supports</li>
<li>Login to comment, rate, and other operations if the source supports</li>
</ul>
<h2>Build from source</h2>
<ol>
<li>Clone the repository</li>
<li>Install flutter, see <a href="https://flutter.dev/docs/get-started/install">flutter.dev</a></li>
<li>Install rust, see <a href="https://rustup.rs/">rustup.rs</a></li>
<li>Build for your platform: e.g. <code>flutter build apk</code></li>
</ol>
<h2>Create a new comic source</h2>
<p>See <a href="https://github.com/venera-app/venera-configs">venera-configs</a></p>
<h2>Thanks</h2>
<h3>Tags Translation</h3>
<p><a href="https://github.com/EhTagTranslation/Database"><img src="https://github-readme-stats.vercel.app/api/pin/?username=EhTagTranslation&amp;repo=Database" alt="Readme Card"></a></p>
<p>The Chinese translation of the manga tags is from this project.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
A comic reader that support reading local and network comics.

View File

@@ -0,0 +1 @@
venera

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '14.0' platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -76,7 +76,7 @@ class _AppbarState extends State<Appbar> {
var content = Container( var content = Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.backgroundColor ?? color: widget.backgroundColor ??
context.colorScheme.surface.withOpacity(0.72), context.colorScheme.surface.toOpacity(0.72),
), ),
height: _kAppBarHeight + context.padding.top, height: _kAppBarHeight + context.padding.top,
child: Row( child: Row(
@@ -189,20 +189,19 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
leading ?? leading ??
(Navigator.of(context).canPop() (Navigator.of(context).canPop()
? Tooltip( ? Tooltip(
message: "Back".tl, message: "Back".tl,
child: IconButton( child: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.maybePop(context), onPressed: () => Navigator.maybePop(context),
), ),
) )
: const SizedBox()), : const SizedBox()),
const SizedBox( const SizedBox(
width: 16, width: 16,
), ),
Expanded( Expanded(
child: DefaultTextStyle( child: DefaultTextStyle(
style: style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
child: title, child: title,
@@ -215,12 +214,12 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
], ],
).paddingTop(topPadding); ).paddingTop(topPadding);
if(style == AppbarStyle.blur) { if (style == AppbarStyle.blur) {
return SizedBox.expand( return SizedBox.expand(
child: BlurEffect( child: BlurEffect(
blur: 15, blur: 15,
child: Material( child: Material(
color: context.colorScheme.surface.withOpacity(0.72), color: context.colorScheme.surface.toOpacity(0.72),
elevation: 0, elevation: 0,
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
child: body, child: body,
@@ -298,12 +297,21 @@ class _FilledTabBarState extends State<FilledTabBar> {
super.dispose(); super.dispose();
} }
PageStorageBucket get bucket => PageStorage.of(context);
@override @override
void didChangeDependencies() { void didChangeDependencies() {
_controller = widget.controller ?? DefaultTabController.of(context); _controller = widget.controller ?? DefaultTabController.of(context);
_controller.animation!.addListener(onTabChanged);
initPainter(); initPainter();
super.didChangeDependencies(); super.didChangeDependencies();
var prevIndex = bucket.readState(context) as int?;
if (prevIndex != null &&
prevIndex != _controller.index &&
prevIndex >= 0 &&
prevIndex < widget.tabs.length) {
_controller.index = prevIndex;
}
_controller.animation!.addListener(onTabChanged);
} }
@override @override
@@ -332,7 +340,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller, animation: _controller.animation ?? _controller,
builder: buildTabBar, builder: buildTabBar,
); );
} }
@@ -347,6 +355,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
controller: scrollController, controller: scrollController,
builder: (context, controller, physics) { builder: (context, controller, physics) {
return SingleChildScrollView( return SingleChildScrollView(
key: const PageStorageKey('scroll'),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
controller: controller, controller: controller,
@@ -387,6 +396,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
} }
updateScrollOffset(i); updateScrollOffset(i);
previousIndex = i; previousIndex = i;
bucket.writeState(context, i);
} }
void updateScrollOffset(int i) { void updateScrollOffset(int i) {
@@ -427,7 +437,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: DefaultTextStyle( child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith( style: DefaultTextStyle.of(context).style.copyWith(
color: i == _controller.index color: i == _controller.animation?.value.round()
? context.colorScheme.primary ? context.colorScheme.primary
: context.colorScheme.onSurface, : context.colorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -724,6 +734,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () { onPressed: () {
editingController.clear(); editingController.clear();
onChanged?.call("");
}, },
); );
}, },

View File

@@ -214,7 +214,7 @@ class _ButtonState extends State<Button> {
boxShadow: (isHover && !isLoading && (widget.type == ButtonType.filled || widget.type == ButtonType.normal)) boxShadow: (isHover && !isLoading && (widget.type == ButtonType.filled || widget.type == ButtonType.normal))
? [ ? [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.toOpacity(0.1),
blurRadius: 2, blurRadius: 2,
offset: const Offset(0, 1), offset: const Offset(0, 1),
) )
@@ -248,7 +248,7 @@ class _ButtonState extends State<Button> {
if (widget.type == ButtonType.filled) { if (widget.type == ButtonType.filled) {
var color = widget.color ?? context.colorScheme.primary; var color = widget.color ?? context.colorScheme.primary;
if (isHover) { if (isHover) {
return color.withOpacity(0.9); return color.toOpacity(0.9);
} else { } else {
return color; return color;
} }
@@ -256,13 +256,13 @@ class _ButtonState extends State<Button> {
if (widget.type == ButtonType.normal) { if (widget.type == ButtonType.normal) {
var color = widget.color ?? context.colorScheme.surfaceContainer; var color = widget.color ?? context.colorScheme.surfaceContainer;
if (isHover) { if (isHover) {
return color.withOpacity(0.9); return color.toOpacity(0.9);
} else { } else {
return color; return color;
} }
} }
if (isHover) { if (isHover) {
return context.colorScheme.outline.withOpacity(0.2); return context.colorScheme.outline.toOpacity(0.2);
} }
return Colors.transparent; return Colors.transparent;
} }
@@ -345,7 +345,7 @@ class _IconButtonState extends State<_IconButton> {
? Theme.of(context) ? Theme.of(context)
.colorScheme .colorScheme
.outlineVariant .outlineVariant
.withOpacity(0.4) .toOpacity(0.4)
: null, : null,
borderRadius: BorderRadius.circular((iconSize + 12) / 2), borderRadius: BorderRadius.circular((iconSize + 12) / 2),
), ),

View File

@@ -43,7 +43,7 @@ class ComicTile extends StatelessWidget {
var renderBox = context.findRenderObject() as RenderBox; var renderBox = context.findRenderObject() as RenderBox;
var size = renderBox.size; var size = renderBox.size;
var location = renderBox.localToGlobal( var location = renderBox.localToGlobal(
Offset(size.width / 2, size.height / 2), Offset((size.width - 242) / 2, size.height / 2),
); );
showMenu(location, context); showMenu(location, context);
} }
@@ -77,7 +77,7 @@ class ComicTile extends StatelessWidget {
icon: Icons.stars_outlined, icon: Icons.stars_outlined,
text: 'Add to favorites'.tl, text: 'Add to favorites'.tl,
onClick: () { onClick: () {
addFavorite(comic); addFavorite([comic]);
}, },
), ),
MenuEntry( MenuEntry(
@@ -144,7 +144,7 @@ class ComicTile extends StatelessWidget {
if (history != null) if (history != null)
Container( Container(
height: 24, height: 24,
color: Colors.blue.withOpacity(0.9), color: Colors.blue.toOpacity(0.9),
constraints: const BoxConstraints(minWidth: 24), constraints: const BoxConstraints(minWidth: 24),
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: CustomPaint( child: CustomPaint(
@@ -163,7 +163,9 @@ class ComicTile extends StatelessWidget {
Widget buildImage(BuildContext context) { Widget buildImage(BuildContext context) {
ImageProvider image; ImageProvider image;
if (comic is LocalComic) { if (comic is LocalComic) {
image = FileImage((comic as LocalComic).coverFile); image = LocalComicImageProvider(comic as LocalComic);
} else if (comic is History) {
image = HistoryImageProvider(comic as History);
} else if (comic.sourceKey == 'local') { } else if (comic.sourceKey == 'local') {
var localComic = LocalManager().find(comic.id, ComicType.local); var localComic = LocalManager().find(comic.id, ComicType.local);
if (localComic == null) { if (localComic == null) {
@@ -291,7 +293,7 @@ class ComicTile extends StatelessWidget {
Radius.circular(10.0), Radius.circular(10.0),
), ),
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.toOpacity(0.5),
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.fromLTRB(8, 6, 8, 6), const EdgeInsets.fromLTRB(8, 6, 8, 6),
@@ -473,7 +475,7 @@ class _ComicDescription extends StatelessWidget {
subtitle, subtitle,
style: TextStyle( style: TextStyle(
fontSize: 10.0, fontSize: 10.0,
color: context.colorScheme.onSurface.withOpacity(0.7)), color: context.colorScheme.onSurface.toOpacity(0.7)),
maxLines: 1, maxLines: 1,
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -778,7 +780,7 @@ class _SliverGridComics extends StatelessWidget {
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.72) ? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72)
: null, : null,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -829,6 +831,8 @@ class ComicList extends StatefulWidget {
this.trailingSliver, this.trailingSliver,
this.errorLeading, this.errorLeading,
this.menuBuilder, this.menuBuilder,
this.controller,
this.refreshHandlerCallback,
}); });
final Future<Res<List<Comic>>> Function(int page)? loadPage; final Future<Res<List<Comic>>> Function(int page)? loadPage;
@@ -843,6 +847,10 @@ class ComicList extends StatefulWidget {
final List<MenuEntry> Function(Comic)? menuBuilder; final List<MenuEntry> Function(Comic)? menuBuilder;
final ScrollController? controller;
final void Function(VoidCallback c)? refreshHandlerCallback;
@override @override
State<ComicList> createState() => ComicListState(); State<ComicList> createState() => ComicListState();
} }
@@ -860,6 +868,51 @@ class ComicListState extends State<ComicList> {
String? _nextUrl; String? _nextUrl;
Map<String, dynamic> get state => {
'maxPage': _maxPage,
'data': _data,
'page': _page,
'error': _error,
'loading': _loading,
'nextUrl': _nextUrl,
};
void restoreState(Map<String, dynamic>? state) {
if (state == null) {
return;
}
_maxPage = state['maxPage'];
_data.clear();
_data.addAll(state['data']);
_page = state['page'];
_error = state['error'];
_loading.clear();
_loading.addAll(state['loading']);
_nextUrl = state['nextUrl'];
}
void storeState() {
PageStorage.of(context).writeState(context, state);
}
void refresh() {
_data.clear();
_page = 1;
_maxPage = null;
_error = null;
_nextUrl = null;
_loading.clear();
storeState();
setState(() {});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
restoreState(PageStorage.of(context).readState(context));
widget.refreshHandlerCallback?.call(refresh);
}
void remove(Comic c) { void remove(Comic c) {
if (_data[_page] == null || !_data[_page]!.remove(c)) { if (_data[_page] == null || !_data[_page]!.remove(c)) {
for (var page in _data.values) { for (var page in _data.values) {
@@ -1007,15 +1060,20 @@ class ComicListState extends State<ComicList> {
while (_data[page] == null) { while (_data[page] == null) {
await _fetchNext(); await _fetchNext();
} }
setState(() {}); if(mounted) {
setState(() {});
}
} catch (e) { } catch (e) {
setState(() { if(mounted) {
_error = e.toString(); setState(() {
}); _error = e.toString();
});
}
} }
} }
} finally { } finally {
_loading[page] = false; _loading[page] = false;
storeState();
} }
} }
@@ -1064,6 +1122,8 @@ class ComicListState extends State<ComicList> {
); );
} }
return SmoothCustomScrollView( return SmoothCustomScrollView(
key: const PageStorageKey('scroll'),
controller: widget.controller,
slivers: [ slivers: [
if (widget.leadingSliver != null) widget.leadingSliver!, if (widget.leadingSliver != null) widget.leadingSliver!,
if (_maxPage != 1) _buildSliverPageSelector(), if (_maxPage != 1) _buildSliverPageSelector(),

View File

@@ -1,5 +1,3 @@
library components;
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
@@ -19,13 +17,14 @@ import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/image_provider/cached_image.dart'; import 'package:venera/foundation/image_provider/cached_image.dart';
import 'package:venera/foundation/image_provider/history_image_provider.dart';
import 'package:venera/foundation/image_provider/local_comic_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/network/cloudflare.dart'; import 'package:venera/network/cloudflare.dart';
import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/foundation/app.dart';
/// patched slider.dart with RtL support /// patched slider.dart with RtL support
class _SliderDefaultsM3 extends SliderThemeData { class _SliderDefaultsM3 extends SliderThemeData {
@@ -15,45 +16,45 @@ class _SliderDefaultsM3 extends SliderThemeData {
Color? get inactiveTrackColor => _colors.surfaceContainerHighest; Color? get inactiveTrackColor => _colors.surfaceContainerHighest;
@override @override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); Color? get secondaryActiveTrackColor => _colors.primary.toOpacity(0.54);
@override @override
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); Color? get disabledActiveTrackColor => _colors.onSurface.toOpacity(0.38);
@override @override
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); Color? get disabledInactiveTrackColor => _colors.onSurface.toOpacity(0.12);
@override @override
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12); Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.toOpacity(0.12);
@override @override
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38); Color? get activeTickMarkColor => _colors.onPrimary.toOpacity(0.38);
@override @override
Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38); Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.toOpacity(0.38);
@override @override
Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38); Color? get disabledActiveTickMarkColor => _colors.onSurface.toOpacity(0.38);
@override @override
Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38); Color? get disabledInactiveTickMarkColor => _colors.onSurface.toOpacity(0.38);
@override @override
Color? get thumbColor => _colors.primary; Color? get thumbColor => _colors.primary;
@override @override
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface); Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.toOpacity(0.38), _colors.surface);
@override @override
Color? get overlayColor => WidgetStateColor.resolveWith((Set<WidgetState> states) { Color? get overlayColor => WidgetStateColor.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.dragged)) { if (states.contains(WidgetState.dragged)) {
return _colors.primary.withOpacity(0.1); return _colors.primary.toOpacity(0.1);
} }
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
return _colors.primary.withOpacity(0.08); return _colors.primary.toOpacity(0.08);
} }
if (states.contains(WidgetState.focused)) { if (states.contains(WidgetState.focused)) {
return _colors.primary.withOpacity(0.1); return _colors.primary.toOpacity(0.1);
} }
return Colors.transparent; return Colors.transparent;

View File

@@ -141,7 +141,7 @@ class FlyoutState extends State<Flyout> {
animation: animation, animation: animation,
builder: (context, builder) { builder: (context, builder) {
return ColoredBox( return ColoredBox(
color: Colors.black.withOpacity(0.3 * animation.value), color: Colors.black.toOpacity(0.3 * animation.value),
); );
}, },
), ),
@@ -185,12 +185,18 @@ class FlyoutContent extends StatelessWidget {
child: Material( child: Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
type: MaterialType.card, type: MaterialType.card,
color: context.colorScheme.surface.withOpacity(0.82), color: context.colorScheme.surface.toOpacity(0.82),
child: Container( child: Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: minFlyoutWidth, minWidth: minFlyoutWidth,
), ),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: context.brightness == ui.Brightness.dark
? Border.all(color: context.colorScheme.outlineVariant)
: null,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -215,108 +221,3 @@ class FlyoutContent extends StatelessWidget {
); );
} }
} }
class FlyoutTextButton extends StatefulWidget {
const FlyoutTextButton(
{super.key,
required this.child,
required this.flyoutBuilder,
this.navigator});
final Widget child;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutTextButton> createState() => _FlyoutTextButtonState();
}
class _FlyoutTextButtonState extends State<FlyoutTextButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: TextButton(
onPressed: () {
_controller.show();
},
child: widget.child,
));
}
}
class FlyoutIconButton extends StatefulWidget {
const FlyoutIconButton(
{super.key,
required this.icon,
required this.flyoutBuilder,
this.navigator});
final Widget icon;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutIconButton> createState() => _FlyoutIconButtonState();
}
class _FlyoutIconButtonState extends State<FlyoutIconButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: IconButton(
onPressed: () {
_controller.show();
},
icon: widget.icon,
));
}
}
class FlyoutFilledButton extends StatefulWidget {
const FlyoutFilledButton(
{super.key,
required this.child,
required this.flyoutBuilder,
this.navigator});
final Widget child;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutFilledButton> createState() => _FlyoutFilledButtonState();
}
class _FlyoutFilledButtonState extends State<FlyoutFilledButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: ElevatedButton(
onPressed: () {
_controller.show();
},
child: widget.child,
));
}
}

View File

@@ -1,7 +1,8 @@
part of 'components.dart'; part of 'components.dart';
class MouseBackDetector extends StatelessWidget { class MouseBackDetector extends StatelessWidget {
const MouseBackDetector({super.key, required this.onTapDown, required this.child}); const MouseBackDetector(
{super.key, required this.onTapDown, required this.child});
final Widget child; final Widget child;
@@ -20,3 +21,61 @@ class MouseBackDetector extends StatelessWidget {
); );
} }
} }
class AnimatedTapRegion extends StatefulWidget {
const AnimatedTapRegion({
super.key,
required this.child,
required this.onTap,
this.borderRadius = 0,
});
final Widget child;
final void Function() onTap;
final double borderRadius;
@override
State<AnimatedTapRegion> createState() => _AnimatedTapRegionState();
}
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
bool isScaled = false;
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) {
isHovered = true;
if (!isScaled) {
Future.delayed(const Duration(milliseconds: 100), () {
if (isHovered) {
setState(() => isScaled = true);
}
});
}
},
onExit: (_) {
isHovered = false;
if(isScaled) {
setState(() => isScaled = false);
}
},
child: GestureDetector(
onTap: widget.onTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
clipBehavior: Clip.antiAlias,
child: AnimatedScale(
duration: _fastAnimationDuration,
scale: isScaled ? 1.1 : 1,
child: widget.child,
),
),
),
);
}
}

View File

@@ -96,6 +96,20 @@ class ListLoadingIndicator extends StatelessWidget {
} }
} }
class SliverListLoadingIndicator extends StatelessWidget {
const SliverListLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
// SliverToBoxAdapter can not been lazy loaded.
// Use SliverList to make sure the animation can be lazy loaded.
return SliverList.list(children: const [
SizedBox(),
ListLoadingIndicator(),
]);
}
}
abstract class LoadingState<T extends StatefulWidget, S extends Object> abstract class LoadingState<T extends StatefulWidget, S extends Object>
extends State<T> { extends State<T> {
bool isLoading = false; bool isLoading = false;
@@ -299,9 +313,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
Widget buildLoading(BuildContext context) { Widget buildLoading(BuildContext context) {
return Center( return Center(
child: const CircularProgressIndicator( child: const CircularProgressIndicator().fixWidth(32).fixHeight(32),
strokeWidth: 2,
).fixWidth(32).fixHeight(32),
); );
} }

View File

@@ -20,6 +20,8 @@ class _MenuRoute<T> extends PopupRoute<T> {
@override @override
String? get barrierLabel => "menu"; String? get barrierLabel => "menu";
double get entryHeight => App.isMobile ? 42 : 36;
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) { Animation<double> secondaryAnimation) {
@@ -30,7 +32,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
left = size.width - width - 10; left = size.width - width - 10;
} }
var top = location.dy; var top = location.dy;
var height = 16 + 32 * entries.length; var height = 16 + entryHeight * entries.length;
if (top + height > size.height - 15) { if (top + height > size.height - 15) {
top = size.height - height - 15; top = size.height - height - 15;
} }
@@ -42,9 +44,12 @@ class _MenuRoute<T> extends PopupRoute<T> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: context.brightness == Brightness.dark
? Border.all(color: context.colorScheme.outlineVariant)
: null,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: context.colorScheme.shadow.withOpacity(0.2), color: context.colorScheme.shadow.toOpacity(0.2),
blurRadius: 8, blurRadius: 8,
blurStyle: BlurStyle.outer, blurStyle: BlurStyle.outer,
), ),
@@ -53,9 +58,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
child: BlurEffect( child: BlurEffect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Material( child: Material(
color: context.brightness == Brightness.light color: context.colorScheme.surface.toOpacity(0.78),
? const Color(0xFFFAFAFA).withOpacity(0.82)
: const Color(0xFF090909).withOpacity(0.82),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Container( child: Container(
width: width, width: width,
@@ -83,7 +86,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
entry.onClick(); entry.onClick();
}, },
child: SizedBox( child: SizedBox(
height: App.isMobile ? 42 : 36, height: entryHeight,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(

View File

@@ -46,21 +46,28 @@ class _ToastOverlay extends StatelessWidget {
child: IconTheme( child: IconTheme(
data: IconThemeData( data: IconThemeData(
color: Theme.of(context).colorScheme.onInverseSurface), color: Theme.of(context).colorScheme.onInverseSurface),
child: Container( child: IntrinsicWidth(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), child: Container(
child: Row( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
mainAxisSize: MainAxisSize.min, constraints: BoxConstraints(
children: [ maxWidth: context.width - 32,
if (icon != null) icon!.paddingRight(8), ),
Text( child: Row(
message, mainAxisSize: MainAxisSize.min,
style: const TextStyle( children: [
fontSize: 16, fontWeight: FontWeight.w500), if (icon != null) icon!.paddingRight(8),
maxLines: 3, Expanded(
overflow: TextOverflow.ellipsis, child: Text(
), message,
if (trailing != null) trailing!.paddingLeft(8) style: const TextStyle(
], fontSize: 16, fontWeight: FontWeight.w500),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
if (trailing != null) trailing!.paddingLeft(8)
],
),
), ),
), ),
), ),
@@ -220,7 +227,7 @@ LoadingDialogController showLoadingDialog(BuildContext context,
); );
}); });
var navigator = Navigator.of(context); var navigator = Navigator.of(context, rootNavigator: true);
navigator.push(loadingDialogRoute).then((value) => controller.closed = true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true);

View File

@@ -23,14 +23,15 @@ class PaneActionEntry {
} }
class NaviPane extends StatefulWidget { class NaviPane extends StatefulWidget {
const NaviPane({required this.paneItems, const NaviPane(
required this.paneActions, {required this.paneItems,
required this.pageBuilder, required this.paneActions,
this.initialPage = 0, required this.pageBuilder,
this.onPageChanged, this.initialPage = 0,
required this.observer, this.onPageChanged,
required this.navigatorKey, required this.observer,
super.key}); required this.navigatorKey,
super.key});
final List<PaneItemEntry> paneItems; final List<PaneItemEntry> paneItems;
@@ -47,10 +48,16 @@ class NaviPane extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
@override @override
State<NaviPane> createState() => _NaviPaneState(); State<NaviPane> createState() => NaviPaneState();
static NaviPaneState of(BuildContext context) {
return context.findAncestorStateOfType<NaviPaneState>()!;
}
} }
class _NaviPaneState extends State<NaviPane> typedef NaviItemTapListener = void Function(int);
class NaviPaneState extends State<NaviPane>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late int _currentPage = widget.initialPage; late int _currentPage = widget.initialPage;
@@ -66,28 +73,41 @@ class _NaviPaneState extends State<NaviPane>
late AnimationController controller; late AnimationController controller;
final _naviItemTapListeners = <NaviItemTapListener>[];
void addNaviItemTapListener(NaviItemTapListener listener) {
_naviItemTapListeners.add(listener);
}
void removeNaviItemTapListener(NaviItemTapListener listener) {
_naviItemTapListeners.remove(listener);
}
static const _kBottomBarHeight = 58.0; static const _kBottomBarHeight = 58.0;
static const _kFoldedSideBarWidth = 80.0; static const _kFoldedSideBarWidth = 72.0;
static const _kSideBarWidth = 256.0; static const _kSideBarWidth = 224.0;
static const _kTopBarHeight = 48.0; static const _kTopBarHeight = 48.0;
double get bottomBarHeight => double get bottomBarHeight =>
_kBottomBarHeight + MediaQuery _kBottomBarHeight + MediaQuery.of(context).padding.bottom;
.of(context)
.padding
.bottom;
void onNavigatorStateChange() { void onNavigatorStateChange() {
onRebuild(context); onRebuild(context);
} }
void updatePage(int index) { void updatePage(int index) {
for (var listener in _naviItemTapListeners) {
listener(index);
}
if (widget.observer.routes.length > 1) { if (widget.observer.routes.length > 1) {
widget.navigatorKey.currentState!.popUntil((route) => route.isFirst); widget.navigatorKey.currentState!.popUntil((route) => route.isFirst);
} }
if (currentPage == index) {
return;
}
setState(() { setState(() {
currentPage = index; currentPage = index;
}); });
@@ -114,10 +134,7 @@ class _NaviPaneState extends State<NaviPane>
} }
double targetFormContext(BuildContext context) { double targetFormContext(BuildContext context) {
var width = MediaQuery var width = MediaQuery.of(context).size.width;
.of(context)
.size
.width;
double target = 0; double target = 0;
if (width > changePoint) { if (width > changePoint) {
target = 2; target = 2;
@@ -186,14 +203,13 @@ class _NaviPaneState extends State<NaviPane>
return Navigator( return Navigator(
observers: [widget.observer], observers: [widget.observer],
key: widget.navigatorKey, key: widget.navigatorKey,
onGenerateRoute: (settings) => onGenerateRoute: (settings) => AppPageRoute(
AppPageRoute( preventRebuild: false,
preventRebuild: false, isRootRoute: true,
isRootRoute: true, builder: (context) {
builder: (context) { return _NaviMainView(state: this);
return _NaviMainView(state: this); },
}, ),
),
); );
} }
@@ -230,20 +246,14 @@ class _NaviPaneState extends State<NaviPane>
Widget buildBottom() { Widget buildBottom() {
return Material( return Material(
textStyle: Theme textStyle: Theme.of(context).textTheme.labelSmall,
.of(context)
.textTheme
.labelSmall,
elevation: 0, elevation: 0,
child: Container( child: Container(
height: _kBottomBarHeight, height: _kBottomBarHeight,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
color: Theme color: Theme.of(context).colorScheme.outlineVariant,
.of(context)
.colorScheme
.outlineVariant,
width: 1, width: 1,
), ),
), ),
@@ -251,7 +261,7 @@ class _NaviPaneState extends State<NaviPane>
child: Row( child: Row(
children: List<Widget>.generate( children: List<Widget>.generate(
widget.paneItems.length, widget.paneItems.length,
(index) { (index) {
return Expanded( return Expanded(
child: _SingleBottomNaviWidget( child: _SingleBottomNaviWidget(
enabled: currentPage == index, enabled: currentPage == index,
@@ -271,7 +281,7 @@ class _NaviPaneState extends State<NaviPane>
Widget buildLeft() { Widget buildLeft() {
final value = controller.value; final value = controller.value;
const paddingHorizontal = 16.0; const paddingHorizontal = 12.0;
return Material( return Material(
child: Container( child: Container(
width: _kFoldedSideBarWidth + width: _kFoldedSideBarWidth +
@@ -281,57 +291,39 @@ class _NaviPaneState extends State<NaviPane>
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
right: BorderSide( right: BorderSide(
color: Theme color: Theme.of(context).colorScheme.outlineVariant,
.of(context) width: 1.0,
.colorScheme
.outlineVariant,
width: 1,
), ),
), ),
), ),
child: Row( child: Column(
children: [ children: [
SizedBox( const SizedBox(height: 16),
width: value == 3 SizedBox(height: MediaQuery.of(context).padding.top),
? (_kSideBarWidth - paddingHorizontal * 2 - 1) ...List<Widget>.generate(
: (_kFoldedSideBarWidth - paddingHorizontal * 2 - 1), widget.paneItems.length,
child: Column( (index) => _SideNaviWidget(
children: [ enabled: currentPage == index,
const SizedBox(height: 16), entry: widget.paneItems[index],
SizedBox(height: MediaQuery showTitle: value == 3,
.of(context) onTap: () {
.padding updatePage(index);
.top), },
...List<Widget>.generate( key: ValueKey(index),
widget.paneItems.length,
(index) =>
_SideNaviWidget(
enabled: currentPage == index,
entry: widget.paneItems[index],
showTitle: value == 3,
onTap: () {
updatePage(index);
},
key: ValueKey(index),
),
),
const Spacer(),
...List<Widget>.generate(
widget.paneActions.length,
(index) =>
_PaneActionWidget(
entry: widget.paneActions[index],
showTitle: value == 3,
key: ValueKey(index + widget.paneItems.length),
),
),
const SizedBox(
height: 16,
)
],
), ),
), ),
const Spacer(), const Spacer(),
...List<Widget>.generate(
widget.paneActions.length,
(index) => _PaneActionWidget(
entry: widget.paneActions[index],
showTitle: value == 3,
key: ValueKey(index + widget.paneItems.length),
),
),
const SizedBox(
height: 16,
)
], ],
), ),
), ),
@@ -339,12 +331,13 @@ class _NaviPaneState extends State<NaviPane>
} }
} }
class _SideNaviWidget extends StatefulWidget { class _SideNaviWidget extends StatelessWidget {
const _SideNaviWidget({required this.enabled, const _SideNaviWidget(
required this.entry, {required this.enabled,
required this.onTap, required this.entry,
required this.showTitle, required this.onTap,
super.key}); required this.showTitle,
super.key});
final bool enabled; final bool enabled;
@@ -354,60 +347,37 @@ class _SideNaviWidget extends StatefulWidget {
final bool showTitle; final bool showTitle;
@override
State<_SideNaviWidget> createState() => _SideNaviWidgetState();
}
class _SideNaviWidgetState extends State<_SideNaviWidget> {
bool isHovering = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme final colorScheme = Theme.of(context).colorScheme;
.of(context) final icon = Icon(enabled ? entry.activeIcon : entry.icon);
.colorScheme; return InkWell(
final icon = borderRadius: BorderRadius.circular(12),
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon); onTap: onTap,
return MouseRegion( child: AnimatedContainer(
cursor: SystemMouseCursors.click, duration: const Duration(milliseconds: 180),
onEnter: (details) => setState(() => isHovering = true), padding: const EdgeInsets.symmetric(horizontal: 12),
onExit: (details) => setState(() => isHovering = false), height: 38,
child: GestureDetector( decoration: BoxDecoration(
behavior: HitTestBehavior.translucent, color: enabled ? colorScheme.primaryContainer : null,
onTap: widget.onTap, borderRadius: BorderRadius.circular(12),
child: AnimatedContainer( ),
duration: const Duration(milliseconds: 180), child: showTitle ? Row(
margin: const EdgeInsets.symmetric(vertical: 4), children: [
padding: const EdgeInsets.symmetric(horizontal: 12), icon,
width: double.infinity, const SizedBox(width: 12),
height: 42, Text(entry.label)
decoration: BoxDecoration( ],
color: widget.enabled ) : Align(
? colorScheme.primaryContainer alignment: Alignment.centerLeft,
: isHovering child: icon,
? colorScheme.surfaceContainerHigh ),
: null,
borderRadius: BorderRadius.circular(8),
),
child: widget.showTitle
? Row(
children: [
icon,
const SizedBox(
width: 12,
),
Text(widget.entry.label)
],
)
: Center(
child: icon,
)),
), ),
); ).paddingVertical(4);
} }
} }
class _PaneActionWidget extends StatefulWidget { class _PaneActionWidget extends StatelessWidget {
const _PaneActionWidget( const _PaneActionWidget(
{required this.entry, required this.showTitle, super.key}); {required this.entry, required this.showTitle, super.key});
@@ -415,58 +385,37 @@ class _PaneActionWidget extends StatefulWidget {
final bool showTitle; final bool showTitle;
@override
State<_PaneActionWidget> createState() => _PaneActionWidgetState();
}
class _PaneActionWidgetState extends State<_PaneActionWidget> {
bool isHovering = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme final icon = Icon(entry.icon);
.of(context) return InkWell(
.colorScheme; onTap: entry.onTap,
final icon = Icon(widget.entry.icon); borderRadius: BorderRadius.circular(12),
return MouseRegion( child: AnimatedContainer(
cursor: SystemMouseCursors.click, duration: const Duration(milliseconds: 180),
onEnter: (details) => setState(() => isHovering = true), padding: const EdgeInsets.symmetric(horizontal: 12),
onExit: (details) => setState(() => isHovering = false), height: 38,
child: GestureDetector( child: showTitle ? Row(
behavior: HitTestBehavior.translucent, children: [
onTap: widget.entry.onTap, icon,
child: AnimatedContainer( const SizedBox(width: 12),
duration: const Duration(milliseconds: 180), Text(entry.label)
margin: const EdgeInsets.symmetric(vertical: 4), ],
padding: const EdgeInsets.symmetric(horizontal: 12), ) : Align(
width: double.infinity, alignment: Alignment.centerLeft,
height: 42, child: icon,
decoration: BoxDecoration( ),
color: isHovering ? colorScheme.surfaceContainerHigh : null,
borderRadius: BorderRadius.circular(8)),
child: widget.showTitle
? Row(
children: [
icon,
const SizedBox(
width: 12,
),
Text(widget.entry.label)
],
)
: Center(
child: icon,
)),
), ),
); ).paddingVertical(4);
} }
} }
class _SingleBottomNaviWidget extends StatefulWidget { class _SingleBottomNaviWidget extends StatefulWidget {
const _SingleBottomNaviWidget({required this.enabled, const _SingleBottomNaviWidget(
required this.entry, {required this.enabled,
required this.onTap, required this.entry,
super.key}); required this.onTap,
super.key});
final bool enabled; final bool enabled;
@@ -534,11 +483,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
Widget buildContent() { Widget buildContent() {
final value = controller.value; final value = controller.value;
final colorScheme = Theme final colorScheme = Theme.of(context).colorScheme;
.of(context)
.colorScheme;
final icon = final icon =
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon); Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
return Center( return Center(
child: Container( child: Container(
width: 64, width: 64,
@@ -639,12 +586,12 @@ class _NaviPopScope extends StatelessWidget {
Widget res = App.isIOS Widget res = App.isIOS
? child ? child
: PopScope( : PopScope(
canPop: App.isAndroid ? false : true, canPop: App.isAndroid ? false : true,
onPopInvokedWithResult: (value, result) { onPopInvokedWithResult: (value, result) {
action(); action();
}, },
child: child, child: child,
); );
if (popGesture) { if (popGesture) {
res = GestureDetector( res = GestureDetector(
onPanStart: (details) { onPanStart: (details) {
@@ -670,14 +617,14 @@ class _NaviPopScope extends StatelessWidget {
class _NaviMainView extends StatefulWidget { class _NaviMainView extends StatefulWidget {
const _NaviMainView({required this.state}); const _NaviMainView({required this.state});
final _NaviPaneState state; final NaviPaneState state;
@override @override
State<_NaviMainView> createState() => _NaviMainViewState(); State<_NaviMainView> createState() => _NaviMainViewState();
} }
class _NaviMainViewState extends State<_NaviMainView> { class _NaviMainViewState extends State<_NaviMainView> {
_NaviPaneState get state => widget.state; NaviPaneState get state => widget.state;
@override @override
void initState() { void initState() {
@@ -703,8 +650,8 @@ class _NaviMainViewState extends State<_NaviMainView> {
), ),
), ),
), ),
if (shouldShowAppBar) state.buildBottom().paddingBottom( if (shouldShowAppBar)
context.padding.bottom), state.buildBottom().paddingBottom(context.padding.bottom),
], ],
); );
} }

View File

@@ -22,8 +22,15 @@ class PopUpWidget<T> extends PopupRoute<T> {
Widget body = PopupIndicatorWidget( Widget body = PopupIndicatorWidget(
child: Container( child: Container(
decoration: showPopUp decoration: showPopUp
? const BoxDecoration( ? BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
boxShadow: context.brightness == ui.Brightness.dark ? [
BoxShadow(
color: Colors.white.withAlpha(50),
blurRadius: 10,
offset: Offset(0, 2),
),
] : null,
) )
: null, : null,
clipBehavior: showPopUp ? Clip.antiAlias : Clip.none, clipBehavior: showPopUp ? Clip.antiAlias : Clip.none,
@@ -86,7 +93,8 @@ class PopupIndicatorWidget extends InheritedWidget {
} }
Future<T> showPopUpWidget<T>(BuildContext context, Widget widget) async { Future<T> showPopUpWidget<T>(BuildContext context, Widget widget) async {
return await Navigator.of(context, rootNavigator: true).push(PopUpWidget(widget)); return await Navigator.of(context, rootNavigator: true)
.push(PopUpWidget(widget));
} }
class PopUpWidgetScaffold extends StatefulWidget { class PopUpWidgetScaffold extends StatefulWidget {
@@ -127,9 +135,8 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
message: "Back".tl, message: "Back".tl,
child: IconButton( child: IconButton(
icon: const Icon(Icons.arrow_back_sharp), icon: const Icon(Icons.arrow_back_sharp),
onPressed: () => context.canPop() onPressed: () =>
? context.pop() context.canPop() ? context.pop() : App.pop(),
: App.pop(),
), ),
), ),
const SizedBox( const SizedBox(

View File

@@ -78,6 +78,9 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
}, },
onPointerSignal: (pointerSignal) { onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) { if (pointerSignal is PointerScrollEvent) {
if (HardwareKeyboard.instance.isShiftPressed) {
return;
}
if (pointerSignal.kind == PointerDeviceKind.mouse && if (pointerSignal.kind == PointerDeviceKind.mouse &&
!_isMouseScroll) { !_isMouseScroll) {
setState(() { setState(() {

View File

@@ -267,13 +267,14 @@ class OptionChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return AnimatedContainer(
duration: _fastAnimationDuration,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? context.colorScheme.primaryContainer ? context.colorScheme.secondaryContainer
: context.colorScheme.surface, : context.colorScheme.surface,
border: isSelected border: isSelected
? Border.all(color: context.colorScheme.primaryContainer) ? Border.all(color: context.colorScheme.secondaryContainer)
: Border.all(color: context.colorScheme.outline), : Border.all(color: context.colorScheme.outline),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),

View File

@@ -57,10 +57,18 @@ class SideBarRoute<T> extends PopupRoute<T> {
body = Container( body = Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: showSideBar borderRadius: showSideBar
? const BorderRadius.horizontal(left: Radius.circular(16)) ? const BorderRadius.horizontal(left: Radius.circular(16))
: null, : null,
color: Theme.of(context).colorScheme.surfaceTint), color: Theme.of(context).colorScheme.surfaceTint,
boxShadow: context.brightness == ui.Brightness.dark ? [
BoxShadow(
color: Colors.white.withAlpha(50),
blurRadius: 10,
offset: Offset(0, 2),
),
] : null,
),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
constraints: BoxConstraints(maxWidth: sideBarWidth), constraints: BoxConstraints(maxWidth: sideBarWidth),
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,

View File

@@ -563,7 +563,7 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
if (!_isMaximized && !_isFullScreen) if (!_isMaximized && !_isFullScreen)
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.toOpacity(0.1),
offset: Offset(0.0, _isFocused ? 4 : 2), offset: Offset(0.0, _isFocused ? 4 : 2),
blurRadius: 6, blurRadius: 6,
) )

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.0.7"; final version = "1.1.2";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;
@@ -63,22 +63,9 @@ class _App {
} }
} }
var mainColor = Colors.blue;
Future<void> init() async { Future<void> init() async {
cachePath = (await getApplicationCacheDirectory()).path; cachePath = (await getApplicationCacheDirectory()).path;
dataPath = (await getApplicationSupportDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path;
mainColor = switch (appdata.settings['color']) {
'red' => Colors.red,
'pink' => Colors.pink,
'purple' => Colors.purple,
'green' => Colors.green,
'orange' => Colors.orange,
'blue' => Colors.blue,
'yellow' => Colors.yellow,
'cyan' => Colors.cyan,
_ => Colors.blue,
};
} }
Function? _forceRebuildHandler; Function? _forceRebuildHandler;

View File

@@ -92,7 +92,7 @@ class _Settings with ChangeNotifier {
final _data = <String, dynamic>{ final _data = <String, dynamic>{
'comicDisplayMode': 'detailed', // detailed, brief 'comicDisplayMode': 'detailed', // detailed, brief
'comicTileScale': 1.00, // 0.75-1.25 'comicTileScale': 1.00, // 0.75-1.25
'color': 'blue', // red, pink, purple, green, orange, blue 'color': 'system', // red, pink, purple, green, orange, blue
'theme_mode': 'system', // light, dark, system 'theme_mode': 'system', // light, dark, system
'newFavoriteAddTo': 'end', // start, end 'newFavoriteAddTo': 'end', // start, end
'moveFavoriteAfterRead': 'none', // none, end, start 'moveFavoriteAfterRead': 'none', // none, end, start
@@ -106,6 +106,7 @@ class _Settings with ChangeNotifier {
'defaultSearchTarget': null, 'defaultSearchTarget': null,
'autoPageTurningInterval': 5, // in seconds 'autoPageTurningInterval': 5, // in seconds
'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
'readerScreenPicNumber': 1, // 1 - 5
'enableTapToTurnPages': true, 'enableTapToTurnPages': true,
'enablePageAnimation': true, 'enablePageAnimation': true,
'language': 'system', // system, zh-CN, zh-TW, en-US 'language': 'system', // system, zh-CN, zh-TW, en-US
@@ -121,6 +122,7 @@ class _Settings with ChangeNotifier {
'enableClockAndBatteryInfoInReader': true, 'enableClockAndBatteryInfoInReader': true,
'ignoreCertificateErrors': false, 'ignoreCertificateErrors': false,
'authorizationRequired': false, 'authorizationRequired': false,
'onClickFavorite': 'viewDetail', // viewDetail, read
}; };
operator [](String key) { operator [](String key) {

View File

@@ -1,4 +1,4 @@
part of comic_source; part of 'comic_source.dart';
class CategoryData { class CategoryData {
/// The title is displayed in the tab bar. /// The title is displayed in the tab bar.

View File

@@ -1,4 +1,4 @@
library comic_source; library;
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';

View File

@@ -145,7 +145,7 @@ class ComicDetails with HistoryMixin {
final int? likesCount; final int? likesCount;
final int? commentsCount; final int? commentCount;
final String? uploader; final String? uploader;
@@ -172,7 +172,7 @@ class ComicDetails with HistoryMixin {
ComicDetails.fromJson(Map<String, dynamic> json) ComicDetails.fromJson(Map<String, dynamic> json)
: title = json["title"], : title = json["title"],
subTitle = json["subTitle"], subTitle = json["subtitle"],
cover = json["cover"], cover = json["cover"],
description = json["description"], description = json["description"],
tags = _generateMap(json["tags"]), tags = _generateMap(json["tags"]),
@@ -189,7 +189,7 @@ class ComicDetails with HistoryMixin {
subId = json["subId"], subId = json["subId"],
likesCount = json["likesCount"], likesCount = json["likesCount"],
isLiked = json["isLiked"], isLiked = json["isLiked"],
commentsCount = json["commentsCount"], commentCount = json["commentCount"],
uploader = json["uploader"], uploader = json["uploader"],
uploadTime = json["uploadTime"], uploadTime = json["uploadTime"],
updateTime = json["updateTime"], updateTime = json["updateTime"],
@@ -216,7 +216,7 @@ class ComicDetails with HistoryMixin {
"subId": subId, "subId": subId,
"isLiked": isLiked, "isLiked": isLiked,
"likesCount": likesCount, "likesCount": likesCount,
"commentsCount": commentsCount, "commentsCount": commentCount,
"uploader": uploader, "uploader": uploader,
"uploadTime": uploadTime, "uploadTime": uploadTime,
"updateTime": updateTime, "updateTime": updateTime,

View File

@@ -90,11 +90,10 @@ class ComicSourceParser {
var className = line1.split("class")[1].split("extends ComicSource").first; var className = line1.split("class")[1].split("extends ComicSource").first;
className = className.trim(); className = className.trim();
JsEngine().runCode(""" JsEngine().runCode("""
(() => { (() => { $js
$js
this['temp'] = new $className() this['temp'] = new $className()
}).call() }).call()
"""); """, className);
_name = JsEngine().runCode("this['temp'].name") ?? _name = JsEngine().runCode("this['temp'].name") ??
(throw ComicSourceParseException('name is required')); (throw ComicSourceParseException('name is required'));
var key = JsEngine().runCode("this['temp'].key") ?? var key = JsEngine().runCode("this['temp'].key") ??

View File

@@ -28,4 +28,12 @@ class ComicType {
} }
static const local = ComicType(0); static const local = ComicType(0);
factory ComicType.fromKey(String key) {
if(key == "local") {
return local;
} else {
return ComicType(key.hashCode);
}
}
} }

View File

@@ -6,6 +6,7 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/image_provider/local_favorite_image.dart'; import 'package:venera/foundation/image_provider/local_favorite_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/utils/tags_translation.dart';
import 'dart:io'; import 'dart:io';
import 'app.dart'; import 'app.dart';
@@ -177,6 +178,28 @@ class LocalFavoritesManager with ChangeNotifier {
source_folder text source_folder text
); );
"""); """);
for (var folder in _getFolderNamesWithDB()) {
var columns = _db.select("""
pragma table_info("$folder");
""");
if (!columns.any((element) => element["name"] == "translated_tags")) {
_db.execute("""
alter table "$folder"
add column translated_tags TEXT;
""");
var comics = getAllComics(folder);
for (var comic in comics) {
var translatedTags = _translateTags(comic.tags);
_db.execute("""
update "$folder"
set translated_tags = ?
where id == ? and type == ?;
""", [translatedTags, comic.id, comic.type.value]);
}
} else {
break;
}
}
} }
List<String> find(String id, ComicType type) { List<String> find(String id, ComicType type) {
@@ -338,6 +361,7 @@ class LocalFavoritesManager with ChangeNotifier {
cover_path TEXT, cover_path TEXT,
time TEXT, time TEXT,
display_order int, display_order int,
translated_tags TEXT,
primary key (id, type) primary key (id, type)
); );
"""); """);
@@ -391,6 +415,17 @@ class LocalFavoritesManager with ChangeNotifier {
return FavoriteItem.fromRow(res.first); return FavoriteItem.fromRow(res.first);
} }
String _translateTags(List<String> tags) {
var res = <String>[];
for (var tag in tags) {
var translated = tag.translateTagsToCN;
if (translated != tag) {
res.add(translated);
}
}
return res.join(",");
}
/// add comic to a folder. /// add comic to a folder.
/// return true if success, false if already exists /// return true if success, false if already exists
bool addComic(String folder, FavoriteItem comic, [int? order]) { bool addComic(String folder, FavoriteItem comic, [int? order]) {
@@ -405,6 +440,7 @@ class LocalFavoritesManager with ChangeNotifier {
if (res.isNotEmpty) { if (res.isNotEmpty) {
return false; return false;
} }
var translatedTags = _translateTags(comic.tags);
final params = [ final params = [
comic.id, comic.id,
comic.name, comic.name,
@@ -412,22 +448,23 @@ class LocalFavoritesManager with ChangeNotifier {
comic.type.value, comic.type.value,
comic.tags.join(","), comic.tags.join(","),
comic.coverPath, comic.coverPath,
comic.time comic.time,
translatedTags
]; ];
if (order != null) { if (order != null) {
_db.execute(""" _db.execute("""
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
values (?, ?, ?, ?, ?, ?, ?, ?); values (?, ?, ?, ?, ?, ?, ?, ?, ?);
""", [...params, order]); """, [...params, order]);
} else if (appdata.settings['newFavoriteAddTo'] == "end") { } else if (appdata.settings['newFavoriteAddTo'] == "end") {
_db.execute(""" _db.execute("""
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
values (?, ?, ?, ?, ?, ?, ?, ?); values (?, ?, ?, ?, ?, ?, ?, ?, ?);
""", [...params, maxValue(folder) + 1]); """, [...params, maxValue(folder) + 1]);
} else { } else {
_db.execute(""" _db.execute("""
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
values (?, ?, ?, ?, ?, ?, ?, ?); values (?, ?, ?, ?, ?, ?, ?, ?, ?);
""", [...params, minValue(folder) - 1]); """, [...params, minValue(folder) - 1]);
} }
notifyListeners(); notifyListeners();
@@ -501,10 +538,11 @@ class LocalFavoritesManager with ChangeNotifier {
int count = 0; int count = 0;
await Future.microtask(() { await Future.microtask(() {
var all = allComics(); var all = allComics();
for(var c in all) { for (var c in all) {
var comicSource = c.type.comicSource; var comicSource = c.type.comicSource;
if ((c.type == ComicType.local && LocalManager().find(c.id, c.type) == null) if ((c.type == ComicType.local &&
|| (c.type != ComicType.local && comicSource == null)) { LocalManager().find(c.id, c.type) == null) ||
(c.type != ComicType.local && comicSource == null)) {
deleteComicWithId(c.folder, c.id, c.type); deleteComicWithId(c.folder, c.id, c.type);
count++; count++;
} }
@@ -593,6 +631,33 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
List<FavoriteItem> searchInFolder(String folder, String keyword) {
var keywordList = keyword.split(" ");
keyword = keywordList.first;
keyword = "%$keyword%";
var res = _db.select("""
SELECT * FROM "$folder"
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
""", [keyword, keyword, keyword, keyword]);
var comics = res.map((e) => FavoriteItem.fromRow(e)).toList();
bool test(FavoriteItem comic, String keyword) {
if (comic.name.contains(keyword)) {
return true;
} else if (comic.author.contains(keyword)) {
return true;
} else if (comic.tags.any((element) => element.contains(keyword))) {
return true;
}
return false;
}
for (var i = 1; i < keywordList.length; i++) {
comics =
comics.where((element) => test(element, keywordList[i])).toList();
}
return comics;
}
List<FavoriteItemWithFolderInfo> search(String keyword) { List<FavoriteItemWithFolderInfo> search(String keyword) {
var keywordList = keyword.split(" "); var keywordList = keyword.split(" ");
keyword = keywordList.first; keyword = keywordList.first;
@@ -601,8 +666,8 @@ class LocalFavoritesManager with ChangeNotifier {
keyword = "%$keyword%"; keyword = "%$keyword%";
var res = _db.select(""" var res = _db.select("""
SELECT * FROM "$table" SELECT * FROM "$table"
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ?; WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
""", [keyword, keyword, keyword]); """, [keyword, keyword, keyword, keyword]);
for (var comic in res) { for (var comic in res) {
comics.add( comics.add(
FavoriteItemWithFolderInfo(FavoriteItem.fromRow(comic), table)); FavoriteItemWithFolderInfo(FavoriteItem.fromRow(comic), table));

View File

@@ -2,7 +2,9 @@ import 'dart:async';
import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/utils/translations.dart';
import 'app.dart'; import 'app.dart';
@@ -22,21 +24,25 @@ abstract mixin class HistoryMixin {
HistoryType get historyType; HistoryType get historyType;
} }
class History { class History implements Comic {
HistoryType type; HistoryType type;
DateTime time; DateTime time;
@override
String title; String title;
@override
String subtitle; String subtitle;
@override
String cover; String cover;
int ep; int ep;
int page; int page;
@override
String id; String id;
/// readEpisode is a set of episode numbers that have been read. /// readEpisode is a set of episode numbers that have been read.
@@ -44,6 +50,7 @@ class History {
/// The number of episodes is 1-based. /// The number of episodes is 1-based.
Set<int> readEpisode; Set<int> readEpisode;
@override
int? maxPage; int? maxPage;
History.fromModel( History.fromModel(
@@ -137,6 +144,47 @@ class History {
@override @override
int get hashCode => Object.hash(id, type); int get hashCode => Object.hash(id, type);
@override
String get description {
var res = "";
if (ep >= 1) {
res += "Chapter @ep".tlParams({
"ep": ep,
});
}
if (page >= 1) {
if (ep >= 1) {
res += " - ";
}
res += "Page @page".tlParams({
"page": page,
});
}
return res;
}
@override
String? get favoriteId => null;
@override
String? get language => null;
@override
String get sourceKey => type == ComicType.local
? 'local'
: type.comicSource?.key ?? "Unknown:${type.value}";
@override
double? get stars => null;
@override
List<String>? get tags => null;
@override
Map<String, dynamic> toJson() {
throw UnimplementedError();
}
} }
class HistoryManager with ChangeNotifier { class HistoryManager with ChangeNotifier {

View File

@@ -1,6 +1,6 @@
import 'dart:async' show Future, StreamController, scheduleMicrotask; import 'dart:async' show Future, StreamController, scheduleMicrotask;
import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui show Codec;
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -11,6 +11,37 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
extends ImageProvider<T> { extends ImageProvider<T> {
const BaseImageProvider(); const BaseImageProvider();
static double? _effectiveScreenWidth;
static const double _normalComicImageRatio = 0.72;
static const double _minComicImageWidth = 1920 * _normalComicImageRatio;
static TargetImageSize _getTargetSize(width, height) {
if (_effectiveScreenWidth == null) {
final screens = PlatformDispatcher.instance.displays;
for (var screen in screens) {
if (screen.size.width > screen.size.height) {
_effectiveScreenWidth = max(
_effectiveScreenWidth ?? 0,
screen.size.height * _normalComicImageRatio,
);
} else {
_effectiveScreenWidth =
max(_effectiveScreenWidth ?? 0, screen.size.width);
}
}
if (_effectiveScreenWidth! < _minComicImageWidth) {
_effectiveScreenWidth = _minComicImageWidth;
}
}
if (width > _effectiveScreenWidth!) {
height = (height * _effectiveScreenWidth! / width).round();
width = _effectiveScreenWidth!.round();
}
return TargetImageSize(width: width, height: height);
}
@override @override
ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>(); final chunkEvents = StreamController<ImageChunkEvent>();
@@ -46,19 +77,12 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
while (data == null && !stop) { while (data == null && !stop) {
try { try {
if(_cache.containsKey(key.key)){ data = await load(chunkEvents);
data = _cache[key.key];
} else {
data = await load(chunkEvents);
_checkCacheSize();
_cache[key.key] = data;
_cacheSize += data.length;
}
} catch (e) { } catch (e) {
if(e.toString().contains("Invalid Status Code: 404")) { if (e.toString().contains("Invalid Status Code: 404")) {
rethrow; rethrow;
} }
if(e.toString().contains("Invalid Status Code: 403")) { if (e.toString().contains("Invalid Status Code: 403")) {
rethrow; rethrow;
} }
if (e.toString().contains("handshake")) { if (e.toString().contains("handshake")) {
@@ -74,23 +98,27 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
} }
if(stop) { if (stop) {
throw Exception("Image loading is stopped"); throw Exception("Image loading is stopped");
} }
if(data!.isEmpty) { if (data!.isEmpty) {
throw Exception("Empty image data"); throw Exception("Empty image data");
} }
try { try {
final buffer = await ImmutableBuffer.fromUint8List(data); final buffer = await ImmutableBuffer.fromUint8List(data);
return await decode(buffer); return await decode(
buffer,
getTargetSize: enableResize ? _getTargetSize : null,
);
} catch (e) { } catch (e) {
await CacheManager().delete(this.key); await CacheManager().delete(this.key);
if (data.length < 2 * 1024) { if (data.length < 2 * 1024) {
// data is too short, it's likely that the data is text, not image // data is too short, it's likely that the data is text, not image
try { try {
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data); var text =
const Utf8Codec(allowMalformed: false).decoder.convert(data);
throw Exception("Expected image data, but got text: $text"); throw Exception("Expected image data, but got text: $text");
} catch (e) { } catch (e) {
// ignore // ignore
@@ -108,30 +136,6 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
} }
static final _cache = LinkedHashMap<String, Uint8List>();
static var _cacheSize = 0;
static var _cacheSizeLimit = 50 * 1024 * 1024;
static void _checkCacheSize(){
while (_cacheSize > _cacheSizeLimit){
var firstKey = _cache.keys.first;
_cacheSize -= _cache[firstKey]!.length;
_cache.remove(firstKey);
}
}
static void clearCache(){
_cache.clear();
_cacheSize = 0;
}
static void setCacheSizeLimit(int size){
_cacheSizeLimit = size;
_checkCacheSize();
}
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents); Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);
String get key; String get key;
@@ -148,6 +152,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
String toString() { String toString() {
return "$runtimeType($key)"; return "$runtimeType($key)";
} }
bool get enableResize => false;
} }
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List); typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);

View File

@@ -1,5 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future, StreamController;
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/network/images.dart'; import 'package:venera/network/images.dart';
@@ -22,22 +21,35 @@ class CachedImageProvider
final String? cid; final String? cid;
static int loadingCount = 0;
static const _kMaxLoadingCount = 8;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
if(url.startsWith("file://")) { while(loadingCount > _kMaxLoadingCount) {
var file = openFilePlatform(url.substring(7)); await Future.delayed(const Duration(milliseconds: 100));
return file.readAsBytes();
} }
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { loadingCount++;
chunkEvents.add(ImageChunkEvent( try {
cumulativeBytesLoaded: progress.currentBytes, if(url.startsWith("file://")) {
expectedTotalBytes: progress.totalBytes, var file = File(url.substring(7));
)); return file.readAsBytes();
if(progress.imageBytes != null) {
return progress.imageBytes!;
} }
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes,
));
if(progress.imageBytes != null) {
return progress.imageBytes!;
}
}
throw "Error: Empty response body.";
}
finally {
loadingCount--;
} }
throw "Error: Empty response body.";
} }
@override @override

View File

@@ -0,0 +1,57 @@
import 'dart:async' show Future, StreamController;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/network/images.dart';
import '../history.dart';
import 'base_image_provider.dart';
import 'history_image_provider.dart' as image_provider;
class HistoryImageProvider
extends BaseImageProvider<image_provider.HistoryImageProvider> {
/// Image provider for normal image.
///
/// [url] is the url of the image. Local file path is also supported.
const HistoryImageProvider(this.history);
final History history;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
var url = history.cover;
if (!url.contains('/')) {
var localComic = LocalManager().find(history.id, history.type);
if (localComic != null) {
return localComic.coverFile.readAsBytes();
}
var comicSource =
history.type.comicSource ?? (throw "Comic source not found.");
var comic = await comicSource.loadComicInfo!(history.id);
url = comic.data.cover;
history.cover = url;
HistoryManager().addHistory(history);
}
await for (var progress in ImageDownloader.loadThumbnail(
url,
history.type.sourceKey,
history.id,
)) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes,
));
if (progress.imageBytes != null) {
return progress.imageBytes!;
}
}
throw "Error: Empty response body.";
}
@override
Future<HistoryImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
String get key => "history${history.id}${history.type.value}";
}

View File

@@ -0,0 +1,66 @@
import 'dart:async' show Future, StreamController;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/utils/io.dart';
import 'base_image_provider.dart';
import 'local_comic_image.dart' as image_provider;
class LocalComicImageProvider
extends BaseImageProvider<image_provider.LocalComicImageProvider> {
/// Image provider for normal image.
///
/// [url] is the url of the image. Local file path is also supported.
const LocalComicImageProvider(this.comic);
final LocalComic comic;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
File? file = comic.coverFile;
if(! await file.exists()) {
file = null;
var dir = Directory(comic.directory);
if (! await dir.exists()) {
throw "Error: Comic not found.";
}
Directory? firstDir;
await for (var entity in dir.list()) {
if(entity is File) {
if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) {
file = entity;
break;
}
} else if(entity is Directory) {
firstDir ??= entity;
}
}
if(file == null && firstDir != null) {
await for (var entity in firstDir.list()) {
if(entity is File) {
if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) {
file = entity;
break;
}
}
}
}
}
if(file == null) {
throw "Error: Cover not found.";
}
var data = await file.readAsBytes();
if(data.isEmpty) {
throw "Exception: Empty file(${file.path}).";
}
return data;
}
@override
Future<LocalComicImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(this);
}
@override
String get key => "local${comic.id}${comic.comicType.value}";
}

View File

@@ -2,6 +2,7 @@ import 'dart:async' show Future, StreamController;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/network/images.dart'; import 'package:venera/network/images.dart';
import 'package:venera/utils/io.dart';
import 'base_image_provider.dart'; import 'base_image_provider.dart';
import 'reader_image.dart' as image_provider; import 'reader_image.dart' as image_provider;
@@ -20,6 +21,14 @@ class ReaderImageProvider
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
if (imageKey.startsWith('file://')) {
var file = File(imageKey);
if (await file.exists()) {
return file.readAsBytes();
}
throw "Error: File not found.";
}
await for (var event await for (var event
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
@@ -40,4 +49,7 @@ class ReaderImageProvider
@override @override
String get key => "$imageKey@$sourceKey@$cid@$eid"; String get key => "$imageKey@$sourceKey@$cid@$eid";
@override
bool get enableResize => true;
} }

View File

@@ -71,12 +71,12 @@ class LocalComic with HistoryMixin implements Comic {
downloadedChapters = List.from(jsonDecode(row[8] as String)), downloadedChapters = List.from(jsonDecode(row[8] as String)),
createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int); createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int);
File get coverFile => openFilePlatform(FilePath.join( File get coverFile => File(FilePath.join(
baseDir, baseDir,
cover, cover,
)); ));
String get baseDir => directory.contains("/") ? directory : FilePath.join(LocalManager().path, directory); String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory);
@override @override
String get description => ""; String get description => "";
@@ -151,6 +151,8 @@ class LocalManager with ChangeNotifier {
/// path to the directory where all the comics are stored /// path to the directory where all the comics are stored
late String path; late String path;
Directory get directory => Directory(path);
// return error message if failed // return error message if failed
Future<String?> setNewPath(String newPath) async { Future<String?> setNewPath(String newPath) async {
var newDir = Directory(newPath); var newDir = Directory(newPath);
@@ -162,7 +164,7 @@ class LocalManager with ChangeNotifier {
} }
try { try {
await copyDirectoryIsolate( await copyDirectoryIsolate(
Directory(path), directory,
newDir, newDir,
); );
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath); await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
@@ -170,7 +172,7 @@ class LocalManager with ChangeNotifier {
Log.error("IO", e, s); Log.error("IO", e, s);
return e.toString(); return e.toString();
} }
await Directory(path).deleteIgnoreError(recursive:true); await directory.deleteContents(recursive: true);
path = newPath; path = newPath;
return null; return null;
} }
@@ -217,15 +219,15 @@ class LocalManager with ChangeNotifier {
'''); ''');
if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) { if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) {
path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync(); path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync();
if (!Directory(path).existsSync()) { if (!directory.existsSync()) {
path = await findDefaultPath(); path = await findDefaultPath();
} }
} else { } else {
path = await findDefaultPath(); path = await findDefaultPath();
} }
try { try {
if (!Directory(path).existsSync()) { if (!directory.existsSync()) {
await Directory(path).create(); await directory.create();
} }
} }
catch(e, s) { catch(e, s) {
@@ -354,12 +356,12 @@ class LocalManager with ChangeNotifier {
throw "Invalid ep"; throw "Invalid ep";
} }
var comic = find(id, type) ?? (throw "Comic Not Found"); var comic = find(id, type) ?? (throw "Comic Not Found");
var directory = openDirectoryPlatform(comic.baseDir); var directory = Directory(comic.baseDir);
if (comic.chapters != null) { if (comic.chapters != null) {
var cid = ep is int var cid = ep is int
? comic.chapters!.keys.elementAt(ep - 1) ? comic.chapters!.keys.elementAt(ep - 1)
: (ep as String); : (ep as String);
directory = openDirectoryPlatform(FilePath.join(directory.path, cid)); directory = Directory(FilePath.join(directory.path, cid));
} }
var files = <File>[]; var files = <File>[];
await for (var entity in directory.list()) { await for (var entity in directory.list()) {
@@ -387,7 +389,7 @@ class LocalManager with ChangeNotifier {
return files.map((e) => "file://${e.path}").toList(); return files.map((e) => "file://${e.path}").toList();
} }
Future<bool> isDownloaded(String id, ComicType type, [int? ep]) async { bool isDownloaded(String id, ComicType type, [int? ep]) {
var comic = find(id, type); var comic = find(id, type);
if (comic == null) return false; if (comic == null) return false;
if (comic.chapters == null || ep == null) return true; if (comic.chapters == null || ep == null) return true;
@@ -406,10 +408,10 @@ class LocalManager with ChangeNotifier {
String id, ComicType type, String name) async { String id, ComicType type, String name) async {
var comic = find(id, type); var comic = find(id, type);
if (comic != null) { if (comic != null) {
return openDirectoryPlatform(FilePath.join(path, comic.directory)); return Directory(FilePath.join(path, comic.directory));
} }
var dir = findValidDirectoryName(path, name); var dir = findValidDirectoryName(path, name);
return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value); return Directory(FilePath.join(path, dir)).create().then((value) => value);
} }
void completeTask(DownloadTask task) { void completeTask(DownloadTask task) {
@@ -468,16 +470,18 @@ class LocalManager with ChangeNotifier {
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) { void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
if(removeFileOnDisk) { if(removeFileOnDisk) {
var dir = openDirectoryPlatform(FilePath.join(path, c.directory)); var dir = Directory(FilePath.join(path, c.directory));
dir.deleteIgnoreError(recursive: true); dir.deleteIgnoreError(recursive: true);
} }
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. // Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
if(HistoryManager().findSync(c.id, c.comicType) != null) { if(c.comicType == ComicType.local) {
HistoryManager().remove(c.id, c.comicType); if(HistoryManager().findSync(c.id, c.comicType) != null) {
} HistoryManager().remove(c.id, c.comicType);
var folders = LocalFavoritesManager().find(c.id, c.comicType); }
for (var f in folders) { var folders = LocalFavoritesManager().find(c.id, c.comicType);
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType); for (var f in folders) {
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
}
} }
remove(c.id, c.comicType); remove(c.id, c.comicType);
notifyListeners(); notifyListeners();

View File

@@ -1,14 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SimpleController extends StateController { class SimpleController extends StateController {
final void Function()? refresh_; final void Function()? refreshFunction;
SimpleController({this.refresh_}); final Map<String, dynamic> Function()? control;
SimpleController({this.refreshFunction, this.control});
@override @override
void refresh() { void refresh() {
(refresh_ ?? super.refresh)(); (refreshFunction ?? super.refresh)();
} }
Map<String, dynamic> get controlMap => control?.call() ?? {};
} }
abstract class StateController { abstract class StateController {
@@ -71,8 +75,8 @@ abstract class StateController {
static SimpleController putSimpleController( static SimpleController putSimpleController(
void Function() onUpdate, Object? tag, void Function() onUpdate, Object? tag,
{void Function()? refresh}) { {void Function()? refresh, Map<String, dynamic> Function()? control}) {
var controller = SimpleController(refresh_: refresh); var controller = SimpleController(refreshFunction: refresh, control: control);
controller.stateUpdaters.add(Pair(null, onUpdate)); controller.stateUpdaters.add(Pair(null, onUpdate));
_controllers.add(StateControllerWrapped(controller, false, tag)); _controllers.add(StateControllerWrapped(controller, false, tag));
return controller; return controller;
@@ -202,6 +206,7 @@ abstract class StateWithController<T extends StatefulWidget> extends State<T> {
}, },
tag, tag,
refresh: refresh, refresh: refresh,
control: () => control,
); );
super.initState(); super.initState();
} }
@@ -218,6 +223,8 @@ abstract class StateWithController<T extends StatefulWidget> extends State<T> {
} }
Object? get tag; Object? get tag;
Map<String, dynamic> get control => {};
} }
class Pair<M, V>{ class Pair<M, V>{

View File

@@ -111,4 +111,10 @@ extension StyledText on TextStyle {
TextStyle get s40 => copyWith(fontSize: 40); TextStyle get s40 => copyWith(fontSize: 40);
TextStyle withColor(Color? color) => copyWith(color: color); TextStyle withColor(Color? color) => copyWith(color: color);
}
extension ColorExt on Color {
Color toOpacity(double opacity) {
return withValues(alpha: opacity);
}
} }

View File

@@ -18,11 +18,11 @@ Future<void> init() async {
await appdata.init(); await appdata.init();
await App.init(); await App.init();
await HistoryManager().init(); await HistoryManager().init();
await TagsTranslation.readData();
await LocalFavoritesManager().init(); await LocalFavoritesManager().init();
SingleInstanceCookieJar("${App.dataPath}/cookie.db"); SingleInstanceCookieJar("${App.dataPath}/cookie.db");
await JsEngine().init(); await JsEngine().init();
await ComicSource.init(); await ComicSource.init();
await LocalManager().init(); await LocalManager().init();
await TagsTranslation.readData();
CacheManager().setLimitSize(appdata.settings['cacheSize']); CacheManager().setLimitSize(appdata.settings['cacheSize']);
} }

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -20,40 +21,42 @@ void main(List<String> args) {
if (runWebViewTitleBarWidget(args)) { if (runWebViewTitleBarWidget(args)) {
return; return;
} }
runZonedGuarded(() async { overrideIO(() {
await Rhttp.init(); runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized(); await Rhttp.init();
await init(); WidgetsFlutterBinding.ensureInitialized();
if (App.isAndroid) { await init();
handleLinks(); if (App.isAndroid) {
} handleLinks();
FlutterError.onError = (details) { }
Log.error( FlutterError.onError = (details) {
"Unhandled Exception", "${details.exception}\n${details.stack}"); Log.error(
}; "Unhandled Exception", "${details.exception}\n${details.stack}");
runApp(const MyApp()); };
if (App.isDesktop) { runApp(const MyApp());
await windowManager.ensureInitialized(); if (App.isDesktop) {
windowManager.waitUntilReadyToShow().then((_) async { await windowManager.ensureInitialized();
await windowManager.setTitleBarStyle( windowManager.waitUntilReadyToShow().then((_) async {
TitleBarStyle.hidden, await windowManager.setTitleBarStyle(
windowButtonVisibility: App.isMacOS, TitleBarStyle.hidden,
); windowButtonVisibility: App.isMacOS,
if (App.isLinux) { );
await windowManager.setBackgroundColor(Colors.transparent); if (App.isLinux) {
} await windowManager.setBackgroundColor(Colors.transparent);
await windowManager.setMinimumSize(const Size(500, 600)); }
if (!App.isLinux) { await windowManager.setMinimumSize(const Size(500, 600));
// https://github.com/leanflutter/window_manager/issues/460 if (!App.isLinux) {
var placement = await WindowPlacement.loadFromFile(); // https://github.com/leanflutter/window_manager/issues/460
await placement.applyToWindow(); var placement = await WindowPlacement.loadFromFile();
await windowManager.show(); await placement.applyToWindow();
WindowPlacement.loop(); await windowManager.show();
} WindowPlacement.loop();
}); }
} });
}, (error, stack) { }
Log.error("Unhandled Exception", "$error\n$stack"); }, (error, stack) {
Log.error("Unhandled Exception", "$error\n$stack");
});
}); });
} }
@@ -126,6 +129,20 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
setState(() {}); setState(() {});
} }
Color translateColorSetting() {
return switch (appdata.settings['color']) {
'red' => Colors.red,
'pink' => Colors.pink,
'purple' => Colors.purple,
'green' => Colors.green,
'orange' => Colors.orange,
'blue' => Colors.blue,
'yellow' => Colors.yellow,
'cyan' => Colors.cyan,
_ => Colors.blue,
};
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget home; Widget home;
@@ -138,90 +155,93 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
} else { } else {
home = const MainPage(); home = const MainPage();
} }
return MaterialApp( return DynamicColorBuilder(builder: (light, dark) {
home: home, if (appdata.settings['color'] != 'system' || light == null || dark == null) {
debugShowCheckedModeBanner: false, var color = translateColorSetting();
theme: ThemeData( light = ColorScheme.fromSeed(
colorScheme: ColorScheme.fromSeed( seedColor: color,
seedColor: App.mainColor, );
surface: Colors.white, dark = ColorScheme.fromSeed(
primary: App.mainColor.shade600, seedColor: color,
// ignore: deprecated_member_use
background: Colors.white,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
),
navigatorKey: App.rootNavigatorKey,
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: App.mainColor,
brightness: Brightness.dark, brightness: Brightness.dark,
surface: Colors.black, );
primary: App.mainColor.shade400, }
// ignore: deprecated_member_use return MaterialApp(
background: Colors.black, home: home,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: light.copyWith(
surface: Colors.white,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
), ),
fontFamily: App.isWindows ? "Microsoft YaHei" : null, navigatorKey: App.rootNavigatorKey,
), darkTheme: ThemeData(
themeMode: switch (appdata.settings['theme_mode']) { colorScheme: dark.copyWith(
'light' => ThemeMode.light, surface: Colors.black,
'dark' => ThemeMode.dark, ),
_ => ThemeMode.system fontFamily: App.isWindows ? "Microsoft YaHei" : null,
}, ),
localizationsDelegates: const [ themeMode: switch (appdata.settings['theme_mode']) {
GlobalMaterialLocalizations.delegate, 'light' => ThemeMode.light,
GlobalWidgetsLocalizations.delegate, 'dark' => ThemeMode.dark,
GlobalCupertinoLocalizations.delegate, _ => ThemeMode.system
], },
locale: () { localizationsDelegates: const [
var lang = appdata.settings['language']; GlobalMaterialLocalizations.delegate,
if (lang == 'system') { GlobalWidgetsLocalizations.delegate,
return null; GlobalCupertinoLocalizations.delegate,
} ],
return switch (lang) { locale: () {
'zh-CN' => const Locale('zh', 'CN'), var lang = appdata.settings['language'];
'zh-TW' => const Locale('zh', 'TW'), if (lang == 'system') {
'en-US' => const Locale('en'), return null;
_ => null }
}; return switch (lang) {
}(), 'zh-CN' => const Locale('zh', 'CN'),
supportedLocales: const [ 'zh-TW' => const Locale('zh', 'TW'),
Locale('en'), 'en-US' => const Locale('en'),
Locale('zh', 'CN'), _ => null
Locale('zh', 'TW'), };
], }(),
builder: (context, widget) { supportedLocales: const [
ErrorWidget.builder = (details) { Locale('en'),
Log.error( Locale('zh', 'CN'),
"Unhandled Exception", "${details.exception}\n${details.stack}"); Locale('zh', 'TW'),
return Material( ],
child: Center( builder: (context, widget) {
child: Text(details.exception.toString()), ErrorWidget.builder = (details) {
), Log.error(
); "Unhandled Exception", "${details.exception}\n${details.stack}");
}; return Material(
if (widget != null) { child: Center(
widget = OverlayWidget(widget); child: Text(details.exception.toString()),
if (App.isDesktop) {
widget = Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent(
App.pop,
),
},
child: MouseBackDetector(
onTapDown: App.pop,
child: WindowFrame(widget),
), ),
); );
};
if (widget != null) {
widget = OverlayWidget(widget);
if (App.isDesktop) {
widget = Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent(
App.pop,
),
},
child: MouseBackDetector(
onTapDown: App.pop,
child: WindowFrame(widget),
),
);
}
return _SystemUiProvider(Material(
child: widget,
));
} }
return _SystemUiProvider(Material( throw ('widget is null');
child: widget, },
)); );
} });
throw ('widget is null');
},
);
} }
} }

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -281,13 +280,8 @@ class RHttpAdapter implements HttpClientAdapter {
headers[key] ??= []; headers[key] ??= [];
headers[key]!.add(entry.$2); headers[key]!.add(entry.$2);
} }
var data = res.body;
if (headers['content-encoding']?.contains('gzip') ?? false) {
// rhttp does not support gzip decoding
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
}
return ResponseBody( return ResponseBody(
data, res.body,
res.statusCode, res.statusCode,
statusMessage: null, statusMessage: null,
isRedirect: false, isRedirect: false,

View File

@@ -146,14 +146,19 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
String? _cover; String? _cover;
/// All images to download, key is chapter name
Map<String, List<String>>? _images; Map<String, List<String>>? _images;
/// Downloaded image count
int _downloadedCount = 0; int _downloadedCount = 0;
/// Total image count
int _totalCount = 0; int _totalCount = 0;
/// Current downloading image index
int _index = 0; int _index = 0;
/// Current downloading chapter, index of [_images]
int _chapter = 0; int _chapter = 0;
var tasks = <int, _ImageDownloadWrapper>{}; var tasks = <int, _ImageDownloadWrapper>{};
@@ -180,10 +185,10 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
if (comic!.chapters != null) { if (comic!.chapters != null) {
saveTo = Directory(FilePath.join( saveTo = Directory(FilePath.join(
path!, path!,
comic!.chapters!.keys.elementAt(_chapter), _images!.keys.elementAt(_chapter),
)); ));
if (!saveTo.existsSync()) { if (!saveTo.existsSync()) {
saveTo.createSync(); saveTo.createSync(recursive: true);
} }
} else { } else {
saveTo = Directory(path!); saveTo = Directory(path!);
@@ -235,20 +240,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
} }
if (path == null) { if (path == null) {
var dir = await LocalManager().findValidDirectory( try {
comicId, var dir = await LocalManager().findValidDirectory(
comicType, comicId,
comic!.title, comicType,
); comic!.title,
if (!(await dir.exists())) { );
try { if (!(await dir.exists())) {
await dir.create(); await dir.create();
} catch (e) {
_setError("Error: $e");
return;
} }
path = dir.path;
} catch (e, s) {
Log.error("Download", e.toString(), s);
_setError("Error: $e");
return;
} }
path = dir.path;
} }
await LocalManager().saveCurrentDownloadingTasks(); await LocalManager().saveCurrentDownloadingTasks();
@@ -266,11 +272,13 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
throw "Failed to download cover"; throw "Failed to download cover";
} }
var fileType = detectFileType(data); var fileType = detectFileType(data);
var file = File(FilePath.join(path!, "cover${fileType.ext}")); var file =
File(FilePath.join(path!, "cover${fileType.ext}"));
file.writeAsBytesSync(data); file.writeAsBytesSync(data);
return "file://${file.path}"; return "file://${file.path}";
}); });
if (res.error) { if (res.error) {
Log.error("Download", res.errorMessage!);
_setError("Error: ${res.errorMessage}"); _setError("Error: ${res.errorMessage}");
return; return;
} else { } else {
@@ -294,6 +302,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
return; return;
} }
if (res.error) { if (res.error) {
Log.error("Download", res.errorMessage!);
_setError("Error: ${res.errorMessage}"); _setError("Error: ${res.errorMessage}");
return; return;
} else { } else {
@@ -323,6 +332,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
return; return;
} }
if (res.error) { if (res.error) {
Log.error("Download", res.errorMessage!);
_setError("Error: ${res.errorMessage}"); _setError("Error: ${res.errorMessage}");
return; return;
} else { } else {
@@ -347,6 +357,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
return; return;
} }
if (task.error != null) { if (task.error != null) {
Log.error("Download", task.error.toString());
_setError("Error: ${task.error}"); _setError("Error: ${task.error}");
return; return;
} }
@@ -375,7 +386,6 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
_message = message; _message = message;
notifyListeners(); notifyListeners();
stopRecorder(); stopRecorder();
Log.error("Download", message);
} }
@override @override
@@ -448,7 +458,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
}).toList(), }).toList(),
directory: Directory(path!).name, directory: Directory(path!).name,
chapters: comic!.chapters, chapters: comic!.chapters,
cover: File(_cover!.split("file://").last).uri.pathSegments.last, cover:
File(_cover!.split("file://").last).name,
comicType: ComicType(source.key.hashCode), comicType: ComicType(source.key.hashCode),
downloadedChapters: chapters ?? [], downloadedChapters: chapters ?? [],
createdAt: DateTime.now(), createdAt: DateTime.now(),
@@ -721,13 +732,12 @@ class ArchiveDownloadTask extends DownloadTask {
_currentBytes = status.downloadedBytes; _currentBytes = status.downloadedBytes;
_expectedBytes = status.totalBytes; _expectedBytes = status.totalBytes;
_message = _message =
"${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}"; "${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}";
_speed = status.bytesPerSecond; _speed = status.bytesPerSecond;
isDownloaded = status.isFinished; isDownloaded = status.isFinished;
notifyListeners(); notifyListeners();
} }
} } catch (e) {
catch(e) {
_setError("Error: $e"); _setError("Error: $e");
return; return;
} }

View File

@@ -0,0 +1,230 @@
import "package:flutter/material.dart";
import "package:shimmer/shimmer.dart";
import "package:venera/components/components.dart";
import "package:venera/foundation/app.dart";
import "package:venera/foundation/comic_source/comic_source.dart";
import "package:venera/foundation/image_provider/cached_image.dart";
import "package:venera/pages/search_result_page.dart";
import "package:venera/utils/translations.dart";
import "comic_page.dart";
class AggregatedSearchPage extends StatefulWidget {
const AggregatedSearchPage({super.key, required this.keyword});
final String keyword;
@override
State<AggregatedSearchPage> createState() => _AggregatedSearchPageState();
}
class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
late final List<ComicSource> sources;
late final SearchBarController controller;
var _keyword = "";
@override
void initState() {
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
_keyword = widget.keyword;
controller = SearchBarController(
currentText: widget.keyword,
onSearch: (text) {
setState(() {
_keyword = text;
});
},
);
super.initState();
}
@override
Widget build(BuildContext context) {
return SmoothCustomScrollView(slivers: [
SliverSearchBar(controller: controller),
SliverList(
key: ValueKey(_keyword),
delegate: SliverChildBuilderDelegate(
(context, index) {
final source = sources[index];
return _SliverSearchResult(source: source, keyword: _keyword);
},
childCount: sources.length,
),
),
]);
}
}
class _SliverSearchResult extends StatefulWidget {
const _SliverSearchResult({required this.source, required this.keyword});
final ComicSource source;
final String keyword;
@override
State<_SliverSearchResult> createState() => _SliverSearchResultState();
}
class _SliverSearchResultState extends State<_SliverSearchResult>
with AutomaticKeepAliveClientMixin {
bool isLoading = true;
static const _kComicHeight = 144.0;
get _comicWidth => _kComicHeight * 0.72;
static const _kLeftPadding = 16.0;
List<Comic>? comics;
void load() async {
final data = widget.source.searchPageData!;
var options =
(data.searchOptions ?? []).map((e) => e.defaultValue).toList();
if (data.loadPage != null) {
var res = await data.loadPage!(widget.keyword, 1, options);
if (!res.error) {
setState(() {
comics = res.data;
isLoading = false;
});
}
} else if (data.loadNext != null) {
var res = await data.loadNext!(widget.keyword, null, options);
if (!res.error) {
setState(() {
comics = res.data;
isLoading = false;
});
}
}
}
@override
void initState() {
super.initState();
load();
}
Widget buildPlaceHolder() {
return Container(
height: _kComicHeight,
width: _comicWidth,
margin: const EdgeInsets.only(left: _kLeftPadding),
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8),
),
);
}
Widget buildComic(Comic c) {
return AnimatedTapRegion(
borderRadius: 8,
onTap: () {
context.to(() => ComicPage(
id: c.id,
sourceKey: c.sourceKey,
));
},
child: Container(
height: _kComicHeight,
width: _comicWidth,
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerLow,
),
child: AnimatedImage(
width: _comicWidth,
height: _kComicHeight,
fit: BoxFit.cover,
image: CachedImageProvider(c.cover),
),
),
).paddingLeft(_kLeftPadding);
}
@override
Widget build(BuildContext context) {
super.build(context);
return InkWell(
onTap: () {
context.to(
() => SearchResultPage(
text: widget.keyword,
sourceKey: widget.source.key,
),
);
},
child: Column(
children: [
ListTile(
mouseCursor: SystemMouseCursors.click,
title: Text(widget.source.name),
),
if (isLoading)
SizedBox(
height: _kComicHeight,
width: double.infinity,
child: Shimmer.fromColors(
baseColor: context.colorScheme.surfaceContainerLow,
highlightColor: context.colorScheme.surfaceContainer,
direction: ShimmerDirection.ltr,
child: LayoutBuilder(builder: (context, constrains) {
var itemWidth = _comicWidth + _kLeftPadding;
var items = (constrains.maxWidth / itemWidth).ceil();
return Stack(
children: [
Positioned(
left: 0,
top: 0,
bottom: 0,
child: Row(
children: List.generate(
items,
(index) => buildPlaceHolder(),
),
),
)
],
);
}),
),
)
else if (comics == null || comics!.isEmpty)
SizedBox(
height: _kComicHeight,
child: Column(
children: [
Row(
children: [
const Icon(Icons.error_outline),
const SizedBox(width: 8),
Text("No search results found".tl),
],
),
const Spacer(),
],
).paddingHorizontal(16),
)
else
SizedBox(
height: _kComicHeight,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (var c in comics!) buildComic(c),
],
),
),
],
).paddingBottom(16),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -53,6 +53,7 @@ class CategoriesPage extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
FilledTabBar( FilledTabBar(
key: PageStorageKey(categories.toString()),
tabs: categories.map((e) { tabs: categories.map((e) {
String title = e; String title = e;
try { try {
@@ -261,7 +262,7 @@ class _CategoryPage extends StatelessWidget {
builder: (context) { builder: (context) {
return Material( return Material(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
color: context.colorScheme.primaryContainer.withOpacity(0.72), color: context.colorScheme.primaryContainer.toOpacity(0.72),
child: InkWell( child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () => onClick(tag, param), onTap: () => onClick(tag, param),

View File

@@ -172,7 +172,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
isLiked = comic.isLiked ?? false; isLiked = comic.isLiked ?? false;
isFavorite = comic.isFavorite ?? false; isFavorite = comic.isFavorite ?? false;
if (comic.chapters == null) { if (comic.chapters == null) {
isDownloaded = await LocalManager().isDownloaded( isDownloaded = LocalManager().isDownloaded(
comic.id, comic.id,
comic.comicType, comic.comicType,
0, 0,
@@ -223,7 +223,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
children: [ children: [
SelectableText(comic.title, style: ts.s18), SelectableText(comic.title, style: ts.s18),
if (comic.subTitle != null) if (comic.subTitle != null)
SelectableText(comic.subTitle!, style: ts.s14), SelectableText(comic.subTitle!, style: ts.s14)
.paddingVertical(4),
Text( Text(
(ComicSource.find(comic.sourceKey)?.name) ?? '', (ComicSource.find(comic.sourceKey)?.name) ?? '',
style: ts.s12, style: ts.s12,
@@ -288,11 +289,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
onLongPressed: quickFavorite, onLongPressed: quickFavorite,
iconColor: context.useTextColor(Colors.purple), iconColor: context.useTextColor(Colors.purple),
), ),
if (comicSource.commentsLoader != null && if (comicSource.commentsLoader != null)
(comic.comments == null || comic.comments!.isEmpty))
_ActionButton( _ActionButton(
icon: const Icon(Icons.comment), icon: const Icon(Icons.comment),
text: (comic.commentsCount ?? 'Comments'.tl).toString(), text: (comic.commentCount ?? 'Comments'.tl).toString(),
onPressed: showComments, onPressed: showComments,
iconColor: context.useTextColor(Colors.green), iconColor: context.useTextColor(Colors.green),
), ),
@@ -679,7 +679,7 @@ abstract mixin class _ComicPageActions {
return; return;
} }
if (comic.chapters == null && if (comic.chapters == null &&
await LocalManager().isDownloaded(comic.id, comic.comicType, 0)) { LocalManager().isDownloaded(comic.id, comic.comicType, 0)) {
App.rootContext.showMessage(message: "The comic is downloaded".tl); App.rootContext.showMessage(message: "The comic is downloaded".tl);
return; return;
} }
@@ -1116,14 +1116,12 @@ class _ComicChaptersState extends State<_ComicChapters> {
(state.history?.readEpisode ?? const {}).contains(i + 1); (state.history?.readEpisode ?? const {}).contains(i + 1);
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: InkWell( child: Material(
borderRadius: const BorderRadius.all(Radius.circular(16)), color: context.colorScheme.surfaceContainer,
child: Material( borderRadius: const BorderRadius.all(Radius.circular(12)),
elevation: 5, child: InkWell(
color: context.colorScheme.surface, onTap: () => state.read(i + 1),
surfaceTintColor: context.colorScheme.surfaceTint,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
shadowColor: Colors.transparent,
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
@@ -1134,19 +1132,18 @@ class _ComicChaptersState extends State<_ComicChapters> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: color: visited ? context.colorScheme.outline : null,
visited ? context.colorScheme.outline : null), ),
), ),
), ),
), ),
), ),
onTap: () => state.read(i + 1),
), ),
); );
}), }),
gridDelegate: const SliverGridDelegateWithFixedHeight( gridDelegate: const SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: 200, itemHeight: 48), maxCrossAxisExtent: 200, itemHeight: 48),
), ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
if (eps.length > 20 && !showAll) if (eps.length > 20 && !showAll)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Align( child: Align(
@@ -1329,9 +1326,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
), ),
) )
else if (isLoading) else if (isLoading)
const SliverToBoxAdapter( const SliverListLoadingIndicator(),
child: ListLoadingIndicator(),
),
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: Divider(), child: Divider(),
), ),

View File

@@ -46,6 +46,18 @@ class _ExplorePageState extends State<ExplorePage>
} }
} }
void onNaviItemTapped(int index) {
if (index == 2) {
int page = controller.index;
String currentPageId = pages[page];
StateController.find<SimpleController>(tag: currentPageId)
.control!()['toTop']
?.call();
}
}
NaviPaneState? naviPane;
@override @override
void initState() { void initState() {
pages = List<String>.from(appdata.settings["explore_pages"]); pages = List<String>.from(appdata.settings["explore_pages"]);
@@ -59,13 +71,21 @@ class _ExplorePageState extends State<ExplorePage>
vsync: this, vsync: this,
); );
appdata.settings.addListener(onSettingsChanged); appdata.settings.addListener(onSettingsChanged);
NaviPane.of(context).addNaviItemTapListener(onNaviItemTapped);
super.initState(); super.initState();
} }
@override
void didChangeDependencies() {
naviPane = NaviPane.of(context);
super.didChangeDependencies();
}
@override @override
void dispose() { void dispose() {
controller.dispose(); controller.dispose();
appdata.settings.removeListener(onSettingsChanged); appdata.settings.removeListener(onSettingsChanged);
naviPane?.removeNaviItemTapListener(onNaviItemTapped);
super.dispose(); super.dispose();
} }
@@ -90,12 +110,14 @@ class _ExplorePageState extends State<ExplorePage>
return Tab(text: i.ts(comicSource.key), key: Key(i)); return Tab(text: i.ts(comicSource.key), key: Key(i));
} }
Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i)); Widget buildBody(String i) => Material(
child: _SingleExplorePage(i, key: PageStorageKey(i)),
);
Widget buildEmpty() { Widget buildEmpty() {
var msg = "No Explore Pages".tl; var msg = "No Explore Pages".tl;
msg += '\n'; msg += '\n';
if(ComicSource.isEmpty) { if (ComicSource.isEmpty) {
msg += "Add a comic source in home page".tl; msg += "Add a comic source in home page".tl;
} else { } else {
msg += "Please check your settings".tl; msg += "Please check your settings".tl;
@@ -127,7 +149,7 @@ class _ExplorePageState extends State<ExplorePage>
Widget tabBar = Material( Widget tabBar = Material(
child: FilledTabBar( child: FilledTabBar(
key: Key(pages.toString()), key: PageStorageKey(pages.toString()),
tabs: pages.map((e) => buildTab(e)).toList(), tabs: pages.map((e) => buildTab(e)).toList(),
controller: controller, controller: controller,
), ),
@@ -220,18 +242,14 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
with AutomaticKeepAliveClientMixin<_SingleExplorePage> { with AutomaticKeepAliveClientMixin<_SingleExplorePage> {
late final ExplorePageData data; late final ExplorePageData data;
bool loading = true;
String? message;
List<ExplorePagePart>? parts;
late final String comicSourceKey; late final String comicSourceKey;
int key = 0;
bool _wantKeepAlive = true; bool _wantKeepAlive = true;
var scrollController = ScrollController();
VoidCallback? refreshHandler;
void onSettingsChanged() { void onSettingsChanged() {
var explorePages = appdata.settings["explore_pages"]; var explorePages = appdata.settings["explore_pages"];
if (!explorePages.contains(widget.title)) { if (!explorePages.contains(widget.title)) {
@@ -266,14 +284,34 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (data.loadMultiPart != null) { if (data.loadMultiPart != null) {
return buildMultiPart(); return _MultiPartExplorePage(
key: const PageStorageKey("comic_list"),
data: data,
controller: scrollController,
comicSourceKey: comicSourceKey,
refreshHandlerCallback: (c) {
refreshHandler = c;
},
);
} else if (data.loadPage != null || data.loadNext != null) { } else if (data.loadPage != null || data.loadNext != null) {
return buildComicList(); return ComicList(
loadPage: data.loadPage,
loadNext: data.loadNext,
key: const PageStorageKey("comic_list"),
controller: scrollController,
refreshHandlerCallback: (c) {
refreshHandler = c;
},
);
} else if (data.loadMixed != null) { } else if (data.loadMixed != null) {
return _MixedExplorePage( return _MixedExplorePage(
data, data,
comicSourceKey, comicSourceKey,
key: ValueKey(key), key: const PageStorageKey("comic_list"),
controller: scrollController,
refreshHandlerCallback: (c) {
refreshHandler = c;
},
); );
} else { } else {
return const Center( return const Center(
@@ -282,91 +320,59 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
} }
} }
Widget buildComicList() {
return ComicList(
loadPage: data.loadPage,
loadNext: data.loadNext,
key: ValueKey(key),
);
}
void load() async {
var res = await data.loadMultiPart!();
loading = false;
if (mounted) {
setState(() {
if (res.error) {
message = res.errorMessage;
} else {
parts = res.data;
}
});
}
}
Widget buildMultiPart() {
if (loading) {
load();
return const Center(
child: CircularProgressIndicator(),
);
} else if (message != null) {
return NetworkError(
message: message!,
retry: refresh,
withAppbar: false,
);
} else {
return buildPage();
}
}
Widget buildPage() {
return SmoothCustomScrollView(
slivers: _buildPage().toList(),
);
}
Iterable<Widget> _buildPage() sync* {
for (var part in parts!) {
yield* _buildExplorePagePart(part, comicSourceKey);
}
}
@override @override
Object? get tag => widget.title; Object? get tag => widget.title;
@override @override
void refresh() { void refresh() {
message = null; refreshHandler?.call();
if (data.loadMultiPart != null) {
setState(() {
loading = true;
});
} else {
setState(() {
key++;
});
}
} }
@override @override
bool get wantKeepAlive => _wantKeepAlive; bool get wantKeepAlive => _wantKeepAlive;
void toTop() {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.minScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
}
}
@override
Map<String, dynamic> get control => {"toTop": toTop};
} }
class _MixedExplorePage extends StatefulWidget { class _MixedExplorePage extends StatefulWidget {
const _MixedExplorePage(this.data, this.sourceKey, {super.key}); const _MixedExplorePage(this.data, this.sourceKey,
{super.key, this.controller, required this.refreshHandlerCallback});
final ExplorePageData data; final ExplorePageData data;
final String sourceKey; final String sourceKey;
final ScrollController? controller;
final void Function(VoidCallback c) refreshHandlerCallback;
@override @override
State<_MixedExplorePage> createState() => _MixedExplorePageState(); State<_MixedExplorePage> createState() => _MixedExplorePageState();
} }
class _MixedExplorePageState class _MixedExplorePageState
extends MultiPageLoadingState<_MixedExplorePage, Object> { extends MultiPageLoadingState<_MixedExplorePage, Object> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.refreshHandlerCallback(refresh);
}
void refresh() {
reset();
}
Iterable<Widget> buildSlivers(BuildContext context, List<Object> data) sync* { Iterable<Widget> buildSlivers(BuildContext context, List<Object> data) sync* {
List<Comic> cache = []; List<Comic> cache = [];
for (var part in data) { for (var part in data) {
@@ -394,9 +400,10 @@ class _MixedExplorePageState
@override @override
Widget buildContent(BuildContext context, List<Object> data) { Widget buildContent(BuildContext context, List<Object> data) {
return SmoothCustomScrollView( return SmoothCustomScrollView(
controller: widget.controller,
slivers: [ slivers: [
...buildSlivers(context, data), ...buildSlivers(context, data),
if (haveNextPage) const ListLoadingIndicator().toSliver() const SliverListLoadingIndicator(),
], ],
); );
} }
@@ -477,3 +484,125 @@ Iterable<Widget> _buildExplorePagePart(
yield buildTitle(part); yield buildTitle(part);
yield buildComics(part); yield buildComics(part);
} }
class _MultiPartExplorePage extends StatefulWidget {
const _MultiPartExplorePage({
super.key,
required this.data,
required this.controller,
required this.comicSourceKey,
required this.refreshHandlerCallback,
});
final ExplorePageData data;
final ScrollController controller;
final String comicSourceKey;
final void Function(VoidCallback c) refreshHandlerCallback;
@override
State<_MultiPartExplorePage> createState() => _MultiPartExplorePageState();
}
class _MultiPartExplorePageState extends State<_MultiPartExplorePage> {
late final ExplorePageData data;
List<ExplorePagePart>? parts;
bool loading = true;
String? message;
Map<String, dynamic> get state => {
"loading": loading,
"message": message,
"parts": parts,
};
void restoreState(dynamic state) {
if (state == null) return;
loading = state["loading"];
message = state["message"];
parts = state["parts"];
}
void storeState() {
PageStorage.of(context).writeState(context, state);
}
void refresh() {
setState(() {
loading = true;
message = null;
parts = null;
});
storeState();
}
@override
void initState() {
super.initState();
data = widget.data;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
restoreState(PageStorage.of(context).readState(context));
widget.refreshHandlerCallback(refresh);
}
void load() async {
var res = await data.loadMultiPart!();
loading = false;
if (mounted) {
setState(() {
if (res.error) {
message = res.errorMessage;
} else {
parts = res.data;
}
});
storeState();
}
}
@override
Widget build(BuildContext context) {
if (loading) {
load();
return const Center(
child: CircularProgressIndicator(),
);
} else if (message != null) {
return NetworkError(
message: message!,
retry: () {
setState(() {
loading = true;
message = null;
});
},
withAppbar: false,
);
} else {
return buildPage();
}
}
Widget buildPage() {
return SmoothCustomScrollView(
key: const PageStorageKey('scroll'),
controller: widget.controller,
slivers: _buildPage().toList(),
);
}
Iterable<Widget> _buildPage() sync* {
for (var part in parts!) {
yield* _buildExplorePagePart(part, widget.comicSourceKey);
}
}
}

View File

@@ -77,13 +77,13 @@ String? validateFolderName(String newFolderName) {
return null; return null;
} }
void addFavorite(Comic comic) { void addFavorite(List<Comic> comics) {
var folders = LocalFavoritesManager().folderNames; var folders = LocalFavoritesManager().folderNames;
showDialog( showDialog(
context: App.rootContext, context: App.rootContext,
builder: (context) { builder: (context) {
String? selectedFolder; String? selectedFolder = appdata.settings['quickFavorite'];
return StatefulBuilder(builder: (context, setState) { return StatefulBuilder(builder: (context, setState) {
return ContentDialog( return ContentDialog(
@@ -105,19 +105,21 @@ void addFavorite(Comic comic) {
FilledButton( FilledButton(
onPressed: () { onPressed: () {
if (selectedFolder != null) { if (selectedFolder != null) {
LocalFavoritesManager().addComic( for (var comic in comics) {
selectedFolder!, LocalFavoritesManager().addComic(
FavoriteItem( selectedFolder!,
id: comic.id, FavoriteItem(
name: comic.title, id: comic.id,
coverPath: comic.cover, name: comic.title,
author: comic.subtitle ?? '', coverPath: comic.cover,
type: ComicType((comic.sourceKey == 'local' author: comic.subtitle ?? '',
? 0 type: ComicType((comic.sourceKey == 'local'
: comic.sourceKey.hashCode)), ? 0
tags: comic.tags ?? [], : comic.sourceKey.hashCode)),
), tags: comic.tags ?? [],
); ),
);
}
context.pop(); context.pop();
} }
}, },

View File

@@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';
@@ -15,6 +14,7 @@ import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.dart'; import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/reader/reader.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -95,7 +95,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
barrierDismissible: true, barrierDismissible: true,
fullscreenDialog: true, fullscreenDialog: true,
opaque: false, opaque: false,
barrierColor: Colors.black.withOpacity(0.36), barrierColor: Colors.black.toOpacity(0.36),
pageBuilder: (context, animation, secondary) { pageBuilder: (context, animation, secondary) {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
@@ -153,14 +153,14 @@ class _FavoritesPageState extends State<FavoritesPage> {
); );
} }
if (!isNetwork) { if (!isNetwork) {
return _LocalFavoritesPage(folder: folder!, key: Key(folder!)); return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
} else { } else {
var favoriteData = getFavoriteDataOrNull(folder!); var favoriteData = getFavoriteDataOrNull(folder!);
if (favoriteData == null) { if (favoriteData == null) {
folder = null; folder = null;
return buildBody(); return buildBody();
} else { } else {
return NetworkFavoritePage(favoriteData, key: Key(folder!)); return NetworkFavoritePage(favoriteData, key: PageStorageKey("network_$folder"));
} }
} }
} }
@@ -170,4 +170,4 @@ abstract interface class FolderList {
void update(); void update();
void updateFolders(); void updateFolders();
} }

View File

@@ -38,7 +38,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
}); });
} else { } else {
setState(() { setState(() {
comics = LocalFavoritesManager().search(keyword); comics = LocalFavoritesManager().searchInFolder(widget.folder, keyword);
}); });
} }
} }
@@ -53,23 +53,57 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
super.initState(); super.initState();
} }
void selectAll() {
setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
});
}
void invertSelection() {
setState(() {
comics.asMap().forEach((k, v) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
});
selectedComics.removeWhere((k, v) => !v);
});
}
bool downloadComic(FavoriteItem c) {
var source = c.type.comicSource;
if (source != null) {
bool isDownloaded = LocalManager().isDownloaded(
c.id,
(c).type,
);
if (isDownloaded) {
return false;
}
LocalManager().addTask(ImagesDownloadTask(
source: source,
comicId: c.id,
comicTitle: c.title,
));
return true;
}
return false;
}
void downloadSelected() {
int count = 0;
for (var c in selectedComics.keys) {
if (downloadComic(c as FavoriteItem)) {
count++;
}
}
if (count > 0) {
context.showMessage(
message: "Added @c comics to download queue.".tlParams({"c": count}),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
void selectAll() {
setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
});
}
void invertSelection() {
setState(() {
comics.asMap().forEach((k, v) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
});
selectedComics.removeWhere((k, v) => !v);
});
}
var body = Scaffold( var body = Scaffold(
body: SmoothCustomScrollView(slivers: [ body: SmoothCustomScrollView(slivers: [
if (!searchMode && !multiSelectMode) if (!searchMode && !multiSelectMode)
@@ -300,6 +334,11 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
}, },
); );
}), }),
MenuEntry(
icon: Icons.download,
text: "Download".tl,
onClick: downloadSelected,
),
]), ]),
], ],
) )
@@ -336,22 +375,43 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
SliverGridComics( SliverGridComics(
comics: comics, comics: comics,
selections: selectedComics, selections: selectedComics,
onTap: multiSelectMode menuBuilder: (c) {
? (c) { return [
setState(() { MenuEntry(
if (selectedComics.containsKey(c as FavoriteItem)) { icon: Icons.download,
selectedComics.remove(c); text: "Download".tl,
_checkExitSelectMode(); onClick: () {
} else { downloadComic(c as FavoriteItem);
selectedComics[c] = true; context.showMessage(
} message: "Download started".tl,
lastSelectedIndex = comics.indexOf(c); );
});
}
: (c) {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
}, },
),
];
},
onTap: (c) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as FavoriteItem)) {
selectedComics.remove(c);
_checkExitSelectMode();
} else {
selectedComics[c] = true;
}
lastSelectedIndex = comics.indexOf(c);
});
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
} else {
App.mainNavigatorKey?.currentContext?.to(
() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
);
}
},
onLongPressed: (c) { onLongPressed: (c) {
setState(() { setState(() {
if (!multiSelectMode) { if (!multiSelectMode) {
@@ -425,7 +485,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
padding: EdgeInsets.only(bottom: context.padding.bottom + 16), padding: EdgeInsets.only(bottom: context.padding.bottom + 16),
child: Container( child: Container(
constraints: constraints:
const BoxConstraints(maxHeight: 700, maxWidth: 500), const BoxConstraints(maxHeight: 700, maxWidth: 500),
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
@@ -443,7 +503,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
targetFolders = LocalFavoritesManager() targetFolders = LocalFavoritesManager()
.folderNames .folderNames
.where((folder) => .where((folder) =>
folder != favPage.folder) folder != favPage.folder)
.toList(); .toList();
}); });
}); });
@@ -482,14 +542,14 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
onChanged: disabled onChanged: disabled
? null ? null
: (v) { : (v) {
setState(() { setState(() {
if (v!) { if (v!) {
selectedLocalFolders.add(folder); selectedLocalFolders.add(folder);
} else { } else {
selectedLocalFolders.remove(folder); selectedLocalFolders.remove(folder);
} }
}); });
}, },
); );
}, },
), ),
@@ -592,12 +652,19 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
late var comics = LocalFavoritesManager().getAllComics(widget.name); late var comics = LocalFavoritesManager().getAllComics(widget.name);
bool changed = false; bool changed = false;
Color lightenColor(Color color, double lightenValue) { static int _floatToInt8(double x) {
int red = (color.red + ((255 - color.red) * lightenValue)).round(); return (x * 255.0).round() & 0xff;
int green = (color.green + ((255 - color.green) * lightenValue)).round(); }
int blue = (color.blue + ((255 - color.blue) * lightenValue)).round();
return Color.fromARGB(color.alpha, red, green, blue); Color lightenColor(Color color, double lightenValue) {
int red =
(_floatToInt8(color.r) + ((255 - color.r) * lightenValue)).round();
int green = (_floatToInt8(color.g) * 255 + ((255 - color.g) * lightenValue))
.round();
int blue = (_floatToInt8(color.b) * 255 + ((255 - color.b) * lightenValue))
.round();
return Color.fromARGB(_floatToInt8(color.a), red, green, blue);
} }
@override @override
@@ -650,7 +717,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
), ),
], ],
), ),
body: ReorderableBuilder( body: ReorderableBuilder<FavoriteItem>(
key: reorderWidgetKey, key: reorderWidgetKey,
scrollController: _scrollController, scrollController: _scrollController,
longPressDelay: App.isDesktop longPressDelay: App.isDesktop
@@ -659,14 +726,14 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
onReorder: (reorderFunc) { onReorder: (reorderFunc) {
changed = true; changed = true;
setState(() { setState(() {
comics = reorderFunc(comics) as List<FavoriteItem>; comics = reorderFunc(comics);
}); });
widget.onReorder(comics); widget.onReorder(comics);
}, },
dragChildBoxDecoration: BoxDecoration( dragChildBoxDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: lightenColor( color: lightenColor(
Theme.of(context).splashColor.withOpacity(1), Theme.of(context).splashColor.withAlpha(255),
0.2, 0.2,
), ),
), ),

View File

@@ -179,7 +179,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? context.colorScheme.primaryContainer.withOpacity(0.36) ? context.colorScheme.primaryContainer.toOpacity(0.36)
: null, : null,
border: Border( border: Border(
left: BorderSide( left: BorderSide(
@@ -214,7 +214,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? context.colorScheme.primaryContainer.withOpacity(0.36) ? context.colorScheme.primaryContainer.toOpacity(0.36)
: null, : null,
border: Border( border: Border(
left: BorderSide( left: BorderSide(

View File

@@ -4,8 +4,6 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
class HistoryPage extends StatefulWidget { class HistoryPage extends StatefulWidget {
@@ -78,33 +76,7 @@ class _HistoryPageState extends State<HistoryPage> {
], ],
), ),
SliverGridComics( SliverGridComics(
comics: comics.map( comics: comics,
(e) {
var cover = e.cover;
if (!cover.isURL) {
var localComic = LocalManager().find(
e.id,
e.type,
);
if(localComic != null) {
cover = "file://${localComic.coverFile.path}";
}
}
return Comic(
e.title,
cover,
e.id,
e.subtitle,
null,
getDescription(e),
e.type == ComicType.local
? 'local'
: e.type.comicSource?.key ?? "Unknown:${e.type.value}",
null,
null,
);
},
).toList(),
badgeBuilder: (c) { badgeBuilder: (c) {
return ComicSource.find(c.sourceKey)?.name; return ComicSource.find(c.sourceKey)?.name;
}, },

View File

@@ -6,7 +6,8 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/image_provider/cached_image.dart'; import 'package:venera/foundation/image_provider/history_image_provider.dart';
import 'package:venera/foundation/image_provider/local_comic_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/pages/accounts_page.dart'; import 'package:venera/pages/accounts_page.dart';
import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_page.dart';
@@ -15,7 +16,6 @@ import 'package:venera/pages/downloading_page.dart';
import 'package:venera/pages/history_page.dart'; import 'package:venera/pages/history_page.dart';
import 'package:venera/pages/search_page.dart'; import 'package:venera/pages/search_page.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/import_comic.dart'; import 'package:venera/utils/import_comic.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -53,7 +53,7 @@ class _SearchBar extends StatelessWidget {
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Material( child: Material(
color: context.colorScheme.surfaceContainer, color: context.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
@@ -264,22 +264,8 @@ class _HistoryState extends State<_History> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: history.length, itemCount: history.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var cover = history[index].cover; return AnimatedTapRegion(
ImageProvider imageProvider = CachedImageProvider( borderRadius: 8,
cover,
sourceKey: history[index].type.comicSource?.key,
cid: history[index].id,
);
if (!cover.isURL) {
var localComic = LocalManager().find(
history[index].id,
history[index].type,
);
if (localComic != null) {
imageProvider = FileImage(localComic.coverFile);
}
}
return InkWell(
onTap: () { onTap: () {
context.to( context.to(
() => ComicPage( () => ComicPage(
@@ -288,11 +274,9 @@ class _HistoryState extends State<_History> {
), ),
); );
}, },
borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
width: 92, width: 92,
height: 114, height: 114,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context) color: Theme.of(context)
@@ -301,14 +285,14 @@ class _HistoryState extends State<_History> {
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: AnimatedImage( child: AnimatedImage(
image: imageProvider, image: HistoryImageProvider(history[index]),
width: 96, width: 96,
height: 128, height: 128,
fit: BoxFit.cover, fit: BoxFit.cover,
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
), ),
), ),
); ).paddingHorizontal(8);
}, },
), ),
).paddingHorizontal(8).paddingBottom(16), ).paddingHorizontal(8).paddingBottom(16),
@@ -401,15 +385,14 @@ class _LocalState extends State<_Local> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: local.length, itemCount: local.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return InkWell( return AnimatedTapRegion(
onTap: () { onTap: () {
local[index].read(); local[index].read();
}, },
borderRadius: BorderRadius.circular(8), borderRadius: 8,
child: Container( child: Container(
width: 92, width: 92,
height: 114, height: 114,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context) color: Theme.of(context)
@@ -418,8 +401,8 @@ class _LocalState extends State<_Local> {
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: AnimatedImage( child: AnimatedImage(
image: FileImage( image: LocalComicImageProvider(
local[index].coverFile, local[index],
), ),
width: 96, width: 96,
height: 128, height: 128,
@@ -427,7 +410,7 @@ class _LocalState extends State<_Local> {
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
), ),
), ),
); ).paddingHorizontal(8);
}, },
), ),
).paddingHorizontal(8), ).paddingHorizontal(8),
@@ -511,13 +494,15 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
String info = [ String info = [
"Select a directory which contains the comic files.".tl, "Select a directory which contains the comic files.".tl,
"Select a directory which contains the comic directories.".tl, "Select a directory which contains the comic directories.".tl,
"Select a cbz file.".tl, "Select a cbz/zip file.".tl,
"Select a directory which contains multiple cbz/zip files.".tl,
"Select an EhViewer database and a download folder.".tl "Select an EhViewer database and a download folder.".tl
][type]; ][type];
List<String> importMethods = [ List<String> importMethods = [
"Single Comic".tl, "Single Comic".tl,
"Multiple Comics".tl, "Multiple Comics".tl,
"A cbz file".tl, "A cbz file".tl,
"Multiple cbz files".tl,
"EhViewer downloads".tl "EhViewer downloads".tl
]; ];
@@ -593,7 +578,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
barrierColor: Colors.black.withOpacity(0.2), barrierColor: Colors.black.toOpacity(0.2),
builder: (context) { builder: (context) {
var help = ''; var help = '';
help += help +=
@@ -645,7 +630,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
0 => await importer.directory(true), 0 => await importer.directory(true),
1 => await importer.directory(false), 1 => await importer.directory(false),
2 => await importer.cbz(), 2 => await importer.cbz(),
3 => await importer.ehViewer(), 3 => await importer.multipleCbz(),
4 => await importer.ehViewer(),
int() => true, int() => true,
}; };
if(result) { if(result) {

View File

@@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/pages/downloading_page.dart'; import 'package:venera/pages/downloading_page.dart';
import 'package:venera/pages/favorites/favorites_page.dart';
import 'package:venera/utils/cbz.dart'; import 'package:venera/utils/cbz.dart';
import 'package:venera/utils/epub.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/pdf.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
class LocalComicsPage extends StatefulWidget { class LocalComicsPage extends StatefulWidget {
@@ -27,7 +30,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool multiSelectMode = false; bool multiSelectMode = false;
Map<Comic, bool> selectedComics = {}; Map<LocalComic, bool> selectedComics = {};
void update() { void update() {
if (keyword.isEmpty) { if (keyword.isEmpty) {
@@ -114,48 +117,55 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
); );
} }
Widget buildMultiSelectMenu() {
return MenuButton(entries: [
MenuEntry(
icon: Icons.delete_outline,
text: "Delete".tl,
onClick: () {
deleteComics(selectedComics.keys.toList()).then((value) {
if (value) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
}
});
},
),
MenuEntry(
icon: Icons.favorite_border,
text: "Add to favorites".tl,
onClick: () {
addFavorite(selectedComics.keys.toList());
},
),
]);
}
void selectAll() {
setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
});
}
void deSelect() {
setState(() {
selectedComics.clear();
});
}
void invertSelection() {
setState(() {
comics.asMap().forEach((k, v) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
});
selectedComics.removeWhere((k, v) => !v);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
void selectAll() {
setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
});
}
void deSelect() {
setState(() {
selectedComics.clear();
});
}
void invertSelection() {
setState(() {
comics.asMap().forEach((k, v) {
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
});
selectedComics.removeWhere((k, v) => !v);
});
}
void selectRange() {
setState(() {
List<int> l = [];
selectedComics.forEach((k, v) {
l.add(comics.indexOf(k as LocalComic));
});
if (l.isEmpty) {
return;
}
l.sort();
int start = l.first;
int end = l.last;
selectedComics.clear();
selectedComics.addEntries(List.generate(end - start + 1, (i) {
return MapEntry(comics[start + i], true);
}));
});
}
List<Widget> selectActions = [ List<Widget> selectActions = [
IconButton( IconButton(
icon: const Icon(Icons.select_all), icon: const Icon(Icons.select_all),
@@ -169,10 +179,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: const Icon(Icons.flip), icon: const Icon(Icons.flip),
tooltip: "Invert Selection".tl, tooltip: "Invert Selection".tl,
onPressed: invertSelection), onPressed: invertSelection),
IconButton( buildMultiSelectMenu(),
icon: const Icon(Icons.border_horizontal_outlined),
tooltip: "Select in range".tl,
onPressed: selectRange),
]; ];
var body = Scaffold( var body = Scaffold(
@@ -209,19 +216,6 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}, },
), ),
), ),
Tooltip(
message: multiSelectMode
? "Exit Multi-Select".tl
: "Multi-Select".tl,
child: IconButton(
icon: const Icon(Icons.checklist),
onPressed: () {
setState(() {
multiSelectMode = !multiSelectMode;
});
},
),
),
], ],
) )
else if (multiSelectMode) else if (multiSelectMode)
@@ -272,66 +266,42 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
SliverGridComics( SliverGridComics(
comics: comics, comics: comics,
selections: selectedComics, selections: selectedComics,
onTap: multiSelectMode onLongPressed: (c) {
? (c) { setState(() {
setState(() { multiSelectMode = true;
if (selectedComics.containsKey(c as LocalComic)) { selectedComics[c as LocalComic] = true;
selectedComics.remove(c); });
} else { },
selectedComics[c] = true; onTap: (c) {
} if(multiSelectMode) {
}); setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c);
} else {
selectedComics[c] = true;
} }
: (c) { if(selectedComics.isEmpty) {
(c as LocalComic).read(); multiSelectMode = false;
}, }
});
} else {
(c as LocalComic).read();
}
},
menuBuilder: (c) { menuBuilder: (c) {
return [ return [
MenuEntry( MenuEntry(
icon: Icons.delete, icon: Icons.delete,
text: "Delete".tl, text: "Delete".tl,
onClick: () { onClick: () {
showDialog( deleteComics([c as LocalComic]).then((value) {
context: context, if (value && multiSelectMode) {
builder: (context) { setState(() {
bool removeComicFile = true; multiSelectMode = false;
return StatefulBuilder(builder: (context, state) { selectedComics.clear();
return ContentDialog(
title: "Delete".tl,
content: CheckboxListTile(
title:
Text("Also remove files on disk".tl),
value: removeComicFile,
onChanged: (v) {
state(() {
removeComicFile = !removeComicFile;
});
},
),
actions: [
FilledButton(
onPressed: () {
context.pop();
if (multiSelectMode) {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(
comic as LocalComic,
removeComicFile);
}
setState(() {
selectedComics.clear();
});
} else {
LocalManager().deleteComic(
c as LocalComic, removeComicFile);
}
},
child: Text("Confirm".tl),
),
],
);
});
}); });
}
});
}), }),
MenuEntry( MenuEntry(
icon: Icons.outbox_outlined, icon: Icons.outbox_outlined,
@@ -342,25 +312,67 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
allowCancel: false, allowCancel: false,
); );
try { try {
if (multiSelectMode) { var file = await CBZ.export(c as LocalComic);
for (var comic in selectedComics.keys) { await saveFile(filename: file.name, file: file);
var file = await CBZ.export(comic as LocalComic); await file.delete();
await saveFile(filename: file.name, file: file);
await file.delete();
}
setState(() {
selectedComics.clear();
});
} else {
var file = await CBZ.export(c as LocalComic);
await saveFile(filename: file.name, file: file);
await file.delete();
}
} catch (e) { } catch (e) {
context.showMessage(message: e.toString()); context.showMessage(message: e.toString());
} }
controller.close(); controller.close();
}), }),
MenuEntry(
icon: Icons.picture_as_pdf_outlined,
text: "Export as pdf".tl,
onClick: () async {
var cache = FilePath.join(App.cachePath, 'temp.pdf');
var controller = showLoadingDialog(
context,
allowCancel: false,
);
try {
await createPdfFromComicIsolate(
comic: c as LocalComic,
savePath: cache,
);
await saveFile(
file: File(cache),
filename: "${c.title}.pdf",
);
} catch (e, s) {
Log.error("PDF Export", e, s);
context.showMessage(message: e.toString());
} finally {
controller.close();
File(cache).deleteIgnoreError();
}
},
),
MenuEntry(
icon: Icons.import_contacts_outlined,
text: "Export as epub".tl,
onClick: () async {
var controller = showLoadingDialog(
context,
allowCancel: false,
);
File? file;
try {
file = await createEpubWithLocalComic(
c as LocalComic,
);
await saveFile(
file: file,
filename: "${c.title}.epub",
);
} catch (e, s) {
Log.error("EPUB Export", e, s);
context.showMessage(message: e.toString());
} finally {
controller.close();
file?.deleteIgnoreError();
}
},
)
]; ];
}, },
), ),
@@ -387,4 +399,44 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
child: body, child: body,
); );
} }
Future<bool> deleteComics(List<LocalComic> comics) async {
bool isDeleted = false;
await showDialog(
context: App.rootContext,
builder: (context) {
bool removeComicFile = true;
return StatefulBuilder(builder: (context, state) {
return ContentDialog(
title: "Delete".tl,
content: CheckboxListTile(
title: Text("Also remove files on disk".tl),
value: removeComicFile,
onChanged: (v) {
state(() {
removeComicFile = !removeComicFile;
});
},
),
actions: [
FilledButton(
onPressed: () {
context.pop();
for (var comic in comics) {
LocalManager().deleteComic(
comic,
removeComicFile,
);
}
isDeleted = true;
},
child: Text("Confirm".tl),
),
],
);
});
},
);
return isDeleted;
}
} }

View File

@@ -62,10 +62,18 @@ class _MainPageState extends State<MainPage> {
} }
final _pages = [ final _pages = [
const HomePage(), const HomePage(
const FavoritesPage(), key: PageStorageKey('home'),
const ExplorePage(), ),
const CategoriesPage(), const FavoritesPage(
key: PageStorageKey('favorites'),
),
const ExplorePage(
key: PageStorageKey('explore'),
),
const CategoriesPage(
key: PageStorageKey('categories'),
),
]; ];
var index = 0; var index = 0;

View File

@@ -103,6 +103,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
} }
void onMouseWheel(bool forward) { void onMouseWheel(bool forward) {
if (HardwareKeyboard.instance.isControlPressed) {
return;
}
if (context.reader.mode.key.startsWith('gallery')) { if (context.reader.mode.key.startsWith('gallery')) {
if (forward) { if (forward) {
if (!context.reader.toNextPage()) { if (!context.reader.toNextPage()) {
@@ -265,5 +268,5 @@ class _DragListener {
void Function(Offset offset)? onMove; void Function(Offset offset)? onMove;
void Function()? onEnd; void Function()? onEnd;
_DragListener({this.onStart, this.onMove, this.onEnd}); _DragListener({this.onMove, this.onEnd});
} }

View File

@@ -25,7 +25,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
if (inProgress) return; if (inProgress) return;
inProgress = true; inProgress = true;
if (reader.type == ComicType.local || if (reader.type == ComicType.local ||
(await LocalManager() (LocalManager()
.isDownloaded(reader.cid, reader.type, reader.chapter))) { .isDownloaded(reader.cid, reader.type, reader.chapter))) {
try { try {
var images = await LocalManager() var images = await LocalManager()
@@ -83,7 +83,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
); );
} else { } else {
if (reader.mode.isGallery) { if (reader.mode.isGallery) {
return _GalleryMode(key: Key(reader.mode.key)); return _GalleryMode(
key: Key('${reader.mode.key}_${reader.imagesPerPage}'));
} else { } else {
return _ContinuousMode(key: Key(reader.mode.key)); return _ContinuousMode(key: Key(reader.mode.key));
} }
@@ -110,6 +111,10 @@ class _GalleryModeState extends State<_GalleryMode>
late _ReaderState reader; late _ReaderState reader;
int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) /
reader.imagesPerPage)
.ceil();
@override @override
void initState() { void initState() {
reader = context.reader; reader = context.reader;
@@ -124,8 +129,14 @@ class _GalleryModeState extends State<_GalleryMode>
void cache(int current) { void cache(int current) {
for (int i = current + 1; i <= current + preCacheCount; i++) { for (int i = current + 1; i <= current + preCacheCount; i++) {
if (i <= reader.maxPage && !cached[i]) { if (i <= totalPages && !cached[i]) {
_precacheImage(i, context); int startIndex = (i - 1) * reader.imagesPerPage;
int endIndex =
math.min(startIndex + reader.imagesPerPage, reader.images!.length);
for (int i = startIndex; i < endIndex; i++) {
precacheImage(
_createImageProviderFromKey(reader.images![i], context), context);
}
cached[i] = true; cached[i] = true;
} }
} }
@@ -141,32 +152,46 @@ class _GalleryModeState extends State<_GalleryMode>
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
? Axis.vertical ? Axis.vertical
: Axis.horizontal, : Axis.horizontal,
itemCount: reader.images!.length + 2, itemCount: totalPages + 2,
builder: (BuildContext context, int index) { builder: (BuildContext context, int index) {
ImageProvider? imageProvider; if (index == 0 || index == totalPages + 1) {
if (index != 0 && index != reader.images!.length + 1) {
imageProvider = _createImageProvider(index, context);
} else {
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
scaleStateController: PhotoViewScaleStateController(), scaleStateController: PhotoViewScaleStateController(),
child: const SizedBox(), child: const SizedBox(),
); );
} else {
int pageIndex = index - 1;
int startIndex = pageIndex * reader.imagesPerPage;
int endIndex = math.min(
startIndex + reader.imagesPerPage, reader.images!.length);
List<String> pageImages =
reader.images!.sublist(startIndex, endIndex);
cached[index] = true;
cache(index);
photoViewControllers[index] = PhotoViewController();
if (reader.imagesPerPage == 1) {
return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium,
controller: photoViewControllers[index],
imageProvider:
_createImageProviderFromKey(pageImages[0], context),
fit: BoxFit.contain,
errorBuilder: (_, error, s, retry) {
return NetworkError(message: error.toString(), retry: retry);
},
);
}
return PhotoViewGalleryPageOptions.customChild(
controller: photoViewControllers[index],
minScale: PhotoViewComputedScale.contained * 1.0,
maxScale: PhotoViewComputedScale.covered * 10.0,
child: buildPageImages(pageImages),
);
} }
cached[index] = true;
cache(index);
photoViewControllers[index] ??= PhotoViewController();
return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium,
controller: photoViewControllers[index],
imageProvider: imageProvider,
fit: BoxFit.contain,
errorBuilder: (_, error, s, retry) {
return NetworkError(message: error.toString(), retry: retry);
},
);
}, },
pageController: controller, pageController: controller,
loadingBuilder: (context, event) => Center( loadingBuilder: (context, event) => Center(
@@ -186,9 +211,9 @@ class _GalleryModeState extends State<_GalleryMode>
if (!reader.toPrevChapter()) { if (!reader.toPrevChapter()) {
reader.toPage(1); reader.toPage(1);
} }
} else if (i == reader.maxPage + 1) { } else if (i == totalPages + 1) {
if (!reader.toNextChapter()) { if (!reader.toNextChapter()) {
reader.toPage(reader.maxPage); reader.toPage(totalPages);
} }
} else { } else {
reader.setPage(i); reader.setPage(i);
@@ -198,9 +223,30 @@ class _GalleryModeState extends State<_GalleryMode>
); );
} }
Widget buildPageImages(List<String> images) {
Axis axis = (reader.mode == ReaderMode.galleryTopToBottom)
? Axis.vertical
: Axis.horizontal;
List<Widget> imageWidgets = images.map((imageKey) {
ImageProvider imageProvider =
_createImageProviderFromKey(imageKey, context);
return Expanded(
child: Image(
image: imageProvider,
fit: BoxFit.contain,
),
);
}).toList();
return axis == Axis.vertical
? Column(children: imageWidgets)
: Row(children: imageWidgets);
}
@override @override
Future<void> animateToPage(int page) { Future<void> animateToPage(int page) {
if ((page - controller.page!).abs() > 1) { if ((page - controller.page!.round()).abs() > 1) {
controller.jumpToPage(page > controller.page! ? page - 1 : page + 1); controller.jumpToPage(page > controller.page! ? page - 1 : page + 1);
} }
return controller.animateToPage( return controller.animateToPage(
@@ -600,19 +646,21 @@ class _ContinuousModeState extends State<_ContinuousMode>
} }
} }
ImageProvider _createImageProviderFromKey(
String imageKey, BuildContext context) {
var reader = context.reader;
return ReaderImageProvider(
imageKey,
reader.type.comicSource?.key,
reader.cid,
reader.eid,
);
}
ImageProvider _createImageProvider(int page, BuildContext context) { ImageProvider _createImageProvider(int page, BuildContext context) {
var reader = context.reader; var reader = context.reader;
var imageKey = reader.images![page - 1]; var imageKey = reader.images![page - 1];
if (imageKey.startsWith('file://')) { return _createImageProviderFromKey(imageKey, context);
return FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
} else {
return ReaderImageProvider(
imageKey,
reader.type.comicSource!.key,
reader.cid,
reader.eid,
);
}
} }
void _precacheImage(int page, BuildContext context) { void _precacheImage(int page, BuildContext context) {

View File

@@ -0,0 +1,101 @@
part of 'reader.dart';
class ReaderWithLoading extends StatefulWidget {
const ReaderWithLoading({
super.key,
required this.id,
required this.sourceKey,
});
final String id;
final String sourceKey;
@override
State<ReaderWithLoading> createState() => _ReaderWithLoadingState();
}
class _ReaderWithLoadingState
extends LoadingState<ReaderWithLoading, ReaderProps> {
@override
Widget buildContent(BuildContext context, ReaderProps data) {
return Reader(
type: data.type,
cid: data.cid,
name: data.name,
chapters: data.chapters,
history: data.history,
);
}
@override
Future<Res<ReaderProps>> loadData() async {
var comicSource = ComicSource.find(widget.sourceKey);
var history = HistoryManager().findSync(
widget.id,
ComicType.fromKey(widget.sourceKey),
);
if (comicSource == null) {
var localComic = LocalManager().find(
widget.id,
ComicType.fromKey(widget.sourceKey),
);
if (localComic == null) {
return Res.error("comic not found");
}
return Res(
ReaderProps(
type: ComicType.fromKey(widget.sourceKey),
cid: widget.id,
name: localComic.title,
chapters: localComic.chapters,
history: history ??
History.fromModel(
model: localComic,
ep: 0,
page: 0,
),
),
);
} else {
var comic = await comicSource.loadComicInfo!(widget.id);
if (comic.error) {
return Res.fromErrorRes(comic);
}
return Res(
ReaderProps(
type: ComicType.fromKey(widget.sourceKey),
cid: widget.id,
name: comic.data.title,
chapters: comic.data.chapters,
history: history ??
History.fromModel(
model: comic.data,
ep: 0,
page: 0,
),
),
);
}
}
}
class ReaderProps {
final ComicType type;
final String cid;
final String name;
final Map<String, String>? chapters;
final History history;
const ReaderProps({
required this.type,
required this.cid,
required this.name,
required this.chapters,
required this.history,
});
}

View File

@@ -1,6 +1,7 @@
library venera_reader; library;
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -8,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_memory_info/flutter_memory_info.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@@ -16,10 +18,13 @@ import 'package:venera/components/custom_slider.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/image_provider/reader_image.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/file_type.dart';
@@ -33,6 +38,7 @@ part 'scaffold.dart';
part 'images.dart'; part 'images.dart';
part 'gesture.dart'; part 'gesture.dart';
part 'comic_image.dart'; part 'comic_image.dart';
part 'loading.dart';
extension _ReaderContext on BuildContext { extension _ReaderContext on BuildContext {
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!; _ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
@@ -59,7 +65,7 @@ class Reader extends StatefulWidget {
final String name; final String name;
/// Map<Chapter ID, Chapter Name>. /// key: Chapter ID, value: Chapter Name
/// null if the comic is a gallery /// null if the comic is a gallery
final Map<String, String>? chapters; final Map<String, String>? chapters;
@@ -82,7 +88,8 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
} }
@override @override
int get maxPage => images?.length ?? 1; int get maxPage =>
((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage;
ComicType get type => widget.type; ComicType get type => widget.type;
@@ -94,6 +101,30 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
late ReaderMode mode; late ReaderMode mode;
int get imagesPerPage => appdata.settings['readerScreenPicNumber'] ?? 1;
int _lastImagesPerPage = appdata.settings['readerScreenPicNumber'] ?? 1;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_checkImagesPerPageChange();
}
void _checkImagesPerPageChange() {
int currentImagesPerPage = imagesPerPage;
if (_lastImagesPerPage != currentImagesPerPage) {
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
_lastImagesPerPage = currentImagesPerPage;
}
}
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
int previousImageIndex = (page - 1) * oldImagesPerPage;
int newPage = (previousImageIndex ~/ newImagesPerPage) + 1;
page = newPage;
}
History? history; History? history;
@override @override
@@ -116,9 +147,27 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
if(appdata.settings['enableTurnPageByVolumeKey']) { if(appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent(); handleVolumeEvent();
} }
setImageCacheSize();
super.initState(); super.initState();
} }
void setImageCacheSize() async {
var availableRAM = await MemoryInfo.getFreePhysicalMemorySize();
if (availableRAM == null) return;
int maxImageCacheSize;
if (availableRAM < 1 << 30) {
maxImageCacheSize = 100 << 20;
} else if (availableRAM < 2 << 30) {
maxImageCacheSize = 200 << 20;
} else if (availableRAM < 4 << 30) {
maxImageCacheSize = 300 << 20;
} else {
maxImageCacheSize = 500 << 20;
}
Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
}
@override @override
void dispose() { void dispose() {
autoPageTurningTimer?.cancel(); autoPageTurningTimer?.cancel();
@@ -128,11 +177,13 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
Future.microtask(() { Future.microtask(() {
DataSync().onDataChanged(); DataSync().onDataChanged();
}); });
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_checkImagesPerPageChange();
return KeyboardListener( return KeyboardListener(
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,

View File

@@ -167,10 +167,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: Container( child: Container(
padding: EdgeInsets.only(top: context.padding.top), padding: EdgeInsets.only(top: context.padding.top),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface.withOpacity(0.82), color: context.colorScheme.surface.toOpacity(0.82),
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: Colors.grey.withOpacity(0.5), color: Colors.grey.toOpacity(0.5),
width: 0.5, width: 0.5,
), ),
), ),
@@ -357,10 +357,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
return BlurEffect( return BlurEffect(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface.withOpacity(0.82), color: context.colorScheme.surface.toOpacity(0.82),
border: Border( border: Border(
top: BorderSide( top: BorderSide(
color: Colors.grey.withOpacity(0.5), color: Colors.grey.toOpacity(0.5),
width: 0.5, width: 0.5,
), ),
), ),
@@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
ImageProvider image; ImageProvider image;
var imageKey = images[index]; var imageKey = images[index];
if (imageKey.startsWith('file://')) { if (imageKey.startsWith('file://')) {
image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", ''))); image = FileImage(File(imageKey.replaceFirst("file://", '')));
} else { } else {
image = ReaderImageProvider( image = ReaderImageProvider(
imageKey, imageKey,
@@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
} }
} }
if (imageKey.startsWith("file://")) { if (imageKey.startsWith("file://")) {
return await openFilePlatform(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}"))!
@@ -641,7 +641,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.surfaceTint .surfaceTint
.withOpacity(0.2), .toOpacity(0.2),
child: const SizedBox.expand(), child: const SizedBox.expand(),
), ),
), ),

View File

@@ -7,6 +7,7 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/state_controller.dart'; import 'package:venera/foundation/state_controller.dart';
import 'package:venera/pages/aggregated_search_page.dart';
import 'package:venera/pages/search_result_page.dart'; import 'package:venera/pages/search_result_page.dart';
import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/app_links.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
@@ -27,6 +28,8 @@ class _SearchPageState extends State<SearchPage> {
String searchTarget = ""; String searchTarget = "";
bool aggregatedSearch = false;
var focusNode = FocusNode(); var focusNode = FocusNode();
var options = <String>[]; var options = <String>[];
@@ -36,15 +39,21 @@ class _SearchPageState extends State<SearchPage> {
} }
void search([String? text]) { void search([String? text]) {
context if (aggregatedSearch) {
.to( context
() => SearchResultPage( .to(() => AggregatedSearchPage(keyword: text ?? controller.text))
text: text ?? controller.text, .then((_) => update());
sourceKey: searchTarget, } else {
options: options, context
), .to(
) () => SearchResultPage(
.then((_) => update()); text: text ?? controller.text,
sourceKey: searchTarget,
options: options,
),
)
.then((_) => update());
}
} }
var suggestions = <Pair<String, TranslationType>>[]; var suggestions = <Pair<String, TranslationType>>[];
@@ -189,6 +198,7 @@ class _SearchPageState extends State<SearchPage> {
children: [ children: [
ListTile( ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.search),
title: Text("Search in".tl), title: Text("Search in".tl),
), ),
Wrap( Wrap(
@@ -197,8 +207,9 @@ class _SearchPageState extends State<SearchPage> {
children: sources.map((e) { children: sources.map((e) {
return OptionChip( return OptionChip(
text: e.name, text: e.name,
isSelected: searchTarget == e.key, isSelected: searchTarget == e.key || aggregatedSearch,
onTap: () { onTap: () {
if (aggregatedSearch) return;
setState(() { setState(() {
searchTarget = e.key; searchTarget = e.key;
useDefaultOptions(); useDefaultOptions();
@@ -207,6 +218,18 @@ class _SearchPageState extends State<SearchPage> {
); );
}).toList(), }).toList(),
), ),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text("Aggregated Search".tl),
leading: Checkbox(
value: aggregatedSearch,
onChanged: (value) {
setState(() {
aggregatedSearch = value ?? false;
});
},
),
),
], ],
), ),
), ),
@@ -221,6 +244,10 @@ class _SearchPageState extends State<SearchPage> {
} }
Widget buildSearchOptions() { Widget buildSearchOptions() {
if (aggregatedSearch) {
return const SliverToBoxAdapter(child: SizedBox());
}
var children = <Widget>[]; var children = <Widget>[];
final searchOptions = final searchOptions =
@@ -262,9 +289,9 @@ class _SearchPageState extends State<SearchPage> {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) {
if (index == 0) { if (index == 0) {
return const Divider( return const SizedBox(
thickness: 0.6, height: 16,
).paddingTop(16); );
} }
if (index == 1) { if (index == 1) {
return ListTile( return ListTile(

View File

@@ -14,14 +14,14 @@ class SearchResultPage extends StatefulWidget {
super.key, super.key,
required this.text, required this.text,
required this.sourceKey, required this.sourceKey,
required this.options, this.options,
}); });
final String text; final String text;
final String sourceKey; final String sourceKey;
final List<String> options; final List<String>? options;
@override @override
State<SearchResultPage> createState() => _SearchResultPageState(); State<SearchResultPage> createState() => _SearchResultPageState();
@@ -99,7 +99,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
onSearch: search, onSearch: search,
); );
sourceKey = widget.sourceKey; sourceKey = widget.sourceKey;
options = widget.options; options = widget.options ?? const [];
validateOptions(); validateOptions();
text = widget.text; text = widget.text;
appdata.addSearchHistory(text); appdata.addSearchHistory(text);

View File

@@ -68,6 +68,13 @@ class _AboutSettingsState extends State<AboutSettings> {
launchUrlString("https://github.com/venera-app/venera"); launchUrlString("https://github.com/venera-app/venera");
}, },
).toSliver(), ).toSliver(),
ListTile(
title: const Text("Telegram"),
trailing: const Icon(Icons.open_in_new),
onTap: () {
launchUrlString("https://t.me/venera_release");
},
).toSliver(),
], ],
); );
} }

View File

@@ -34,25 +34,8 @@ class _AppSettingsState extends State<AppSettings> {
callback: () async { callback: () async {
String? result; String? result;
if (App.isAndroid) { if (App.isAndroid) {
var channel = const MethodChannel("venera/storage"); var picker = DirectoryPicker();
var permission = await channel.invokeMethod(''); result = (await picker.pickDirectory())?.path;
if (permission != true) {
context.showMessage(message: "Permission denied".tl);
return;
}
var path = await selectDirectory();
if (path != null) {
// check if the path is writable
var testFile = File(FilePath.join(path, "test"));
try {
await testFile.writeAsBytes([1]);
await testFile.delete();
} catch (e) {
context.showMessage(message: "Permission denied".tl);
return;
}
result = path;
}
} else if (App.isIOS) { } else if (App.isIOS) {
result = await selectDirectoryIOS(); result = await selectDirectoryIOS();
} else { } else {
@@ -127,16 +110,23 @@ class _AppSettingsState extends State<AppSettings> {
title: "Import App Data".tl, title: "Import App Data".tl,
callback: () async { callback: () async {
var controller = showLoadingDialog(context); var controller = showLoadingDialog(context);
var file = await selectFile(ext: ['venera']); var file = await selectFile(ext: ['venera', 'picadata']);
if (file != null) { if (file != null) {
var cacheFile = File(FilePath.join(App.cachePath, "temp.venera")); var cacheFile = File(FilePath.join(App.cachePath, "import_data_temp"));
await file.saveTo(cacheFile.path); await file.saveTo(cacheFile.path);
try { try {
await importAppData(cacheFile); if(file.name.endsWith('picadata')) {
await importPicaData(cacheFile);
} else {
await importAppData(cacheFile);
}
} catch (e, s) { } catch (e, s) {
Log.error("Import data", e.toString(), s); Log.error("Import data", e.toString(), s);
context.showMessage(message: "Failed to import data".tl); context.showMessage(message: "Failed to import data".tl);
} }
finally {
cacheFile.deleteIgnoreError();
}
} }
controller.close(); controller.close();
}, },

View File

@@ -29,6 +29,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
title: "Theme Color".tl, title: "Theme Color".tl,
settingKey: "color", settingKey: "color",
optionTranslation: { optionTranslation: {
"system": "System".tl,
"red": "Red".tl, "red": "Red".tl,
"pink": "Pink".tl, "pink": "Pink".tl,
"purple": "Purple".tl, "purple": "Purple".tl,

View File

@@ -94,7 +94,7 @@ class _ExploreSettingsState extends State<ExploreSettings> {
} }
class _ManageBlockingWordView extends StatefulWidget { class _ManageBlockingWordView extends StatefulWidget {
const _ManageBlockingWordView({super.key}); const _ManageBlockingWordView();
@override @override
State<_ManageBlockingWordView> createState() => State<_ManageBlockingWordView> createState() =>
@@ -135,7 +135,7 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
void add() { void add() {
showDialog( showDialog(
context: App.rootContext, context: App.rootContext,
barrierColor: Colors.black.withOpacity(0.1), barrierColor: Colors.black.toOpacity(0.1),
builder: (context) { builder: (context) {
var controller = TextEditingController(); var controller = TextEditingController();
String? error; String? error;

View File

@@ -16,18 +16,18 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
SelectSetting( SelectSetting(
title: "Add new favorite to".tl, title: "Add new favorite to".tl,
settingKey: "newFavoriteAddTo", settingKey: "newFavoriteAddTo",
optionTranslation: const { optionTranslation: {
"start": "Start", "start": "Start".tl,
"end": "End", "end": "End".tl,
}, },
).toSliver(), ).toSliver(),
SelectSetting( SelectSetting(
title: "Move favorite after reading".tl, title: "Move favorite after reading".tl,
settingKey: "moveFavoriteAfterRead", settingKey: "moveFavoriteAfterRead",
optionTranslation: const { optionTranslation: {
"none": "None", "none": "None".tl,
"end": "End", "end": "End".tl,
"start": "Start", "start": "Start".tl,
}, },
).toSliver(), ).toSliver(),
SelectSetting( SelectSetting(
@@ -48,6 +48,14 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
}, },
actionTitle: 'Delete'.tl, actionTitle: 'Delete'.tl,
).toSliver(), ).toSliver(),
SelectSetting(
title: "Click favorite".tl,
settingKey: "onClickFavorite",
optionTranslation: {
"viewDetail": "View Detail".tl,
"read": "Read".tl,
},
).toSliver(),
], ],
); );
} }

View File

@@ -41,6 +41,11 @@ class _ReaderSettingsState extends State<ReaderSettings> {
"continuousTopToBottom": "Continuous (Top to Bottom)".tl, "continuousTopToBottom": "Continuous (Top to Bottom)".tl,
}, },
onChanged: () { onChanged: () {
var readerMode = appdata.settings['readerMode'];
if (readerMode?.toLowerCase().startsWith('continuous') ?? false) {
appdata.settings['readerScreenPicNumber'] = 1;
widget.onChanged?.call('readerScreenPicNumber');
}
widget.onChanged?.call("readerMode"); widget.onChanged?.call("readerMode");
}, },
).toSliver(), ).toSliver(),
@@ -54,6 +59,25 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("autoPageTurningInterval"); widget.onChanged?.call("autoPageTurningInterval");
}, },
).toSliver(), ).toSliver(),
SliverToBoxAdapter(
child: AbsorbPointer(
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
child: AnimatedOpacity(
opacity: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false) ? 0.5 : 1.0,
duration: Duration(milliseconds: 300),
child: _SliderSetting(
title: "The number of pic in screen (Only Gallery Mode)".tl,
settingsIndex: "readerScreenPicNumber",
interval: 1,
min: 1,
max: 5,
onChanged: () {
widget.onChanged?.call("readerScreenPicNumber");
},
),
),
),
),
_SwitchSetting( _SwitchSetting(
title: 'Long press to zoom'.tl, title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom', settingKey: 'enableLongPressToZoom',

View File

@@ -384,7 +384,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var tiles = keys.map((e) => buildItem(e)).toList(); var tiles = keys.map((e) => buildItem(e)).toList();
var view = ReorderableBuilder( var view = ReorderableBuilder<String>(
key: reorderWidgetKey, key: reorderWidgetKey,
scrollController: scrollController, scrollController: scrollController,
longPressDelay: App.isDesktop longPressDelay: App.isDesktop
@@ -542,7 +542,7 @@ class _SettingPartTitle extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: context.colorScheme.onSurface.withOpacity(0.1), color: context.colorScheme.onSurface.withValues(alpha: 0.1),
), ),
), ),
), ),

View File

@@ -178,8 +178,9 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
Positioned.fill(child: buildLeft()), Positioned.fill(child: buildLeft()),
Positioned( Positioned(
left: offset, left: offset,
width: MediaQuery.of(context).size.width, right: 0,
height: MediaQuery.of(context).size.height, top: 0,
bottom: 0,
child: Listener( child: Listener(
onPointerDown: handlePointerDown, onPointerDown: handlePointerDown,
child: AnimatedSwitcher( child: AnimatedSwitcher(
@@ -266,7 +267,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
height: 46, height: 46,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? colors.primaryContainer.withOpacity(0.36) : null, color: selected ? colors.primaryContainer.toOpacity(0.36) : null,
border: Border( border: Border(
left: BorderSide( left: BorderSide(
color: selected ? colors.primary : Colors.transparent, color: selected ? colors.primary : Colors.transparent,

View File

@@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
@@ -63,7 +62,7 @@ abstract class CBZ {
var cache = Directory(FilePath.join(App.cachePath, 'cbz_import')); var cache = Directory(FilePath.join(App.cachePath, 'cbz_import'));
if (cache.existsSync()) cache.deleteSync(recursive: true); if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync(); cache.createSync();
await Isolate.run(() => ZipFile.openAndExtract(file.path, cache.path)); await ZipFile.openAndExtractAsync(file.path, cache.path, 4);
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json')); var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
ComicMetaData? metaData; ComicMetaData? metaData;
if (metaDataFile.existsSync()) { if (metaDataFile.existsSync()) {
@@ -104,14 +103,14 @@ abstract class CBZ {
FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)), FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)),
); );
dest.createSync(); dest.createSync();
coverFile.copy( coverFile.copyMem(
FilePath.join(dest.path, 'cover.${coverFile.path.split('.').last}')); FilePath.join(dest.path, 'cover.${coverFile.extension}'));
if (metaData.chapters == null) { if (metaData.chapters == null) {
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
var src = files[i]; var src = files[i];
var dst = File( var dst = File(
FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}')); FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}'));
await src.copy(dst.path); await src.copyMem(dst.path);
} }
} else { } else {
dest.createSync(); dest.createSync();
@@ -129,7 +128,7 @@ abstract class CBZ {
var src = chapter.value[i]; var src = chapter.value[i];
var dst = File(FilePath.join( var dst = File(FilePath.join(
chapterDir.path, '${i + 1}.${src.path.split('.').last}')); chapterDir.path, '${i + 1}.${src.path.split('.').last}'));
await src.copy(dst.path); await src.copyMem(dst.path);
} }
} }
} }
@@ -142,10 +141,9 @@ abstract class CBZ {
directory: dest.name, directory: dest.name,
chapters: cpMap, chapters: cpMap,
downloadedChapters: cpMap?.keys.toList() ?? [], downloadedChapters: cpMap?.keys.toList() ?? [],
cover: 'cover.${coverFile.path.split('.').last}', cover: 'cover.${coverFile.extension}',
createdAt: DateTime.now(), createdAt: DateTime.now(),
); );
LocalManager().add(comic);
await cache.delete(recursive: true); await cache.delete(recursive: true);
return comic; return comic;
} }
@@ -164,7 +162,7 @@ abstract class CBZ {
var dstName = var dstName =
'${i.toString().padLeft(width, '0')}.${image.split('.').last}'; '${i.toString().padLeft(width, '0')}.${image.split('.').last}';
var dst = File(FilePath.join(cache.path, dstName)); var dst = File(FilePath.join(cache.path, dstName));
await src.copy(dst.path); await src.copyMem(dst.path);
i++; i++;
} }
} else { } else {
@@ -187,18 +185,18 @@ abstract class CBZ {
} }
int i = 1; int i = 1;
for (var image in allImages) { for (var image in allImages) {
var src = openFilePlatform(image); var src = File(image);
var width = allImages.length.toString().length; var width = allImages.length.toString().length;
var dstName = var dstName =
'${i.toString().padLeft(width, '0')}.${image.split('.').last}'; '${i.toString().padLeft(width, '0')}.${image.split('.').last}';
var dst = File(FilePath.join(cache.path, dstName)); var dst = File(FilePath.join(cache.path, dstName));
await src.copy(dst.path); await src.copyMem(dst.path);
i++; i++;
} }
} }
var cover = comic.coverFile; var cover = comic.coverFile;
await cover await cover
.copy(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}')); .copyMem(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}'));
await File(FilePath.join(cache.path, 'metadata.json')).writeAsString( await File(FilePath.join(cache.path, 'metadata.json')).writeAsString(
jsonEncode( jsonEncode(
ComicMetaData( ComicMetaData(
@@ -209,13 +207,13 @@ abstract class CBZ {
).toJson(), ).toJson(),
), ),
); );
var cbz = File(FilePath.join(App.cachePath, '${comic.title}.cbz')); var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
await _compress(cache.path, cbz.path); await _compress(cache.path, cbz.path);
cache.deleteSync(recursive: true); cache.deleteSync(recursive: true);
return cbz; return cbz;
} }
static _compress(String src, String dst) async { static _compress(String src, String dst) async {
await Isolate.run(() => ZipFile.compressFolder(src, dst)); await ZipFile.compressFolderAsync(src, dst, 4);
} }
} }

View File

@@ -1,11 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate'; import 'dart:isolate';
import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/cookie_jar.dart'; import 'package:venera/network/cookie_jar.dart';
import 'package:zip_flutter/zip_flutter.dart'; import 'package:zip_flutter/zip_flutter.dart';
@@ -43,61 +46,165 @@ Future<File> exportAppData() async {
Future<void> importAppData(File file, [bool checkVersion = false]) async { Future<void> importAppData(File file, [bool checkVersion = false]) async {
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data'); var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
var cacheDir = Directory(cacheDirPath); var cacheDir = Directory(cacheDirPath);
await Isolate.run(() { if (cacheDir.existsSync()) {
ZipFile.openAndExtract(file.path, cacheDirPath); cacheDir.deleteSync(recursive: true);
});
var historyFile = cacheDir.joinFile("history.db");
var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
var appdataFile = cacheDir.joinFile("appdata.json");
var cookieFile = cacheDir.joinFile("cookie.db");
if (checkVersion && appdataFile.existsSync()) {
var data = jsonDecode(await appdataFile.readAsString());
var version = data["settings"]["dataVersion"];
if (version is int && version <= appdata.settings["dataVersion"]) {
return;
}
} }
if (await historyFile.exists()) { cacheDir.createSync();
HistoryManager().close(); try {
File(FilePath.join(App.dataPath, "history.db")).deleteIfExistsSync(); await Isolate.run(() {
historyFile.renameSync(FilePath.join(App.dataPath, "history.db")); ZipFile.openAndExtract(file.path, cacheDirPath);
HistoryManager().init(); });
} var historyFile = cacheDir.joinFile("history.db");
if (await localFavoriteFile.exists()) { var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
LocalFavoritesManager().close(); var appdataFile = cacheDir.joinFile("appdata.json");
File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync(); var cookieFile = cacheDir.joinFile("cookie.db");
localFavoriteFile if (checkVersion && appdataFile.existsSync()) {
.renameSync(FilePath.join(App.dataPath, "local_favorite.db")); var data = jsonDecode(await appdataFile.readAsString());
LocalFavoritesManager().init(); var version = data["settings"]["dataVersion"];
} if (version is int && version <= appdata.settings["dataVersion"]) {
if (await appdataFile.exists()) { return;
// proxy settings & authorization setting should be kept
var proxySettings = appdata.settings["proxy"];
var authSettings = appdata.settings["authorizationRequired"];
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
await appdata.init();
appdata.settings["proxy"] = proxySettings;
appdata.settings["authorizationRequired"] = authSettings;
appdata.saveData();
}
if (await cookieFile.exists()) {
SingleInstanceCookieJar.instance?.dispose();
File(FilePath.join(App.dataPath, "cookie.db")).deleteIfExistsSync();
cookieFile.renameSync(FilePath.join(App.dataPath, "cookie.db"));
SingleInstanceCookieJar.instance =
SingleInstanceCookieJar(FilePath.join(App.dataPath, "cookie.db"))
..init();
}
var comicSourceDir = FilePath.join(cacheDirPath, "comic_source");
if (Directory(comicSourceDir).existsSync()) {
for (var file in Directory(comicSourceDir).listSync()) {
if (file is File) {
var targetFile = FilePath.join(App.dataPath, "comic_source", file.name);
File(targetFile).deleteIfExistsSync();
await file.copy(targetFile);
} }
} }
await ComicSource.reload(); if (await historyFile.exists()) {
HistoryManager().close();
File(FilePath.join(App.dataPath, "history.db")).deleteIfExistsSync();
historyFile.renameSync(FilePath.join(App.dataPath, "history.db"));
HistoryManager().init();
}
if (await localFavoriteFile.exists()) {
LocalFavoritesManager().close();
File(FilePath.join(App.dataPath, "local_favorite.db"))
.deleteIfExistsSync();
localFavoriteFile
.renameSync(FilePath.join(App.dataPath, "local_favorite.db"));
LocalFavoritesManager().init();
}
if (await appdataFile.exists()) {
// proxy settings & authorization setting should be kept
var proxySettings = appdata.settings["proxy"];
var authSettings = appdata.settings["authorizationRequired"];
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
await appdata.init();
appdata.settings["proxy"] = proxySettings;
appdata.settings["authorizationRequired"] = authSettings;
appdata.saveData();
}
if (await cookieFile.exists()) {
SingleInstanceCookieJar.instance?.dispose();
File(FilePath.join(App.dataPath, "cookie.db")).deleteIfExistsSync();
cookieFile.renameSync(FilePath.join(App.dataPath, "cookie.db"));
SingleInstanceCookieJar.instance =
SingleInstanceCookieJar(FilePath.join(App.dataPath, "cookie.db"))
..init();
}
var comicSourceDir = FilePath.join(cacheDirPath, "comic_source");
if (Directory(comicSourceDir).existsSync()) {
for (var file in Directory(comicSourceDir).listSync()) {
if (file is File) {
var targetFile =
FilePath.join(App.dataPath, "comic_source", file.name);
File(targetFile).deleteIfExistsSync();
await file.copy(targetFile);
}
}
await ComicSource.reload();
}
} finally {
cacheDir.deleteIgnoreError(recursive: true);
}
}
Future<void> importPicaData(File file) async {
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
var cacheDir = Directory(cacheDirPath);
if (cacheDir.existsSync()) {
cacheDir.deleteSync(recursive: true);
}
cacheDir.createSync();
try {
await Isolate.run(() {
ZipFile.openAndExtract(file.path, cacheDirPath);
});
var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
if (localFavoriteFile.existsSync()) {
var db = sqlite3.open(localFavoriteFile.path);
try {
var folderNames = db
.select("SELECT name FROM sqlite_master WHERE type='table';")
.map((e) => e["name"] as String)
.toList();
folderNames.removeWhere((e) => e == "folder_order" || e == "folder_sync");
for (var folderName in folderNames) {
if (!LocalFavoritesManager().existsFolder(folderName)) {
LocalFavoritesManager().createFolder(folderName);
}
for (var comic in db.select("SELECT * FROM \"$folderName\";")) {
LocalFavoritesManager().addComic(
folderName,
FavoriteItem(
id: comic['target'],
name: comic['name'],
coverPath: comic['cover_path'],
author: comic['author'],
type: ComicType(switch(comic['type']) {
0 => 'picacg'.hashCode,
1 => 'ehentai'.hashCode,
2 => 'jm'.hashCode,
3 => 'hitomi'.hashCode,
4 => 'wnacg'.hashCode,
6 => 'nhentai'.hashCode,
_ => comic['type']
}),
tags: comic['tags'].split(','),
),
);
}
}
}
catch(e) {
Log.error("Import Data", "Failed to import local favorite: $e");
}
finally {
db.dispose();
}
}
var historyFile = cacheDir.joinFile("history.db");
if (historyFile.existsSync()) {
var db = sqlite3.open(historyFile.path);
try {
for (var comic in db.select("SELECT * FROM history;")) {
HistoryManager().addHistory(
History.fromMap({
"type": switch(comic['type']) {
0 => 'picacg'.hashCode,
1 => 'ehentai'.hashCode,
2 => 'jm'.hashCode,
3 => 'hitomi'.hashCode,
4 => 'wnacg'.hashCode,
6 => 'nhentai'.hashCode,
_ => comic['type']
},
"id": comic['target'],
"maxPage": comic["max_page"],
"ep": comic["ep"],
"page": comic["page"],
"time": comic["time"],
"title": comic["title"],
"subtitle": comic["subtitle"],
"cover": comic["cover"],
}),
);
}
}
catch(e) {
Log.error("Import Data", "Failed to import history: $e");
}
finally {
db.dispose();
}
}
} finally {
cacheDir.deleteIgnoreError(recursive: true);
} }
} }

210
lib/utils/epub.dart Normal file
View File

@@ -0,0 +1,210 @@
import 'dart:isolate';
import 'package:uuid/uuid.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/utils/file_type.dart';
import 'package:venera/utils/io.dart';
import 'package:zip_flutter/zip_flutter.dart';
class EpubData {
final String title;
final String author;
final File cover;
final Map<String, List<File>> chapters;
const EpubData({
required this.title,
required this.author,
required this.cover,
required this.chapters,
});
}
Future<File> createEpubComic(EpubData data, String cacheDir) async {
final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
if (workingDir.existsSync()) {
workingDir.deleteSync(recursive: true);
}
workingDir.createSync(recursive: true);
// mimetype
workingDir.joinFile('mimetype').writeAsStringSync('application/epub+zip');
// META-INF
Directory(FilePath.join(workingDir.path, 'META-INF')).createSync();
File(FilePath.join(workingDir.path, 'META-INF', 'container.xml'))
.writeAsStringSync('''
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
''');
Directory(FilePath.join(workingDir.path, 'OEBPS')).createSync();
// copy images, create html files
final imageDir = Directory(FilePath.join(workingDir.path, 'OEBPS', 'images'));
imageDir.createSync();
final coverExt = data.cover.extension;
final coverMime = FileType.fromExtension(coverExt).mime;
imageDir
.joinFile('cover.$coverExt')
.writeAsBytesSync(data.cover.readAsBytesSync());
int imgIndex = 0;
int chapterIndex = 0;
var manifestStrBuilder = StringBuffer();
manifestStrBuilder.writeln(
' <item id="cover_image" href="OEBPS/images/cover.$coverExt" media-type="$coverMime"/>');
manifestStrBuilder.writeln(
' <item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>');
for (final chapter in data.chapters.keys) {
var images = <String>[];
for (final image in data.chapters[chapter]!) {
final ext = image.extension;
imageDir
.joinFile('img$imgIndex.$ext')
.writeAsBytesSync(image.readAsBytesSync());
images.add('images/img$imgIndex.$ext');
var mime = FileType.fromExtension(ext).mime;
manifestStrBuilder.writeln(
' <item id="img$imgIndex" href="OEBPS/images/img$imgIndex$ext" media-type="$mime"/>');
imgIndex++;
}
var html =
File(FilePath.join(workingDir.path, 'OEBPS', '$chapterIndex.html'));
html.writeAsStringSync('''
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>$chapter</title>
<style type="text/css">
img {
max-width: 100%;
height: auto;
}
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<h1>$chapter</h1>
<div>
${images.map((e) => ' <img src="$e" alt="$e"/>').join('\n')}
</div>
</body>
</html>
''');
manifestStrBuilder.writeln(
' <item id="chapter$chapterIndex" href="OEBPS/$chapterIndex.html" media-type="application/xhtml+xml"/>');
chapterIndex++;
}
// content.opf
final contentOpf =
File(FilePath.join(workingDir.path, 'content.opf'));
final uuid = const Uuid().v4();
var spineStrBuilder = StringBuffer();
for (var i = 0; i < chapterIndex; i++) {
var idRef = 'idref="chapter$i"';
spineStrBuilder.writeln(' <itemref $idRef/>');
}
contentOpf.writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0"
xmlns="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata>
<dc:title>${data.title}</dc:title>
<dc:creator>${data.author}</dc:creator>
<dc:identifier id="book_id">urn:uuid:$uuid</dc:identifier>
<meta name="cover" content="cover_image"/>
</metadata>
<manifest>
${manifestStrBuilder.toString()}
</manifest>
<spine toc="toc">
${spineStrBuilder.toString()}
</spine>
</package>
''');
// toc.ncx
final tocNcx = File(FilePath.join(workingDir.path, 'toc.ncx'));
var navMapStrBuilder = StringBuffer();
var playOrder = 2;
final chapterNames = data.chapters.keys.toList();
for (var i = 0; i < chapterIndex; i++) {
navMapStrBuilder
.writeln(' <navPoint id="chapter$i" playOrder="$playOrder">');
navMapStrBuilder.writeln(
' <navLabel><text>${chapterNames[i]}</text></navLabel>');
navMapStrBuilder.writeln(' <content src="OEBPS/$i.html"/>');
navMapStrBuilder.writeln(' </navPoint>');
playOrder++;
}
tocNcx.writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:$uuid"/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text>${data.title}</text>
</docTitle>
<navMap>
${navMapStrBuilder.toString()}
</navMap>
</ncx>
''');
// zip
final zipPath = FilePath.join(cacheDir, '${data.title}.epub');
ZipFile.compressFolder(workingDir.path, zipPath);
workingDir.deleteSync(recursive: true);
return File(zipPath);
}
Future<File> createEpubWithLocalComic(LocalComic comic) async {
var chapters = <String, List<File>>{};
if (comic.chapters == null) {
chapters[comic.title] =
(await LocalManager().getImages(comic.id, comic.comicType, 0))
.map((e) => File(e))
.toList();
} else {
for (var chapter in comic.chapters!.keys) {
chapters[comic.chapters![chapter]!] = (await LocalManager()
.getImages(comic.id, comic.comicType, chapter))
.map((e) => File(e))
.toList();
}
}
var data = EpubData(
title: comic.title,
author: comic.subtitle,
cover: comic.coverFile,
chapters: chapters,
);
final cacheDir = App.cachePath;
return Isolate.run(() => overrideIO(() async {
return createEpubComic(data, cacheDir);
}));
}

View File

@@ -13,7 +13,7 @@ class FileType {
var mime = lookupMimeType('no-file.$ext') ?? 'application/octet-stream'; var mime = lookupMimeType('no-file.$ext') ?? 'application/octet-stream';
// Android doesn't support some mime types // Android doesn't support some mime types
mime = switch(mime) { mime = switch(mime) {
'text/javascript' => 'application/javascript', 'text/javascript' => 'application/octet-stream',
'application/x-cbr' => 'application/octet-stream', 'application/x-cbr' => 'application/octet-stream',
_ => mime, _ => mime,
}; };

View File

@@ -20,9 +20,9 @@ class ImportComic {
const ImportComic({this.selectedFolder, this.copyToLocal = true}); const ImportComic({this.selectedFolder, this.copyToLocal = true});
Future<bool> cbz() async { Future<bool> cbz() async {
var file = await selectFile(ext: ['cbz']); var file = await selectFile(ext: ['cbz', 'zip']);
Map<String?, List<LocalComic>> imported = {}; Map<String?, List<LocalComic>> imported = {};
if(file == null) { if (file == null) {
return false; return false;
} }
var controller = showLoadingDialog(App.rootContext, allowCancel: false); var controller = showLoadingDialog(App.rootContext, allowCancel: false);
@@ -34,7 +34,34 @@ class ImportComic {
App.rootContext.showMessage(message: e.toString()); App.rootContext.showMessage(message: e.toString());
} }
controller.close(); controller.close();
return registerComics(imported, true); return registerComics(imported, false);
}
Future<bool> multipleCbz() async {
var picker = DirectoryPicker();
var dir = await picker.pickDirectory();
if (dir != null) {
var files = (await dir.list().toList()).whereType<File>().toList();
files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip');
Map<String?, List<LocalComic>> imported = {};
var controller = showLoadingDialog(App.rootContext, allowCancel: false);
var comics = <LocalComic>[];
for (var file in files) {
try {
var comic = await CBZ.import(file);
comics.add(comic);
} catch (e, s) {
Log.error("Import Comic", e.toString(), s);
}
}
if (comics.isEmpty) {
App.rootContext.showMessage(message: "No valid comics found".tl);
}
imported[selectedFolder] = comics;
controller.close();
return registerComics(imported, false);
}
return false;
} }
Future<bool> ehViewer() async { Future<bool> ehViewer() async {
@@ -60,10 +87,10 @@ class ImportComic {
if (cancelled) { if (cancelled) {
return imported; return imported;
} }
var comicDir = openDirectoryPlatform( var comicDir = Directory(
FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
String titleJP = String titleJP =
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
String title = titleJP == "" ? comic['TITLE'] as String : titleJP; String title = titleJP == "" ? comic['TITLE'] as String : titleJP;
int timeStamp = comic['TIME'] as int; int timeStamp = comic['TIME'] as int;
DateTime downloadTime = timeStamp != 0 DateTime downloadTime = timeStamp != 0
@@ -105,8 +132,7 @@ class ImportComic {
if (cancelled) { if (cancelled) {
break; break;
} }
var folderName = var folderName = tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag';
tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag';
var comicList = db.select(""" var comicList = db.select("""
SELECT * SELECT *
FROM DOWNLOAD_DIRNAME DN FROM DOWNLOAD_DIRNAME DN
@@ -133,7 +159,7 @@ class ImportComic {
App.rootContext.showMessage(message: e.toString()); App.rootContext.showMessage(message: e.toString());
} }
controller.close(); controller.close();
if(cancelled) return false; if (cancelled) return false;
return registerComics(imported, copyToLocal); return registerComics(imported, copyToLocal);
} }
@@ -173,11 +199,10 @@ class ImportComic {
//Automatically search for cover image and chapters //Automatically search for cover image and chapters
Future<LocalComic?> _checkSingleComic(Directory directory, Future<LocalComic?> _checkSingleComic(Directory directory,
{String? id, {String? id,
String? title, String? title,
String? subtitle, String? subtitle,
List<String>? tags, List<String>? tags,
DateTime? createTime}) DateTime? createTime}) async {
async {
if (!(await directory.exists())) return null; if (!(await directory.exists())) return null;
var name = title ?? directory.name; var name = title ?? directory.name;
if (LocalManager().findByName(name) != null) { if (LocalManager().findByName(name) != null) {
@@ -207,17 +232,18 @@ class ImportComic {
} }
} }
if(fileList.isEmpty) { if (fileList.isEmpty) {
return null; return null;
} }
fileList.sort(); fileList.sort();
coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ?? fileList.first; coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ??
fileList.first;
chapters.sort(); chapters.sort();
if (hasChapters && coverPath == '') { if (hasChapters && coverPath == '') {
// use the first image in the first chapter as the cover // use the first image in the first chapter as the cover
var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}'); var firstChapter = Directory('${directory.path}/${chapters.first}');
await for (var entry in firstChapter.list()) { await for (var entry in firstChapter.list()) {
if (entry is File) { if (entry is File) {
coverPath = entry.name; coverPath = entry.name;
@@ -243,26 +269,29 @@ class ImportComic {
); );
} }
static Future<Map<String, String>> _copyDirectories(Map<String, dynamic> data) async { static Future<Map<String, String>> _copyDirectories(
var toBeCopied = data['toBeCopied'] as List<String>; Map<String, dynamic> data) async {
var destination = data['destination'] as String; return overrideIO(() async {
Map<String, String> result = {}; var toBeCopied = data['toBeCopied'] as List<String>;
for (var dir in toBeCopied) { var destination = data['destination'] as String;
var source = openDirectoryPlatform(dir); Map<String, String> result = {};
var dest = openDirectoryPlatform("$destination/${source.name}"); for (var dir in toBeCopied) {
if (dest.existsSync()) { var source = Directory(dir);
// The destination directory already exists, and it is not managed by the app. var dest = Directory("$destination/${source.name}");
// Rename the old directory to avoid conflicts. if (dest.existsSync()) {
Log.info("Import Comic", // The destination directory already exists, and it is not managed by the app.
"Directory already exists: ${source.name}\nRenaming the old directory."); // Rename the old directory to avoid conflicts.
await dest.rename( Log.info("Import Comic",
findValidDirectoryName(dest.parent.path, "${dest.path}_old")); "Directory already exists: ${source.name}\nRenaming the old directory.");
dest.renameSync(
findValidDirectoryName(dest.parent.path, "${dest.path}_old"));
}
dest.createSync();
await copyDirectory(source, dest);
result[source.path] = dest.path;
} }
dest.createSync(); return result;
await copyDirectory(source, dest); });
result[source.path] = dest.path;
}
return result;
} }
Future<Map<String?, List<LocalComic>>> _copyComicsToLocalDir( Future<Map<String?, List<LocalComic>>> _copyComicsToLocalDir(
@@ -284,36 +313,36 @@ class ImportComic {
// copy the comics to the local directory // copy the comics to the local directory
var pathMap = await compute<Map<String, dynamic>, Map<String, String>>( var pathMap = await compute<Map<String, dynamic>, Map<String, String>>(
_copyDirectories, { _copyDirectories, {
'toBeCopied': comics[favoriteFolder]!.map((e) => e.directory).toList(), 'toBeCopied':
comics[favoriteFolder]!.map((e) => e.directory).toList(),
'destination': destPath, 'destination': destPath,
}); });
//Construct a new object since LocalComic.directory is a final String //Construct a new object since LocalComic.directory is a final String
for (var c in comics[favoriteFolder]!) { for (var c in comics[favoriteFolder]!) {
result[favoriteFolder]!.add( result[favoriteFolder]!.add(LocalComic(
LocalComic( id: c.id,
id: c.id, title: c.title,
title: c.title, subtitle: c.subtitle,
subtitle: c.subtitle, tags: c.tags,
tags: c.tags, directory: pathMap[c.directory]!,
directory: pathMap[c.directory]!, chapters: c.chapters,
chapters: c.chapters, cover: c.cover,
cover: c.cover, comicType: c.comicType,
comicType: c.comicType, downloadedChapters: c.downloadedChapters,
downloadedChapters: c.downloadedChapters, createdAt: c.createdAt,
createdAt: c.createdAt ));
)
);
} }
} catch (e) { } catch (e, s) {
App.rootContext.showMessage(message: "Failed to copy comics".tl); App.rootContext.showMessage(message: "Failed to copy comics".tl);
Log.error("Import Comic", e.toString()); Log.error("Import Comic", e.toString(), s);
return result; return result;
} }
} }
return result; return result;
} }
Future<bool> registerComics(Map<String?, List<LocalComic>> importedComics, bool copy) async { Future<bool> registerComics(
Map<String?, List<LocalComic>> importedComics, bool copy) async {
try { try {
if (copy) { if (copy) {
importedComics = await _copyComicsToLocalDir(importedComics); importedComics = await _copyComicsToLocalDir(importedComics);
@@ -328,25 +357,23 @@ class ImportComic {
LocalFavoritesManager().addComic( LocalFavoritesManager().addComic(
folder, folder,
FavoriteItem( FavoriteItem(
id: id, id: id,
name: comic.title, name: comic.title,
coverPath: comic.cover, coverPath: comic.cover,
author: comic.subtitle, author: comic.subtitle,
type: comic.comicType, type: comic.comicType,
tags: comic.tags, tags: comic.tags,
favoriteTime: comic.createdAt favoriteTime: comic.createdAt));
)
);
} }
} }
} }
App.rootContext.showMessage( App.rootContext.showMessage(
message: "Imported @a comics".tlParams({ message: "Imported @a comics".tlParams({
'a': importedCount, 'a': importedCount,
})); }));
} catch(e) { } catch (e, s) {
App.rootContext.showMessage(message: "Failed to register comics".tl); App.rootContext.showMessage(message: "Failed to register comics".tl);
Log.error("Import Comic", e.toString()); Log.error("Import Comic", e.toString(), s);
return false; return false;
} }
return true; return true;

View File

@@ -72,7 +72,17 @@ extension FileSystemEntityExt on FileSystemEntity {
} }
extension FileExtension on File { extension FileExtension on File {
/// Get the file extension, not including the dot.
String get extension => path.split('.').last; String get extension => path.split('.').last;
/// Copy the file to the specified path using memory.
///
/// This method prevents errors caused by files from different file systems.
Future<void> copyMem(String newPath) async {
var newFile = File(newPath);
// Stream is not usable since [AndroidFile] does not support [openRead].
await newFile.writeAsBytes(await readAsBytes());
}
} }
extension DirectoryExtension on Directory { extension DirectoryExtension on Directory {
@@ -81,7 +91,7 @@ extension DirectoryExtension on Directory {
int total = 0; int total = 0;
for (var f in listSync(recursive: true)) { for (var f in listSync(recursive: true)) {
if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) { if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) {
total += await openFilePlatform(f.path).length(); total += await File(f.path).length();
} }
} }
return total; return total;
@@ -93,7 +103,21 @@ extension DirectoryExtension on Directory {
} }
File joinFile(String name) { File joinFile(String name) {
return openFilePlatform(FilePath.join(path, name)); return File(FilePath.join(path, name));
}
void deleteContentsSync({recursive = true}) {
if (!existsSync()) return;
for (var f in listSync()) {
f.deleteIfExistsSync(recursive: recursive);
}
}
Future<void> deleteContents({recursive = true}) async {
if (!existsSync()) return;
for (var f in listSync()) {
await f.deleteIfExists(recursive: recursive);
}
} }
} }
@@ -124,14 +148,15 @@ String sanitizeFileName(String fileName) {
Future<void> copyDirectory(Directory source, Directory destination) async { Future<void> copyDirectory(Directory source, Directory destination) async {
List<FileSystemEntity> contents = source.listSync(); List<FileSystemEntity> contents = source.listSync();
for (FileSystemEntity content in contents) { for (FileSystemEntity content in contents) {
String newPath = destination.path + String newPath = FilePath.join(destination.path, content.name);
Platform.pathSeparator +
content.path.split(Platform.pathSeparator).last;
if (content is File) { if (content is File) {
content.copySync(newPath); var resultFile = File(newPath);
resultFile.createSync();
var data = content.readAsBytesSync();
resultFile.writeAsBytesSync(data);
} else if (content is Directory) { } else if (content is Directory) {
Directory newDirectory = openDirectoryPlatform(newPath); Directory newDirectory = Directory(newPath);
newDirectory.createSync(); newDirectory.createSync();
copyDirectory(content.absolute, newDirectory.absolute); copyDirectory(content.absolute, newDirectory.absolute);
} }
@@ -140,18 +165,16 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
Future<void> copyDirectoryIsolate( Future<void> copyDirectoryIsolate(
Directory source, Directory destination) async { Directory source, Directory destination) async {
await Isolate.run(() { await Isolate.run(() => overrideIO(() => copyDirectory(source, destination)));
copyDirectory(source, destination);
});
} }
String findValidDirectoryName(String path, String directory) { String findValidDirectoryName(String path, String directory) {
var name = sanitizeFileName(directory); var name = sanitizeFileName(directory);
var dir = openDirectoryPlatform("$path/$name"); var dir = Directory("$path/$name");
var i = 1; var i = 1;
while (dir.existsSync() && dir.listSync().isNotEmpty) { while (dir.existsSync() && dir.listSync().isNotEmpty) {
name = sanitizeFileName("$directory($i)"); name = sanitizeFileName("$directory($i)");
dir = openDirectoryPlatform("$path/$name"); dir = Directory("$path/$name");
i++; i++;
} }
return name; return name;
@@ -184,11 +207,12 @@ class DirectoryPicker {
directory = (await AndroidDirectory.pickDirectory())?.path; directory = (await AndroidDirectory.pickDirectory())?.path;
} else { } else {
// ios, macos // ios, macos
directory = await _methodChannel.invokeMethod<String?>("getDirectoryPath"); directory =
await _methodChannel.invokeMethod<String?>("getDirectoryPath");
} }
if (directory == null) return null; if (directory == null) return null;
_finalizer.attach(this, directory); _finalizer.attach(this, directory);
return openDirectoryPlatform(directory); return Directory(directory);
} finally { } finally {
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
IO._isSelectingFiles = false; IO._isSelectingFiles = false;
@@ -249,7 +273,9 @@ Future<FileSelectResult?> selectFile({required List<String> ext}) async {
file = FileSelectResult(xFile.path); file = FileSelectResult(xFile.path);
} }
if (!ext.contains(file.path.split(".").last)) { if (!ext.contains(file.path.split(".").last)) {
App.rootContext.showMessage(message: "Invalid file type"); App.rootContext.showMessage(
message: "Invalid file type: ${file.path.split(".").last}",
);
return null; return null;
} }
return file; return file;
@@ -311,31 +337,43 @@ Future<void> saveFile(
} }
} }
Directory openDirectoryPlatform(String path) { class _IOOverrides extends IOOverrides {
if(App.isAndroid) { @override
var dir = AndroidDirectory.fromPathSync(path); Directory createDirectory(String path) {
if(dir == null) { if (App.isAndroid) {
return Directory(path); var dir = AndroidDirectory.fromPathSync(path);
if (dir == null) {
return super.createDirectory(path);
}
return dir;
} else {
return super.createDirectory(path);
} }
return dir;
} else {
return Directory(path);
} }
@override
File createFile(String path) {
if (path.startsWith("file://")) {
path = path.substring(7);
}
if (App.isAndroid) {
var f = AndroidFile.fromPathSync(path);
if (f == null) {
return super.createFile(path);
}
return f;
} else {
return super.createFile(path);
}
}
} }
File openFilePlatform(String path) { T overrideIO<T>(T Function() f) {
if(path.startsWith("file://")) { return IOOverrides.runWithIOOverrides<T>(
path = path.substring(7); f,
} _IOOverrides(),
if(App.isAndroid) { );
var f = AndroidFile.fromPathSync(path);
if(f == null) {
return File(path);
}
return f;
} else {
return File(path);
}
} }
class Share { class Share {
@@ -396,4 +434,4 @@ class FileSelectResult {
} }
String get name => File(path).name; String get name => File(path).name;
} }

92
lib/utils/pdf.dart Normal file
View File

@@ -0,0 +1,92 @@
import 'dart:isolate';
import 'package:pdf/widgets.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/utils/io.dart';
Future<void> _createPdfFromComic({
required LocalComic comic,
required String savePath,
required String localPath,
}) async {
final pdf = Document(
title: comic.title,
author: comic.subTitle ?? "",
producer: "Venera",
);
pdf.document.outline;
var baseDir = comic.directory.contains('/') || comic.directory.contains('\\')
? comic.directory
: FilePath.join(localPath, comic.directory);
// add cover
var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync();
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
bool multiChapters = comic.chapters != null;
void reorderFiles(List<FileSystemEntity> files) {
files.removeWhere(
(element) => element is! File || element.path.startsWith('cover'));
files.sort((a, b) {
var aName = (a as File).name;
var bName = (b as File).name;
var aNumber = int.tryParse(aName);
var bNumber = int.tryParse(bName);
if (aNumber != null && bNumber != null) {
return aNumber.compareTo(bNumber);
}
return aName.compareTo(bName);
});
}
if (!multiChapters) {
var files = Directory(baseDir).listSync();
reorderFiles(files);
for (var file in files) {
var imageData = (file as File).readAsBytesSync();
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
}
} else {
for (var chapter in comic.chapters!.keys) {
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
reorderFiles(files);
for (var file in files) {
var imageData = (file as File).readAsBytesSync();
pdf.addPage(Page(
build: (Context context) {
return Image(MemoryImage(imageData), fit: BoxFit.contain);
},
));
}
}
}
final file = File(savePath);
file.writeAsBytesSync(await pdf.save());
}
Future<void> createPdfFromComicIsolate({
required LocalComic comic,
required String savePath,
}) async {
var localPath = LocalManager().path;
return Isolate.run(() => overrideIO(() async {
return await _createPdfFromComic(
comic: comic,
savePath: savePath,
localPath: localPath,
);
}));
}

43
macos/Podfile Normal file
View File

@@ -0,0 +1,43 @@
platform :osx, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View File

@@ -557,7 +557,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
@@ -639,7 +639,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@@ -689,7 +689,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -33,6 +33,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -49,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.8"
battery_plus: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -65,6 +81,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
bidi:
dependency: transitive
description:
name: bidi
sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d"
url: "https://pub.dev"
source: hosted
version: "2.0.12"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -101,18 +125,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
name: convert name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@@ -133,10 +157,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: csslib name: csslib
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.2"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -170,6 +194,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
url: "https://pub.dev"
source: hosted
version: "1.7.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -190,10 +222,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.1"
file_selector: file_selector:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -206,10 +238,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_android name: file_selector_android
sha256: ec439df07c4999faad319ce8ad9e971795c2f1d7132ad5a793b9370a863c6128 sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1+10" version: "0.5.1+12"
file_selector_ios: file_selector_ios:
dependency: transitive dependency: transitive
description: description:
@@ -222,10 +254,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_linux name: file_selector_linux
sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3" version: "0.9.3+2"
file_selector_macos: file_selector_macos:
dependency: transitive dependency: transitive
description: description:
@@ -262,10 +294,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fixnum name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -299,10 +331,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_internal_annotations name: flutter_inappwebview_internal_annotations
sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.2.0"
flutter_inappwebview_ios: flutter_inappwebview_ios:
dependency: transitive dependency: transitive
description: description:
@@ -347,15 +379,23 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "5.0.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_memory_info:
dependency: "direct main"
description:
name: flutter_memory_info
sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74"
url: "https://pub.dev"
source: hosted
version: "0.0.1"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -368,8 +408,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: ade0b9d ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f"
resolved-ref: ade0b9d67331118c13a2b836684858e251512373 resolved-ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f"
url: "https://github.com/wgh136/flutter_qjs" url: "https://github.com/wgh136/flutter_qjs"
source: git source: git
version: "0.3.7" version: "0.3.7"
@@ -377,24 +417,24 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_reorderable_grid_view name: flutter_reorderable_grid_view
sha256: "93a2b9e279bf40b9333428a67e70e520ca1528554984eb6f6304538400897e64" sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.3.2" version: "5.4.0"
flutter_rust_bridge: flutter_rust_bridge:
dependency: transitive dependency: transitive
description: description:
name: flutter_rust_bridge name: flutter_rust_bridge
sha256: "5fe868d3cb8cbc4d83091748552e03f00ccfa41b8e44691bc382611f831d5f8b" sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.0"
flutter_saf: flutter_saf:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "829a566b738a26ea98e523807f49838e21308543" ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
resolved-ref: "829a566b738a26ea98e523807f49838e21308543" resolved-ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
url: "https://github.com/pkuislm/flutter_saf.git" url: "https://github.com/pkuislm/flutter_saf.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@@ -406,11 +446,10 @@ packages:
flutter_to_arch: flutter_to_arch:
dependency: "direct dev" dependency: "direct dev"
description: description:
path: "." name: flutter_to_arch
ref: HEAD sha256: "656cffc182b05af38aa96a1115931620b8865c4b0cfe00813b26fcff0875f2ab"
resolved-ref: "15bfead0380fda79b0256b37c73b886b0882f1bf" url: "https://pub.dev"
url: "https://github.com/wgh136/flutter_to_arch" source: hosted
source: git
version: "1.0.0" version: "1.0.0"
flutter_to_debian: flutter_to_debian:
dependency: "direct dev" dependency: "direct dev"
@@ -461,10 +500,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.1.1"
http_profile:
dependency: transitive
description:
name: http_profile
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
image:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "4.3.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -477,10 +532,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: io name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
js: js:
dependency: transitive dependency: transitive
description: description:
@@ -501,18 +556,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "10.0.7"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.8"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@@ -525,10 +580,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "5.1.0"
local_auth: local_auth:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -573,8 +628,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 resolved-ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
url: "https://github.com/venera-app/lodepng_flutter" url: "https://github.com/venera-app/lodepng_flutter"
source: git source: git
version: "0.0.1" version: "0.0.1"
@@ -626,30 +681,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.10" version: "2.2.15"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -674,6 +737,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07"
url: "https://pub.dev"
source: hosted
version: "3.11.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -695,10 +766,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -715,14 +786,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.1" version: "3.9.1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
rhttp: rhttp:
dependency: "direct main" dependency: "direct main"
description: description:
name: rhttp name: rhttp
sha256: "92fb57dea6338370efe1e4e2101e8b521f91f15bc60ef6908469b4392dd9803a" sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.1" version: "0.9.6"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -788,11 +867,19 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "5.0.1"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
sliver_tools: sliver_tools:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -821,26 +908,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3 name: sqlite3
sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.7" version: "2.5.0"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.24" version: "0.5.27"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -853,10 +940,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -869,18 +956,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.3"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.4.0"
upower: upower:
dependency: transitive dependency: transitive
description: description:
@@ -893,42 +980,42 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.3.1"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.9" version: "6.3.14"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.1" version: "6.3.2"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.2"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -949,10 +1036,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
uuid: uuid:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -973,10 +1060,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.3.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@@ -998,10 +1085,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.4" version: "5.9.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1014,10 +1101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.1.0"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@@ -1037,12 +1124,11 @@ packages:
zip_flutter: zip_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: zip_flutter
ref: HEAD sha256: "955b53d58709fcd9feefbed3d41b5522bc5273e677603e9fc67017a81e568d24"
resolved-ref: d5721f1fd8179ee4a5db59f932ae7c89d94e12a0 url: "https://pub.dev"
url: "https://github.com/wgh136/zip_flutter" source: hosted
source: git version: "0.0.5"
version: "0.0.1"
sdks: sdks:
dart: ">=3.5.4 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.5" flutter: ">=3.27.1"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.0.7+107 version: 1.1.2+112
environment: environment:
sdk: '>=3.5.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
flutter: 3.24.5 flutter: 3.27.1
dependencies: dependencies:
flutter: flutter:
@@ -14,14 +14,14 @@ dependencies:
path_provider: any path_provider: any
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
intl: any intl: ^0.19.0
window_manager: ^0.4.3 window_manager: ^0.4.3
sqlite3: ^2.4.7 sqlite3: ^2.4.7
sqlite3_flutter_libs: any sqlite3_flutter_libs: any
flutter_qjs: flutter_qjs:
git: git:
url: https://github.com/wgh136/flutter_qjs url: https://github.com/wgh136/flutter_qjs
ref: ade0b9d ref: 1657f62fe7545ac43a339e0a5ee2b82bacd81e9f
crypto: ^3.0.6 crypto: ^3.0.6
dio: ^5.7.0 dio: ^5.7.0
html: ^0.15.5 html: ^0.15.5
@@ -39,7 +39,7 @@ dependencies:
url: https://github.com/venera-app/flutter.widgets url: https://github.com/venera-app/flutter.widgets
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59 ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
path: packages/scrollable_positioned_list path: packages/scrollable_positioned_list
flutter_reorderable_grid_view: 5.3.2 flutter_reorderable_grid_view: ^5.4.0
yaml: any yaml: any
uuid: ^4.5.1 uuid: ^4.5.1
desktop_webview_window: desktop_webview_window:
@@ -51,14 +51,12 @@ dependencies:
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3 file_selector: ^1.0.3
zip_flutter: zip_flutter: ^0.0.5
git:
url: https://github.com/wgh136/zip_flutter
lodepng_flutter: lodepng_flutter:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
rhttp: 0.9.1 rhttp: 0.9.6
webdav_client: webdav_client:
git: git:
url: https://github.com/wgh136/webdav_client url: https://github.com/wgh136/webdav_client
@@ -68,14 +66,17 @@ dependencies:
flutter_saf: flutter_saf:
git: git:
url: https://github.com/pkuislm/flutter_saf.git url: https://github.com/pkuislm/flutter_saf.git
ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
pdf: ^3.11.1
dynamic_color: ^1.7.0
shimmer: ^3.0.0
flutter_memory_info: ^0.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_lints: ^5.0.0
flutter_to_arch: flutter_to_arch: ^1.0.0
git: https://github.com/wgh136/flutter_to_arch
flutter_to_debian: flutter_to_debian:
flutter: flutter:
@@ -95,4 +96,4 @@ flutter_to_arch:
url: https://github.com/venera-app/venera url: https://github.com/venera-app/venera
depends: depends:
- gtk3 - gtk3
- webkit2gtk-4.1 - webkit2gtk-4.1

View File

@@ -58,6 +58,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
@@ -66,4 +67,4 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run] [Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall