72 Commits

Author SHA1 Message Date
b8492b3adc remove permission_handler 2024-11-10 18:10:30 +08:00
nyne
0f37feb318 Merge pull request #25 from venera-app/dev
v1.0.4
2024-11-10 17:59:58 +08:00
6e2c5c6e07 update version number 2024-11-10 17:59:06 +08:00
64d8bcba9a [Android] Turn page by volume keys 2024-11-10 17:50:20 +08:00
160d0df935 fix setting new download path 2024-11-10 17:48:12 +08:00
6a60194ffb support setting new download path on android 2024-11-10 17:27:27 +08:00
93193bddc0 Merge branch 'refs/heads/master' into dev 2024-11-10 16:01:45 +08:00
aa415f201e quick favorite 2024-11-10 15:57:52 +08:00
4f4411fcc3 sync data using webdav 2024-11-10 10:38:46 +08:00
nyne
afd690ed07 Merge pull request #24 from boa-z/master
Experimental Support for Setting New Storage Path on iOS
2024-11-09 17:16:52 +08:00
nyne
a3936f64da Delete tg.yaml 2024-11-09 17:09:02 +08:00
boa-z
7bf8cf569f experimental support for set new storage path on iOS 2024-11-09 11:04:34 +08:00
boa-z
856ec23586 add copy storage path button 2024-11-08 23:32:18 +08:00
boa-z
d910b8a35d add multiSelect for local_comics_page 2024-11-07 23:30:01 +08:00
nyne
234bf218a9 Merge pull request #23 from venera-app/telegram
Create tg.yaml
2024-11-07 18:33:19 +08:00
nyne
0226477256 Create tg.yaml 2024-11-07 18:33:06 +08:00
nyne
42ded1221a Merge pull request #22 from venera-app/dev
v 1.0.3
2024-11-07 10:26:40 +08:00
a9a22ace14 update README.md 2024-11-07 10:20:50 +08:00
99bbea80dc update version code 2024-11-07 09:56:10 +08:00
26fa41f503 improve translation 2024-11-07 09:41:14 +08:00
082aa36316 improve reader; fix #21 2024-11-07 09:31:57 +08:00
5a14ea48c1 fix changing search target in search result page 2024-11-07 09:02:03 +08:00
5d43f5c556 fix favorites page 2024-11-07 08:59:49 +08:00
e51a58ba4f fix deleting files when canceling a task 2024-11-07 08:49:32 +08:00
5234de434a improve network log 2024-11-06 22:08:23 +08:00
22f2ac99ad fix http 2024-11-06 18:06:20 +08:00
b08b5d0abe update action 2024-11-06 17:43:36 +08:00
nyne
96c6323c07 Merge pull request #18 from venera-app/dev
v1.0.2-patch
2024-11-06 09:21:29 +08:00
ae80715db1 update windows build script 2024-11-06 08:57:06 +08:00
3d7f30af00 update .gitignore 2024-11-06 08:53:57 +08:00
f12cb55bbc update windows build script 2024-11-06 08:51:20 +08:00
nyne
1cc30c5748 Merge pull request #17 from venera-app/dev
v1.0.2
2024-11-05 22:55:32 +08:00
af371df2a4 update windows build script 2024-11-05 22:53:01 +08:00
98b9e6e9d9 fix http 2024-11-05 20:18:10 +08:00
96c75300d0 update info 2024-11-05 17:03:19 +08:00
a6608b6fa2 improve ui 2024-11-05 16:50:32 +08:00
b09e2e6f12 use rhttp 2024-11-05 16:46:01 +08:00
7991f1a385 check updates on start 2024-11-05 16:04:10 +08:00
afa320e863 add 'Long press to zoom' setting 2024-11-05 15:34:05 +08:00
adb6cdd0c1 improve ui 2024-11-05 15:27:46 +08:00
b49e528ff4 improve image api & update version code 2024-11-05 13:13:32 +08:00
07f8f2a4af fix aes decryption 2024-11-04 17:47:58 +08:00
0fbe9677b9 image api 2024-11-04 12:28:58 +08:00
45e7f0dfc2 add download threads setting 2024-11-03 15:49:34 +08:00
deltamaya
9e0e318107 format code 2024-11-03 11:51:00 +08:00
deltamaya
03727d114c added like button interaction 2024-11-03 11:48:01 +08:00
deltamaya
6cf5c7b27b centered the episode text 2024-11-03 11:42:34 +08:00
deltamaya
173689b57e format code 2024-11-03 11:14:04 +08:00
deltamaya
8fb39b1ec8 fix refresh button overlap with next page button 2024-11-03 11:13:27 +08:00
deltamaya
679462f272 update .gitignore 2024-11-03 10:46:40 +08:00
nyne
ee944a2869 Merge pull request #15 from boa-z/master
Enhancements for accounts_page and macOS build
2024-11-03 10:02:23 +08:00
boa-z
bbb414757d fix: Open in browser and Copy Link 2024-11-03 08:18:33 +08:00
boa-z
f2335894a4 macos build action
Create the DMG file with Applications shortcut
2024-11-02 23:33:38 +08:00
boa-z
77ef0fb404 support autofill in accounts_page 2024-11-02 23:31:12 +08:00
nyne
28913adc86 update README.md 2024-11-02 20:35:15 +08:00
nyne
cd607ff337 Merge branch 'refs/heads/dev' 2024-11-02 20:31:43 +08:00
nyne
eecd30f77d update version code 2024-11-02 20:31:02 +08:00
nyne
49174a7d8e local favorites search page 2024-11-02 20:29:44 +08:00
nyne
c4d867db89 data exporting & importing 2024-11-02 20:12:48 +08:00
nyne
19a93cbbce improve history 2024-11-02 19:14:03 +08:00
nyne
877e2d5e63 fix #14 2024-11-02 18:59:41 +08:00
nyne
98ae67a6a5 implement view more 2024-11-02 12:05:45 +08:00
nyne
2db3f5a72e make explore pages keep alive and listen for settings change 2024-11-02 10:00:23 +08:00
nyne
2d628ec9b1 fix #11 2024-11-01 23:15:11 +08:00
nyne
b1b516381d Merge pull request #10 from Pacalini/flbtn
continuous mode: fix floating button
2024-11-01 15:20:05 +08:00
Pacalini
048a68f76a continuous mode: fix floating button 2024-11-01 11:41:34 +08:00
nyne
11bbbdca0e Merge pull request #9 from Pacalini/rmrf
local migrate: delete recursively
2024-10-31 23:37:17 +08:00
Pacalini
d48edc6331 local migrate: delete recursively 2024-10-31 22:41:33 +08:00
nyne
13c775b7ce Merge pull request #8 from boa-z/master
fix: ReaderScaffold Bottom not fully hidden on iOS
2024-10-31 21:20:22 +08:00
boa-z
d0e76dd3a0 fix: ReaderScaffold Bottom not fully hidden on iOS 2024-10-31 18:21:50 +08:00
nyne
37997af173 Merge pull request #6 from Pacalini/accountbadge
main page: fix dulplicated account badge
2024-10-31 17:26:17 +08:00
Pacalini
82478fa247 main page: fix dulplicated account badge 2024-10-31 16:10:14 +08:00
69 changed files with 2792 additions and 481 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch: {}
jobs:
Build_MacOS:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
@@ -12,7 +12,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: flutter pub get
# Step 1: Decode and install the certificate
- name: Decode and install certificate
@@ -27,23 +27,23 @@ jobs:
- name: Build Flutter macOS App
run: flutter build macos --release
# Step 4: Create the DMG file
# Step 3: Create the DMG file
- name: Create DMG
run: |
mkdir -p dist
hdiutil create -volname "venera" -srcfolder build/macos/Build/Products/Release/venera.app -ov -format UDZO "dist/venera.dmg"
mkdir -p dist/dmg_contents
cp -R build/macos/Build/Products/Release/venera.app dist/dmg_contents/
ln -s /Applications dist/dmg_contents/Applications
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg"
# Step 8: Attach and upload artifacts (optional)
# Step 4: Attach and upload artifacts (optional)
- name: Upload DMG
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: venera.dmg
path: dist/venera.dmg
Build_IOS:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
@@ -51,7 +51,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: flutter pub get
- run: flutter build ios --release --no-codesign
- run: |

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
add_translation.py

View File

@@ -4,14 +4,26 @@
[![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![stars](https://img.shields.io/github/stars/venera-app/venera)](https://github.com/venera-app/venera/stargazers)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/+Ws-IpmUutzkxMjhl)
A comic reader that support reading local and network comics.
## Current Status
## Features
The project is still under development, and the current version is not stable.
- Read local comics
- Use javascript to create comic sources
- Read comics from network sources
- Manage favorite comics
- Download comics
- View comments, tags, and other information of comics if the source supports
- Login to comment, rate, and other operations if the source supports
Use the project at your own risk.
## Build from source
1. Clone the repository
2. Install flutter, see [flutter.dev](https://flutter.dev/docs/get-started/install)
3. Install rust, see [rustup.rs](https://rustup.rs/)
4. Build for your platform: e.g. `flutter build apk`
## Create a new comic source

View File

@@ -75,6 +75,9 @@ android {
buildTypes {
release {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.all { output ->

View File

@@ -1,5 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="venera"
android:name="${applicationName}"

View File

@@ -3,8 +3,16 @@ package com.github.wgh136.venera
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.view.KeyEvent
import android.Manifest
import android.os.Environment
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -23,6 +31,9 @@ class MainActivity : FlutterActivity() {
private lateinit var result: MethodChannel.Result
private val storageRequestCode = 0x10
private var storagePermissionRequest: ((Boolean) -> Unit)? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == pickDirectoryCode) {
@@ -43,6 +54,11 @@ class MainActivity : FlutterActivity() {
result.error("Failed to Copy Files", e.toString(), null)
}
}.start()
} else if (requestCode == storageRequestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
storagePermissionRequest?.invoke(Environment.isExternalStorageManager())
}
storagePermissionRequest = null
}
}
@@ -89,6 +105,13 @@ class MainActivity : FlutterActivity() {
listening = false
}
})
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
storageChannel.setMethodCallHandler { _, res ->
requestStoragePermission {result ->
res.success(result)
}
}
}
private fun getProxy(): String {
@@ -145,6 +168,61 @@ class MainActivity : FlutterActivity() {
}
}
}
private fun requestStoragePermission(result: (Boolean) -> Unit) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val readPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
val writePermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
if (!readPermission || !writePermission) {
storagePermissionRequest = result
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
storageRequestCode
)
} else {
result(true)
}
} else {
if (!Environment.isExternalStorageManager()) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = Uri.parse("package:" + context.packageName)
startActivityForResult(intent, storageRequestCode)
} catch (e: Exception) {
result(false)
}
} else {
result(true)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == storageRequestCode) {
storagePermissionRequest?.invoke(grantResults.all {
it == PackageManager.PERMISSION_GRANTED
})
storagePermissionRequest = null
}
}
}
class VolumeListen{

View File

@@ -224,7 +224,25 @@ let Convert = {
key: key,
isEncode: false
});
}
},
/** Encode bytes to hex string
* @param bytes {ArrayBuffer}
* @return {string}
*/
hexEncode: (bytes) => {
const hexDigits = '0123456789abcdef';
const view = new Uint8Array(bytes);
let charCodes = new Uint8Array(view.length * 2);
let j = 0;
for (let i = 0; i < view.length; i++) {
let byte = view[i];
charCodes[j++] = hexDigits.charCodeAt((byte >> 4) & 0xF);
charCodes[j++] = hexDigits.charCodeAt(byte & 0xF);
}
return String.fromCharCode(...charCodes);
},
}
/**
@@ -999,4 +1017,118 @@ class ComicSource {
init() { }
static sources = {}
}
}
/// A reference to dart object.
/// The api can only be used in the comic.onImageLoad.modifyImage function
class Image {
key = 0;
constructor(key) {
this.key = key;
}
/**
* Copy the specified range of the image
* @param x
* @param y
* @param width
* @param height
* @returns {Image|null}
*/
copyRange(x, y, width, height) {
let key = sendMessage({
method: "image",
function: "copyRange",
key: this.key,
x: x,
y: y,
width: width,
height: height
})
if(key == null) return null;
return new Image(key);
}
/**
* Copy the image and rotate 90 degrees
* @returns {Image|null}
*/
copyAndRotate90() {
let key = sendMessage({
method: "image",
function: "copyAndRotate90",
key: this.key
})
if(key == null) return null;
return new Image(key);
}
/**
* fill [image] to this image at (x, y)
* @param x
* @param y
* @param image
*/
fillImageAt(x, y, image) {
sendMessage({
method: "image",
function: "fillImageAt",
key: this.key,
x: x,
y: y,
image: image.key
})
}
/**
* fill [image] with range(srcX, srcY, width, height) to this image at (x, y)
* @param x
* @param y
* @param image
* @param srcX
* @param srcY
* @param width
* @param height
*/
fillImageRangeAt(x, y, image, srcX, srcY, width, height) {
sendMessage({
method: "image",
function: "fillImageRangeAt",
key: this.key,
x: x,
y: y,
image: image.key,
srcX: srcX,
srcY: srcY,
width: width,
height: height
})
}
get width() {
return sendMessage({
method: "image",
function: "getWidth",
key: this.key
})
}
get height() {
return sendMessage({
method: "image",
function: "getHeight",
key: this.key
})
}
static empty(width, height) {
let key = sendMessage({
method: "image",
function: "emptyImage",
width: width,
height: height
})
return new Image(key);
}
}

View File

