mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 00:07:24 +00:00
Compare commits
112 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bf634f8654 | ||
![]() |
bda215ebb7 | ||
a70b690d3c | |||
0b8ae2d377 | |||
24c5a1bb01 | |||
ea973a2787 | |||
![]() |
17bce96143 | ||
![]() |
909c0014ac | ||
eb1abfc02a | |||
![]() |
788e41f584 | ||
929ec88e84 | |||
abaeaf4f77 | |||
![]() |
a614e83470 | ||
8b9fd0d03d | |||
![]() |
1964c4c0d5 | ||
43d724dd27 | |||
f9c42aef4b | |||
06a6e5156a | |||
![]() |
be45a06981 | ||
4763b9c7b4 | |||
7e608be70f | |||
211e6ab8c8 | |||
![]() |
100dc6458b | ||
![]() |
8dab5f9e88 | ||
d08383e14b | |||
a55e4eff67 | |||
ab3953292b | |||
b49e0974ab | |||
![]() |
b6cccb7749 | ||
![]() |
dac07cfac4 | ||
![]() |
da12b3bcca | ||
![]() |
017f964705 | ||
![]() |
bed0f78e81 | ||
![]() |
092eb59c10 | ||
![]() |
a5d3d160c8 | ||
![]() |
d3c3748ce5 | ||
![]() |
586874de15 | ||
bda2c6c2e1 | |||
e9aa6fcf30 | |||
60c6be08c5 | |||
e4e2d264f5 | |||
c2cfd066f6 | |||
d7b91f6a50 | |||
da025b16ff | |||
08e0082186 | |||
463805f5ed | |||
72b146a9bf | |||
1104d28f14 | |||
cf7be85f29 | |||
cab66619df | |||
bdd0724788 | |||
617c452e07 | |||
c8e6e1311c | |||
0bdb1299ca | |||
af9835eb8f | |||
4801457e0e | |||
0c9f7126a2 | |||
3cf9228e2a | |||
07f8cd2455 | |||
659b211038 | |||
4e121748cd | |||
14fe901144 | |||
835b40860d | |||
ef435dcaa5 | |||
e999652a3e | |||
425cbed8a1 | |||
![]() |
488299bcfb | ||
b8bdda16c6 | |||
1a50b8bc27 | |||
546f619063 | |||
![]() |
0e831468ee | ||
a4cc0a3af2 | |||
80811bf12d | |||
21bf9d72c0 | |||
035a84380c | |||
5ddb6f47ca | |||
c1672d01f8 | |||
![]() |
66ebdb03b1 | ||
df2ba6efd1 | |||
705c448cfe | |||
a711335012 | |||
305ef9263d | |||
f8b8811aaa | |||
a868fe3fff | |||
873cbd779e | |||
d56e3fd59f | |||
d96b36414d | |||
b30bd11d1a | |||
![]() |
72507d907a | ||
9b821f1b46 | |||
867b2a4b64 | |||
8f07c8a2bb | |||
![]() |
7aed61a65e | ||
674b5c9636 | |||
153f1a9dfe | |||
6c5df47663 | |||
24188b51c0 | |||
070c803f97 | |||
b425eec561 | |||
![]() |
95c98eeaed | ||
60f7b4d3b0 | |||
2ee2a01550 | |||
a2f628001a | |||
de4503a2de | |||
30b2aa2f99 | |||
2f4927f719 | |||
9fb3482474 | |||
2063eee82b | |||
91b765ffba | |||
bbfe87fff2 | |||
![]() |
430b6eeb3a | ||
![]() |
06094fc5fc |
16
.github/workflows/fastlane.yml
vendored
Normal file
16
.github/workflows/fastlane.yml
vendored
Normal 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
|
45
.github/workflows/main.yml
vendored
45
.github/workflows/main.yml
vendored
@@ -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 }}
|
||||||
|
@@ -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'
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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" : "未找到有效的漫畫"
|
||||||
}
|
}
|
||||||
}
|
}
|
40
fastlane/metadata/android/en-US/full_description.txt
Normal file
40
fastlane/metadata/android/en-US/full_description.txt
Normal 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&logo=telegram&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&repo=Database" alt="Readme Card"></a></p>
|
||||||
|
|
||||||
|
<p>The Chinese translation of the manga tags is from this project.</p>
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
A comic reader that support reading local and network comics.
|
1
fastlane/metadata/android/en-US/title.txt
Normal file
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
venera
|
@@ -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'
|
||||||
|
@@ -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("");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -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),
|
||||||
),
|
),
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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(
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -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),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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(
|
||||||
|
@@ -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(() {
|
||||||
|
@@ -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),
|
||||||
),
|
),
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
)
|
)
|
||||||
|
@@ -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;
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
library comic_source;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
@@ -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,
|
||||||
|
@@ -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") ??
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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));
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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);
|
||||||
|
@@ -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
|
||||||
|
57
lib/foundation/image_provider/history_image_provider.dart
Normal file
57
lib/foundation/image_provider/history_image_provider.dart
Normal 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}";
|
||||||
|
}
|
66
lib/foundation/image_provider/local_comic_image.dart
Normal file
66
lib/foundation/image_provider/local_comic_image.dart
Normal 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}";
|
||||||
|
}
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
@@ -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>{
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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']);
|
||||||
}
|
}
|
246
lib/main.dart
246
lib/main.dart
@@ -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');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
230
lib/pages/aggregated_search_page.dart
Normal file
230
lib/pages/aggregated_search_page.dart
Normal 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;
|
||||||
|
}
|
@@ -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),
|
||||||
|
@@ -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(),
|
||||||
),
|
),
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -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(
|
||||||
|
@@ -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;
|
||||||
},
|
},
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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});
|
||||||
}
|
}
|
@@ -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) {
|
||||||
|
101
lib/pages/reader/loading.dart
Normal file
101
lib/pages/reader/loading.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
@@ -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,
|
||||||
|
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -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(
|
||||||
|
@@ -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);
|
||||||
|
@@ -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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
},
|
},
|
||||||
|
@@ -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,
|
||||||
|
@@ -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;
|
||||||
|
@@ -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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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',
|
||||||
|
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
210
lib/utils/epub.dart
Normal 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);
|
||||||
|
}));
|
||||||
|
}
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
92
lib/utils/pdf.dart
Normal 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
43
macos/Podfile
Normal 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
|
@@ -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;
|
||||||
|
270
pubspec.lock
270
pubspec.lock
@@ -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"
|
||||||
|
33
pubspec.yaml
33
pubspec.yaml
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user