@@ -19,7 +19,7 @@
"Select": "选择",
"Imported @a comics": "已导入 @a 部漫画",
"Downloading": "下载中",
"Back": "返回",
"Back": "后退",
"Delete": "删除",
"Full Screen": "全屏",
"Auto Page Turning": "自动翻页",
@@ -146,7 +146,50 @@
"Select a cbz file." : "选择一个cbz文件",
"A cbz file" : "一个cbz文件",
"Fullscreen": "全屏",
"Exit": "退出"
"Exit": "退出",
"View more": "查看更多",
"Sort": "排序",
"Name": "名称",
"Date": "日期",
"Date Desc": "日期降序",
"Start": "开始",
"Export App Data": "导出应用数据",
"Import App Data": "导入应用数据",
"Export": "导出",
"Download Threads": "下载线程数",
"Update Time": "更新时间",
"Copy ID": "复制ID",
"Copy URL": "复制URL",
"Create": "创建",
"Folder Name": "文件夹名称",
"Ranking": "排行",
"Download Selected": "下载选中",
"Download All": "下载全部",
"Order": "顺序",
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "长按缩放",
"Updates Available": "更新可用",
"Unselected": "未选择",
"Long press and drag to reorder.": "长按并拖动以重新排序。",
"Limit image width": "限制图片宽度",
"When using Continuous(Top to Bottom) mode": "当使用连续(从上到下)模式",
"Open link": "打开链接",
"Open comic": "打开漫画",
"Move To First": "移动到最前",
"Cancel": "取消",
"Paused": "已暂停",
"Pause": "暂停",
"Operation": "操作",
"Upload": "上传",
"Saved": "已保存",
"Sync Data": "同步数据",
"Syncing Data": "正在同步数据",
"Data Sync": "数据同步",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加",
"Turn page by volume keys": "使用音量键翻页"
},
"zh_TW": {
"Home": "首頁",
@@ -169,7 +212,7 @@
"Select": "選擇",
"Imported @a comics": "已匯入 @a 部漫畫",
"Downloading": "下載中",
"Back": "返回",
"Back": "後退",
"Delete": "刪除",
"Full Screen": "全螢幕",
"Auto Page Turning": "自動翻頁",
@@ -295,6 +338,49 @@
"Select a cbz file." : "選擇一個cbz文件",
"A cbz file" : "一個cbz文件",
"Fullscreen": "全螢幕",
"Exit": "退出"
"Exit": "退出",
"View more": "查看更多",
"Sort": "排序",
"Name": "名稱",
"Date": "日期",
"Date Desc": "日期降序",
"Start": "開始",
"Export App Data": "匯出應用數據",
"Import App Data": "匯入應用數據",
"Export": "匯出",
"Download Threads": "下載線程數",
"Update Time": "更新時間",
"Copy ID": "複製ID",
"Copy URL": "複製URL",
"Create": "創建",
"Folder Name": "文件夾名稱",
"Ranking": "排行",
"Download Selected": "下載選中",
"Download All": "下載全部",
"Order": "順序",
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "長按縮放",
"Updates Available": "更新可用",
"Unselected": "未選擇",
"Long press and drag to reorder.": "長按並拖動以重新排序。",
"Limit image width": "限制圖片寬度",
"When using Continuous(Top to Bottom) mode": "當使用連續(從上到下)模式",
"Open link": "打開鏈接",
"Open comic": "打開漫畫",
"Move To First": "移動到最前",
"Cancel": "取消",
"Paused": "已暫停",
"Pause": "暫停",
"Operation": "操作",
"Upload": "上傳",
"Saved": "已保存",
"Sync Data": "同步數據",
"Syncing Data": "正在同步數據",
"Data Sync": "數據同步",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
"Added": "已添加",
"Turn page by volume keys": "使用音量鍵翻頁"
}
}

View File

@@ -15,6 +15,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C0086D072CDEFE6E004596D9 /* DirectoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -59,6 +60,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryPicker.swift; sourceTree = "<group>"; };
C22B8A9F3177D4A68EB8F66B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -133,6 +135,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */,
);
path = Runner;
sourceTree = "<group>";
@@ -144,7 +147,6 @@
730F73FE38E23FCF3E461640 /* Pods-Runner.release.xcconfig */,
29B89F848F26E839605E1D88 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -336,6 +338,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0086D072CDEFE6E004596D9 /* DirectoryPicker.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);

View File

@@ -1,12 +1,16 @@
import Flutter
import UIKit
import UniformTypeIdentifiers
import Foundation //
@main
@objc class AppDelegate: FlutterAppDelegate, UIDocumentPickerDelegate {
var flutterResult: FlutterResult?
var directoryPath: URL!
//
private var directoryPicker: DirectoryPicker?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -42,6 +46,9 @@ import UniformTypeIdentifiers
self.directoryPath?.stopAccessingSecurityScopedResource()
self.directoryPath = nil
result(nil)
} else if call.method == "selectDirectory" {
self.directoryPicker = DirectoryPicker()
self.directoryPicker?.selectDirectory(result: result)
} else {
result(FlutterMethodNotImplemented)
}

View File

@@ -0,0 +1,36 @@
import UIKit
import Flutter
class DirectoryPicker: NSObject, UIDocumentPickerDelegate {
private var result: FlutterResult?
//
func selectDirectory(result: @escaping FlutterResult) {
self.result = result
// UIDocumentPicker
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
//
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
rootViewController.present(documentPicker, animated: true, completion: nil)
}
}
//
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
//
if let url = urls.first {
result?(url.path)
} else {
result?(nil)
}
}
//
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
result?(nil)
}
}

View File

@@ -46,6 +46,10 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose images</string>
<string>Choose images</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict>
</plist>

View File

@@ -156,7 +156,7 @@ class _ButtonState extends State<Button> {
@override
Widget build(BuildContext context) {
var padding = widget.padding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const EdgeInsets.symmetric(horizontal: 16, vertical: 4);
var width = widget.width;
if (width != null) {
width = width - padding.horizontal;
@@ -172,7 +172,7 @@ class _ButtonState extends State<Button> {
child: DefaultTextStyle(
style: TextStyle(
color: textColor,
fontSize: 16,
fontSize: 14,
),
child: isLoading
? CircularProgressIndicator(
@@ -210,11 +210,11 @@ class _ButtonState extends State<Button> {
decoration: BoxDecoration(
color: buttonColor,
borderRadius: BorderRadius.circular(16),
boxShadow: (isHover && !isLoading && widget.type == ButtonType.filled)
boxShadow: (isHover && !isLoading && (widget.type == ButtonType.filled || widget.type == ButtonType.normal))
? [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
blurRadius: 2,
offset: const Offset(0, 1),
)
]
@@ -252,6 +252,14 @@ class _ButtonState extends State<Button> {
return color;
}
}
if (widget.type == ButtonType.normal) {
var color = widget.color ?? context.colorScheme.surfaceContainer;
if (isHover) {
return color.withOpacity(0.9);
} else {
return color;
}
}
if (isHover) {
return context.colorScheme.outline.withOpacity(0.2);
}

View File

@@ -454,7 +454,9 @@ class _ComicDescription extends StatelessWidget {
),
).toAlign(Alignment.topCenter);
}),
),
)
else
const Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@@ -872,6 +874,7 @@ class ComicListState extends State<ComicList> {
try {
if (widget.loadPage != null) {
var res = await widget.loadPage!(page);
if(!mounted) return;
if (res.success) {
if (res.data.isEmpty) {
_data[page] = const [];

View File

@@ -129,13 +129,14 @@ void showDialogMessage(BuildContext context, String title, String message) {
);
}
void showConfirmDialog({
Future<void> showConfirmDialog({
required BuildContext context,
required String title,
required String content,
required void Function() onConfirm,
String confirmText = "Confirm",
}) {
showDialog(
return showDialog(
context: context,
builder: (context) => ContentDialog(
title: title,
@@ -146,7 +147,7 @@ void showConfirmDialog({
context.pop();
onConfirm();
},
child: Text("Confirm".tl),
child: Text(confirmText.tl),
),
],
),

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
final version = "1.0.0";
final version = "1.0.4";
bool get isAndroid => Platform.isAndroid;

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/utils/io.dart';
@@ -85,7 +86,7 @@ class _Appdata {
final appdata = _Appdata();
class _Settings {
class _Settings with ChangeNotifier {
_Settings();
final _data = <String, dynamic>{
@@ -109,6 +110,14 @@ class _Settings {
'enablePageAnimation': true,
'language': 'system', // system, zh-CN, zh-TW, en-US
'cacheSize': 2048, // in MB
'downloadThreads': 5,
'enableLongPressToZoom': true,
'checkUpdateOnStart': true,
'limitImageWidth': true,
'webdav': [], // empty means not configured
'dataVersion': 0,
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,
};
operator [](String key) {
@@ -117,6 +126,7 @@ class _Settings {
operator []=(String key, dynamic value) {
_data[key] = value;
notifyListeners();
}
@override

View File

@@ -12,6 +12,7 @@ import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import '../js_engine.dart';
import '../log.dart';

View File

@@ -106,7 +106,9 @@ class ComicSourceParser {
if (minAppVersion != null) {
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
throw ComicSourceParseException(
"minAppVersion $minAppVersion is required");
"minAppVersion @version is required"
.tlParams({"version": minAppVersion}),
);
}
}
for (var source in ComicSource.all()) {
@@ -728,7 +730,7 @@ class ComicSourceParser {
return retryZone(func);
};
if(_checkExists("favorites.addFolder")) {
if (_checkExists("favorites.addFolder")) {
addFolder = (name) async {
try {
await JsEngine().runCode("""
@@ -741,7 +743,7 @@ class ComicSourceParser {
}
};
}
if(_checkExists("favorites.deleteFolder")) {
if (_checkExists("favorites.deleteFolder")) {
deleteFolder = (key) async {
try {
await JsEngine().runCode("""

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
@@ -83,7 +84,9 @@ class FavoriteItem implements Comic {
int? get maxPage => null;
@override
String get sourceKey => type == ComicType.local ? 'local' : type.comicSource?.key ?? "Unknown:${type.value}";
String get sourceKey => type == ComicType.local
? 'local'
: type.comicSource?.key ?? "Unknown:${type.value}";
@override
double? get stars => null;
@@ -108,17 +111,17 @@ class FavoriteItem implements Comic {
static FavoriteItem fromJson(Map<String, dynamic> json) {
var type = json["type"] as int;
if(type == 0 && json['coverPath'].toString().startsWith('http')) {
if (type == 0 && json['coverPath'].toString().startsWith('http')) {
type = 'picacg'.hashCode;
} else if(type == 1) {
} else if (type == 1) {
type = 'ehentai'.hashCode;
} else if(type == 2) {
} else if (type == 2) {
type = 'jm'.hashCode;
} else if(type == 3) {
} else if (type == 3) {
type = 'hitomi'.hashCode;
} else if(type == 4) {
} else if (type == 4) {
type = 'wnacg'.hashCode;
} else if(type == 6) {
} else if (type == 6) {
type = 'nhentai'.hashCode;
}
return FavoriteItem(
@@ -132,24 +135,21 @@ class FavoriteItem implements Comic {
}
}
class FavoriteItemWithFolderInfo {
FavoriteItem comic;
class FavoriteItemWithFolderInfo extends FavoriteItem {
String folder;
FavoriteItemWithFolderInfo(this.comic, this.folder);
@override
bool operator ==(Object other) {
return other is FavoriteItemWithFolderInfo &&
other.comic == comic &&
other.folder == folder;
}
@override
int get hashCode => comic.hashCode ^ folder.hashCode;
FavoriteItemWithFolderInfo(FavoriteItem item, this.folder)
: super(
id: item.id,
name: item.name,
coverPath: item.coverPath,
author: item.author,
type: item.type,
tags: item.tags,
);
}
class LocalFavoritesManager {
class LocalFavoritesManager with ChangeNotifier {
factory LocalFavoritesManager() =>
cache ?? (cache = LocalFavoritesManager._create());
@@ -234,6 +234,7 @@ class LocalFavoritesManager {
values (?, ?);
""", [folder, order[folder]]);
}
notifyListeners();
}
int count(String folderName) {
@@ -273,6 +274,7 @@ class LocalFavoritesManager {
set tags = '$tag,' || tags
where id == ?
""", [id]);
notifyListeners();
}
List<FavoriteItemWithFolderInfo> allComics() {
@@ -325,6 +327,7 @@ class LocalFavoritesManager {
primary key (id, type)
);
""");
notifyListeners();
return name;
}
@@ -387,6 +390,7 @@ class LocalFavoritesManager {
values (?, ?, ?, ?, ?, ?, ?, ?);
""", [...params, minValue(folder) - 1]);
}
notifyListeners();
}
/// delete a folder
@@ -395,6 +399,7 @@ class LocalFavoritesManager {
_db.execute("""
drop table "$name";
""");
notifyListeners();
}
void deleteComic(String folder, FavoriteItem comic) {
@@ -409,6 +414,7 @@ class LocalFavoritesManager {
delete from "$folder"
where id == ? and type == ?;
""", [id, type.value]);
notifyListeners();
}
Future<void> clearAll() async {
@@ -426,6 +432,7 @@ class LocalFavoritesManager {
for (int i = 0; i < newFolder.length; i++) {
addComic(folder, newFolder[i], i);
}
notifyListeners();
}
void rename(String before, String after) {
@@ -439,6 +446,7 @@ class LocalFavoritesManager {
ALTER TABLE "$before"
RENAME TO "$after";
""");
notifyListeners();
}
void onReadEnd(String id, ComicType type) async {
@@ -476,6 +484,7 @@ class LocalFavoritesManager {
""", [newTime, id]);
}
}
notifyListeners();
}
List<FavoriteItemWithFolderInfo> search(String keyword) {
@@ -498,11 +507,11 @@ class LocalFavoritesManager {
}
bool test(FavoriteItemWithFolderInfo comic, String keyword) {
if (comic.comic.name.contains(keyword)) {
if (comic.name.contains(keyword)) {
return true;
} else if (comic.comic.author.contains(keyword)) {
} else if (comic.author.contains(keyword)) {
return true;
} else if (comic.comic.tags.any((element) => element.contains(keyword))) {
} else if (comic.tags.any((element) => element.contains(keyword))) {
return true;
}
return false;
@@ -522,6 +531,7 @@ class LocalFavoritesManager {
set tags = ?
where id == ?;
""", [tags.join(","), id]);
notifyListeners();
}
final _cachedFavoritedIds = <String, bool>{};
@@ -561,6 +571,7 @@ class LocalFavoritesManager {
comic.id,
comic.type.value
]);
notifyListeners();
}
String folderToJson(String folder) {
@@ -577,7 +588,7 @@ class LocalFavoritesManager {
void fromJson(String json) {
var data = jsonDecode(json);
var folder = data["name"];
if(folder == null || folder is! String) {
if (folder == null || folder is! String) {
throw "Invalid data";
}
if (folderNames.contains(folder)) {
@@ -591,10 +602,13 @@ class LocalFavoritesManager {
for (var comic in data["comics"]) {
try {
addComic(folder, FavoriteItem.fromJson(comic));
}
catch(e) {
} catch (e) {
Log.error("Import Data", e.toString());
}
}
}
void close() {
_db.dispose();
}
}

View File

@@ -172,6 +172,8 @@ class HistoryManager with ChangeNotifier {
max_page int
);
""");
notifyListeners();
}
/// add history. if exists, update time.
@@ -275,4 +277,8 @@ class HistoryManager with ChangeNotifier {
""");
return res.first[0] as int;
}
void close() {
_db.dispose();
}
}

View File

@@ -87,17 +87,16 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
return await decode(buffer);
} catch (e) {
await CacheManager().delete(this.key);
Object error = e;
if (data.length < 2 * 1024) {
// data is too short, it's likely that the data is text, not image
try {
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data);
error = Exception("Expected image data, but got text: $text");
throw Exception("Expected image data, but got text: $text");
} catch (e) {
// ignore
}
}
throw error;
rethrow;
}
} catch (e) {
scheduleMicrotask(() {

View File

@@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
import 'package:html/parser.dart' as html;
@@ -238,7 +237,7 @@ mixin class _JSEngineApi {
Log.warning(
"JS Engine",
"Too many documents, deleting the oldest: $shouldDelete\n"
"Current documents: ${_documents.keys}",
"Current documents: ${_documents.keys}",
);
_documents.remove(shouldDelete);
}
@@ -350,9 +349,6 @@ mixin class _JSEngineApi {
case "utf8":
return isEncode ? utf8.encode(value) : utf8.decode(value);
case "base64":
if (value is String) {
value = utf8.encode(value);
}
return isEncode ? base64Encode(value) : base64Decode(value);
case "md5":
return Uint8List.fromList(md5.convert(value).bytes);
@@ -383,8 +379,21 @@ mixin class _JSEngineApi {
if (!isEncode) {
var key = data["key"];
var cipher = ECBBlockCipher(AESEngine());
cipher.init(false, KeyParameter(key));
return cipher.process(value);
cipher.init(
false,
KeyParameter(key),
);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
}
return null;
case "aes-cbc":
@@ -393,7 +402,17 @@ mixin class _JSEngineApi {
var iv = data["iv"];
var cipher = CBCBlockCipher(AESEngine());
cipher.init(false, ParametersWithIV(KeyParameter(key), iv));
return cipher.process(value);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
}
return null;
case "aes-cfb":
@@ -402,7 +421,17 @@ mixin class _JSEngineApi {
var blockSize = data["blockSize"];
var cipher = CFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key));
return cipher.process(value);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
}
return null;
case "aes-ofb":
@@ -411,7 +440,17 @@ mixin class _JSEngineApi {
var blockSize = data["blockSize"];
var cipher = OFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key));
return cipher.process(value);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
}
return null;
case "rsa":
@@ -426,8 +465,8 @@ mixin class _JSEngineApi {
default:
return value;
}
} catch (e) {
Log.error("JS Engine", "Failed to convert $type: $e");
} catch (e, s) {
Log.error("JS Engine", "Failed to convert $type: $e", s);
return null;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.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/log.dart';
import 'package:venera/network/download.dart';
import 'package:venera/pages/reader/reader.dart';
import 'package:venera/utils/ext.dart';
@@ -158,15 +159,16 @@ class LocalManager with ChangeNotifier {
return "Directory is not empty";
}
try {
await copyDirectory(
await copyDirectoryIsolate(
Directory(path),
newDir,
);
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(path);
} catch (e) {
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
} catch (e, s) {
Log.error("IO", e, s);
return e.toString();
}
await Directory(path).deleteIgnoreError();
await Directory(path).deleteIgnoreError(recursive:true);
path = newPath;
return null;
}
@@ -261,8 +263,14 @@ class LocalManager with ChangeNotifier {
notifyListeners();
}
List<LocalComic> getComics() {
final res = _db.select('SELECT * FROM comics;');
List<LocalComic> getComics(LocalSortType sortType) {
var res = _db.select('''
SELECT * FROM comics
ORDER BY
${sortType.value == 'name' ? 'title' : 'created_at'}
${sortType.value == 'time_asc' ? 'ASC' : 'DESC'}
;
''');
return res.map((row) => LocalComic.fromRow(row)).toList();
}
@@ -310,6 +318,15 @@ class LocalManager with ChangeNotifier {
return LocalComic.fromRow(res.first);
}
List<LocalComic> search(String keyword) {
final res = _db.select('''
SELECT * FROM comics
WHERE title LIKE ? OR tags LIKE ? OR subtitle LIKE ?
ORDER BY created_at DESC;
''', ['%$keyword%', '%$keyword%', '%$keyword%']);
return res.map((row) => LocalComic.fromRow(row)).toList();
}
Future<List<String>> getImages(String id, ComicType type, Object ep) async {
if(ep is! String && ep is! int) {
throw "Invalid ep";
@@ -429,3 +446,22 @@ class LocalManager with ChangeNotifier {
notifyListeners();
}
}
enum LocalSortType {
name("name"),
timeAsc("time_asc"),
timeDesc("time_desc");
final String value;
const LocalSortType(this.value);
static LocalSortType fromString(String value) {
for (var type in values) {
if (type.value == value) {
return type;
}
}
return name;
}
}

View File

@@ -82,11 +82,12 @@ class Log {
addLog(LogLevel.warning, title, content);
}
static error(String title, String content, [Object? stackTrace]) {
static error(String title, Object content, [Object? stackTrace]) {
var info = content.toString();
if(stackTrace != null) {
content += "\n${stackTrace.toString()}";
info += "\n${stackTrace.toString()}";
}
addLog(LogLevel.error, title, content);
addLog(LogLevel.error, title, info);
}
static void clear() => _logs.clear();

View File

@@ -3,8 +3,12 @@ import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/main_page.dart';
import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/app_links.dart';
import 'package:window_manager/window_manager.dart';
import 'components/components.dart';
@@ -18,6 +22,7 @@ void main(List<String> args) {
return;
}
runZonedGuarded(() async {
await Rhttp.init();
WidgetsFlutterBinding.ensureInitialized();
await init();
if (App.isAndroid) {
@@ -63,6 +68,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
@override
void initState() {
checkUpdates();
App.registerForceRebuild(forceRebuild);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.initState();
@@ -163,6 +169,22 @@ class _MyAppState extends State<MyApp> {
},
);
}
void checkUpdates() async {
if(!appdata.settings['checkUpdateOnStart']) {
return;
}
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
var now = DateTime.now().millisecondsSinceEpoch;
if(now - lastCheck < 24 * 60 * 60 * 1000) {
return;
}
appdata.implicitData['lastCheckUpdate'] = now;
appdata.writeImplicitData();
await Future.delayed(const Duration(milliseconds: 300));
await checkUpdateUi(false);
await ComicSourcePage.checkComicSourceUpdate(true);
}
}
class _SystemUiProvider extends StatelessWidget {

View File

@@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/services.dart';
import 'package:rhttp/rhttp.dart' as rhttp;
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/cache.dart';
@@ -108,11 +108,11 @@ class AppDio with DioMixin {
AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions();
interceptors.add(MyLogInterceptor());
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings());
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager());
interceptors.add(CloudflareInterceptor());
interceptors.add(MyLogInterceptor());
}
static HttpClient createHttpClient() {
@@ -136,8 +136,9 @@ class AppDio with DioMixin {
static String? proxy;
static Future<String?> getProxy() async {
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct")
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") {
return null;
}
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
String res;
@@ -187,17 +188,14 @@ class AppDio with DioMixin {
}) async {
proxy = await getProxy();
if (_proxy != proxy) {
Log.info("Network", "Proxy changed to $proxy");
_proxy = proxy;
(httpClientAdapter as IOHttpClientAdapter).close();
httpClientAdapter =
IOHttpClientAdapter(createHttpClient: createHttpClient);
httpClientAdapter = RHttpAdapter(rhttp.ClientSettings(
proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!),
));
}
Log.info(
"Network",
"${options?.method ?? 'GET'} $path\n"
"Headers: ${options?.headers}\n"
"Data: $data\n",
);
return super.request(
path,
data: data,
@@ -209,3 +207,76 @@ class AppDio with DioMixin {
);
}
}
class RHttpAdapter implements HttpClientAdapter {
rhttp.ClientSettings settings;
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
settings = settings.copyWith(
redirectSettings: const rhttp.RedirectSettings.limited(5),
timeoutSettings: const rhttp.TimeoutSettings(
connectTimeout: Duration(seconds: 15),
keepAliveTimeout: Duration(seconds: 60),
keepAlivePing: Duration(seconds: 30),
),
throwOnStatusCode: false,
);
}
@override
void close({bool force = false}) {}
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
var res = await rhttp.Rhttp.request(
method: switch (options.method) {
'GET' => rhttp.HttpMethod.get,
'POST' => rhttp.HttpMethod.post,
'PUT' => rhttp.HttpMethod.put,
'PATCH' => rhttp.HttpMethod.patch,
'DELETE' => rhttp.HttpMethod.delete,
'HEAD' => rhttp.HttpMethod.head,
'OPTIONS' => rhttp.HttpMethod.options,
'TRACE' => rhttp.HttpMethod.trace,
'CONNECT' => rhttp.HttpMethod.connect,
_ => throw ArgumentError('Unsupported method: ${options.method}'),
},
url: options.uri.toString(),
settings: settings,
expectBody: rhttp.HttpExpectBody.stream,
body: requestStream == null ? null : rhttp.HttpBody.stream(requestStream),
headers: rhttp.HttpHeaders.rawMap(
Map.fromEntries(
options.headers.entries.map(
(e) => MapEntry(e.key, e.value.toString().trim()),
),
),
),
);
if (res is! rhttp.HttpStreamResponse) {
throw Exception("Invalid response type: ${res.runtimeType}");
}
var headers = <String, List<String>>{};
for (var entry in res.headers) {
var key = entry.$1.toLowerCase();
headers[key] ??= [];
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(
data,
res.statusCode,
statusMessage: null,
isRedirect: false,
headers: headers,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/local.dart';
@@ -89,7 +90,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
var local = LocalManager().find(id, comicType);
if (path != null) {
if (local == null) {
Directory(path!).deleteIgnoreError();
Directory(path!).deleteIgnoreError(recursive: true);
} else if (chapters != null) {
for (var c in chapters!) {
var dir = Directory(FilePath.join(path!, c));
@@ -155,7 +156,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
var tasks = <int, _ImageDownloadWrapper>{};
int get _maxConcurrentTasks => 5;
int get _maxConcurrentTasks => (appdata.settings["downloadThreads"] as num).toInt();
void _scheduleTasks() {
var images = _images![_images!.keys.elementAt(_chapter)]!;
@@ -197,6 +198,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
_scheduleTasks();
}
});
downloading++;
}
}
@@ -590,5 +592,7 @@ abstract mixin class _TransferSpeedMixin {
void stopRecorder() {
timer?.cancel();
timer = null;
_currentSpeed = 0;
_bytesSinceLastSecond = 0;
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/utils/image.dart';
import 'app_dio.dart';
@@ -27,8 +28,8 @@ class ImageDownloader {
configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {};
}
configs['headers'] ??= {};
if(configs['headers']['user-agent'] == null
&& configs['headers']['User-Agent'] == null) {
if (configs['headers']['user-agent'] == null &&
configs['headers']['User-Agent'] == null) {
configs['headers']['user-agent'] = webUA;
}
@@ -120,11 +121,22 @@ class ImageDownloader {
buffer = configs['onResponse'](buffer);
}
await CacheManager().writeCache(cacheKey, buffer);
var data = Uint8List.fromList(buffer);
buffer.clear();
if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript(
data,
configs['modifyImage'],
);
data = newData;
}
await CacheManager().writeCache(cacheKey, data);
yield ImageDownloadProgress(
currentBytes: buffer.length,
totalBytes: buffer.length,
imageBytes: Uint8List.fromList(buffer),
currentBytes: data.length,
totalBytes: data.length,
imageBytes: data,
);
}
}

View File

@@ -70,6 +70,7 @@ class AccountsPage extends StatelessWidget {
),
);
element.saveData();
ComicSource.notifyListeners();
logic.update();
},
);
@@ -124,6 +125,7 @@ class AccountsPage extends StatelessWidget {
element.data["account"] = null;
element.account?.logout();
element.saveData();
ComicSource.notifyListeners();
logic.update();
},
trailing: const Icon(Icons.logout),
@@ -171,84 +173,88 @@ class _LoginPageState extends State<_LoginPage> {
child: Container(
padding: const EdgeInsets.all(16),
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Login".tl, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 32),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
enabled: widget.config.login != null,
onChanged: (s) {
username = s;
},
).paddingBottom(16),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.login != null,
onChanged: (s) {
password = s;
},
onSubmitted: (s) => login(),
).paddingBottom(16),
for (var field in widget.config.cookieFields ?? <String>[])
TextField(
decoration: InputDecoration(
labelText: field,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.validateCookies != null,
onChanged: (s) {
_cookies[field] = s;
},
).paddingBottom(16),
if (widget.config.login == null &&
widget.config.cookieFields == null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline),
const SizedBox(width: 8),
Text("Login with password is disabled".tl),
],
)
else
Button.filled(
isLoading: loading,
onPressed: login,
child: Text("Continue".tl),
),
const SizedBox(height: 24),
if (widget.config.loginWebsite != null)
TextButton(
onPressed: loginWithWebview,
child: Text("Login with webview".tl),
),
const SizedBox(height: 8),
if (widget.config.registerWebsite != null)
TextButton(
onPressed: () =>
launchUrlString(widget.config.registerWebsite!),
child: Row(
child: AutofillGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Login".tl, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 32),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
enabled: widget.config.login != null,
onChanged: (s) {
username = s;
},
autofillHints: const [AutofillHints.username],
).paddingBottom(16),
if (widget.config.cookieFields == null)
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.login != null,
onChanged: (s) {
password = s;
},
onSubmitted: (s) => login(),
autofillHints: const [AutofillHints.password],
).paddingBottom(16),
for (var field in widget.config.cookieFields ?? <String>[])
TextField(
decoration: InputDecoration(
labelText: field,
border: const OutlineInputBorder(),
),
obscureText: true,
enabled: widget.config.validateCookies != null,
onChanged: (s) {
_cookies[field] = s;
},
).paddingBottom(16),
if (widget.config.login == null &&
widget.config.cookieFields == null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.link),
const Icon(Icons.error_outline),
const SizedBox(width: 8),
Text("Create Account".tl),
Text("Login with password is disabled".tl),
],
)
else
Button.filled(
isLoading: loading,
onPressed: login,
child: Text("Continue".tl),
),
),
],
const SizedBox(height: 24),
if (widget.config.loginWebsite != null)
TextButton(
onPressed: loginWithWebview,
child: Text("Login with webview".tl),
),
const SizedBox(height: 8),
if (widget.config.registerWebsite != null)
TextButton(
onPressed: () =>
launchUrlString(widget.config.registerWebsite!),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.link),
const SizedBox(width: 8),
Text("Create Account".tl),
],
),
),
],
),
),
),
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/consts.dart';
@@ -42,12 +43,41 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
bool isDownloaded = false;
void updateHistory() async {
var newHistory = await HistoryManager()
.find(widget.id, ComicType(widget.sourceKey.hashCode));
if (newHistory?.ep != history?.ep || newHistory?.page != history?.page) {
history = newHistory;
update();
}
}
@override
Widget buildLoading() {
return Column(
children: [
const Appbar(title: Text("")),
Expanded(
child: super.buildLoading(),
),
],
);
}
@override
void initState() {
scrollController.addListener(onScroll);
HistoryManager().addListener(updateHistory);
super.initState();
}
@override
void dispose() {
scrollController.removeListener(onScroll);
HistoryManager().removeListener(updateHistory);
super.dispose();
}
@override
void update() {
setState(() {});
@@ -205,6 +235,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
Widget buildActions() {
bool isMobile = context.width < changePoint;
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
return SliverToBoxAdapter(
child: Column(
children: [
@@ -212,17 +243,17 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: [
if (history != null && (history!.ep > 1 || history!.page > 1))
if (hasHistory && !isMobile)
_ActionButton(
icon: const Icon(Icons.menu_book),
text: 'Continue'.tl,
onPressed: continueRead,
iconColor: context.useTextColor(Colors.yellow),
),
if (!isMobile)
if (!isMobile || hasHistory)
_ActionButton(
icon: const Icon(Icons.play_circle_outline),
text: 'Read'.tl,
text: 'Start'.tl,
onPressed: read,
iconColor: context.useTextColor(Colors.orange),
),
@@ -238,7 +269,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
icon: const Icon(Icons.favorite_border),
activeIcon: const Icon(Icons.favorite),
isActive: isLiked,
text: (data!.likesCount ?? (isLiked ? 'Liked'.tl : 'Like'.tl))
text: ((data!.likesCount != null)
? (data!.likesCount! + (isLiked ? 1 : 0))
: (isLiked ? 'Liked'.tl : 'Like'.tl))
.toString(),
isLoading: isLiking,
onPressed: likeOrUnlike,
@@ -250,6 +283,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
isActive: isFavorite || isAddToLocalFav,
text: 'Favorite'.tl,
onPressed: openFavPanel,
onLongPressed: quickFavorite,
iconColor: context.useTextColor(Colors.purple),
),
if (comicSource.commentsLoader != null)
@@ -278,7 +312,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
),
const SizedBox(width: 16),
Expanded(
child: FilledButton(onPressed: read, child: Text("Read".tl)),
child: hasHistory
? FilledButton(
onPressed: continueRead, child: Text("Continue".tl))
: FilledButton(onPressed: read, child: Text("Read".tl)),
)
],
).paddingHorizontal(16).paddingVertical(8),
@@ -398,23 +435,23 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
Text(comic.stars!.toStringAsFixed(2)),
],
).paddingLeft(16).paddingVertical(8),
for (var e in comic.tags.entries)
buildWrap(
children: [
if(e.value.isNotEmpty)
for (var e in comic.tags.entries)
buildWrap(
children: [
if (e.value.isNotEmpty)
buildTag(text: e.key.ts(comicSource.key), isTitle: true),
for (var tag in e.value)
buildTag(
text: enableTranslation
? TagsTranslation.translationTagWithNamespace(
tag,
e.key.toLowerCase(),
)
: tag,
onTap: () => onTapTag(tag, e.key),
),
],
),
for (var tag in e.value)
buildTag(
text: enableTranslation
? TagsTranslation.translationTagWithNamespace(
tag,
e.key.toLowerCase(),
)
: tag,
onTap: () => onTapTag(tag, e.key),
),
],
),
if (comic.uploader != null)
buildWrap(
children: [
@@ -458,7 +495,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
}
Widget buildRecommend() {
if (comic.recommend == null||comic.recommend!.isEmpty) {
if (comic.recommend == null || comic.recommend!.isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero);
}
return SliverMainAxisGroup(slivers: [
@@ -503,12 +540,22 @@ abstract mixin class _ComicPageActions {
bool isFavorite = false;
void openFavPanel() {
FavoriteItem _toFavoriteItem() {
var tags = <String>[];
for (var e in comic.tags.entries) {
tags.addAll(e.value.map((tag) => '${e.key}:$tag'));
}
return FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subTitle ?? comic.uploader ?? '',
type: comic.comicType,
tags: tags,
);
}
void openFavPanel() {
showSideBar(
App.rootContext,
_FavoritePanel(
@@ -520,18 +567,25 @@ abstract mixin class _ComicPageActions {
isAddToLocalFav = local ?? isAddToLocalFav;
update();
},
favoriteItem: FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subTitle ?? comic.uploader ?? '',
type: comic.comicType,
tags: tags,
),
favoriteItem: _toFavoriteItem(),
),
);
}
void quickFavorite() {
var folder = appdata.settings['quickFavorite'];
if(folder is! String) {
return;
}
LocalFavoritesManager().addComic(
folder,
_toFavoriteItem(),
);
isAddToLocalFav = true;
update();
App.rootContext.showMessage(message: "Added".tl);
}
void share() {
var text = comic.title;
if (comic.url != null) {
@@ -765,11 +819,13 @@ class _ActionButton extends StatelessWidget {
required this.icon,
required this.text,
required this.onPressed,
this.onLongPressed,
this.activeIcon,
this.isActive,
this.isLoading,
this.iconColor,
});
final Widget icon;
final Widget? activeIcon;
@@ -783,6 +839,9 @@ class _ActionButton extends StatelessWidget {
final bool? isLoading;
final Color? iconColor;
final void Function()? onLongPressed;
@override
Widget build(BuildContext context) {
return Container(
@@ -800,6 +859,7 @@ class _ActionButton extends StatelessWidget {
onPressed();
}
},
onLongPress: onLongPressed,
borderRadius: BorderRadius.circular(18),
child: IconTheme.merge(
data: IconThemeData(size: 20, color: iconColor),
@@ -1539,7 +1599,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
const SizedBox(width: 16),
Expanded(
child: FilledButton(
onPressed: () {
onPressed: selected.isEmpty ? null : () {
widget.finishSelect(selected);
context.pop();
},
@@ -1550,7 +1610,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
],
),
),
SizedBox(height: MediaQuery.of(context).padding.bottom + 4),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
);

View File

@@ -14,11 +14,11 @@ import 'package:venera/utils/translations.dart';
class ComicSourcePage extends StatefulWidget {
const ComicSourcePage({super.key});
static void checkComicSourceUpdate([bool showLoading = false]) async {
static Future<void> checkComicSourceUpdate([bool implicit = false]) async {
if (ComicSource.all().isEmpty) {
return;
}
var controller = showLoading ? showLoadingDialog(App.rootContext) : null;
var controller = implicit ? null : showLoadingDialog(App.rootContext);
var dio = AppDio();
var res = await dio.get<String>(
"https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json");
@@ -40,6 +40,9 @@ class ComicSourcePage extends StatefulWidget {
}
controller?.close();
if (shouldUpdate.isEmpty) {
if(!implicit) {
App.rootContext.showMessage(message: "No Update Available".tl);
}
return;
}
var msg = "";
@@ -47,10 +50,11 @@ class ComicSourcePage extends StatefulWidget {
msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n";
}
msg = msg.trim();
showConfirmDialog(
await showConfirmDialog(
context: App.rootContext,
title: "Updates Available".tl,
content: msg,
confirmText: "Update",
onConfirm: () {
for (var key in shouldUpdate) {
var source = ComicSource.find(key);
@@ -103,7 +107,7 @@ class _BodyState extends State<_Body> {
child: ListTile(
leading: const Icon(Icons.update_outlined),
title: Text("Check updates".tl),
onTap: () => ComicSourcePage.checkComicSourceUpdate(true),
onTap: () => ComicSourcePage.checkComicSourceUpdate(false),
trailing: const Icon(Icons.arrow_right),
),
);
@@ -160,71 +164,76 @@ class _BodyState extends State<_Body> {
for (var item in source.settings!.entries) {
var key = item.key;
String type = item.value['type'];
if (type == "select") {
var current = source.data['settings'][key];
if (current == null) {
var d = item.value['default'];
for (var option in item.value['options']) {
if (option['value'] == d) {
current = option['text'] ?? option['value'];
break;
try {
if (type == "select") {
var current = source.data['settings'][key];
if (current == null) {
var d = item.value['default'];
for (var option in item.value['options']) {
if (option['value'] == d) {
current = option['text'] ?? option['value'];
break;
}
}
}
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>(
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] = item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle: Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
}
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
.map<String>(
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
source.data['settings'][key] = item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
),
);
} else if (type == "switch") {
var current = source.data['settings'][key] ?? item.value['default'];
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
trailing: Switch(
value: current,
onChanged: (v) {
source.data['settings'][key] = v;
source.saveData();
setState(() {});
},
),
);
} else if (type == "input") {
var current =
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle: Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showInputDialog(
context: context,
title: (item.value['title'] as String).ts(source.key),
initialValue: current,
inputValidator: item.value['validator'] == null
? null
: RegExp(item.value['validator']),
onConfirm: (value) {
source.data['settings'][key] = value;
source.saveData();
setState(() {});
return null;
},
);
},
),
);
}
catch(e, s) {
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
}
}
}
@@ -445,10 +454,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
itemBuilder: (context, index) {
var key = json![index]["key"];
var action = currentKey.contains(key)
? const Icon(Icons.check)
? const Icon(Icons.check, size: 20).paddingRight(8)
: Tooltip(
message: "Add",
child: IconButton(
child: Button.icon(
color: context.colorScheme.primary,
icon: const Icon(Icons.add),
onPressed: () async {
await widget.onAdd(

View File

@@ -5,8 +5,12 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/foundation/state_controller.dart';
import 'package:venera/pages/search_result_page.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart';
import 'category_comics_page.dart';
class ExplorePage extends StatefulWidget {
const ExplorePage({super.key});
@@ -15,7 +19,7 @@ class ExplorePage extends StatefulWidget {
}
class _ExplorePageState extends State<ExplorePage>
with TickerProviderStateMixin {
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin<ExplorePage> {
late TabController controller;
bool showFB = true;
@@ -24,6 +28,24 @@ class _ExplorePageState extends State<ExplorePage>
late List<String> pages;
void onSettingsChanged() {
var explorePages = List<String>.from(appdata.settings["explore_pages"]);
var all = ComicSource.all()
.map((e) => e.explorePages)
.expand((e) => e.map((e) => e.title))
.toList();
explorePages = explorePages.where((e) => all.contains(e)).toList();
if (!pages.isEqualsTo(explorePages)) {
setState(() {
pages = explorePages;
controller = TabController(
length: pages.length,
vsync: this,
);
});
}
}
@override
void initState() {
pages = List<String>.from(appdata.settings["explore_pages"]);
@@ -36,9 +58,17 @@ class _ExplorePageState extends State<ExplorePage>
length: pages.length,
vsync: this,
);
appdata.settings.addListener(onSettingsChanged);
super.initState();
}
@override
void dispose() {
controller.dispose();
appdata.settings.removeListener(onSettingsChanged);
super.dispose();
}
void refresh() {
int page = controller.index;
String currentPageId = pages[page];
@@ -83,12 +113,14 @@ class _ExplorePageState extends State<ExplorePage>
@override
Widget build(BuildContext context) {
super.build(context);
if (pages.isEmpty) {
return buildEmpty();
}
Widget tabBar = Material(
child: FilledTabBar(
key: Key(pages.toString()),
tabs: pages.map((e) => buildTab(e)).toList(),
controller: controller,
),
@@ -97,48 +129,52 @@ class _ExplorePageState extends State<ExplorePage>
return Stack(
children: [
Positioned.fill(
child: Column(
children: [
tabBar,
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (notifications) {
if (notifications.metrics.axis == Axis.horizontal) {
if (!showFB) {
child: Column(
children: [
tabBar,
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (notifications) {
if (notifications.metrics.axis == Axis.horizontal) {
if (!showFB) {
setState(() {
showFB = true;
});
}
return true;
}
var current = notifications.metrics.pixels;
var overflow = notifications.metrics.outOfRange;
if (current > location && current != 0 && showFB) {
setState(() {
showFB = false;
});
} else if ((current < location - 50 || current == 0) &&
!showFB) {
setState(() {
showFB = true;
});
}
return true;
}
var current = notifications.metrics.pixels;
if ((current > location && current != 0) && showFB) {
setState(() {
showFB = false;
});
} else if ((current < location || current == 0) && !showFB) {
setState(() {
showFB = true;
});
}
location = current;
return false;
},
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: TabBarView(
controller: controller,
children: pages.map((e) => buildBody(e)).toList(),
if ((current > location || current < location - 50) &&
!overflow) {
location = current;
}
return false;
},
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: TabBarView(
controller: controller,
children: pages.map((e) => buildBody(e)).toList(),
),
),
),
),
)
],
)),
)
],
),
),
Positioned(
right: 16,
bottom: 16,
@@ -159,6 +195,9 @@ class _ExplorePageState extends State<ExplorePage>
],
);
}
@override
bool get wantKeepAlive => true;
}
class _SingleExplorePage extends StatefulWidget {
@@ -170,7 +209,8 @@ class _SingleExplorePage extends StatefulWidget {
State<_SingleExplorePage> createState() => _SingleExplorePageState();
}
class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
with AutomaticKeepAliveClientMixin<_SingleExplorePage> {
late final ExplorePageData data;
bool loading = true;
@@ -183,6 +223,16 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
int key = 0;
bool _wantKeepAlive = true;
void onSettingsChanged() {
var explorePages = appdata.settings["explore_pages"];
if (!explorePages.contains(widget.title)) {
_wantKeepAlive = false;
updateKeepAlive();
}
}
@override
void initState() {
super.initState();
@@ -195,11 +245,19 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
}
}
}
appdata.settings.addListener(onSettingsChanged);
throw "Explore Page ${widget.title} Not Found!";
}
@override
void dispose() {
appdata.settings.removeListener(onSettingsChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
if (data.loadMultiPart != null) {
return buildMultiPart();
} else if (data.loadPage != null || data.loadNext != null) {
@@ -284,6 +342,9 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
});
}
}
@override
bool get wantKeepAlive => _wantKeepAlive;
}
class _MixedExplorePage extends StatefulWidget {
@@ -367,13 +428,12 @@ Iterable<Widget> _buildExplorePagePart(
if (part.viewMore != null)
TextButton(
onPressed: () {
// TODO: view more
/*
var context = App.mainNavigatorKey!.currentContext!;
if (part.viewMore!.startsWith("search:")) {
context.to(
() => SearchResultPage(
keyword: part.viewMore!.replaceFirst("search:", ""),
() => SearchResultPage(
text: part.viewMore!.replaceFirst("search:", ""),
options: const [],
sourceKey: sourceKey,
),
);
@@ -385,16 +445,16 @@ Iterable<Widget> _buildExplorePagePart(
p = null;
}
context.to(
() => CategoryComicsPage(
() => CategoryComicsPage(
category: c,
categoryKey:
ComicSource.find(sourceKey)!.categoryData!.key,
ComicSource.find(sourceKey)!.categoryData!.key,
param: p,
),
);
}*/
}
},
child: Text("查看更多".tl),
child: Text("View more".tl),
)
],
),

View File

@@ -6,7 +6,6 @@ Future<void> newFolder() async {
context: App.rootContext,
builder: (context) {
var controller = TextEditingController();
var folders = LocalFavoritesManager().folderNames;
String? error;
return StatefulBuilder(builder: (context, setState) {

View File

@@ -17,6 +17,7 @@ part 'favorite_actions.dart';
part 'side_bar.dart';
part 'local_favorites_page.dart';
part 'network_favorites_page.dart';
part 'local_search_page.dart';
const _kLeftBarWidth = 256.0;
@@ -151,7 +152,8 @@ class _FavoritesPageState extends State<FavoritesPage> {
} else {
var favoriteData = getFavoriteDataOrNull(folder!);
if (favoriteData == null) {
return const Center(child: Text("Unknown source"));
folder = null;
return buildBody();
} else {
return NetworkFavoritePage(favoriteData, key: Key(folder!));
}

View File

@@ -15,10 +15,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
late List<FavoriteItem> comics;
void updateComics() {
print(comics.length);
setState(() {
comics = LocalFavoritesManager().getAllComics(widget.folder);
print(comics.length);
});
}
@@ -107,7 +105,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
},
).then(
(value) {
setState(() {});
if(mounted) {
setState(() {});
}
},
);
}),
@@ -199,6 +199,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
var comicSource = e.type.comicSource;
return ComicTile(
key: Key(e.hashCode.toString()),
enableLongPressed: false,
comic: Comic(
e.name,
e.coverPath,

View File

@@ -0,0 +1,41 @@
part of 'favorites_page.dart';
class LocalSearchPage extends StatefulWidget {
const LocalSearchPage({super.key});
@override
State<LocalSearchPage> createState() => _LocalSearchPageState();
}
class _LocalSearchPageState extends State<LocalSearchPage> {
String keyword = '';
var comics = <FavoriteItemWithFolderInfo>[];
late final SearchBarController controller;
@override
void initState() {
super.initState();
controller = SearchBarController(onSearch: (text) {
keyword = text;
comics = LocalFavoritesManager().search(keyword);
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SmoothCustomScrollView(slivers: [
SliverSearchBar(controller: controller),
SliverGridComics(
comics: comics,
badgeBuilder: (c) {
return (c as FavoriteItemWithFolderInfo).folder;
},
),
]),
);
}
}

View File

@@ -88,6 +88,13 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
const SizedBox(width: 12),
Text("Local".tl),
const Spacer(),
IconButton(
icon: const Icon(Icons.search),
color: context.colorScheme.primary,
onPressed: () {
context.to(() => const LocalSearchPage());
},
),
IconButton(
icon: const Icon(Icons.add),
color: context.colorScheme.primary,
@@ -112,6 +119,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
if (index == 0) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12),
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
border: Border(
top: BorderSide(

View File

@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -17,6 +18,7 @@ import 'package:venera/pages/downloading_page.dart';
import 'package:venera/pages/history_page.dart';
import 'package:venera/pages/search_page.dart';
import 'package:venera/utils/cbz.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
@@ -32,6 +34,7 @@ class HomePage extends StatelessWidget {
slivers: [
SliverPadding(padding: EdgeInsets.only(top: context.padding.top)),
const _SearchBar(),
const _SyncDataWidget(),
const _History(),
const _Local(),
const _ComicSourceWidget(),
@@ -77,6 +80,97 @@ class _SearchBar extends StatelessWidget {
}
}
class _SyncDataWidget extends StatefulWidget {
const _SyncDataWidget();
@override
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
}
class _SyncDataWidgetState extends State<_SyncDataWidget> {
@override
void initState() {
super.initState();
DataSync().addListener(update);
}
void update() {
if(mounted) {
setState(() {});
}
}
@override
void dispose() {
super.dispose();
DataSync().removeListener(update);
}
@override
Widget build(BuildContext context) {
Widget child;
if(!DataSync().isEnabled) {
child = const SliverPadding(padding: EdgeInsets.zero);
} else if (DataSync().isUploading || DataSync().isDownloading) {
child = SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: const Icon(Icons.sync),
title: Text('Syncing Data'.tl),
trailing: const CircularProgressIndicator(strokeWidth: 2)
.fixWidth(18)
.fixHeight(18),
),
),
);
} else {
child = SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: const Icon(Icons.sync),
title: Text('Sync Data'.tl),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.cloud_upload_outlined),
onPressed: () async {
DataSync().uploadData();
}
),
IconButton(
icon: const Icon(Icons.cloud_download_outlined),
onPressed: () async {
DataSync().downloadData();
}
),
],
),
),
),
);
}
return SliverAnimatedPaintExtent(
duration: const Duration(milliseconds: 200),
child: child,
);
}
}
class _History extends StatefulWidget {
const _History();
@@ -529,14 +623,16 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
await xFile!.saveTo(cache);
var comic = await CBZ.import(File(cache));
if (selectedFolder != null) {
LocalFavoritesManager().addComic(selectedFolder!, FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
}
await File(cache).deleteIgnoreError();
} catch (e, s) {
@@ -610,14 +706,16 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
for (var comic in comics.values) {
LocalManager().add(comic, LocalManager().findValidId(ComicType.local));
if (selectedFolder != null) {
LocalFavoritesManager().addComic(selectedFolder!, FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
}
}
context.pop();
@@ -820,6 +918,7 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
void onComicSourceChange() {
setState(() {
accounts.clear();
for (var c in ComicSource.all()) {
if (c.isLogged) {
accounts.add(c.name);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/pages/downloading_page.dart';
import 'package:venera/utils/cbz.dart';
@@ -17,15 +18,33 @@ class LocalComicsPage extends StatefulWidget {
class _LocalComicsPageState extends State<LocalComicsPage> {
late List<LocalComic> comics;
late LocalSortType sortType;
String keyword = "";
bool searchMode = false;
bool multiSelectMode = false;
Map<LocalComic, bool> selectedComics = {};
void update() {
setState(() {
comics = LocalManager().getComics();
});
if (keyword.isEmpty) {
setState(() {
comics = LocalManager().getComics(sortType);
});
} else {
setState(() {
comics = LocalManager().search(keyword);
});
}
}
@override
void initState() {
comics = LocalManager().getComics();
var sort = appdata.implicitData["local_sort"] ?? "name";
sortType = LocalSortType.fromString(sort);
comics = LocalManager().getComics(sortType);
LocalManager().addListener(update);
super.initState();
}
@@ -36,37 +55,194 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
super.dispose();
}
void sort() {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, setState) {
return ContentDialog(
title: "Sort".tl,
content: Column(
children: [
RadioListTile<LocalSortType>(
title: Text("Name".tl),
value: LocalSortType.name,
groupValue: sortType,
onChanged: (v) {
setState(() {
sortType = v!;
});
},
),
RadioListTile<LocalSortType>(
title: Text("Date".tl),
value: LocalSortType.timeAsc,
groupValue: sortType,
onChanged: (v) {
setState(() {
sortType = v!;
});
},
),
RadioListTile<LocalSortType>(
title: Text("Date Desc".tl),
value: LocalSortType.timeDesc,
groupValue: sortType,
onChanged: (v) {
setState(() {
sortType = v!;
});
},
),
],
),
actions: [
FilledButton(
onPressed: () {
appdata.implicitData["local_sort"] =sortType.value;
appdata.writeImplicitData();
Navigator.pop(context);
update();
},
child: Text("Confirm".tl),
),
],
);
});
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SmoothCustomScrollView(
slivers: [
SliverAppbar(
title: Text("Local".tl),
actions: [
Tooltip(
message: "Downloading".tl,
child: IconButton(
icon: const Icon(Icons.download),
if (!searchMode && !multiSelectMode)
SliverAppbar(
title: Text("Local".tl),
actions: [
Tooltip(
message: "Search".tl,
child: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
searchMode = true;
});
},
),
),
Tooltip(
message: "Sort".tl,
child: IconButton(
icon: const Icon(Icons.sort),
onPressed: sort,
),
),
Tooltip(
message: "Downloading".tl,
child: IconButton(
icon: const Icon(Icons.download),
onPressed: () {
showPopUpWidget(context, const DownloadingPage());
},
),
),
Tooltip(
message: multiSelectMode
? "Exit Multi-Select".tl
: "Multi-Select".tl,
child: IconButton(
icon: const Icon(Icons.checklist),
onPressed: () {
setState(() {
multiSelectMode = !multiSelectMode;
});
},
),
),
],
)
else if (multiSelectMode)
SliverAppbar(
title: Text("Selected ${selectedComics.length} comics"),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
showPopUpWidget(context, const DownloadingPage());
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
},
),
)
],
),
],
)
else if (searchMode)
SliverAppbar(
title: TextField(
autofocus: true,
decoration: InputDecoration(
hintText: "Search".tl,
border: InputBorder.none,
),
onChanged: (v) {
keyword = v;
update();
},
),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
],
),
SliverGridComics(
comics: comics,
onTap: (c) {
(c as LocalComic).read();
},
onTap: multiSelectMode
? (c) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c as LocalComic);
} else {
selectedComics[c as LocalComic] = true;
}
});
}
: (c) {
(c as LocalComic).read();
},
menuBuilder: (c) {
return [
MenuEntry(
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
LocalManager().deleteComic(c as LocalComic);
if (multiSelectMode) {
showConfirmDialog(
context: context,
title: "Delete".tl,
content: "Delete selected comics?".tl,
onConfirm: () {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(comic);
}
setState(() {
selectedComics.clear();
});
},
);
} else {
LocalManager().deleteComic(c as LocalComic);
}
}),
MenuEntry(
icon: Icons.outbox_outlined,
@@ -77,11 +253,21 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
allowCancel: false,
);
try {
var file = await CBZ.export(c as LocalComic);
await saveFile(filename: file.name, file: file);
await file.delete();
}
catch (e) {
if (multiSelectMode) {
for (var comic in selectedComics.keys) {
var file = await CBZ.export(comic);
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) {
context.showMessage(message: e.toString());
}
controller.close();

View File

@@ -22,6 +22,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
_DragListener? dragListener;
int fingers = 0;
@override
void initState() {
_tapGestureRecognizer = TapGestureRecognizer()
@@ -38,6 +40,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
fingers++;
_lastTapPointer = event.pointer;
_lastTapMoveDistance = Offset.zero;
_tapGestureRecognizer.addPointer(event);
@@ -46,7 +49,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
_dragInProgress = false;
}
Future.delayed(_kLongPressMinTime, () {
if (_lastTapPointer == event.pointer) {
if (_lastTapPointer == event.pointer && fingers == 1) {
if(_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
onLongPressedDown(event.position);
_longPressInProgress = true;
@@ -67,6 +70,19 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
}
},
onPointerUp: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}
if(_dragInProgress) {
dragListener?.onEnd?.call();
_dragInProgress = false;
}
_lastTapPointer = null;
_lastTapMoveDistance = null;
},
onPointerCancel: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}

View File

@@ -223,6 +223,9 @@ class _GalleryModeState extends State<_GalleryMode>
@override
void handleLongPressDown(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size;
@@ -234,6 +237,9 @@ class _GalleryModeState extends State<_GalleryMode>
@override
void handleLongPressUp(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target);
@@ -465,18 +471,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
},
child: widget,
);
var width = MediaQuery.of(context).size.width;
var height = MediaQuery.of(context).size.height;
if(appdata.settings['limitImageWidth'] && width / height > 0.7) {
width = height * 0.7;
}
return PhotoView.customChild(
backgroundDecoration: BoxDecoration(
color: context.colorScheme.surface,
),
childSize: Size(width, height),
minScale: 1.0,
maxScale: 2.5,
strictScale: true,
controller: photoViewController,
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
width: width,
height: height,
child: widget,
),
);
@@ -509,6 +521,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override
void handleLongPressDown(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size;
photoViewController.animateScale?.call(
@@ -519,6 +534,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override
void handleLongPressUp(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target);
}

View File

@@ -23,6 +23,7 @@ import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/file_type.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:venera/utils/volume.dart';
import 'package:window_manager/window_manager.dart';
part 'scaffold.dart';
@@ -97,6 +98,8 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
var focusNode = FocusNode();
VolumeListener? volumeListener;
@override
void initState() {
page = widget.initialPage ?? 1;
@@ -107,6 +110,9 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
updateHistory();
});
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
if(appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent();
}
super.initState();
}
@@ -115,6 +121,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
autoPageTurningTimer?.cancel();
focusNode.dispose();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
stopVolumeEvent();
super.dispose();
}
@@ -152,6 +159,31 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
HistoryManager().addHistory(history!);
}
}
void handleVolumeEvent() {
if(!App.isAndroid) {
// Currently only support Android
return;
}
if(volumeListener != null) {
volumeListener?.cancel();
}
volumeListener = VolumeListener(
onDown: () {
toNextPage();
},
onUp: () {
toPrevPage();
},
)..listen();
}
void stopVolumeEvent() {
if(volumeListener != null) {
volumeListener?.cancel();
volumeListener = null;
}
}
}
abstract mixin class _ReaderLocation {

View File

@@ -107,7 +107,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
void openOrClose() {
if(!_isOpen) {
if (!_isOpen) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
@@ -147,7 +147,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
),
AnimatedPositioned(
duration: const Duration(milliseconds: 180),
bottom: _isOpen ? 0 : -kBottomBarHeight,
bottom: _isOpen
? 0
: -(kBottomBarHeight + MediaQuery.of(context).padding.bottom),
left: 0,
right: 0,
child: buildBottom(),
@@ -218,7 +220,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (!context.reader.toPrevChapter()) {
context.reader.toPage(1);
} else {
if(showFloatingButtonValue != 0) {
if (showFloatingButtonValue != 0) {
setState(() {
showFloatingButtonValue = 0;
});
@@ -235,7 +237,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (!context.reader.toNextChapter()) {
context.reader.toPage(context.reader.maxPage);
} else {
if(showFloatingButtonValue != 0) {
if (showFloatingButtonValue != 0) {
setState(() {
showFloatingButtonValue = 0;
});
@@ -260,7 +262,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
color: Theme.of(context).colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(text),
child: Center(
child: Text(text),
),
),
const Spacer(),
if (App.isWindows)
@@ -389,7 +393,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Widget buildPageInfoText() {
var epName = context.reader.widget.chapters?.values
.elementAt(context.reader.chapter - 1) ??
.elementAtOrNull(context.reader.chapter - 1) ??
"E${context.reader.chapter}";
if (epName.length > 8) {
epName = "${epName.substring(0, 8)}...";
@@ -466,6 +470,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
App.rootContext.pop();
}
if (key == "enableTurnPageByVolumeKey") {
if(appdata.settings[key]) {
context.reader.handleVolumeEvent();
} else {
context.reader.stopVolumeEvent();
}
}
context.reader.update();
},
),
@@ -513,12 +524,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
color: Colors.transparent,
child: InkWell(
onTap: () {
setFloatingButton(0);
if (showFloatingButtonValue == 1) {
context.reader.toNextChapter();
} else {
} else if (showFloatingButtonValue == -1) {
context.reader.toPrevChapter();
}
setFloatingButton(0);
},
borderRadius: BorderRadius.circular(16),
child: Center(
@@ -539,7 +550,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
bottom: 0,
left: 0,
right: 0,
height: value.clamp(0, 58*3) / 3,
height: value.clamp(0, 58 * 3) / 3,
child: ColoredBox(
color: Theme.of(context)
.colorScheme

View File

@@ -369,6 +369,9 @@ class _SearchPageState extends State<SearchPage> {
),
trailing: const Icon(Icons.arrow_right),
onTap: () {
setState(() {
suggestions.clear();
});
handleAppLink(Uri.parse(controller.text));
},
);

View File

@@ -42,7 +42,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
void search([String? text]) {
if (text != null) {
if(suggestionsController.entry != null) {
if (suggestionsController.entry != null) {
suggestionsController.remove();
}
setState(() {
@@ -135,20 +135,24 @@ class _SearchResultPageState extends State<SearchResultPage> {
onChanged: onChanged,
action: buildAction(),
),
loadPage: source!.searchPageData!.loadPage == null ? null : (i) {
return source.searchPageData!.loadPage!(
text,
i,
options,
);
},
loadNext: source.searchPageData!.loadNext == null ? null : (i) {
return source.searchPageData!.loadNext!(
text,
i,
options,
);
},
loadPage: source!.searchPageData!.loadPage == null
? null
: (i) {
return source.searchPageData!.loadPage!(
text,
i,
options,
);
},
loadNext: source.searchPageData!.loadNext == null
? null
: (i) {
return source.searchPageData!.loadNext!(
text,
i,
options,
);
},
);
}
@@ -424,6 +428,11 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
setState(() {
searchTarget = e.key;
options.clear();
final searchOptions = ComicSource.find(searchTarget)!
.searchPageData!
.searchOptions ??
<SearchOptions>[];
options = searchOptions.map((e) => e.defaultValue).toList();
onChanged();
});
},

View File

@@ -53,30 +53,7 @@ class _AboutSettingsState extends State<AboutSettings> {
setState(() {
isCheckingUpdate = true;
});
checkUpdate().then((value) {
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?"
.tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
]);
});
} else {
context.showMessage(message: "No new version available".tl);
}
checkUpdateUi().then((value) {
setState(() {
isCheckingUpdate = false;
});
@@ -108,6 +85,33 @@ Future<bool> checkUpdate() async {
return false;
}
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
var value = await checkUpdate();
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?".tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
],
);
});
} else if (showMessageIfNoUpdate) {
App.rootContext.showMessage(message: "No new version available".tl);
}
}
/// return true if version1 > version2
bool _compareVersion(String version1, String version2) {
var v1 = version1.split(".");

View File

@@ -20,16 +20,44 @@ class _AppSettingsState extends State<AppSettings> {
ListTile(
title: Text("Storage Path for local comics".tl),
subtitle: Text(LocalManager().path, softWrap: false),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: LocalManager().path));
context.showMessage(message: "Path copied to clipboard".tl);
},
),
).toSliver(),
_CallbackSetting(
title: "Set New Storage Path".tl,
actionTitle: "Set".tl,
callback: () async {
if (App.isMobile) {
context.showMessage(message: "Not supported".tl);
return;
String? result;
if (App.isAndroid) {
var channel = const MethodChannel("venera/storage");
var permission = await channel.invokeMethod('');
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) {
result = await selectDirectoryIOS();
} else {
result = await selectDirectory();
}
var result = await selectDirectory();
if (result == null) return;
var loadingDialog = showLoadingDialog(
App.rootContext,
@@ -78,14 +106,49 @@ class _AppSettingsState extends State<AppSettings> {
appdata.settings['cacheSize'] = int.parse(value);
appdata.saveData();
setState(() {});
CacheManager()
.setLimitSize(appdata.settings['cacheSize']);
CacheManager().setLimitSize(appdata.settings['cacheSize']);
return null;
},
);
},
actionTitle: 'Set'.tl,
).toSliver(),
_CallbackSetting(
title: "Export App Data".tl,
callback: () async {
var controller = showLoadingDialog(context);
var file = await exportAppData();
await saveFile(filename: "data.venera", file: file);
controller.close();
},
actionTitle: 'Export'.tl,
).toSliver(),
_CallbackSetting(
title: "Import App Data".tl,
callback: () async {
var controller = showLoadingDialog(context);
var file = await selectFile(ext: ['venera']);
if (file != null) {
var cacheFile = File(FilePath.join(App.cachePath, "temp.venera"));
await file.saveTo(cacheFile.path);
try {
await importAppData(cacheFile);
} catch (e, s) {
Log.error("Import data", e.toString(), s);
context.showMessage(message: "Failed to import data".tl);
}
}
controller.close();
},
actionTitle: 'Import'.tl,
).toSliver(),
_CallbackSetting(
title: "Data Sync".tl,
callback: () async {
showPopUpWidget(context, const _WebdavSetting());
},
actionTitle: 'Set'.tl,
).toSliver(),
_SettingPartTitle(
title: "Log".tl,
icon: Icons.error_outline,
@@ -241,3 +304,129 @@ class _LogsPageState extends State<LogsPage> {
saveFile(data: utf8.encode(log), filename: 'log.txt');
}
}
class _WebdavSetting extends StatefulWidget {
const _WebdavSetting();
@override
State<_WebdavSetting> createState() => _WebdavSettingState();
}
class _WebdavSettingState extends State<_WebdavSetting> {
String url = "";
String user = "";
String pass = "";
bool isTesting = false;
bool upload = true;
@override
void initState() {
super.initState();
if (appdata.settings['webdav'] is! List) {
appdata.settings['webdav'] = [];
}
var configs = appdata.settings['webdav'] as List;
if (configs.whereType<String>().length != 3) {
return;
}
url = configs[0];
user = configs[1];
pass = configs[2];
}
@override
Widget build(BuildContext context) {
return PopUpWidgetScaffold(
title: "Webdav",
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(
labelText: "URL",
border: OutlineInputBorder(),
),
controller: TextEditingController(text: url),
onChanged: (value) => url = value,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: user),
onChanged: (value) => user = value,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: pass),
onChanged: (value) => pass = value,
),
const SizedBox(height: 12),
Row(
children: [
Text("Operation".tl),
Radio<bool>(
groupValue: upload,
value: true,
onChanged: (value) {
setState(() {
upload = value!;
});
},
),
Text("Upload".tl),
Radio<bool>(
groupValue: upload,
value: false,
onChanged: (value) {
setState(() {
upload = value!;
});
},
),
Text("Download".tl),
],
),
const SizedBox(height: 16),
Center(
child: Button.filled(
isLoading: isTesting,
onPressed: () async {
var oldConfig = appdata.settings['webdav'];
appdata.settings['webdav'] = [url, user, pass];
setState(() {
isTesting = true;
});
var testResult = upload
? await DataSync().uploadData()
: await DataSync().downloadData();
if (testResult.error) {
setState(() {
isTesting = false;
});
appdata.settings['webdav'] = oldConfig;
context.showMessage(message: testResult.errorMessage!);
return;
}
appdata.saveData();
context.showMessage(message: "Saved".tl);
App.rootPop();
},
child: Text("Continue".tl),
),
)
],
).paddingHorizontal(16),
),
);
}
}

View File

@@ -21,6 +21,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
"light": "Light".tl,
"dark": "Dark".tl,
},
onChanged: () async {
App.forceRebuild();
},
).toSliver(),
SelectSetting(
title: "Theme Color".tl,

View File

@@ -24,12 +24,20 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
SelectSetting(
title: "Move favorite after reading".tl,
settingKey: "moveFavoriteAfterRead",
optionTranslation: {
optionTranslation: const {
"none": "None",
"end": "End",
"start": "Start",
},
).toSliver(),
SelectSetting(
title: "Quick Favorite".tl,
settingKey: "quickFavorite",
help: "Long press on the favorite button to quickly add to this folder".tl,
optionTranslation: {
for (var e in LocalFavoritesManager().folderNames) e: e
},
).toSliver(),
],
);
}

View File

@@ -17,6 +17,13 @@ class _NetworkSettingsState extends State<NetworkSettings> {
title: "Proxy".tl,
builder: () => const _ProxySettingView(),
).toSliver(),
_SliderSetting(
title: "Download Threads".tl,
settingsIndex: 'downloadThreads',
interval: 1,
min: 1,
max: 16,
).toSliver(),
],
);
}
@@ -42,50 +49,50 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
// USERNAME:PASSWORD@HOST:PORT
String toProxyStr() {
if(type == 'direct') {
if (type == 'direct') {
return 'direct';
} else if(type == 'system') {
} else if (type == 'system') {
return 'system';
}
var res = '';
if(username.isNotEmpty) {
if (username.isNotEmpty) {
res += username;
if(password.isNotEmpty) {
if (password.isNotEmpty) {
res += ':$password';
}
res += '@';
}
res += host;
if(port.isNotEmpty) {
if (port.isNotEmpty) {
res += ':$port';
}
return res;
}
void parseProxyString(String proxy) {
if(proxy == 'direct') {
if (proxy == 'direct') {
type = 'direct';
return;
} else if(proxy == 'system') {
} else if (proxy == 'system') {
type = 'system';
return;
}
type = 'manual';
var parts = proxy.split('@');
if(parts.length == 2) {
if (parts.length == 2) {
var auth = parts[0].split(':');
if(auth.length == 2) {
if (auth.length == 2) {
username = auth[0];
password = auth[1];
}
parts = parts[1].split(':');
if(parts.length == 2) {
if (parts.length == 2) {
host = parts[0];
port = parts[1];
}
} else {
parts = proxy.split(':');
if(parts.length == 2) {
if (parts.length == 2) {
host = parts[0];
port = parts[1];
}
@@ -140,7 +147,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
});
},
),
if(type == 'manual') buildManualProxy(),
if (type == 'manual') buildManualProxy(),
],
),
),
@@ -164,7 +171,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
host = v;
},
validator: (v) {
if(v?.isEmpty ?? false) {
if (v?.isEmpty ?? false) {
return "Host cannot be empty".tl;
}
return null;
@@ -181,10 +188,10 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
port = v;
},
validator: (v) {
if(v?.isEmpty ?? true) {
if (v?.isEmpty ?? true) {
return null;
}
if(int.tryParse(v!) == null) {
if (int.tryParse(v!) == null) {
return "Port must be a number".tl;
}
return null;
@@ -201,7 +208,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
username = v;
},
validator: (v) {
if((v?.isEmpty ?? false) && password.isNotEmpty) {
if ((v?.isEmpty ?? false) && password.isNotEmpty) {
return "Username cannot be empty".tl;
}
return null;
@@ -221,7 +228,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
const SizedBox(height: 16),
FilledButton(
onPressed: () {
if(formKey.currentState?.validate() ?? false) {
if (formKey.currentState?.validate() ?? false) {
appdata.settings['proxy'] = toProxyStr();
appdata.saveData();
App.rootContext.pop();

View File

@@ -54,6 +54,29 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("autoPageTurningInterval");
},
).toSliver(),
_SwitchSetting(
title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom',
onChanged: () {
widget.onChanged?.call('enableLongPressToZoom');
},
).toSliver(),
_SwitchSetting(
title: 'Limit image width'.tl,
subtitle: 'When using Continuous(Top to Bottom) mode'.tl,
settingKey: 'limitImageWidth',
onChanged: () {
widget.onChanged?.call('limitImageWidth');
},
).toSliver(),
if(App.isAndroid)
_SwitchSetting(
title: 'Turn page by volume key'.tl,
settingKey: 'enableTurnPageByVolumeKey',
onChanged: () {
widget.onChanged?.call('enableTurnPageByVolumeKey');
},
).toSliver(),
],
);
}

View File

@@ -5,6 +5,7 @@ class _SwitchSetting extends StatefulWidget {
required this.title,
required this.settingKey,
this.onChanged,
this.subtitle,
});
final String title;
@@ -13,6 +14,8 @@ class _SwitchSetting extends StatefulWidget {
final VoidCallback? onChanged;
final String? subtitle;
@override
State<_SwitchSetting> createState() => _SwitchSettingState();
}
@@ -24,6 +27,7 @@ class _SwitchSettingState extends State<_SwitchSetting> {
return ListTile(
title: Text(widget.title),
subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),
trailing: Switch(
value: appdata.settings[widget.settingKey],
onChanged: (value) {
@@ -45,6 +49,7 @@ class SelectSetting extends StatelessWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -55,6 +60,8 @@ class SelectSetting extends StatelessWidget {
final VoidCallback? onChanged;
final String? help;
@override
Widget build(BuildContext context) {
return SizedBox(
@@ -67,6 +74,7 @@ class SelectSetting extends StatelessWidget {
settingKey: settingKey,
optionTranslation: optionTranslation,
onChanged: onChanged,
help: help,
);
} else {
return _EndSelectorSelectSetting(
@@ -74,6 +82,7 @@ class SelectSetting extends StatelessWidget {
settingKey: settingKey,
optionTranslation: optionTranslation,
onChanged: onChanged,
help: help,
);
}
},
@@ -88,6 +97,7 @@ class _DoubleLineSelectSettings extends StatefulWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -98,6 +108,8 @@ class _DoubleLineSelectSettings extends StatefulWidget {
final VoidCallback? onChanged;
final String? help;
@override
State<_DoubleLineSelectSettings> createState() =>
_DoubleLineSelectSettingsState();
@@ -107,9 +119,37 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.title),
subtitle:
Text(widget.optionTranslation[appdata.settings[widget.settingKey]]!),
title: Row(
children: [
Text(widget.title),
const SizedBox(width: 4),
if (widget.help != null)
Button.icon(
size: 18,
icon: const Icon(Icons.help_outline),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ContentDialog(
title: "Help".tl,
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
child: Text("OK".tl),
),
],
);
},
);
},
),
],
),
subtitle: Text(
widget.optionTranslation[appdata.settings[widget.settingKey]] ??
"None"),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () {
var renderBox = context.findRenderObject() as RenderBox;
@@ -152,6 +192,7 @@ class _EndSelectorSelectSetting extends StatefulWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -162,6 +203,8 @@ class _EndSelectorSelectSetting extends StatefulWidget {
final VoidCallback? onChanged;
final String? help;
@override
State<_EndSelectorSelectSetting> createState() =>
_EndSelectorSelectSettingState();
@@ -172,10 +215,38 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
Widget build(BuildContext context) {
var options = widget.optionTranslation;
return ListTile(
title: Text(widget.title),
title: Row(
children: [
Text(widget.title),
const SizedBox(width: 4),
if (widget.help != null)
Button.icon(
size: 18,
icon: const Icon(Icons.help_outline),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ContentDialog(
title: "Help".tl,
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
child: Text("OK".tl),
),
],
);
},
);
},
),
],
),
trailing: Select(
current: options[appdata.settings[widget.settingKey]]!,
current: options[appdata.settings[widget.settingKey]],
values: options.values.toList(),
minWidth: 64,
onTap: (index) {
setState(() {
appdata.settings[widget.settingKey] = options.keys.elementAt(index);
@@ -434,7 +505,7 @@ class _CallbackSetting extends StatelessWidget {
return ListTile(
title: Text(title),
subtitle: subtitle == null ? null : Text(subtitle!),
trailing: FilledButton(
trailing: Button.normal(
onPressed: callback,
child: Text(actionTitle),
).fixHeight(28),

View File

@@ -11,9 +11,12 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/data.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:yaml/yaml.dart';

View File

@@ -98,14 +98,14 @@ class _AppWebviewState extends State<AppWebview> {
0),
items: [
PopupMenuItem(
child: Text("Open in browser".tl),
onTap: () async =>
launchUrlString((await controller?.getUrl())!.path),
child: Text("Open in browser".tl),
onTap: () async =>
launchUrlString((await controller?.getUrl())!.toString()),
),
PopupMenuItem(
child: Text("Copy link".tl),
onTap: () async => Clipboard.setData(ClipboardData(
text: (await controller?.getUrl())!.path)),
child: Text("Copy link".tl),
onTap: () async => Clipboard.setData(ClipboardData(
text: (await controller?.getUrl())!.toString())),
),
PopupMenuItem(
child: Text("Reload".tl),

79
lib/utils/data.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:zip_flutter/zip_flutter.dart';
import 'io.dart';
Future<File> exportAppData() async {
var time = DateTime.now().millisecondsSinceEpoch ~/ 1000;
var cacheFilePath = FilePath.join(App.cachePath, '$time.venera');
var cacheFile = File(cacheFilePath);
var dataPath = App.dataPath;
if(await cacheFile.exists()) {
await cacheFile.delete();
}
await Isolate.run(() {
var zipFile = ZipFile.open(cacheFilePath);
var historyFile = FilePath.join(dataPath, "history.db");
var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db");
var appdata = FilePath.join(dataPath, "appdata.json");
zipFile.addFile("history.db", historyFile);
zipFile.addFile("local_favorite.db", localFavoriteFile);
zipFile.addFile("appdata.json", appdata);
for(var file in Directory(FilePath.join(dataPath, "comic_source")).listSync()) {
if(file is File) {
zipFile.addFile("comic_source/${file.name}", file.path);
}
}
zipFile.close();
});
return cacheFile;
}
Future<void> importAppData(File file, [bool checkVersion = false]) async {
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
var cacheDir = Directory(cacheDirPath);
await Isolate.run(() {
ZipFile.openAndExtract(file.path, cacheDirPath);
});
var historyFile = cacheDir.joinFile("history.db");
var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
var appdataFile = cacheDir.joinFile("appdata.json");
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()) {
HistoryManager().close();
historyFile.copySync(FilePath.join(App.dataPath, "history.db"));
HistoryManager().init();
}
if(await localFavoriteFile.exists()) {
LocalFavoritesManager().close();
localFavoriteFile.copySync(FilePath.join(App.dataPath, "local_favorite.db"));
LocalFavoritesManager().init();
}
if(await appdataFile.exists()) {
await appdataFile.copy(FilePath.join(App.dataPath, "appdata.json"));
appdata.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);
await file.copy(targetFile);
}
}
await ComicSource.reload();
}
}

205
lib/utils/data_sync.dart Normal file
View File

@@ -0,0 +1,205 @@
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/data.dart';
import 'package:venera/utils/ext.dart';
import 'package:webdav_client/webdav_client.dart' hide File;
import 'io.dart';
class DataSync with ChangeNotifier {
DataSync._() {
if (isEnabled) {
downloadData();
}
HistoryManager().addListener(onDataChanged);
LocalFavoritesManager().addListener(onDataChanged);
ComicSource.addListener(onDataChanged);
}
void onDataChanged() {
if (isEnabled) {
uploadData();
}
}
static DataSync? instance;
factory DataSync() => instance ?? (instance = DataSync._());
bool isDownloading = false;
bool isUploading = false;
bool haveWaitingTask = false;
bool get isEnabled {
var config = appdata.settings['webdav'];
return config is List && config.isNotEmpty;
}
List<String>? _validateConfig() {
var config = appdata.settings['webdav'];
if (config is! List || (config.isNotEmpty && config.length != 3)) {
return null;
}
if (config.whereType<String>().length != 3) {
return null;
}
return List.from(config);
}
Future<Res<bool>> uploadData() async {
if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) {
haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100));
}
haveWaitingTask = false;
isUploading = true;
notifyListeners();
try {
var config = _validateConfig();
if (config == null) {
return const Res.error('Invalid WebDAV configuration');
}
if (config.isEmpty) {
return const Res(true);
}
String url = config[0];
String user = config[1];
String pass = config[2];
var proxy = await AppDio.getProxy();
var client = newClient(
url,
user: user,
password: pass,
adapter: IOHttpClientAdapter(
createHttpClient: () {
return HttpClient()
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
},
),
);
try {
await client.ping();
} catch (e) {
Log.error("Upload Data", 'Failed to connect to WebDAV server');
return const Res.error('Failed to connect to WebDAV server');
}
try {
appdata.settings['dataVersion']++;
await appdata.saveData();
var data = await exportAppData();
var time =
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
var filename = time;
filename += '-';
filename += appdata.settings['dataVersion'].toString();
filename += '.venera';
var files = await client.readDir('/');
files = files.where((e) => e.name!.endsWith('.venera')).toList();
var old = files.firstWhereOrNull( (e) => e.name!.startsWith("$time-"));
if (old != null) {
await client.remove(old.name!);
}
if (files.length >= 10) {
files.sort((a, b) => a.name!.compareTo(b.name!));
await client.remove(files.first.name!);
}
await client.write(filename, await data.readAsBytes());
Log.info("Upload Data", "Data uploaded successfully");
return const Res(true);
} catch (e, s) {
Log.error("Upload Data", e, s);
return Res.error(e.toString());
}
} finally {
isUploading = false;
notifyListeners();
}
}
Future<Res<bool>> downloadData() async {
if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) {
haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100));
}
haveWaitingTask = false;
isDownloading = true;
notifyListeners();
try {
var config = _validateConfig();
if (config == null) {
return const Res.error('Invalid WebDAV configuration');
}
if (config.isEmpty) {
return const Res(true);
}
String url = config[0];
String user = config[1];
String pass = config[2];
var proxy = await AppDio.getProxy();
var client = newClient(
url,
user: user,
password: pass,
adapter: IOHttpClientAdapter(
createHttpClient: () {
return HttpClient()
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
},
),
);
try {
await client.ping();
} catch (e) {
Log.error("Data Sync", 'Failed to connect to WebDAV server');
return const Res.error('Failed to connect to WebDAV server');
}
try {
var files = await client.readDir('/');
files.sort((a, b) => b.name!.compareTo(a.name!));
var file = files.firstWhereOrNull((e) => e.name!.endsWith('.venera'));
var version =
file!.name!.split('-').elementAtOrNull(1)?.split('.').first;
if (version != null && int.tryParse(version) != null) {
var currentVersion = appdata.settings['dataVersion'];
if (currentVersion != null && int.parse(version) <= currentVersion) {
Log.info("Data Sync", 'No new data to download');
return const Res(true);
}
}
Log.info("Data Sync", "Downloading data from WebDAV server");
var localFile = File(FilePath.join(App.cachePath, file.name!));
await client.read2File(file.name!, localFile.path);
await importAppData(localFile, true);
await localFile.delete();
Log.info("Data Sync", "Data downloaded successfully");
return const Res(true);
} catch (e, s) {
Log.error("Data Sync", e, s);
return Res.error(e.toString());
}
} finally {
isDownloading = false;
notifyListeners();
}
}
}

View File

@@ -24,6 +24,18 @@ extension ListExt<T> on List<T>{
add(value);
}
}
bool isEqualsTo(List<T> list){
if(length != list.length){
return false;
}
for(int i=0; i<length; i++){
if(this[i] != list[i]){
return false;
}
}
return true;
}
}
extension StringExt on String{

316
lib/utils/image.dart Normal file
View File

@@ -0,0 +1,316 @@
import 'dart:ffi';
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:lodepng_flutter/lodepng_flutter.dart' as lodepng;
class Image {
final Uint32List _data;
final int width;
final int height;
Image(this._data, this.width, this.height) {
if (_data.length != width * height) {
throw ArgumentError(
'Invalid argument: data length must be equal to width * height.');
}
}
Image.empty(this.width, this.height) : _data = Uint32List(width * height);
static Future<Image> decodeImage(Uint8List data) async {
var codec = await ui.instantiateImageCodec(data);
var frame = await codec.getNextFrame();
codec.dispose();
var info = await frame.image.toByteData();
if (info == null) {
throw Exception('Failed to decode image');
}
var image = Image(
info.buffer.asUint32List(),
frame.image.width,
frame.image.height,
);
frame.image.dispose();
return image;
}
Image copyRange(int x, int y, int width, int height) {
if (width + x > this.width) {
throw ArgumentError('''
Invalid argument: x + width must be less than or equal to the image width.
x: $x, width: $width, image width: ${this.width}
'''
.trim());
}
if (height + y > this.height) {
throw ArgumentError('''
Invalid argument: y + height must be less than or equal to the image height.
y: $y, height: $height, image height: ${this.height}
'''
.trim());
}
var data = Uint32List(width * height);
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++) {
data[j * width + i] = _data[(j + y) * this.width + i + x];
}
}
return Image(data, width, height);
}
void fillImageAt(int x, int y, Image image) {
if (x + image.width > width) {
throw ArgumentError('''
Invalid argument: x + image width must be less than or equal to the image width.
x: $x, image width: ${image.width}, image width: $width
'''
.trim());
}
if (y + image.height > height) {
throw ArgumentError('''
Invalid argument: y + image height must be less than or equal to the image height.
y: $y, image height: ${image.height}, image height: $height
'''
.trim());
}
for (var j = 0; j < image.height && (j + y) < height; j++) {
for (var i = 0; i < image.width && (i + x) < width; i++) {
_data[(j + y) * width + i + x] = image._data[j * image.width + i];
}
}
}
void fillImageRangeAt(
int x, int y, Image image, int srcX, int srcY, int width, int height) {
if (x + width > this.width) {
throw ArgumentError('''
Invalid argument: x + width must be less than or equal to the image width.
x: $x, width: $width, image width: ${this.width}
'''
.trim());
}
if (y + height > this.height) {
throw ArgumentError('''
Invalid argument: y + height must be less than or equal to the image height.
y: $y, height: $height, image height: ${this.height}
'''
.trim());
}
if (srcX + width > image.width) {
throw ArgumentError('''
Invalid argument: srcX + width must be less than or equal to the image width.
srcX: $srcX, width: $width, image width: ${image.width}
'''
.trim());
}
if (srcY + height > image.height) {
throw ArgumentError('''
Invalid argument: srcY + height must be less than or equal to the image height.
srcY: $srcY, height: $height, image height: ${image.height}
'''
.trim());
}
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++) {
_data[(j + y) * this.width + i + x] =
image._data[(j + srcY) * image.width + i + srcX];
}
}
}
Image copyAndRotate90() {
var data = Uint32List(width * height);
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++) {
data[i * height + height - j - 1] = _data[j * width + i];
}
}
return Image(data, height, width);
}
Color getPixel(int x, int y) {
if (x < 0 || x >= width) {
throw ArgumentError(
'Invalid argument: x must be in the range of [0, $width).');
}
if (y < 0 || y >= height) {
throw ArgumentError(
'Invalid argument: y must be in the range of [0, $height).');
}
return Color.fromValue(_data[y * width + x]);
}
void setPixel(int x, int y, Color color) {
if (x < 0 || x >= width) {
throw ArgumentError(
'Invalid argument: x must be in the range of [0, $width).');
}
if (y < 0 || y >= height) {
throw ArgumentError(
'Invalid argument: y must be in the range of [0, $height).');
}
_data[y * width + x] = color.value;
}
Uint8List encodePng() {
var data = lodepng.encodePngToPointer(lodepng.Image(
_data.buffer.asUint8List(),
width,
height,
));
return Pointer<Uint8>.fromAddress(data.address).asTypedList(data.length,
finalizer: lodepng.ByteBuffer.finalizer);
}
}
class Color {
final int value;
Color(int r, int g, int b, [int a = 255])
: value = (a << 24) | (r << 16) | (g << 8) | b;
Color.fromValue(this.value);
int get r => (value >> 16) & 0xFF;
int get g => (value >> 8) & 0xFF;
int get b => value & 0xFF;
int get a => (value >> 24) & 0xFF;
}
class JsEngine {
static final JsEngine _instance = JsEngine._();
factory JsEngine() => _instance;
JsEngine._() {
_engine = FlutterQjs();
_engine!.dispatch();
var setGlobalFunc =
_engine!.evaluate("(key, value) => { this[key] = value; }");
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
setGlobalFunc.free();
}
FlutterQjs? _engine;
dynamic runCode(String js, [String? name]) {
return _engine!.evaluate(js, name: name);
}
var images = <int, Image>{};
int _key = 0;
int setImage(Image image) {
var key = _key++;
images[key] = image;
return key;
}
Object? _messageReceiver(dynamic message) {
if (message is! Map) return null;
var method = message['method'];
if (method == 'image') {
switch (message['function']) {
case 'copyRange':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var x = message['x'];
var y = message['y'];
var width = message['width'];
var height = message['height'];
var newImage = image.copyRange(x, y, width, height);
return setImage(newImage);
case 'copyAndRotate90':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var newImage = image.copyAndRotate90();
return setImage(newImage);
case 'fillImageAt':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var x = message['x'];
var y = message['y'];
var key2 = message['image'];
var image2 = images[key2];
if (image2 == null) return null;
image.fillImageAt(x, y, image2);
return null;
case 'fillImageRangeAt':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var x = message['x'];
var y = message['y'];
var key2 = message['image'];
var image2 = images[key2];
if (image2 == null) return null;
var srcX = message['srcX'];
var srcY = message['srcY'];
var width = message['width'];
var height = message['height'];
image.fillImageRangeAt(x, y, image2, srcX, srcY, width, height);
return null;
case 'getWidth':
var key = message['key'];
var image = images[key];
if (image == null) return null;
return image.width;
case 'getHeight':
var key = message['key'];
var image = images[key];
if (image == null) return null;
return image.height;
case 'emptyImage':
var width = message['width'];
var height = message['height'];
var newImage = Image.empty(width, height);
return setImage(newImage);
}
}
return null;
}
}
var _tasksCount = 0;
Future<Uint8List> modifyImageWithScript(Uint8List data, String script) async {
while (_tasksCount > 3) {
await Future.delayed(const Duration(milliseconds: 200));
}
_tasksCount++;
try {
var image = await Image.decodeImage(data);
var initJs = await rootBundle.loadString('assets/init.js');
return await Isolate.run(() {
var jsEngine = JsEngine();
jsEngine.runCode(initJs, '<init>');
jsEngine.runCode(script);
var key = jsEngine.setImage(image);
var res = jsEngine.runCode('''
let func = () => {
let image = new Image($key);
let result = modifyImage(image);
return result.key;
}
func();
''');
var newImage = jsEngine.images[res];
var data = newImage!.encodePng();
return Uint8List.fromList(data);
});
} finally {
_tasksCount--;
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
@@ -113,6 +114,12 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
}
}
Future<void> copyDirectoryIsolate(Directory source, Directory destination) async {
await Isolate.run(() {
copyDirectory(source, destination);
});
}
String findValidDirectoryName(String path, String directory) {
var name = sanitizeFileName(directory);
var dir = Directory("$path/$name");
@@ -160,6 +167,22 @@ class DirectoryPicker {
}
}
class IOSDirectoryPicker {
static const MethodChannel _channel = MethodChannel("venera/method_channel");
// 调用 iOS 目录选择方法
static Future<String?> selectDirectory() async {
try {
final String? path = await _channel.invokeMethod('selectDirectory');
return path;
} catch (e) {
print("Error selecting directory: $e");
// 返回报错信息
return e.toString();
}
}
}
Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
label: 'files',
@@ -169,7 +192,8 @@ Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
acceptedTypeGroups: <file_selector.XTypeGroup>[typeGroup],
);
if (file == null) return null;
if (!ext.contains(file?.path.split(".").last)) {
if (!ext.contains(file.path.split(".").last)) {
App.rootContext.showMessage(message: "Invalid file type");
return null;
}
return file;
@@ -180,6 +204,11 @@ Future<String?> selectDirectory() async {
return path;
}
// selectDirectoryIOS
Future<String?> selectDirectoryIOS() async {
return IOSDirectoryPicker.selectDirectory();
}
Future<void> saveFile(
{Uint8List? data, required String filename, File? file}) async {
if (data == null && file == null) {

31
lib/utils/volume.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'dart:async';
import 'package:flutter/services.dart';
class VolumeListener {
static const channel = EventChannel('venera/volume');
void Function()? onUp;
void Function()? onDown;
VolumeListener({this.onUp, this.onDown});
StreamSubscription? stream;
void listen() {
stream = channel.receiveBroadcastStream().listen(onEvent);
}
void onEvent(event) {
if (event == 1) {
onUp!();
} else if (event == 2) {
onDown!();
}
}
void cancel() {
stream?.cancel();
}
}

View File

@@ -10,7 +10,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
@@ -28,9 +28,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@@ -7,13 +7,15 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_qjs
gtk
screen_retriever
screen_retriever_linux
sqlite3_flutter_libs
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter
)

View File

@@ -10,7 +10,7 @@ import desktop_webview_window
import file_selector_macos
import flutter_inappwebview_macos
import path_provider_foundation
import screen_retriever
import screen_retriever_macos
import share_plus
import sqlite3_flutter_libs
import url_launcher_macos
@@ -22,7 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
@@ -337,10 +345,18 @@ packages:
dependency: "direct main"
description:
name: flutter_reorderable_grid_view
sha256: "40abcc5bff228ebff119326502e7357ee6399956b60b80b17385e9770b7458c0"
sha256: "93a2b9e279bf40b9333428a67e70e520ca1528554984eb6f6304538400897e64"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "5.3.2"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "5fe868d3cb8cbc4d83091748552e03f00ccfa41b8e44691bc382611f831d5f8b"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -368,6 +384,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
gtk:
dependency: transitive
description:
@@ -424,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker:
dependency: transitive
description:
@@ -456,6 +488,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
lodepng_flutter:
dependency: "direct main"
description:
path: "."
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
url: "https://github.com/venera-app/lodepng_flutter"
source: git
version: "0.0.1"
matcher:
dependency: transitive
description:
@@ -552,6 +593,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
url: "https://pub.dev"
source: hosted
version: "12.0.13"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
url: "https://pub.dev"
source: hosted
version: "9.4.5"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
photo_view:
dependency: "direct main"
description:
@@ -585,14 +682,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.9.1"
rhttp:
dependency: "direct main"
description:
name: rhttp
sha256: "92fb57dea6338370efe1e4e2101e8b521f91f15bc60ef6908469b4392dd9803a"
url: "https://pub.dev"
source: hosted
version: "0.9.1"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
scrollable_positioned_list:
dependency: "direct main"
description:
@@ -807,6 +944,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
webdav_client:
dependency: "direct main"
description:
path: "."
ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
resolved-ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
url: "https://github.com/wgh136/webdav_client"
source: git
version: "1.2.2"
win32:
dependency: transitive
description:
@@ -819,10 +965,10 @@ packages:
dependency: "direct main"
description:
name: window_manager
sha256: ab8b2a7f97543d3db2b506c9d875e637149d48ee0c6a5cb5f5fd6e0dac463792
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
version: "0.4.3"
xdg_directories:
dependency: transitive
description:
@@ -831,6 +977,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: "direct main"
description:
@@ -849,5 +1003,5 @@ packages:
source: git
version: "0.0.1"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.5.4 <4.0.0"
flutter: ">=3.24.4"

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.0.0+1
version: 1.0.4+104
environment:
sdk: '>=3.5.0 <4.0.0'
@@ -15,7 +15,7 @@ dependencies:
flutter_localizations:
sdk: flutter
intl: any
window_manager: ^0.4.2
window_manager: ^0.4.3
sqlite3: any
sqlite3_flutter_libs: any
flutter_qjs:
@@ -39,7 +39,7 @@ dependencies:
url: https://github.com/venera-app/flutter.widgets
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
path: packages/scrollable_positioned_list
flutter_reorderable_grid_view: 5.0.1
flutter_reorderable_grid_view: 5.3.2
yaml: any
uuid: ^4.5.1
desktop_webview_window:
@@ -54,6 +54,15 @@ dependencies:
zip_flutter:
git:
url: https://github.com/wgh136/zip_flutter
lodepng_flutter:
git:
url: https://github.com/venera-app/lodepng_flutter
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
rhttp: 0.9.1
webdav_client:
git:
url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
dev_dependencies:
flutter_test:

View File

@@ -52,7 +52,11 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\WebView2Loader.dll"; DestD
Source: "{#RootPath}\build\windows\x64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.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\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
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

View File

@@ -11,7 +11,8 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -28,8 +29,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterQjsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(

View File

@@ -8,7 +8,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_inappwebview_windows
flutter_qjs
screen_retriever
permission_handler_windows
screen_retriever_windows
share_plus
sqlite3_flutter_libs
url_launcher_windows
@@ -16,6 +17,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter
)