mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 08:17:25 +00:00
Compare commits
84 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b56f8d7398 | ||
![]() |
8375fb721e | ||
9876da85da | |||
4b19ab57d2 | |||
91ee48cc6c | |||
![]() |
7495c11944 | ||
08e8a45236 | |||
fb1b017bc9 | |||
99a3788f4a | |||
a747179cc4 | |||
1ca8da1c83 | |||
8eddab5e13 | |||
030007159d | |||
43a054c12a | |||
51a6456dad | |||
3a320feda9 | |||
![]() |
a88bbe9ea6 | ||
![]() |
5be2dbcfd7 | ||
68a203a1c1 | |||
c06709aeb7 | |||
95649ca9fe | |||
1e09d69507 | |||
![]() |
a5c745f40d | ||
d27efb180a | |||
1f5382ff8c | |||
2238fcc68f | |||
df42cf320c | |||
eb14f973e4 | |||
99454041d3 | |||
1ae33c43b1 | |||
bed30d3cea | |||
06f953c1bc | |||
0b96d01afb | |||
6023e462d7 | |||
0e22574002 | |||
e1b2f83c48 | |||
e77424e00e | |||
9f67cd0d07 | |||
6a79f68909 | |||
aa66111f2c | |||
ddeaaf0856 | |||
18f450a0db | |||
a217b86c08 | |||
![]() |
79d2c91723 | ||
![]() |
731510e11d | ||
![]() |
b3d3c141f9 | ||
![]() |
bea861a83c | ||
![]() |
4a595a8aca | ||
![]() |
bf634f8654 | ||
![]() |
bda215ebb7 | ||
a70b690d3c | |||
0b8ae2d377 | |||
24c5a1bb01 | |||
ea973a2787 | |||
![]() |
17bce96143 | ||
![]() |
909c0014ac | ||
eb1abfc02a | |||
![]() |
788e41f584 | ||
929ec88e84 | |||
abaeaf4f77 | |||
![]() |
a614e83470 | ||
8b9fd0d03d | |||
![]() |
1964c4c0d5 | ||
43d724dd27 | |||
f9c42aef4b | |||
06a6e5156a | |||
![]() |
be45a06981 | ||
4763b9c7b4 | |||
7e608be70f | |||
211e6ab8c8 | |||
![]() |
100dc6458b | ||
![]() |
8dab5f9e88 | ||
d08383e14b | |||
a55e4eff67 | |||
ab3953292b | |||
b49e0974ab | |||
![]() |
b6cccb7749 | ||
![]() |
dac07cfac4 | ||
![]() |
da12b3bcca | ||
![]() |
017f964705 | ||
![]() |
bed0f78e81 | ||
![]() |
092eb59c10 | ||
![]() |
a5d3d160c8 | ||
![]() |
d3c3748ce5 |
19
.github/workflows/analyze.yml
vendored
Normal file
19
.github/workflows/analyze.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: "analyze"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version-file: pubspec.yaml
|
||||||
|
architecture: x64
|
||||||
|
- run: flutter pub get
|
||||||
|
- uses: invertase/github-action-dart-analyzer@v1
|
16
.github/workflows/fastlane.yml
vendored
Normal file
16
.github/workflows/fastlane.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Validate Fastlane metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Validate Fastlane Supply Metadata
|
||||||
|
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
|
@@ -1,9 +1,9 @@
|
|||||||
# venera
|
# venera
|
||||||
|
|
||||||
[](https://flutter.dev/)
|
[](https://flutter.dev/)
|
||||||
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
||||||
[](https://github.com/venera-app/venera/releases)
|
[](https://github.com/venera-app/venera/releases)
|
||||||
[](https://github.com/venera-app/venera/stargazers)
|
[](https://github.com/venera-app/venera/stargazers)
|
||||||
[](https://t.me/+Ws-IpmUutzkxMjhl)
|
[](https://t.me/+Ws-IpmUutzkxMjhl)
|
||||||
|
|
||||||
A comic reader that support reading local and network comics.
|
A comic reader that support reading local and network comics.
|
||||||
|
@@ -5,6 +5,8 @@ plugins {
|
|||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext.abiCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86_64": 3]
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file("local.properties")
|
def localPropertiesFile = rootProject.file("local.properties")
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
@@ -35,7 +37,7 @@ android {
|
|||||||
splits{
|
splits{
|
||||||
abi {
|
abi {
|
||||||
reset()
|
reset()
|
||||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||||
enable true
|
enable true
|
||||||
universalApk true
|
universalApk true
|
||||||
}
|
}
|
||||||
@@ -78,7 +80,7 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
@@ -86,13 +88,25 @@ android {
|
|||||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||||
if (abi != null) {
|
if (abi != null) {
|
||||||
outputFileName = "venera-${variant.versionName}-${abi}.apk"
|
outputFileName = "venera-${variant.versionName}-${abi}.apk"
|
||||||
|
def abiVersionCode = project.ext.abiCodes.get(abi)
|
||||||
|
if (abiVersionCode != null) {
|
||||||
|
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
outputFileName = "venera-${variant.versionName}.apk"
|
outputFileName = "venera-${variant.versionName}.apk"
|
||||||
|
versionCodeOverride = variant.versionCode * 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
// Disables dependency metadata when building APKs.
|
||||||
|
includeInApk = false
|
||||||
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@@ -102,4 +116,4 @@ flutter {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.activity:activity-ktx:1.9.2"
|
implementation "androidx.activity:activity-ktx:1.9.2"
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,8 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<!-- [flutter 3.27.1] Impeller is still worse than skia, disable it -->
|
||||||
|
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||||
|
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.2.1' apply false
|
id "com.android.application" version '8.3.2' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -218,7 +218,6 @@
|
|||||||
"Create Folder": "新建文件夹",
|
"Create Folder": "新建文件夹",
|
||||||
"Select an image on screen": "选择屏幕上的图片",
|
"Select an image on screen": "选择屏幕上的图片",
|
||||||
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
|
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
|
||||||
"Ignore Certificate Errors": "忽略证书错误",
|
|
||||||
"Authorization Required": "需要身份验证",
|
"Authorization Required": "需要身份验证",
|
||||||
"Sync": "同步",
|
"Sync": "同步",
|
||||||
"The folder is Linked to @source": "文件夹已关联到 @source",
|
"The folder is Linked to @source": "文件夹已关联到 @source",
|
||||||
@@ -251,7 +250,22 @@
|
|||||||
"Aggregated Search": "聚合搜索",
|
"Aggregated Search": "聚合搜索",
|
||||||
"No search results found": "未找到搜索结果",
|
"No search results found": "未找到搜索结果",
|
||||||
"Added @c comics to download queue." : "已添加 @c 本漫画到下载队列",
|
"Added @c comics to download queue." : "已添加 @c 本漫画到下载队列",
|
||||||
"Download started": "下载已开始"
|
"Download started": "下载已开始",
|
||||||
|
"Click favorite": "点击收藏",
|
||||||
|
"End": "末尾",
|
||||||
|
"None": "无",
|
||||||
|
"View Detail": "查看详情",
|
||||||
|
"Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录",
|
||||||
|
"Multiple cbz files" : "多个cbz文件",
|
||||||
|
"No valid comics found" : "未找到有效的漫画",
|
||||||
|
"Enable DNS Overrides": "启用DNS覆写",
|
||||||
|
"DNS Overrides": "DNS覆写",
|
||||||
|
"Custom Image Processing": "自定义图片处理",
|
||||||
|
"Enable": "启用",
|
||||||
|
"Aggregated": "聚合",
|
||||||
|
"Default Search Target": "默认搜索目标",
|
||||||
|
"Auto Language Filters": "自动语言筛选",
|
||||||
|
"Check for updates on startup": "启动时检查更新"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -472,7 +486,6 @@
|
|||||||
"Create Folder": "新建文件夾",
|
"Create Folder": "新建文件夾",
|
||||||
"Select an image on screen": "選擇屏幕上的圖片",
|
"Select an image on screen": "選擇屏幕上的圖片",
|
||||||
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
|
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
|
||||||
"Ignore Certificate Errors": "忽略證書錯誤",
|
|
||||||
"Authorization Required": "需要身份驗證",
|
"Authorization Required": "需要身份驗證",
|
||||||
"Sync": "同步",
|
"Sync": "同步",
|
||||||
"The folder is Linked to @source": "文件夾已關聯到 @source",
|
"The folder is Linked to @source": "文件夾已關聯到 @source",
|
||||||
@@ -505,6 +518,21 @@
|
|||||||
"Aggregated Search": "聚合搜索",
|
"Aggregated Search": "聚合搜索",
|
||||||
"No search results found": "未找到搜索結果",
|
"No search results found": "未找到搜索結果",
|
||||||
"Added @c comics to download queue." : "已添加 @c 本漫畫到下載隊列",
|
"Added @c comics to download queue." : "已添加 @c 本漫畫到下載隊列",
|
||||||
"Download started": "下載已開始"
|
"Download started": "下載已開始",
|
||||||
|
"Click favorite": "點擊收藏",
|
||||||
|
"End": "末尾",
|
||||||
|
"None": "無",
|
||||||
|
"View Detail": "查看詳情",
|
||||||
|
"Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄",
|
||||||
|
"Multiple cbz files" : "多個cbz文件",
|
||||||
|
"No valid comics found" : "未找到有效的漫畫",
|
||||||
|
"Enable DNS Overrides": "啟用DNS覆寫",
|
||||||
|
"DNS Overrides": "DNS覆寫",
|
||||||
|
"Custom Image Processing": "自定義圖片處理",
|
||||||
|
"Enable": "啟用",
|
||||||
|
"Aggregated": "聚合",
|
||||||
|
"Default Search Target": "默認搜索目標",
|
||||||
|
"Auto Language Filters": "自動語言篩選",
|
||||||
|
"Check for updates on startup": "啟動時檢查更新"
|
||||||
}
|
}
|
||||||
}
|
}
|
1
debian/gui/venera.desktop
vendored
1
debian/gui/venera.desktop
vendored
@@ -1,5 +1,4 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version={{Version}}
|
|
||||||
Name=Venera
|
Name=Venera
|
||||||
GenericName=Venera
|
GenericName=Venera
|
||||||
Comment=venera
|
Comment=venera
|
||||||
|
40
fastlane/metadata/android/en-US/full_description.txt
Normal file
40
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<p><a href="https://flutter.dev/"><img src="https://img.shields.io/badge/flutter-3.24.4-blue" alt="flutter"></a>
|
||||||
|
<a href="https://github.com/venera-app/venera/blob/master/LICENSE"><img src="https://img.shields.io/github/license/venera-app/venera" alt="License"></a>
|
||||||
|
<a href="https://github.com/venera-app/venera/releases"><img src="https://img.shields.io/github/v/release/venera-app/venera" alt="Download"></a>
|
||||||
|
<a href="https://github.com/venera-app/venera/stargazers"><img src="https://img.shields.io/github/stars/venera-app/venera" alt="stars"></a>
|
||||||
|
<a href="https://t.me/+Ws-IpmUutzkxMjhl"><img src="https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white" alt="Telegram"></a></p>
|
||||||
|
|
||||||
|
<p>A comic reader that support reading local and network comics.</p>
|
||||||
|
|
||||||
|
<h2>Features</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Read local comics</li>
|
||||||
|
<li>Use javascript to create comic sources</li>
|
||||||
|
<li>Read comics from network sources</li>
|
||||||
|
<li>Manage favorite comics</li>
|
||||||
|
<li>Download comics</li>
|
||||||
|
<li>View comments, tags, and other information of comics if the source supports</li>
|
||||||
|
<li>Login to comment, rate, and other operations if the source supports</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Build from source</h2>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Clone the repository</li>
|
||||||
|
<li>Install flutter, see <a href="https://flutter.dev/docs/get-started/install">flutter.dev</a></li>
|
||||||
|
<li>Install rust, see <a href="https://rustup.rs/">rustup.rs</a></li>
|
||||||
|
<li>Build for your platform: e.g. <code>flutter build apk</code></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Create a new comic source</h2>
|
||||||
|
|
||||||
|
<p>See <a href="https://github.com/venera-app/venera-configs">venera-configs</a></p>
|
||||||
|
|
||||||
|
<h2>Thanks</h2>
|
||||||
|
|
||||||
|
<h3>Tags Translation</h3>
|
||||||
|
|
||||||
|
<p><a href="https://github.com/EhTagTranslation/Database"><img src="https://github-readme-stats.vercel.app/api/pin/?username=EhTagTranslation&repo=Database" alt="Readme Card"></a></p>
|
||||||
|
|
||||||
|
<p>The Chinese translation of the manga tags is from this project.</p>
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
A comic reader that support reading local and network comics.
|
1
fastlane/metadata/android/en-US/title.txt
Normal file
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
venera
|
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '14.0'
|
platform :ios, '15.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
383
lib/components/code.dart
Normal file
383
lib/components/code.dart
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
part of 'components.dart';
|
||||||
|
|
||||||
|
class CodeEditor extends StatefulWidget {
|
||||||
|
const CodeEditor({super.key, this.initialValue, this.onChanged});
|
||||||
|
|
||||||
|
final String? initialValue;
|
||||||
|
|
||||||
|
final void Function(String value)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CodeEditor> createState() => _CodeEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CodeEditorState extends State<CodeEditor> {
|
||||||
|
late _CodeTextEditingController _controller;
|
||||||
|
late FocusNode _focusNode;
|
||||||
|
var horizontalScrollController = ScrollController();
|
||||||
|
var verticalScrollController = ScrollController();
|
||||||
|
int lineCount = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = _CodeTextEditingController(text: widget.initialValue);
|
||||||
|
_focusNode = FocusNode()
|
||||||
|
..onKeyEvent = (node, event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
handleTab();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
lineCount = calculateLineCount(widget.initialValue ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
future = _controller.init(context.brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTab() {
|
||||||
|
var text = _controller.text;
|
||||||
|
var start = _controller.selection.start;
|
||||||
|
var end = _controller.selection.end;
|
||||||
|
_controller.text = '${text.substring(0, start)} ${text.substring(end)}';
|
||||||
|
_controller.selection = TextSelection.collapsed(offset: start + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
int calculateLineCount(String text) {
|
||||||
|
return text.split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLineNumbers() {
|
||||||
|
return SizedBox(
|
||||||
|
width: 32,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (var i = 1; i <= lineCount; i++)
|
||||||
|
SizedBox(
|
||||||
|
height: 14 * 1.5,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
i.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.outline,
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.0,
|
||||||
|
fontFamily: 'Consolas',
|
||||||
|
fontFamilyFallback: ['Courier New', 'monospace'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingVertical(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
late Future future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: future,
|
||||||
|
builder: (context, value) {
|
||||||
|
if (value.connectionState == ConnectionState.waiting) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_controller.selection = TextSelection.collapsed(
|
||||||
|
offset: _controller.text.length,
|
||||||
|
);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
controller: verticalScrollController,
|
||||||
|
notificationPredicate: (notif) =>
|
||||||
|
notif.metrics.axis == Axis.vertical,
|
||||||
|
child: Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
controller: horizontalScrollController,
|
||||||
|
notificationPredicate: (notif) =>
|
||||||
|
notif.metrics.axis == Axis.horizontal,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: _CustomScrollBehavior(),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: horizontalScrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: verticalScrollController,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
buildLineNumbers(),
|
||||||
|
IntrinsicWidth(
|
||||||
|
stepWidth: 100,
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
maxLines: null,
|
||||||
|
cursorHeight: 1.5 * 14,
|
||||||
|
style: TextStyle(height: 1.5, fontSize: 14),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
widget.onChanged?.call(value);
|
||||||
|
if (lineCount != calculateLineCount(value)) {
|
||||||
|
setState(() {
|
||||||
|
lineCount = calculateLineCount(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
const _CustomScrollBehavior();
|
||||||
|
@override
|
||||||
|
Widget buildScrollbar(
|
||||||
|
BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CodeTextEditingController extends TextEditingController {
|
||||||
|
_CodeTextEditingController({super.text});
|
||||||
|
|
||||||
|
HighlighterTheme? _theme;
|
||||||
|
|
||||||
|
Future<void> init(Brightness brightness) async {
|
||||||
|
Highlighter.addLanguage('js', _jsGrammer);
|
||||||
|
_theme = await HighlighterTheme.loadForBrightness(brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan buildTextSpan(
|
||||||
|
{required BuildContext context,
|
||||||
|
TextStyle? style,
|
||||||
|
required bool withComposing}) {
|
||||||
|
var highlighter = Highlighter(
|
||||||
|
language: 'js',
|
||||||
|
theme: _theme!,
|
||||||
|
);
|
||||||
|
var result = highlighter.highlight(text);
|
||||||
|
style = TextStyle(
|
||||||
|
height: 1.5,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Consolas',
|
||||||
|
fontFamilyFallback: ['Courier New', 'Roboto Mono', 'monospace'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return mergeTextStyle(result, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan mergeTextStyle(TextSpan span, TextStyle style) {
|
||||||
|
var result = TextSpan(
|
||||||
|
style: style.merge(span.style),
|
||||||
|
children: span.children
|
||||||
|
?.whereType()
|
||||||
|
.map((e) => mergeTextStyle(e, style))
|
||||||
|
.toList(),
|
||||||
|
text: span.text,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _jsGrammer = r'''
|
||||||
|
{
|
||||||
|
"name": "JavaScript",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"fileTypes": ["js", "mjs", "cjs"],
|
||||||
|
"scopeName": "source.js",
|
||||||
|
|
||||||
|
"foldingStartMarker": "\\{\\s*$",
|
||||||
|
"foldingStopMarker": "^\\s*\\}",
|
||||||
|
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.preprocessor.script.js",
|
||||||
|
"match": "^(#!.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.import-export.js",
|
||||||
|
"begin": "\\b(import|export)\\b",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": {
|
||||||
|
"name": "keyword.control.import.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"end": ";",
|
||||||
|
"endCaptures": {
|
||||||
|
"0": {
|
||||||
|
"name": "punctuation.terminator.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#strings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.control.import.js",
|
||||||
|
"match": "\\b(as|from)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#keywords"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#constants-and-special-vars"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#operators"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#strings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"repository": {
|
||||||
|
"comments": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "comment.block.js",
|
||||||
|
"begin": "/\\*",
|
||||||
|
"end": "\\*/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "comment.line.double-slash.js",
|
||||||
|
"match": "//.*$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "keyword.control.js",
|
||||||
|
"match": "\\b(if|else|for|while|do|switch|case|default|break|continue|return|throw|try|catch|finally)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.js",
|
||||||
|
"match": "\\b(instanceof|typeof|new|delete|in|void)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "storage.type.js",
|
||||||
|
"match": "\\b(var|let|const|function|class|extends)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.declaration.js",
|
||||||
|
"match": "\\b(export|import|default)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"constants-and-special-vars": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "constant.language.js",
|
||||||
|
"match": "\\b(true|false|null|undefined|NaN|Infinity)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "constant.numeric.js",
|
||||||
|
"match": "\\b(0x[0-9A-Fa-f]+|[0-9]+\\.?[0-9]*(e[+-]?[0-9]+)?)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"operators": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.assignment.js",
|
||||||
|
"match": "(=|\\+=|-=|\\*=|/=|%=|\\|=|&=|\\^=|<<=|>>=|>>>=)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.comparison.js",
|
||||||
|
"match": "(==|!=|===|!==|<|<=|>|>=)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.logical.js",
|
||||||
|
"match": "(&&|\\|\\||!)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.arithmetic.js",
|
||||||
|
"match": "(-|\\+|\\*|/|%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keyword.operator.bitwise.js",
|
||||||
|
"match": "(\\||&|\\^|~|<<|>>|>>>)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strings": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "string.quoted.double.js",
|
||||||
|
"begin": "\"",
|
||||||
|
"end": "\"",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#string-interpolation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string.quoted.single.js",
|
||||||
|
"begin": "'",
|
||||||
|
"end": "'",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#string-interpolation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "string.template.js",
|
||||||
|
"begin": "`",
|
||||||
|
"end": "`",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#string-interpolation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"string-interpolation": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "variable.parameter.js",
|
||||||
|
"begin": "\\$\\{",
|
||||||
|
"end": "\\}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''';
|
@@ -77,7 +77,7 @@ class ComicTile extends StatelessWidget {
|
|||||||
icon: Icons.stars_outlined,
|
icon: Icons.stars_outlined,
|
||||||
text: 'Add to favorites'.tl,
|
text: 'Add to favorites'.tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
addFavorite(comic);
|
addFavorite([comic]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
@@ -235,128 +235,145 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBriefMode(BuildContext context) {
|
Widget _buildBriefMode(BuildContext context) {
|
||||||
return Padding(
|
return LayoutBuilder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
builder: (context, constraints) {
|
||||||
child: LayoutBuilder(
|
return InkWell(
|
||||||
builder: (context, constraints) {
|
borderRadius: BorderRadius.circular(8),
|
||||||
return InkWell(
|
onTap: _onTap,
|
||||||
borderRadius: BorderRadius.circular(8),
|
onLongPress: enableLongPressed ? () => _onLongPressed(context) : null,
|
||||||
onTap: _onTap,
|
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
|
||||||
onLongPress:
|
child: Column(
|
||||||
enableLongPressed ? () => _onLongPressed(context) : null,
|
children: [
|
||||||
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
|
Expanded(
|
||||||
child: Column(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Positioned.fill(
|
||||||
child: SizedBox(
|
child: Container(
|
||||||
child: Stack(
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: context.colorScheme.secondaryContainer,
|
||||||
Positioned.fill(
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Container(
|
boxShadow: [
|
||||||
decoration: BoxDecoration(
|
BoxShadow(
|
||||||
color: Theme.of(context)
|
color: Colors.black.toOpacity(0.2),
|
||||||
.colorScheme
|
blurRadius: 2,
|
||||||
.secondaryContainer,
|
offset: const Offset(0, 2),
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: buildImage(context),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Align(
|
),
|
||||||
alignment: Alignment.bottomRight,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: (() {
|
child: buildImage(context),
|
||||||
final subtitle =
|
|
||||||
comic.subtitle?.replaceAll('\n', '').trim();
|
|
||||||
final text = comic.description.isNotEmpty
|
|
||||||
? comic.description.split('|').join('\n')
|
|
||||||
: (subtitle?.isNotEmpty == true
|
|
||||||
? subtitle
|
|
||||||
: null);
|
|
||||||
final scale =
|
|
||||||
(appdata.settings['comicTileScale'] as num)
|
|
||||||
.toDouble();
|
|
||||||
final fortSize = scale < 0.85
|
|
||||||
? 8.0 // 小尺寸
|
|
||||||
: (scale < 1.0 ? 10.0 : 12.0);
|
|
||||||
|
|
||||||
if (text == null) {
|
|
||||||
return const SizedBox
|
|
||||||
.shrink(); // 如果没有文本,则不显示任何内容
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 2, vertical: 2),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(10.0),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.toOpacity(0.5),
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: constraints.maxWidth,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: fortSize,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Align(
|
||||||
Padding(
|
alignment: Alignment.bottomRight,
|
||||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
|
child: (() {
|
||||||
child: Text(
|
final subtitle =
|
||||||
comic.title.replaceAll('\n', ''),
|
comic.subtitle?.replaceAll('\n', '').trim();
|
||||||
style: const TextStyle(
|
final text = comic.description.isNotEmpty
|
||||||
fontWeight: FontWeight.w500,
|
? comic.description.split('|').join('\n')
|
||||||
),
|
: (subtitle?.isNotEmpty == true ? subtitle : null);
|
||||||
maxLines: 1,
|
final fortSize = constraints.maxWidth < 80
|
||||||
overflow: TextOverflow.ellipsis,
|
? 8.0
|
||||||
|
: constraints.maxWidth < 150
|
||||||
|
? 10.0
|
||||||
|
: 12.0;
|
||||||
|
|
||||||
|
if (text == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
var children = <Widget>[];
|
||||||
|
for (var line in text.split('\n')) {
|
||||||
|
children.add(Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(2, 0, 2, 2),
|
||||||
|
padding: constraints.maxWidth < 80
|
||||||
|
? const EdgeInsets.fromLTRB(3, 1, 3, 1)
|
||||||
|
: constraints.maxWidth < 150
|
||||||
|
? const EdgeInsets.fromLTRB(4, 2, 4, 2)
|
||||||
|
: const EdgeInsets.fromLTRB(5, 2, 5, 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Colors.black.toOpacity(0.5),
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
line,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: fortSize,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
})(),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
Padding(
|
||||||
},
|
padding: const EdgeInsets.fromLTRB(4, 4, 4, 0),
|
||||||
));
|
child: TextScroll(
|
||||||
|
comic.title.replaceAll('\n', ''),
|
||||||
|
mode: TextScrollMode.endless,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
delayBefore: Duration(milliseconds: 500),
|
||||||
|
velocity: const Velocity(pixelsPerSecond: Offset(40, 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(6).paddingVertical(8),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _splitText(String text) {
|
List<String> _splitText(String text) {
|
||||||
// split text by space, comma. text in brackets will be kept together.
|
// split text by comma, brackets
|
||||||
var words = <String>[];
|
var words = <String>[];
|
||||||
var buffer = StringBuffer();
|
var buffer = StringBuffer();
|
||||||
var inBracket = false;
|
var inBracket = false;
|
||||||
|
String? prevBracket;
|
||||||
for (var i = 0; i < text.length; i++) {
|
for (var i = 0; i < text.length; i++) {
|
||||||
var c = text[i];
|
var c = text[i];
|
||||||
if (c == '[' || c == '(') {
|
if (c == '[' || c == '(') {
|
||||||
inBracket = true;
|
|
||||||
} else if (c == ']' || c == ')') {
|
|
||||||
inBracket = false;
|
|
||||||
} else if (c == ' ' || c == ',') {
|
|
||||||
if (inBracket) {
|
if (inBracket) {
|
||||||
buffer.write(c);
|
buffer.write(c);
|
||||||
} else {
|
} else {
|
||||||
words.add(buffer.toString());
|
if (buffer.isNotEmpty) {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
inBracket = true;
|
||||||
|
prevBracket = c;
|
||||||
|
}
|
||||||
|
} else if (c == ']' || c == ')') {
|
||||||
|
if (prevBracket == '[' && c == ']' || prevBracket == '(' && c == ')') {
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
inBracket = false;
|
||||||
|
} else {
|
||||||
|
buffer.write(c);
|
||||||
|
}
|
||||||
|
} else if (c == ',') {
|
||||||
|
if (inBracket) {
|
||||||
|
buffer.write(c);
|
||||||
|
} else {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -364,8 +381,10 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.isNotEmpty) {
|
if (buffer.isNotEmpty) {
|
||||||
words.add(buffer.toString());
|
words.add(buffer.toString().trim());
|
||||||
}
|
}
|
||||||
|
words.removeWhere((element) => element == "");
|
||||||
|
words = words.toSet().toList();
|
||||||
return words;
|
return words;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,26 +402,33 @@ class ComicTile extends StatelessWidget {
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: 'Block'.tl,
|
title: 'Block'.tl,
|
||||||
content: Wrap(
|
content: ConstrainedBox(
|
||||||
runSpacing: 8,
|
constraints: BoxConstraints(
|
||||||
spacing: 8,
|
maxHeight: math.min(400, context.height - 136),
|
||||||
children: [
|
),
|
||||||
for (var word in all)
|
child: SingleChildScrollView(
|
||||||
OptionChip(
|
child: Wrap(
|
||||||
text: word,
|
runSpacing: 8,
|
||||||
isSelected: words.contains(word),
|
spacing: 8,
|
||||||
onTap: () {
|
children: [
|
||||||
setState(() {
|
for (var word in all)
|
||||||
if (!words.contains(word)) {
|
OptionChip(
|
||||||
words.add(word);
|
text: word,
|
||||||
} else {
|
isSelected: words.contains(word),
|
||||||
words.remove(word);
|
onTap: () {
|
||||||
}
|
setState(() {
|
||||||
});
|
if (!words.contains(word)) {
|
||||||
},
|
words.add(word);
|
||||||
),
|
} else {
|
||||||
],
|
words.remove(word);
|
||||||
).paddingHorizontal(16),
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -780,7 +806,10 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72)
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer
|
||||||
|
.toOpacity(0.72)
|
||||||
: null,
|
: null,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -833,6 +862,7 @@ class ComicList extends StatefulWidget {
|
|||||||
this.menuBuilder,
|
this.menuBuilder,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.refreshHandlerCallback,
|
this.refreshHandlerCallback,
|
||||||
|
this.enablePageStorage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||||
@@ -851,6 +881,8 @@ class ComicList extends StatefulWidget {
|
|||||||
|
|
||||||
final void Function(VoidCallback c)? refreshHandlerCallback;
|
final void Function(VoidCallback c)? refreshHandlerCallback;
|
||||||
|
|
||||||
|
final bool enablePageStorage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ComicList> createState() => ComicListState();
|
State<ComicList> createState() => ComicListState();
|
||||||
}
|
}
|
||||||
@@ -868,17 +900,19 @@ class ComicListState extends State<ComicList> {
|
|||||||
|
|
||||||
String? _nextUrl;
|
String? _nextUrl;
|
||||||
|
|
||||||
|
late bool enablePageStorage = widget.enablePageStorage;
|
||||||
|
|
||||||
Map<String, dynamic> get state => {
|
Map<String, dynamic> get state => {
|
||||||
'maxPage': _maxPage,
|
'maxPage': _maxPage,
|
||||||
'data': _data,
|
'data': _data,
|
||||||
'page': _page,
|
'page': _page,
|
||||||
'error': _error,
|
'error': _error,
|
||||||
'loading': _loading,
|
'loading': _loading,
|
||||||
'nextUrl': _nextUrl,
|
'nextUrl': _nextUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
void restoreState(Map<String, dynamic>? state) {
|
void restoreState(Map<String, dynamic>? state) {
|
||||||
if (state == null) {
|
if (state == null || !enablePageStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_maxPage = state['maxPage'];
|
_maxPage = state['maxPage'];
|
||||||
@@ -892,7 +926,9 @@ class ComicListState extends State<ComicList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void storeState() {
|
void storeState() {
|
||||||
PageStorage.of(context).writeState(context, state);
|
if (enablePageStorage) {
|
||||||
|
PageStorage.of(context).writeState(context, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
@@ -1060,11 +1096,11 @@ class ComicListState extends State<ComicList> {
|
|||||||
while (_data[page] == null) {
|
while (_data[page] == null) {
|
||||||
await _fetchNext();
|
await _fetchNext();
|
||||||
}
|
}
|
||||||
if(mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
});
|
});
|
||||||
@@ -1122,7 +1158,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
key: const PageStorageKey('scroll'),
|
key: enablePageStorage ? PageStorageKey('scroll$_page') : null,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
if (widget.leadingSliver != null) widget.leadingSliver!,
|
if (widget.leadingSliver != null) widget.leadingSliver!,
|
||||||
|
@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:syntax_highlight/syntax_highlight.dart';
|
||||||
|
import 'package:text_scroll/text_scroll.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/app_page_route.dart';
|
import 'package:venera/foundation/app_page_route.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
@@ -44,4 +46,5 @@ part 'select.dart';
|
|||||||
part 'side_bar.dart';
|
part 'side_bar.dart';
|
||||||
part 'comic.dart';
|
part 'comic.dart';
|
||||||
part 'effects.dart';
|
part 'effects.dart';
|
||||||
part 'gesture.dart';
|
part 'gesture.dart';
|
||||||
|
part 'code.dart';
|
@@ -41,13 +41,29 @@ class AnimatedTapRegion extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
||||||
|
bool isScaled = false;
|
||||||
|
|
||||||
bool isHovered = false;
|
bool isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) => setState(() => isHovered = true),
|
onEnter: (_) {
|
||||||
onExit: (_) => setState(() => isHovered = false),
|
isHovered = true;
|
||||||
|
if (!isScaled) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
if (isHovered) {
|
||||||
|
setState(() => isScaled = true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
isHovered = false;
|
||||||
|
if(isScaled) {
|
||||||
|
setState(() => isScaled = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
@@ -55,7 +71,7 @@ class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
duration: _fastAnimationDuration,
|
duration: _fastAnimationDuration,
|
||||||
scale: isHovered ? 1.1 : 1,
|
scale: isScaled ? 1.1 : 1,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -28,6 +28,9 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
|||||||
var width = entries.first.icon == null ? 216.0 : 242.0;
|
var width = entries.first.icon == null ? 216.0 : 242.0;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
var left = location.dx;
|
var left = location.dx;
|
||||||
|
if (left < 10) {
|
||||||
|
left = 10;
|
||||||
|
}
|
||||||
if (left + width > size.width - 10) {
|
if (left + width > size.width - 10) {
|
||||||
left = size.width - width - 10;
|
left = size.width - width - 10;
|
||||||
}
|
}
|
||||||
|
@@ -290,6 +290,7 @@ class ContentDialog extends StatelessWidget {
|
|||||||
: const EdgeInsets.symmetric(horizontal: 16),
|
: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shadowColor: context.colorScheme.shadow,
|
shadowColor: context.colorScheme.shadow,
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
|
@@ -22,8 +22,15 @@ class PopUpWidget<T> extends PopupRoute<T> {
|
|||||||
Widget body = PopupIndicatorWidget(
|
Widget body = PopupIndicatorWidget(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: showPopUp
|
decoration: showPopUp
|
||||||
? const BoxDecoration(
|
? BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
boxShadow: context.brightness == ui.Brightness.dark ? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.white.withAlpha(50),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
] : null,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
clipBehavior: showPopUp ? Clip.antiAlias : Clip.none,
|
clipBehavior: showPopUp ? Clip.antiAlias : Clip.none,
|
||||||
@@ -86,7 +93,8 @@ class PopupIndicatorWidget extends InheritedWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<T> showPopUpWidget<T>(BuildContext context, Widget widget) async {
|
Future<T> showPopUpWidget<T>(BuildContext context, Widget widget) async {
|
||||||
return await Navigator.of(context, rootNavigator: true).push(PopUpWidget(widget));
|
return await Navigator.of(context, rootNavigator: true)
|
||||||
|
.push(PopUpWidget(widget));
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopUpWidgetScaffold extends StatefulWidget {
|
class PopUpWidgetScaffold extends StatefulWidget {
|
||||||
@@ -127,9 +135,8 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
|
|||||||
message: "Back".tl,
|
message: "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_sharp),
|
icon: const Icon(Icons.arrow_back_sharp),
|
||||||
onPressed: () => context.canPop()
|
onPressed: () =>
|
||||||
? context.pop()
|
context.canPop() ? context.pop() : App.pop(),
|
||||||
: App.pop(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -148,6 +155,9 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
|
|||||||
),
|
),
|
||||||
NotificationListener<ScrollNotification>(
|
NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notifications) {
|
onNotification: (notifications) {
|
||||||
|
if (notifications.metrics.axisDirection != AxisDirection.down) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (notifications.metrics.pixels ==
|
if (notifications.metrics.pixels ==
|
||||||
notifications.metrics.minScrollExtent &&
|
notifications.metrics.minScrollExtent &&
|
||||||
!top) {
|
!top) {
|
||||||
|
@@ -57,10 +57,18 @@ class SideBarRoute<T> extends PopupRoute<T> {
|
|||||||
|
|
||||||
body = Container(
|
body = Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: showSideBar
|
borderRadius: showSideBar
|
||||||
? const BorderRadius.horizontal(left: Radius.circular(16))
|
? const BorderRadius.horizontal(left: Radius.circular(16))
|
||||||
: null,
|
: null,
|
||||||
color: Theme.of(context).colorScheme.surfaceTint),
|
color: Theme.of(context).colorScheme.surfaceTint,
|
||||||
|
boxShadow: context.brightness == ui.Brightness.dark ? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.white.withAlpha(50),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
] : null,
|
||||||
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
constraints: BoxConstraints(maxWidth: sideBarWidth),
|
constraints: BoxConstraints(maxWidth: sideBarWidth),
|
||||||
height: MediaQuery.of(context).size.height,
|
height: MediaQuery.of(context).size.height,
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.0.8";
|
final version = "1.1.4";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
class _Appdata {
|
class _Appdata {
|
||||||
@@ -12,7 +13,7 @@ class _Appdata {
|
|||||||
|
|
||||||
bool _isSavingData = false;
|
bool _isSavingData = false;
|
||||||
|
|
||||||
Future<void> saveData() async {
|
Future<void> saveData([bool sync = true]) async {
|
||||||
if (_isSavingData) {
|
if (_isSavingData) {
|
||||||
await Future.doWhile(() async {
|
await Future.doWhile(() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
@@ -24,6 +25,9 @@ class _Appdata {
|
|||||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||||
await file.writeAsString(data);
|
await file.writeAsString(data);
|
||||||
_isSavingData = false;
|
_isSavingData = false;
|
||||||
|
if (sync) {
|
||||||
|
DataSync().uploadData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addSearchHistory(String keyword) {
|
void addSearchHistory(String keyword) {
|
||||||
@@ -76,6 +80,25 @@ class _Appdata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Following fields are related to device-specific data and should not be synced.
|
||||||
|
static const _disableSync = [
|
||||||
|
"proxy",
|
||||||
|
"authorizationRequired",
|
||||||
|
"customImageProcessing",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Sync data from another device
|
||||||
|
void syncData(Map<String, dynamic> data) {
|
||||||
|
for (var key in data.keys) {
|
||||||
|
if (_disableSync.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
settings[key] = data[key];
|
||||||
|
}
|
||||||
|
searchHistory = List.from(data['searchHistory']);
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
|
||||||
var implicitData = <String, dynamic>{};
|
var implicitData = <String, dynamic>{};
|
||||||
|
|
||||||
void writeImplicitData() {
|
void writeImplicitData() {
|
||||||
@@ -113,15 +136,21 @@ class _Settings with ChangeNotifier {
|
|||||||
'cacheSize': 2048, // in MB
|
'cacheSize': 2048, // in MB
|
||||||
'downloadThreads': 5,
|
'downloadThreads': 5,
|
||||||
'enableLongPressToZoom': true,
|
'enableLongPressToZoom': true,
|
||||||
'checkUpdateOnStart': true,
|
'checkUpdateOnStart': false,
|
||||||
'limitImageWidth': true,
|
'limitImageWidth': true,
|
||||||
'webdav': [], // empty means not configured
|
'webdav': [], // empty means not configured
|
||||||
'dataVersion': 0,
|
'dataVersion': 0,
|
||||||
'quickFavorite': null,
|
'quickFavorite': null,
|
||||||
'enableTurnPageByVolumeKey': true,
|
'enableTurnPageByVolumeKey': true,
|
||||||
'enableClockAndBatteryInfoInReader': true,
|
'enableClockAndBatteryInfoInReader': true,
|
||||||
'ignoreCertificateErrors': false,
|
|
||||||
'authorizationRequired': false,
|
'authorizationRequired': false,
|
||||||
|
'onClickFavorite': 'viewDetail', // viewDetail, read
|
||||||
|
'enableDnsOverrides': false,
|
||||||
|
'dnsOverrides': {},
|
||||||
|
'enableCustomImageProcessing': false,
|
||||||
|
'customImageProcessing': _defaultCustomImageProcessing,
|
||||||
|
'sni': true,
|
||||||
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
@@ -138,3 +167,16 @@ class _Settings with ChangeNotifier {
|
|||||||
return _data.toString();
|
return _data.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _defaultCustomImageProcessing = '''
|
||||||
|
/**
|
||||||
|
* Process an image
|
||||||
|
* @param image {ArayBuffer} - The image to process
|
||||||
|
* @param cid {string} - The comic ID
|
||||||
|
* @param eid {string} - The episode ID
|
||||||
|
* @returns {Promise<ArrayBuffer>} - The processed image
|
||||||
|
*/
|
||||||
|
async function processImage(image, cid, eid) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
''';
|
@@ -145,7 +145,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
final int? likesCount;
|
final int? likesCount;
|
||||||
|
|
||||||
final int? commentsCount;
|
final int? commentCount;
|
||||||
|
|
||||||
final String? uploader;
|
final String? uploader;
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
subId = json["subId"],
|
subId = json["subId"],
|
||||||
likesCount = json["likesCount"],
|
likesCount = json["likesCount"],
|
||||||
isLiked = json["isLiked"],
|
isLiked = json["isLiked"],
|
||||||
commentsCount = json["commentsCount"],
|
commentCount = json["commentCount"],
|
||||||
uploader = json["uploader"],
|
uploader = json["uploader"],
|
||||||
uploadTime = json["uploadTime"],
|
uploadTime = json["uploadTime"],
|
||||||
updateTime = json["updateTime"],
|
updateTime = json["updateTime"],
|
||||||
@@ -216,7 +216,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
"subId": subId,
|
"subId": subId,
|
||||||
"isLiked": isLiked,
|
"isLiked": isLiked,
|
||||||
"likesCount": likesCount,
|
"likesCount": likesCount,
|
||||||
"commentsCount": commentsCount,
|
"commentsCount": commentCount,
|
||||||
"uploader": uploader,
|
"uploader": uploader,
|
||||||
"uploadTime": uploadTime,
|
"uploadTime": uploadTime,
|
||||||
"updateTime": updateTime,
|
"updateTime": updateTime,
|
||||||
|
@@ -28,4 +28,12 @@ class ComicType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const local = ComicType(0);
|
static const local = ComicType(0);
|
||||||
|
|
||||||
|
factory ComicType.fromKey(String key) {
|
||||||
|
if(key == "local") {
|
||||||
|
return local;
|
||||||
|
} else {
|
||||||
|
return ComicType(key.hashCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -73,6 +73,7 @@ class FavoriteItem implements Comic {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get description {
|
String get description {
|
||||||
|
var time = this.time.substring(0, 10);
|
||||||
return appdata.settings['comicDisplayMode'] == 'detailed'
|
return appdata.settings['comicDisplayMode'] == 'detailed'
|
||||||
? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"
|
? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"
|
||||||
: "${type.comicSource?.name ?? "Unknown"} | $time";
|
: "${type.comicSource?.name ?? "Unknown"} | $time";
|
||||||
|
@@ -201,8 +201,6 @@ class HistoryManager with ChangeNotifier {
|
|||||||
|
|
||||||
Map<String, bool>? _cachedHistory;
|
Map<String, bool>? _cachedHistory;
|
||||||
|
|
||||||
static const _kMaxHistoryLength = 200;
|
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = sqlite3.open("${App.dataPath}/history.db");
|
_db = sqlite3.open("${App.dataPath}/history.db");
|
||||||
|
|
||||||
@@ -228,12 +226,6 @@ class HistoryManager with ChangeNotifier {
|
|||||||
///
|
///
|
||||||
/// This function would be called when user start reading.
|
/// This function would be called when user start reading.
|
||||||
Future<void> addHistory(History newItem) async {
|
Future<void> addHistory(History newItem) async {
|
||||||
while(count() >= _kMaxHistoryLength) {
|
|
||||||
_db.execute("""
|
|
||||||
delete from history
|
|
||||||
where time == (select min(time) from history);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page)
|
insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page)
|
||||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
@@ -27,10 +27,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
screen.size.height * _normalComicImageRatio,
|
screen.size.height * _normalComicImageRatio,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_effectiveScreenWidth = max(
|
_effectiveScreenWidth =
|
||||||
_effectiveScreenWidth ?? 0,
|
max(_effectiveScreenWidth ?? 0, screen.size.width);
|
||||||
screen.size.width
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_effectiveScreenWidth! < _minComicImageWidth) {
|
if (_effectiveScreenWidth! < _minComicImageWidth) {
|
||||||
@@ -110,7 +108,10 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(data);
|
final buffer = await ImmutableBuffer.fromUint8List(data);
|
||||||
return await decode(buffer, getTargetSize: _getTargetSize);
|
return await decode(
|
||||||
|
buffer,
|
||||||
|
getTargetSize: enableResize ? _getTargetSize : null,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await CacheManager().delete(this.key);
|
await CacheManager().delete(this.key);
|
||||||
if (data.length < 2 * 1024) {
|
if (data.length < 2 * 1024) {
|
||||||
@@ -151,6 +152,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "$runtimeType($key)";
|
return "$runtimeType($key)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get enableResize => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
|
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:async' show Future, StreamController;
|
import 'dart:async' show Future, StreamController;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
import 'package:venera/network/images.dart';
|
import 'package:venera/network/images.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'base_image_provider.dart';
|
import 'base_image_provider.dart';
|
||||||
import 'reader_image.dart' as image_provider;
|
import 'reader_image.dart' as image_provider;
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
|
||||||
class ReaderImageProvider
|
class ReaderImageProvider
|
||||||
extends BaseImageProvider<image_provider.ReaderImageProvider> {
|
extends BaseImageProvider<image_provider.ReaderImageProvider> {
|
||||||
@@ -21,25 +24,50 @@ class ReaderImageProvider
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
Uint8List? imageBytes;
|
||||||
if (imageKey.startsWith('file://')) {
|
if (imageKey.startsWith('file://')) {
|
||||||
var file = File(imageKey);
|
var file = File(imageKey);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
return file.readAsBytes();
|
imageBytes = await file.readAsBytes();
|
||||||
|
} else {
|
||||||
|
throw "Error: File not found.";
|
||||||
}
|
}
|
||||||
throw "Error: File not found.";
|
} else {
|
||||||
}
|
await for (var event
|
||||||
|
|
||||||
await for (var event
|
|
||||||
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
cumulativeBytesLoaded: event.currentBytes,
|
cumulativeBytesLoaded: event.currentBytes,
|
||||||
expectedTotalBytes: event.totalBytes,
|
expectedTotalBytes: event.totalBytes,
|
||||||
));
|
));
|
||||||
if (event.imageBytes != null) {
|
if (event.imageBytes != null) {
|
||||||
return event.imageBytes!;
|
imageBytes = event.imageBytes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw "Error: Empty response body.";
|
if (imageBytes == null) {
|
||||||
|
throw "Error: Empty response body.";
|
||||||
|
}
|
||||||
|
if (appdata.settings['enableCustomImageProcessing']) {
|
||||||
|
var script = appdata.settings['customImageProcessing'].toString();
|
||||||
|
if (!script.contains('async function processImage')) {
|
||||||
|
return imageBytes;
|
||||||
|
}
|
||||||
|
var func = JsEngine().runCode('''
|
||||||
|
(() => {
|
||||||
|
$script
|
||||||
|
return processImage;
|
||||||
|
})()
|
||||||
|
''');
|
||||||
|
if (func is JSInvokable) {
|
||||||
|
var result = await func.invoke([imageBytes, cid, eid]);
|
||||||
|
func.free();
|
||||||
|
if (result is Uint8List) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,4 +77,7 @@ class ReaderImageProvider
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get key => "$imageKey@$sourceKey@$cid@$eid";
|
String get key => "$imageKey@$sourceKey@$cid@$eid";
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get enableResize => true;
|
||||||
}
|
}
|
||||||
|
@@ -58,6 +58,11 @@ class JsEngine with _JSEngineApi {
|
|||||||
JsEngine().init();
|
JsEngine().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetDio() {
|
||||||
|
_dio = AppDio(BaseOptions(
|
||||||
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (!_closed) {
|
if (!_closed) {
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +203,8 @@ class JsEngine with _JSEngineApi {
|
|||||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
dio.interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
dio.interceptors
|
||||||
|
.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
dio.interceptors.add(LogInterceptor());
|
dio.interceptors.add(LogInterceptor());
|
||||||
}
|
}
|
||||||
response = await dio!.request(req["url"],
|
response = await dio!.request(req["url"],
|
||||||
|
@@ -76,15 +76,16 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
cover,
|
cover,
|
||||||
));
|
));
|
||||||
|
|
||||||
String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory);
|
String get baseDir => (directory.contains('/') || directory.contains('\\'))
|
||||||
|
? directory
|
||||||
|
: FilePath.join(LocalManager().path, directory);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get description => "";
|
String get description => "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sourceKey => comicType == ComicType.local
|
String get sourceKey =>
|
||||||
? "local"
|
comicType == ComicType.local ? "local" : comicType.sourceKey;
|
||||||
: comicType.sourceKey;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -112,11 +113,12 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
initialChapter: history?.ep,
|
initialChapter: history?.ep,
|
||||||
initialPage: history?.page,
|
initialPage: history?.page,
|
||||||
history: history ?? History.fromModel(
|
history: history ??
|
||||||
model: this,
|
History.fromModel(
|
||||||
ep: 0,
|
model: this,
|
||||||
page: 0,
|
ep: 0,
|
||||||
),
|
page: 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,6 +155,15 @@ class LocalManager with ChangeNotifier {
|
|||||||
|
|
||||||
Directory get directory => Directory(path);
|
Directory get directory => Directory(path);
|
||||||
|
|
||||||
|
void _checkNoMedia() {
|
||||||
|
if (App.isAndroid) {
|
||||||
|
var file = File(FilePath.join(path, '.nomedia'));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
file.createSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return error message if failed
|
// return error message if failed
|
||||||
Future<String?> setNewPath(String newPath) async {
|
Future<String?> setNewPath(String newPath) async {
|
||||||
var newDir = Directory(newPath);
|
var newDir = Directory(newPath);
|
||||||
@@ -167,13 +178,15 @@ class LocalManager with ChangeNotifier {
|
|||||||
directory,
|
directory,
|
||||||
newDir,
|
newDir,
|
||||||
);
|
);
|
||||||
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
|
await File(FilePath.join(App.dataPath, 'local_path'))
|
||||||
|
.writeAsString(newPath);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("IO", e, s);
|
Log.error("IO", e, s);
|
||||||
return e.toString();
|
return e.toString();
|
||||||
}
|
}
|
||||||
await directory.deleteContents(recursive: true);
|
await directory.deleteContents(recursive: true);
|
||||||
path = newPath;
|
path = newPath;
|
||||||
|
_checkNoMedia();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +200,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else if (App.isIOS) {
|
} else if (App.isIOS) {
|
||||||
var oldPath = FilePath.join(App.dataPath, 'local');
|
var oldPath = FilePath.join(App.dataPath, 'local');
|
||||||
if (Directory(oldPath).existsSync() && Directory(oldPath).listSync().isNotEmpty) {
|
if (Directory(oldPath).existsSync() &&
|
||||||
|
Directory(oldPath).listSync().isNotEmpty) {
|
||||||
return oldPath;
|
return oldPath;
|
||||||
} else {
|
} else {
|
||||||
var directory = await getApplicationDocumentsDirectory();
|
var directory = await getApplicationDocumentsDirectory();
|
||||||
@@ -198,6 +212,18 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkPathValidation() async {
|
||||||
|
var testFile = File(FilePath.join(path, 'venera_test'));
|
||||||
|
try {
|
||||||
|
testFile.createSync();
|
||||||
|
testFile.deleteSync();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("IO",
|
||||||
|
"Failed to create test file in local path: $e\nUsing default path instead.");
|
||||||
|
path = await findDefaultPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = sqlite3.open(
|
_db = sqlite3.open(
|
||||||
'${App.dataPath}/local.db',
|
'${App.dataPath}/local.db',
|
||||||
@@ -229,10 +255,11 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (!directory.existsSync()) {
|
if (!directory.existsSync()) {
|
||||||
await directory.create();
|
await directory.create();
|
||||||
}
|
}
|
||||||
}
|
} catch (e, s) {
|
||||||
catch(e, s) {
|
|
||||||
Log.error("IO", "Failed to create local folder: $e", s);
|
Log.error("IO", "Failed to create local folder: $e", s);
|
||||||
}
|
}
|
||||||
|
_checkPathValidation();
|
||||||
|
_checkNoMedia();
|
||||||
restoreDownloadingTasks();
|
restoreDownloadingTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +269,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
SELECT id FROM comics WHERE comic_type = ?
|
SELECT id FROM comics WHERE comic_type = ?
|
||||||
ORDER BY CAST(id AS INTEGER) DESC
|
ORDER BY CAST(id AS INTEGER) DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
''', [type.value],
|
''',
|
||||||
|
[type.value],
|
||||||
);
|
);
|
||||||
if (res.isEmpty) {
|
if (res.isEmpty) {
|
||||||
return '1';
|
return '1';
|
||||||
@@ -352,15 +380,14 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getImages(String id, ComicType type, Object ep) async {
|
Future<List<String>> getImages(String id, ComicType type, Object ep) async {
|
||||||
if(ep is! String && ep is! int) {
|
if (ep is! String && ep is! int) {
|
||||||
throw "Invalid ep";
|
throw "Invalid ep";
|
||||||
}
|
}
|
||||||
var comic = find(id, type) ?? (throw "Comic Not Found");
|
var comic = find(id, type) ?? (throw "Comic Not Found");
|
||||||
var directory = Directory(comic.baseDir);
|
var directory = Directory(comic.baseDir);
|
||||||
if (comic.chapters != null) {
|
if (comic.chapters != null) {
|
||||||
var cid = ep is int
|
var cid =
|
||||||
? comic.chapters!.keys.elementAt(ep - 1)
|
ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String);
|
||||||
: (ep as String);
|
|
||||||
directory = Directory(FilePath.join(directory.path, cid));
|
directory = Directory(FilePath.join(directory.path, cid));
|
||||||
}
|
}
|
||||||
var files = <File>[];
|
var files = <File>[];
|
||||||
@@ -372,7 +399,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//Hidden file in some file system
|
//Hidden file in some file system
|
||||||
if(entity.name.startsWith('.')) {
|
if (entity.name.startsWith('.')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
files.add(entity);
|
files.add(entity);
|
||||||
@@ -394,7 +421,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (comic == null) return false;
|
if (comic == null) return false;
|
||||||
if (comic.chapters == null || ep == null) return true;
|
if (comic.chapters == null || ep == null) return true;
|
||||||
return comic.downloadedChapters
|
return comic.downloadedChapters
|
||||||
.contains(comic.chapters!.keys.elementAt(ep-1));
|
.contains(comic.chapters!.keys.elementAt(ep - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DownloadTask> downloadingTasks = [];
|
List<DownloadTask> downloadingTasks = [];
|
||||||
@@ -451,12 +478,17 @@ class LocalManager with ChangeNotifier {
|
|||||||
void restoreDownloadingTasks() {
|
void restoreDownloadingTasks() {
|
||||||
var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json'));
|
var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json'));
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
var tasks = jsonDecode(file.readAsStringSync());
|
try {
|
||||||
for (var e in tasks) {
|
var tasks = jsonDecode(file.readAsStringSync());
|
||||||
var task = DownloadTask.fromJson(e);
|
for (var e in tasks) {
|
||||||
if (task != null) {
|
var task = DownloadTask.fromJson(e);
|
||||||
downloadingTasks.add(task);
|
if (task != null) {
|
||||||
|
downloadingTasks.add(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
file.delete();
|
||||||
|
Log.error("LocalManager", "Failed to restore downloading tasks: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,17 +501,19 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
||||||
if(removeFileOnDisk) {
|
if (removeFileOnDisk) {
|
||||||
var dir = Directory(FilePath.join(path, c.directory));
|
var dir = Directory(FilePath.join(path, c.directory));
|
||||||
dir.deleteIgnoreError(recursive: true);
|
dir.deleteIgnoreError(recursive: true);
|
||||||
}
|
}
|
||||||
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
// Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
||||||
if(HistoryManager().findSync(c.id, c.comicType) != null) {
|
if (c.comicType == ComicType.local) {
|
||||||
HistoryManager().remove(c.id, c.comicType);
|
if (HistoryManager().findSync(c.id, c.comicType) != null) {
|
||||||
}
|
HistoryManager().remove(c.id, c.comicType);
|
||||||
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
}
|
||||||
for (var f in folders) {
|
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
||||||
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
|
for (var f in folders) {
|
||||||
|
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
remove(c.id, c.comicType);
|
remove(c.id, c.comicType);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -503,4 +537,4 @@ enum LocalSortType {
|
|||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,23 +6,37 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/js_engine.dart';
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'foundation/appdata.dart';
|
import 'foundation/appdata.dart';
|
||||||
|
|
||||||
|
extension FutureInit<T> on Future<T> {
|
||||||
|
/// Prevent unhandled exception
|
||||||
|
///
|
||||||
|
/// A unhandled exception occurred in init() will cause the app to crash.
|
||||||
|
Future<void> wait() async {
|
||||||
|
try {
|
||||||
|
await this;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("init", "$e\n$s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await SAFTaskWorker().init();
|
await SAFTaskWorker().init().wait();
|
||||||
await AppTranslation.init();
|
await AppTranslation.init().wait();
|
||||||
await appdata.init();
|
await appdata.init().wait();
|
||||||
await App.init();
|
await App.init().wait();
|
||||||
await HistoryManager().init();
|
await HistoryManager().init().wait();
|
||||||
await TagsTranslation.readData();
|
await TagsTranslation.readData().wait();
|
||||||
await LocalFavoritesManager().init();
|
await LocalFavoritesManager().init().wait();
|
||||||
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
||||||
await JsEngine().init();
|
await JsEngine().init().wait();
|
||||||
await ComicSource.init();
|
await ComicSource.init().wait();
|
||||||
await LocalManager().init();
|
await LocalManager().init().wait();
|
||||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||||
}
|
}
|
@@ -156,30 +156,40 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
home = const MainPage();
|
home = const MainPage();
|
||||||
}
|
}
|
||||||
return DynamicColorBuilder(builder: (light, dark) {
|
return DynamicColorBuilder(builder: (light, dark) {
|
||||||
if (appdata.settings['color'] != 'system' || light == null || dark == null) {
|
if (appdata.settings['color'] != 'system' ||
|
||||||
|
light == null ||
|
||||||
|
dark == null) {
|
||||||
var color = translateColorSetting();
|
var color = translateColorSetting();
|
||||||
light = ColorScheme.fromSeed(
|
light = ColorScheme.fromSeed(
|
||||||
seedColor: color,
|
seedColor: color,
|
||||||
|
surface: Colors.white,
|
||||||
);
|
);
|
||||||
dark = ColorScheme.fromSeed(
|
dark = ColorScheme.fromSeed(
|
||||||
seedColor: color,
|
seedColor: color,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
surface: Colors.black,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
light = ColorScheme.fromSeed(
|
||||||
|
seedColor: light.primary,
|
||||||
|
surface: Colors.white,
|
||||||
|
);
|
||||||
|
dark = ColorScheme.fromSeed(
|
||||||
|
seedColor: dark.primary,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
surface: Colors.black,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: home,
|
home: home,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: light.copyWith(
|
colorScheme: light,
|
||||||
surface: Colors.white,
|
|
||||||
),
|
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
),
|
),
|
||||||
navigatorKey: App.rootNavigatorKey,
|
navigatorKey: App.rootNavigatorKey,
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
colorScheme: dark.copyWith(
|
colorScheme: dark,
|
||||||
surface: Colors.black,
|
|
||||||
),
|
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
),
|
),
|
||||||
themeMode: switch (appdata.settings['theme_mode']) {
|
themeMode: switch (appdata.settings['theme_mode']) {
|
||||||
@@ -211,8 +221,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
ErrorWidget.builder = (details) {
|
ErrorWidget.builder = (details) {
|
||||||
Log.error(
|
Log.error("Unhandled Exception",
|
||||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
"${details.exception}\n${details.stack}");
|
||||||
return Material(
|
return Material(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(details.exception.toString()),
|
child: Text(details.exception.toString()),
|
||||||
|
@@ -108,7 +108,6 @@ class MyLogInterceptor implements Interceptor {
|
|||||||
|
|
||||||
class AppDio with DioMixin {
|
class AppDio with DioMixin {
|
||||||
String? _proxy = proxy;
|
String? _proxy = proxy;
|
||||||
static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
|
|
||||||
|
|
||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
@@ -116,9 +115,6 @@ class AppDio with DioMixin {
|
|||||||
proxySettings: proxy == null
|
proxySettings: proxy == null
|
||||||
? const rhttp.ProxySettings.noProxy()
|
? const rhttp.ProxySettings.noProxy()
|
||||||
: rhttp.ProxySettings.proxy(proxy!),
|
: rhttp.ProxySettings.proxy(proxy!),
|
||||||
tlsSettings: rhttp.TlsSettings(
|
|
||||||
verifyCertificates: !ignoreCertificateErrors,
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(NetworkCacheManager());
|
||||||
@@ -196,9 +192,6 @@ class AppDio with DioMixin {
|
|||||||
proxySettings: proxy == null
|
proxySettings: proxy == null
|
||||||
? const rhttp.ProxySettings.noProxy()
|
? const rhttp.ProxySettings.noProxy()
|
||||||
: rhttp.ProxySettings.proxy(proxy!),
|
: rhttp.ProxySettings.proxy(proxy!),
|
||||||
tlsSettings: rhttp.TlsSettings(
|
|
||||||
verifyCertificates: !ignoreCertificateErrors,
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -222,6 +215,22 @@ class AppDio with DioMixin {
|
|||||||
class RHttpAdapter implements HttpClientAdapter {
|
class RHttpAdapter implements HttpClientAdapter {
|
||||||
rhttp.ClientSettings settings;
|
rhttp.ClientSettings settings;
|
||||||
|
|
||||||
|
static Map<String, List<String>> _getOverrides() {
|
||||||
|
if (!appdata.settings['enableDnsOverrides'] == true) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
var config = appdata.settings["dnsOverrides"];
|
||||||
|
var result = <String, List<String>>{};
|
||||||
|
if (config is Map) {
|
||||||
|
for (var entry in config.entries) {
|
||||||
|
if (entry.key is String && entry.value is String) {
|
||||||
|
result[entry.key] = [entry.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
|
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
|
||||||
settings = settings.copyWith(
|
settings = settings.copyWith(
|
||||||
redirectSettings: const rhttp.RedirectSettings.limited(5),
|
redirectSettings: const rhttp.RedirectSettings.limited(5),
|
||||||
@@ -231,8 +240,9 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
keepAlivePing: Duration(seconds: 30),
|
keepAlivePing: Duration(seconds: 30),
|
||||||
),
|
),
|
||||||
throwOnStatusCode: false,
|
throwOnStatusCode: false,
|
||||||
|
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
||||||
tlsSettings: rhttp.TlsSettings(
|
tlsSettings: rhttp.TlsSettings(
|
||||||
verifyCertificates: !AppDio.ignoreCertificateErrors,
|
sni: appdata.settings['sni'] != false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -146,14 +146,19 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
|
|
||||||
String? _cover;
|
String? _cover;
|
||||||
|
|
||||||
|
/// All images to download, key is chapter name
|
||||||
Map<String, List<String>>? _images;
|
Map<String, List<String>>? _images;
|
||||||
|
|
||||||
|
/// Downloaded image count
|
||||||
int _downloadedCount = 0;
|
int _downloadedCount = 0;
|
||||||
|
|
||||||
|
/// Total image count
|
||||||
int _totalCount = 0;
|
int _totalCount = 0;
|
||||||
|
|
||||||
|
/// Current downloading image index
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
|
|
||||||
|
/// Current downloading chapter, index of [_images]
|
||||||
int _chapter = 0;
|
int _chapter = 0;
|
||||||
|
|
||||||
var tasks = <int, _ImageDownloadWrapper>{};
|
var tasks = <int, _ImageDownloadWrapper>{};
|
||||||
@@ -180,10 +185,10 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
if (comic!.chapters != null) {
|
if (comic!.chapters != null) {
|
||||||
saveTo = Directory(FilePath.join(
|
saveTo = Directory(FilePath.join(
|
||||||
path!,
|
path!,
|
||||||
comic!.chapters!.keys.elementAt(_chapter),
|
_images!.keys.elementAt(_chapter),
|
||||||
));
|
));
|
||||||
if (!saveTo.existsSync()) {
|
if (!saveTo.existsSync()) {
|
||||||
saveTo.createSync();
|
saveTo.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
saveTo = Directory(path!);
|
saveTo = Directory(path!);
|
||||||
|
@@ -172,7 +172,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
isLiked = comic.isLiked ?? false;
|
isLiked = comic.isLiked ?? false;
|
||||||
isFavorite = comic.isFavorite ?? false;
|
isFavorite = comic.isFavorite ?? false;
|
||||||
if (comic.chapters == null) {
|
if (comic.chapters == null) {
|
||||||
isDownloaded = await LocalManager().isDownloaded(
|
isDownloaded = LocalManager().isDownloaded(
|
||||||
comic.id,
|
comic.id,
|
||||||
comic.comicType,
|
comic.comicType,
|
||||||
0,
|
0,
|
||||||
@@ -292,7 +292,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (comicSource.commentsLoader != null)
|
if (comicSource.commentsLoader != null)
|
||||||
_ActionButton(
|
_ActionButton(
|
||||||
icon: const Icon(Icons.comment),
|
icon: const Icon(Icons.comment),
|
||||||
text: (comic.commentsCount ?? 'Comments'.tl).toString(),
|
text: (comic.commentCount ?? 'Comments'.tl).toString(),
|
||||||
onPressed: showComments,
|
onPressed: showComments,
|
||||||
iconColor: context.useTextColor(Colors.green),
|
iconColor: context.useTextColor(Colors.green),
|
||||||
),
|
),
|
||||||
@@ -679,7 +679,7 @@ abstract mixin class _ComicPageActions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (comic.chapters == null &&
|
if (comic.chapters == null &&
|
||||||
await LocalManager().isDownloaded(comic.id, comic.comicType, 0)) {
|
LocalManager().isDownloaded(comic.id, comic.comicType, 0)) {
|
||||||
App.rootContext.showMessage(message: "The comic is downloaded".tl);
|
App.rootContext.showMessage(message: "The comic is downloaded".tl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -111,13 +111,12 @@ class _BodyState extends State<_Body> {
|
|||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (App.isDesktop)
|
Tooltip(
|
||||||
Tooltip(
|
message: "Edit".tl,
|
||||||
message: "Edit".tl,
|
child: IconButton(
|
||||||
child: IconButton(
|
onPressed: () => edit(source),
|
||||||
onPressed: () => edit(source),
|
icon: const Icon(Icons.edit_note)),
|
||||||
icon: const Icon(Icons.edit_note)),
|
),
|
||||||
),
|
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Update".tl,
|
message: "Update".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -163,6 +162,10 @@ class _BodyState extends State<_Body> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
current = item.value['options']
|
||||||
|
.firstWhere((e) => e['value'] == current)['text'] ??
|
||||||
|
current;
|
||||||
}
|
}
|
||||||
yield ListTile(
|
yield ListTile(
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
title: Text((item.value['title'] as String).ts(source.key)),
|
||||||
@@ -246,27 +249,35 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void edit(ComicSource source) async {
|
void edit(ComicSource source) async {
|
||||||
try {
|
if (App.isDesktop) {
|
||||||
await Process.run("code", [source.filePath], runInShell: true);
|
try {
|
||||||
await showDialog(
|
await Process.run("code", [source.filePath], runInShell: true);
|
||||||
|
await showDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text("Reload Configs"),
|
title: const Text("Reload Configs"),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text("cancel")),
|
child: const Text("cancel")),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ComicSource.reload();
|
await ComicSource.reload();
|
||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
},
|
},
|
||||||
child: const Text("continue")),
|
child: const Text("continue")),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
} catch (e) {
|
);
|
||||||
context.showMessage(message: "Failed to launch vscode");
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
context.to(() => _EditFilePage(source.filePath)).then((value) async {
|
||||||
|
await ComicSource.reload();
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> update(ComicSource source) async {
|
static Future<void> update(ComicSource source) async {
|
||||||
@@ -297,12 +308,14 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCard(BuildContext context) {
|
Widget buildCard(BuildContext context) {
|
||||||
Widget buildButton({required Widget child, required VoidCallback onPressed}) {
|
Widget buildButton(
|
||||||
|
{required Widget child, required VoidCallback onPressed}) {
|
||||||
return Button.normal(
|
return Button.normal(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: child,
|
child: child,
|
||||||
).fixHeight(32);
|
).fixHeight(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -558,3 +571,51 @@ void _addAllPagesWithComicSource(ComicSource source) {
|
|||||||
|
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _EditFilePage extends StatefulWidget {
|
||||||
|
const _EditFilePage(this.path);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EditFilePage> createState() => __EditFilePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __EditFilePageState extends State<_EditFilePage> {
|
||||||
|
var current = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
current = File(widget.path).readAsStringSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
File(widget.path).writeAsStringSync(current);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: Appbar(
|
||||||
|
title: Text("Edit".tl),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 0.6,
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: CodeEditor(
|
||||||
|
initialValue: current,
|
||||||
|
onChanged: (value) => current = value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -295,6 +295,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
);
|
);
|
||||||
} else if (data.loadPage != null || data.loadNext != null) {
|
} else if (data.loadPage != null || data.loadNext != null) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
|
enablePageStorage: true,
|
||||||
loadPage: data.loadPage,
|
loadPage: data.loadPage,
|
||||||
loadNext: data.loadNext,
|
loadNext: data.loadNext,
|
||||||
key: const PageStorageKey("comic_list"),
|
key: const PageStorageKey("comic_list"),
|
||||||
|
@@ -77,7 +77,7 @@ String? validateFolderName(String newFolderName) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addFavorite(Comic comic) {
|
void addFavorite(List<Comic> comics) {
|
||||||
var folders = LocalFavoritesManager().folderNames;
|
var folders = LocalFavoritesManager().folderNames;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -105,19 +105,21 @@ void addFavorite(Comic comic) {
|
|||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (selectedFolder != null) {
|
if (selectedFolder != null) {
|
||||||
LocalFavoritesManager().addComic(
|
for (var comic in comics) {
|
||||||
selectedFolder!,
|
LocalFavoritesManager().addComic(
|
||||||
FavoriteItem(
|
selectedFolder!,
|
||||||
id: comic.id,
|
FavoriteItem(
|
||||||
name: comic.title,
|
id: comic.id,
|
||||||
coverPath: comic.cover,
|
name: comic.title,
|
||||||
author: comic.subtitle ?? '',
|
coverPath: comic.cover,
|
||||||
type: ComicType((comic.sourceKey == 'local'
|
author: comic.subtitle ?? '',
|
||||||
? 0
|
type: ComicType((comic.sourceKey == 'local'
|
||||||
: comic.sourceKey.hashCode)),
|
? 0
|
||||||
tags: comic.tags ?? [],
|
: comic.sourceKey.hashCode)),
|
||||||
),
|
tags: comic.tags ?? [],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -144,6 +146,18 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
|||||||
|
|
||||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||||
|
|
||||||
|
var newTags = <String>[];
|
||||||
|
for(var entry in newInfo.tags.entries) {
|
||||||
|
const shouldIgnore = ['author', 'artist', 'time'];
|
||||||
|
var namespace = entry.key;
|
||||||
|
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(var tag in entry.value) {
|
||||||
|
newTags.add("$namespace:$tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
comics[index] = FavoriteItem(
|
comics[index] = FavoriteItem(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: newInfo.title,
|
name: newInfo.title,
|
||||||
@@ -152,7 +166,7 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
|||||||
newInfo.tags['author']?.firstOrNull ??
|
newInfo.tags['author']?.firstOrNull ??
|
||||||
c.author,
|
c.author,
|
||||||
type: c.type,
|
type: c.type,
|
||||||
tags: c.tags,
|
tags: newTags,
|
||||||
);
|
);
|
||||||
|
|
||||||
LocalFavoritesManager().updateInfo(folder, comics[index]);
|
LocalFavoritesManager().updateInfo(folder, comics[index]);
|
||||||
|
@@ -14,6 +14,7 @@ import 'package:venera/foundation/local.dart';
|
|||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
@@ -152,14 +153,14 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isNetwork) {
|
if (!isNetwork) {
|
||||||
return _LocalFavoritesPage(folder: folder!, key: Key(folder!));
|
return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
|
||||||
} else {
|
} else {
|
||||||
var favoriteData = getFavoriteDataOrNull(folder!);
|
var favoriteData = getFavoriteDataOrNull(folder!);
|
||||||
if (favoriteData == null) {
|
if (favoriteData == null) {
|
||||||
folder = null;
|
folder = null;
|
||||||
return buildBody();
|
return buildBody();
|
||||||
} else {
|
} else {
|
||||||
return NetworkFavoritePage(favoriteData, key: Key(folder!));
|
return NetworkFavoritePage(favoriteData, key: PageStorageKey("network_$folder"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,4 +170,4 @@ abstract interface class FolderList {
|
|||||||
void update();
|
void update();
|
||||||
|
|
||||||
void updateFolders();
|
void updateFolders();
|
||||||
}
|
}
|
||||||
|
@@ -102,10 +102,13 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var body = Scaffold(
|
Widget body = SmoothCustomScrollView(
|
||||||
body: SmoothCustomScrollView(slivers: [
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
if (!searchMode && !multiSelectMode)
|
if (!searchMode && !multiSelectMode)
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
style: context.width < changePoint
|
style: context.width < changePoint
|
||||||
@@ -387,24 +390,44 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (appdata.settings["onClickFavorite"] == "viewDetail")
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.menu_book_outlined,
|
||||||
|
text: "Read".tl,
|
||||||
|
onClick: () {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
|
() => ReaderWithLoading(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onTap: multiSelectMode
|
onTap: (c) {
|
||||||
? (c) {
|
if (multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as FavoriteItem)) {
|
if (selectedComics.containsKey(c as FavoriteItem)) {
|
||||||
selectedComics.remove(c);
|
selectedComics.remove(c);
|
||||||
_checkExitSelectMode();
|
_checkExitSelectMode();
|
||||||
} else {
|
} else {
|
||||||
selectedComics[c] = true;
|
selectedComics[c] = true;
|
||||||
}
|
|
||||||
lastSelectedIndex = comics.indexOf(c);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
: (c) {
|
lastSelectedIndex = comics.indexOf(c);
|
||||||
App.mainNavigatorKey?.currentContext
|
});
|
||||||
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
|
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
|
||||||
},
|
App.mainNavigatorKey?.currentContext
|
||||||
|
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
|
||||||
|
} else {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
|
() => ReaderWithLoading(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongPressed: (c) {
|
onLongPressed: (c) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (!multiSelectMode) {
|
if (!multiSelectMode) {
|
||||||
@@ -440,7 +463,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
);
|
||||||
|
body = Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
thickness: App.isDesktop ? 8 : 12,
|
||||||
|
radius: const Radius.circular(8),
|
||||||
|
interactive: true,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !multiSelectMode && !searchMode,
|
canPop: !multiSelectMode && !searchMode,
|
||||||
|
@@ -166,6 +166,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
enablePageStorage: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,6 +549,7 @@ class _FavoriteFolder extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
key: comicListKey,
|
key: comicListKey,
|
||||||
|
enablePageStorage: true,
|
||||||
leadingSliver: SliverAppbar(
|
leadingSliver: SliverAppbar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
actions: [
|
actions: [
|
||||||
|
@@ -264,7 +264,8 @@ class _HistoryState extends State<_History> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return InkWell(
|
return AnimatedTapRegion(
|
||||||
|
borderRadius: 8,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.to(
|
context.to(
|
||||||
() => ComicPage(
|
() => ComicPage(
|
||||||
@@ -273,11 +274,9 @@ class _HistoryState extends State<_History> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 92,
|
width: 92,
|
||||||
height: 114,
|
height: 114,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
@@ -293,7 +292,7 @@ class _HistoryState extends State<_History> {
|
|||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
).paddingHorizontal(8);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8).paddingBottom(16),
|
).paddingHorizontal(8).paddingBottom(16),
|
||||||
@@ -386,15 +385,14 @@ class _LocalState extends State<_Local> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: local.length,
|
itemCount: local.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return InkWell(
|
return AnimatedTapRegion(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
local[index].read();
|
local[index].read();
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: 8,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 92,
|
width: 92,
|
||||||
height: 114,
|
height: 114,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
@@ -412,7 +410,7 @@ class _LocalState extends State<_Local> {
|
|||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
).paddingHorizontal(8);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8),
|
).paddingHorizontal(8),
|
||||||
@@ -497,12 +495,14 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
"Select a directory which contains the comic files.".tl,
|
"Select a directory which contains the comic files.".tl,
|
||||||
"Select a directory which contains the comic directories.".tl,
|
"Select a directory which contains the comic directories.".tl,
|
||||||
"Select a cbz/zip file.".tl,
|
"Select a cbz/zip file.".tl,
|
||||||
|
"Select a directory which contains multiple cbz/zip files.".tl,
|
||||||
"Select an EhViewer database and a download folder.".tl
|
"Select an EhViewer database and a download folder.".tl
|
||||||
][type];
|
][type];
|
||||||
List<String> importMethods = [
|
List<String> importMethods = [
|
||||||
"Single Comic".tl,
|
"Single Comic".tl,
|
||||||
"Multiple Comics".tl,
|
"Multiple Comics".tl,
|
||||||
"A cbz file".tl,
|
"A cbz file".tl,
|
||||||
|
"Multiple cbz files".tl,
|
||||||
"EhViewer downloads".tl
|
"EhViewer downloads".tl
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -630,7 +630,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
0 => await importer.directory(true),
|
0 => await importer.directory(true),
|
||||||
1 => await importer.directory(false),
|
1 => await importer.directory(false),
|
||||||
2 => await importer.cbz(),
|
2 => await importer.cbz(),
|
||||||
3 => await importer.ehViewer(),
|
3 => await importer.multipleCbz(),
|
||||||
|
4 => await importer.ehViewer(),
|
||||||
int() => true,
|
int() => true,
|
||||||
};
|
};
|
||||||
if(result) {
|
if(result) {
|
||||||
|
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/utils/cbz.dart';
|
import 'package:venera/utils/cbz.dart';
|
||||||
import 'package:venera/utils/epub.dart';
|
import 'package:venera/utils/epub.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
@@ -30,7 +31,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
|
|
||||||
bool multiSelectMode = false;
|
bool multiSelectMode = false;
|
||||||
|
|
||||||
Map<Comic, bool> selectedComics = {};
|
Map<LocalComic, bool> selectedComics = {};
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
if (keyword.isEmpty) {
|
if (keyword.isEmpty) {
|
||||||
@@ -117,48 +118,68 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildMultiSelectMenu() {
|
||||||
|
return MenuButton(entries: [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.delete_outline,
|
||||||
|
text: "Delete".tl,
|
||||||
|
onClick: () {
|
||||||
|
deleteComics(selectedComics.keys.toList()).then((value) {
|
||||||
|
if (value) {
|
||||||
|
setState(() {
|
||||||
|
multiSelectMode = false;
|
||||||
|
selectedComics.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.favorite_border,
|
||||||
|
text: "Add to favorites".tl,
|
||||||
|
onClick: () {
|
||||||
|
addFavorite(selectedComics.keys.toList());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
|
text: "View Detail".tl,
|
||||||
|
onClick: () {
|
||||||
|
context.to(() => ComicPage(
|
||||||
|
id: selectedComics.keys.first.id,
|
||||||
|
sourceKey: selectedComics.keys.first.sourceKey,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
...exportActions(selectedComics.keys.first),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectAll() {
|
||||||
|
setState(() {
|
||||||
|
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void deSelect() {
|
||||||
|
setState(() {
|
||||||
|
selectedComics.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void invertSelection() {
|
||||||
|
setState(() {
|
||||||
|
comics.asMap().forEach((k, v) {
|
||||||
|
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
|
||||||
|
});
|
||||||
|
selectedComics.removeWhere((k, v) => !v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
void selectAll() {
|
|
||||||
setState(() {
|
|
||||||
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void deSelect() {
|
|
||||||
setState(() {
|
|
||||||
selectedComics.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void invertSelection() {
|
|
||||||
setState(() {
|
|
||||||
comics.asMap().forEach((k, v) {
|
|
||||||
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
|
|
||||||
});
|
|
||||||
selectedComics.removeWhere((k, v) => !v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectRange() {
|
|
||||||
setState(() {
|
|
||||||
List<int> l = [];
|
|
||||||
selectedComics.forEach((k, v) {
|
|
||||||
l.add(comics.indexOf(k as LocalComic));
|
|
||||||
});
|
|
||||||
if (l.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
l.sort();
|
|
||||||
int start = l.first;
|
|
||||||
int end = l.last;
|
|
||||||
selectedComics.clear();
|
|
||||||
selectedComics.addEntries(List.generate(end - start + 1, (i) {
|
|
||||||
return MapEntry(comics[start + i], true);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> selectActions = [
|
List<Widget> selectActions = [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.select_all),
|
icon: const Icon(Icons.select_all),
|
||||||
@@ -172,78 +193,66 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
icon: const Icon(Icons.flip),
|
icon: const Icon(Icons.flip),
|
||||||
tooltip: "Invert Selection".tl,
|
tooltip: "Invert Selection".tl,
|
||||||
onPressed: invertSelection),
|
onPressed: invertSelection),
|
||||||
IconButton(
|
buildMultiSelectMenu(),
|
||||||
icon: const Icon(Icons.border_horizontal_outlined),
|
];
|
||||||
tooltip: "Select in range".tl,
|
|
||||||
onPressed: selectRange),
|
List<Widget> normalActions = [
|
||||||
|
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());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
var body = Scaffold(
|
var body = Scaffold(
|
||||||
body: SmoothCustomScrollView(
|
body: SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
if (!searchMode && !multiSelectMode)
|
if (!searchMode)
|
||||||
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(
|
SliverAppbar(
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
message: "Cancel".tl,
|
message: multiSelectMode ? "Cancel".tl : "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
if (multiSelectMode) {
|
||||||
multiSelectMode = false;
|
setState(() {
|
||||||
selectedComics.clear();
|
multiSelectMode = false;
|
||||||
});
|
selectedComics.clear();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
icon: multiSelectMode
|
||||||
|
? const Icon(Icons.close)
|
||||||
|
: const Icon(Icons.arrow_back),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: multiSelectMode
|
||||||
"Selected @c comics".tlParams({"c": selectedComics.length})),
|
? Text(selectedComics.length.toString())
|
||||||
actions: selectActions,
|
: Text("Local".tl),
|
||||||
|
actions: multiSelectMode ? selectActions : normalActions,
|
||||||
)
|
)
|
||||||
else if (searchMode)
|
else if (searchMode)
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
@@ -275,149 +284,45 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
SliverGridComics(
|
SliverGridComics(
|
||||||
comics: comics,
|
comics: comics,
|
||||||
selections: selectedComics,
|
selections: selectedComics,
|
||||||
onTap: multiSelectMode
|
onLongPressed: (c) {
|
||||||
? (c) {
|
setState(() {
|
||||||
setState(() {
|
multiSelectMode = true;
|
||||||
if (selectedComics.containsKey(c as LocalComic)) {
|
selectedComics[c as LocalComic] = true;
|
||||||
selectedComics.remove(c);
|
});
|
||||||
} else {
|
},
|
||||||
selectedComics[c] = true;
|
onTap: (c) {
|
||||||
}
|
if (multiSelectMode) {
|
||||||
});
|
setState(() {
|
||||||
|
if (selectedComics.containsKey(c as LocalComic)) {
|
||||||
|
selectedComics.remove(c);
|
||||||
|
} else {
|
||||||
|
selectedComics[c] = true;
|
||||||
}
|
}
|
||||||
: (c) {
|
if (selectedComics.isEmpty) {
|
||||||
(c as LocalComic).read();
|
multiSelectMode = false;
|
||||||
},
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(c as LocalComic).read();
|
||||||
|
}
|
||||||
|
},
|
||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
text: "Delete".tl,
|
text: "Delete".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
showDialog(
|
deleteComics([c as LocalComic]).then((value) {
|
||||||
context: context,
|
if (value && multiSelectMode) {
|
||||||
builder: (context) {
|
setState(() {
|
||||||
bool removeComicFile = true;
|
multiSelectMode = false;
|
||||||
return StatefulBuilder(builder: (context, state) {
|
selectedComics.clear();
|
||||||
return ContentDialog(
|
});
|
||||||
title: "Delete".tl,
|
|
||||||
content: CheckboxListTile(
|
|
||||||
title: Text("Also remove files on disk".tl),
|
|
||||||
value: removeComicFile,
|
|
||||||
onChanged: (v) {
|
|
||||||
state(() {
|
|
||||||
removeComicFile = !removeComicFile;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.pop();
|
|
||||||
if (multiSelectMode) {
|
|
||||||
for (var comic in selectedComics.keys) {
|
|
||||||
LocalManager().deleteComic(
|
|
||||||
comic as LocalComic,
|
|
||||||
removeComicFile);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
selectedComics.clear();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
LocalManager().deleteComic(
|
|
||||||
c as LocalComic, removeComicFile);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text("Confirm".tl),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.outbox_outlined,
|
|
||||||
text: "Export as cbz".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
if (multiSelectMode) {
|
|
||||||
for (var comic in selectedComics.keys) {
|
|
||||||
var file = await CBZ.export(comic as LocalComic);
|
|
||||||
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();
|
});
|
||||||
}),
|
},
|
||||||
if (!multiSelectMode)
|
),
|
||||||
MenuEntry(
|
...exportActions(c as LocalComic),
|
||||||
icon: Icons.picture_as_pdf_outlined,
|
|
||||||
text: "Export as pdf".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await createPdfFromComicIsolate(
|
|
||||||
comic: c as LocalComic,
|
|
||||||
savePath: cache,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: File(cache),
|
|
||||||
filename: "${c.title}.pdf",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("PDF Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
File(cache).deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!multiSelectMode)
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.import_contacts_outlined,
|
|
||||||
text: "Export as epub".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
File? file;
|
|
||||||
try {
|
|
||||||
file = await createEpubWithLocalComic(
|
|
||||||
c as LocalComic,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: file,
|
|
||||||
filename: "${c.title}.epub",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("EPUB Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
file?.deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -444,4 +349,120 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
child: body,
|
child: body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteComics(List<LocalComic> comics) async {
|
||||||
|
bool isDeleted = false;
|
||||||
|
await showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (context) {
|
||||||
|
bool removeComicFile = true;
|
||||||
|
return StatefulBuilder(builder: (context, state) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: "Delete".tl,
|
||||||
|
content: CheckboxListTile(
|
||||||
|
title: Text("Also remove files on disk".tl),
|
||||||
|
value: removeComicFile,
|
||||||
|
onChanged: (v) {
|
||||||
|
state(() {
|
||||||
|
removeComicFile = !removeComicFile;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
for (var comic in comics) {
|
||||||
|
LocalManager().deleteComic(
|
||||||
|
comic,
|
||||||
|
removeComicFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
isDeleted = true;
|
||||||
|
},
|
||||||
|
child: Text("Confirm".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MenuEntry> exportActions(LocalComic c) {
|
||||||
|
return [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.outbox_outlined,
|
||||||
|
text: "Export as cbz".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
var file = await CBZ.export(c);
|
||||||
|
await saveFile(filename: file.name, file: file);
|
||||||
|
await file.delete();
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
Log.error("CBZ Export", e, s);
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
}),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.picture_as_pdf_outlined,
|
||||||
|
text: "Export as pdf".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await createPdfFromComicIsolate(
|
||||||
|
comic: c,
|
||||||
|
savePath: cache,
|
||||||
|
);
|
||||||
|
await saveFile(
|
||||||
|
file: File(cache),
|
||||||
|
filename: "${c.title}.pdf",
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("PDF Export", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
File(cache).deleteIgnoreError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.import_contacts_outlined,
|
||||||
|
text: "Export as epub".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
File? file;
|
||||||
|
try {
|
||||||
|
file = await createEpubWithLocalComic(
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
await saveFile(
|
||||||
|
file: file,
|
||||||
|
filename: "${c.title}.epub",
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("EPUB Export", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
file?.deleteIgnoreError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,9 +62,7 @@ class _MainPageState extends State<MainPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final _pages = [
|
final _pages = [
|
||||||
const HomePage(
|
const HomePage(),
|
||||||
key: PageStorageKey('home'),
|
|
||||||
),
|
|
||||||
const FavoritesPage(
|
const FavoritesPage(
|
||||||
key: PageStorageKey('favorites'),
|
key: PageStorageKey('favorites'),
|
||||||
),
|
),
|
||||||
|
@@ -25,7 +25,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
if (inProgress) return;
|
if (inProgress) return;
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
if (reader.type == ComicType.local ||
|
if (reader.type == ComicType.local ||
|
||||||
(await LocalManager()
|
(LocalManager()
|
||||||
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
|
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
|
||||||
try {
|
try {
|
||||||
var images = await LocalManager()
|
var images = await LocalManager()
|
||||||
@@ -356,6 +356,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
var isCTRLPressed = false;
|
var isCTRLPressed = false;
|
||||||
static var _isMouseScrolling = false;
|
static var _isMouseScrolling = false;
|
||||||
var fingers = 0;
|
var fingers = 0;
|
||||||
|
bool disableScroll = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -426,7 +427,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal,
|
: Axis.horizontal,
|
||||||
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
||||||
physics: isCTRLPressed || _isMouseScrolling
|
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: const ClampingScrollPhysics(),
|
: const ClampingScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -460,6 +461,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
widget = Listener(
|
widget = Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
fingers++;
|
fingers++;
|
||||||
|
if(fingers > 1 && !disableScroll) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
futurePosition = null;
|
futurePosition = null;
|
||||||
if (_isMouseScrolling) {
|
if (_isMouseScrolling) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -469,6 +475,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
},
|
},
|
||||||
onPointerUp: (event) {
|
onPointerUp: (event) {
|
||||||
fingers--;
|
fingers--;
|
||||||
|
if(fingers <= 1 && disableScroll) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onPointerPanZoomUpdate: (event) {
|
onPointerPanZoomUpdate: (event) {
|
||||||
if (event.scale == 1.0) {
|
if (event.scale == 1.0) {
|
||||||
@@ -651,7 +662,7 @@ ImageProvider _createImageProviderFromKey(
|
|||||||
var reader = context.reader;
|
var reader = context.reader;
|
||||||
return ReaderImageProvider(
|
return ReaderImageProvider(
|
||||||
imageKey,
|
imageKey,
|
||||||
reader.type.comicSource!.key,
|
reader.type.comicSource?.key,
|
||||||
reader.cid,
|
reader.cid,
|
||||||
reader.eid,
|
reader.eid,
|
||||||
);
|
);
|
||||||
|
103
lib/pages/reader/loading.dart
Normal file
103
lib/pages/reader/loading.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
part of 'reader.dart';
|
||||||
|
|
||||||
|
class ReaderWithLoading extends StatefulWidget {
|
||||||
|
const ReaderWithLoading({
|
||||||
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
required this.sourceKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReaderWithLoading> createState() => _ReaderWithLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReaderWithLoadingState
|
||||||
|
extends LoadingState<ReaderWithLoading, ReaderProps> {
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, ReaderProps data) {
|
||||||
|
return Reader(
|
||||||
|
type: data.type,
|
||||||
|
cid: data.cid,
|
||||||
|
name: data.name,
|
||||||
|
chapters: data.chapters,
|
||||||
|
history: data.history,
|
||||||
|
initialChapter: data.history.ep,
|
||||||
|
initialPage: data.history.page,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<ReaderProps>> loadData() async {
|
||||||
|
var comicSource = ComicSource.find(widget.sourceKey);
|
||||||
|
var history = HistoryManager().findSync(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
if (comicSource == null) {
|
||||||
|
var localComic = LocalManager().find(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
if (localComic == null) {
|
||||||
|
return Res.error("comic not found");
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
ReaderProps(
|
||||||
|
type: ComicType.fromKey(widget.sourceKey),
|
||||||
|
cid: widget.id,
|
||||||
|
name: localComic.title,
|
||||||
|
chapters: localComic.chapters,
|
||||||
|
history: history ??
|
||||||
|
History.fromModel(
|
||||||
|
model: localComic,
|
||||||
|
ep: 0,
|
||||||
|
page: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var comic = await comicSource.loadComicInfo!(widget.id);
|
||||||
|
if (comic.error) {
|
||||||
|
return Res.fromErrorRes(comic);
|
||||||
|
}
|
||||||
|
return Res(
|
||||||
|
ReaderProps(
|
||||||
|
type: ComicType.fromKey(widget.sourceKey),
|
||||||
|
cid: widget.id,
|
||||||
|
name: comic.data.title,
|
||||||
|
chapters: comic.data.chapters,
|
||||||
|
history: history ??
|
||||||
|
History.fromModel(
|
||||||
|
model: comic.data,
|
||||||
|
ep: 0,
|
||||||
|
page: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReaderProps {
|
||||||
|
final ComicType type;
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
final Map<String, String>? chapters;
|
||||||
|
|
||||||
|
final History history;
|
||||||
|
|
||||||
|
const ReaderProps({
|
||||||
|
required this.type,
|
||||||
|
required this.cid,
|
||||||
|
required this.name,
|
||||||
|
required this.chapters,
|
||||||
|
required this.history,
|
||||||
|
});
|
||||||
|
}
|
@@ -18,11 +18,13 @@ import 'package:venera/components/custom_slider.dart';
|
|||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/file_type.dart';
|
import 'package:venera/utils/file_type.dart';
|
||||||
@@ -36,6 +38,7 @@ part 'scaffold.dart';
|
|||||||
part 'images.dart';
|
part 'images.dart';
|
||||||
part 'gesture.dart';
|
part 'gesture.dart';
|
||||||
part 'comic_image.dart';
|
part 'comic_image.dart';
|
||||||
|
part 'loading.dart';
|
||||||
|
|
||||||
extension _ReaderContext on BuildContext {
|
extension _ReaderContext on BuildContext {
|
||||||
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
||||||
@@ -209,7 +212,11 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
if(history != null) {
|
if(history != null) {
|
||||||
history!.page = page;
|
history!.page = page;
|
||||||
history!.ep = chapter;
|
history!.ep = chapter;
|
||||||
|
if (maxPage > 1) {
|
||||||
|
history!.maxPage = maxPage;
|
||||||
|
}
|
||||||
history!.readEpisode.add(chapter);
|
history!.readEpisode.add(chapter);
|
||||||
|
history!.time = DateTime.now();
|
||||||
HistoryManager().addHistory(history!);
|
HistoryManager().addHistory(history!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -456,58 +456,63 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
var imagesOnScreen =
|
var imagesOnScreen =
|
||||||
continuesState.itemPositionsListener.itemPositions.value;
|
continuesState.itemPositionsListener.itemPositions.value;
|
||||||
var images = imagesOnScreen
|
var images = imagesOnScreen
|
||||||
.map((e) => context.reader.images![e.index - 1])
|
.map((e) => context.reader.images!.elementAtOrNull(e.index - 1))
|
||||||
|
.whereType<String>()
|
||||||
.toList();
|
.toList();
|
||||||
String? selected;
|
String? selected;
|
||||||
await showPopUpWidget(
|
if (images.length > 1) {
|
||||||
context,
|
await showPopUpWidget(
|
||||||
PopUpWidgetScaffold(
|
context,
|
||||||
title: "Select an image on screen".tl,
|
PopUpWidgetScaffold(
|
||||||
body: GridView.builder(
|
title: "Select an image on screen".tl,
|
||||||
itemCount: images.length,
|
body: GridView.builder(
|
||||||
itemBuilder: (context, index) {
|
itemCount: images.length,
|
||||||
ImageProvider image;
|
itemBuilder: (context, index) {
|
||||||
var imageKey = images[index];
|
ImageProvider image;
|
||||||
if (imageKey.startsWith('file://')) {
|
var imageKey = images[index];
|
||||||
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
if (imageKey.startsWith('file://')) {
|
||||||
} else {
|
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
image = ReaderImageProvider(
|
} else {
|
||||||
imageKey,
|
image = ReaderImageProvider(
|
||||||
reader.type.comicSource!.key,
|
imageKey,
|
||||||
reader.cid,
|
reader.type.comicSource!.key,
|
||||||
reader.eid,
|
reader.cid,
|
||||||
);
|
reader.eid,
|
||||||
}
|
);
|
||||||
return InkWell(
|
}
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
return InkWell(
|
||||||
onTap: () {
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
selected = images[index];
|
onTap: () {
|
||||||
App.rootContext.pop();
|
selected = images[index];
|
||||||
},
|
App.rootContext.pop();
|
||||||
child: Container(
|
},
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
color: Theme.of(context).colorScheme.outline,
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
child: Image(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
image: image,
|
child: Image(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: image,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).padding(const EdgeInsets.all(8));
|
||||||
).padding(const EdgeInsets.all(8));
|
},
|
||||||
},
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
maxCrossAxisExtent: 200,
|
||||||
maxCrossAxisExtent: 200,
|
childAspectRatio: 0.7,
|
||||||
childAspectRatio: 0.7,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
selected = images.first;
|
||||||
|
}
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -139,7 +139,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
||||||
if (defaultSearchTarget != null &&
|
if (defaultSearchTarget == "_aggregated_") {
|
||||||
|
aggregatedSearch = true;
|
||||||
|
} else if (defaultSearchTarget != null &&
|
||||||
ComicSource.find(defaultSearchTarget) != null) {
|
ComicSource.find(defaultSearchTarget) != null) {
|
||||||
searchTarget = defaultSearchTarget;
|
searchTarget = defaultSearchTarget;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -45,8 +45,9 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
if (suggestionsController.entry != null) {
|
if (suggestionsController.entry != null) {
|
||||||
suggestionsController.remove();
|
suggestionsController.remove();
|
||||||
}
|
}
|
||||||
|
text = checkAutoLanguage(text);
|
||||||
setState(() {
|
setState(() {
|
||||||
this.text = text;
|
this.text = text!;
|
||||||
});
|
});
|
||||||
appdata.addSearchHistory(text);
|
appdata.addSearchHistory(text);
|
||||||
controller.currentText = text;
|
controller.currentText = text;
|
||||||
@@ -92,13 +93,33 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String checkAutoLanguage(String text) {
|
||||||
|
var setting = appdata.settings["autoAddLanguageFilter"] ?? 'none';
|
||||||
|
if (setting == 'none') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
var searchSource = sourceKey;
|
||||||
|
// TODO: Move it to a better place
|
||||||
|
const enabledSources = [
|
||||||
|
'nhentai',
|
||||||
|
'ehentai',
|
||||||
|
];
|
||||||
|
if (!enabledSources.contains(searchSource)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (!text.contains('language:')) {
|
||||||
|
return '$text language:$setting';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
sourceKey = widget.sourceKey;
|
||||||
controller = SearchBarController(
|
controller = SearchBarController(
|
||||||
currentText: widget.text,
|
currentText: checkAutoLanguage(widget.text),
|
||||||
onSearch: search,
|
onSearch: search,
|
||||||
);
|
);
|
||||||
sourceKey = widget.sourceKey;
|
|
||||||
options = widget.options ?? const [];
|
options = widget.options ?? const [];
|
||||||
validateOptions();
|
validateOptions();
|
||||||
text = widget.text;
|
text = widget.text;
|
||||||
@@ -162,6 +183,12 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (suggestionOverlay != null) {
|
||||||
|
suggestionsController.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousOptions = options;
|
||||||
|
var previousSourceKey = sourceKey;
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
@@ -169,7 +196,11 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
return _SearchSettingsDialog(state: this);
|
return _SearchSettingsDialog(state: this);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
setState(() {});
|
if (previousOptions != options || previousSourceKey != sourceKey) {
|
||||||
|
text = checkAutoLanguage(controller.text);
|
||||||
|
controller.currentText = text;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -61,6 +61,10 @@ class _AboutSettingsState extends State<AboutSettings> {
|
|||||||
},
|
},
|
||||||
).fixHeight(32),
|
).fixHeight(32),
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Check for updates on startup".tl,
|
||||||
|
settingKey: "checkUpdateOnStart",
|
||||||
|
).toSliver(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text("Github"),
|
title: const Text("Github"),
|
||||||
trailing: const Icon(Icons.open_in_new),
|
trailing: const Icon(Icons.open_in_new),
|
||||||
@@ -102,7 +106,9 @@ Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
|
|||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "New version available".tl,
|
title: "New version available".tl,
|
||||||
content: Text(
|
content: Text(
|
||||||
"A new version is available. Do you want to update now?".tl),
|
"A new version is available. Do you want to update now?"
|
||||||
|
.tl)
|
||||||
|
.paddingHorizontal(8),
|
||||||
actions: [
|
actions: [
|
||||||
Button.text(
|
Button.text(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@@ -88,6 +88,30 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
|||||||
title: "Keyword blocking".tl,
|
title: "Keyword blocking".tl,
|
||||||
builder: () => const _ManageBlockingWordView(),
|
builder: () => const _ManageBlockingWordView(),
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Default Search Target".tl,
|
||||||
|
settingKey: "defaultSearchTarget",
|
||||||
|
optionTranslation: {
|
||||||
|
'_aggregated_': "Aggregated".tl,
|
||||||
|
...((){
|
||||||
|
var map = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
map[c.key] = c.name;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}()),
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Auto Language Filters".tl,
|
||||||
|
settingKey: "autoAddLanguageFilter",
|
||||||
|
optionTranslation: {
|
||||||
|
'none': "None".tl,
|
||||||
|
'chinese': "Chinese",
|
||||||
|
'english': "English",
|
||||||
|
'japanese': "Japanese",
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,7 +174,7 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
|
|||||||
errorText: error,
|
errorText: error,
|
||||||
),
|
),
|
||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
if(error != null){
|
if (error != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
error = null;
|
error = null;
|
||||||
});
|
});
|
||||||
@@ -160,7 +184,8 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
|
|||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if(appdata.settings["blockedWords"].contains(controller.text)){
|
if (appdata.settings["blockedWords"]
|
||||||
|
.contains(controller.text)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
error = "Keyword already exists".tl;
|
error = "Keyword already exists".tl;
|
||||||
});
|
});
|
||||||
|
@@ -16,18 +16,18 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
|||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Add new favorite to".tl,
|
title: "Add new favorite to".tl,
|
||||||
settingKey: "newFavoriteAddTo",
|
settingKey: "newFavoriteAddTo",
|
||||||
optionTranslation: const {
|
optionTranslation: {
|
||||||
"start": "Start",
|
"start": "Start".tl,
|
||||||
"end": "End",
|
"end": "End".tl,
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Move favorite after reading".tl,
|
title: "Move favorite after reading".tl,
|
||||||
settingKey: "moveFavoriteAfterRead",
|
settingKey: "moveFavoriteAfterRead",
|
||||||
optionTranslation: const {
|
optionTranslation: {
|
||||||
"none": "None",
|
"none": "None".tl,
|
||||||
"end": "End",
|
"end": "End".tl,
|
||||||
"start": "Start",
|
"start": "Start".tl,
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SelectSetting(
|
SelectSetting(
|
||||||
@@ -48,6 +48,14 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
|||||||
},
|
},
|
||||||
actionTitle: 'Delete'.tl,
|
actionTitle: 'Delete'.tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Click favorite".tl,
|
||||||
|
settingKey: "onClickFavorite",
|
||||||
|
optionTranslation: {
|
||||||
|
"viewDetail": "View Detail".tl,
|
||||||
|
"read": "Read".tl,
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,10 @@ class _NetworkSettingsState extends State<NetworkSettings> {
|
|||||||
title: "Proxy".tl,
|
title: "Proxy".tl,
|
||||||
builder: () => const _ProxySettingView(),
|
builder: () => const _ProxySettingView(),
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "DNS Overrides".tl,
|
||||||
|
builder: () => const _DNSOverrides(),
|
||||||
|
).toSliver(),
|
||||||
_SliderSetting(
|
_SliderSetting(
|
||||||
title: "Download Threads".tl,
|
title: "Download Threads".tl,
|
||||||
settingsIndex: 'downloadThreads',
|
settingsIndex: 'downloadThreads',
|
||||||
@@ -42,7 +46,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
String port = '';
|
String port = '';
|
||||||
String username = '';
|
String username = '';
|
||||||
String password = '';
|
String password = '';
|
||||||
bool ignoreCertificateErrors = false;
|
|
||||||
|
|
||||||
// USERNAME:PASSWORD@HOST:PORT
|
// USERNAME:PASSWORD@HOST:PORT
|
||||||
String toProxyStr() {
|
String toProxyStr() {
|
||||||
@@ -100,7 +103,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
var proxy = appdata.settings['proxy'];
|
var proxy = appdata.settings['proxy'];
|
||||||
parseProxyString(proxy);
|
parseProxyString(proxy);
|
||||||
ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,17 +148,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (type == 'manual') buildManualProxy(),
|
if (type == 'manual') buildManualProxy(),
|
||||||
SwitchListTile(
|
|
||||||
title: Text("Ignore Certificate Errors".tl),
|
|
||||||
value: ignoreCertificateErrors,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
ignoreCertificateErrors = v;
|
|
||||||
});
|
|
||||||
appdata.settings['ignoreCertificateErrors'] = ignoreCertificateErrors;
|
|
||||||
appdata.saveData();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -250,3 +241,139 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
).paddingHorizontal(16).paddingTop(16);
|
).paddingHorizontal(16).paddingTop(16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DNSOverrides extends StatefulWidget {
|
||||||
|
const _DNSOverrides();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DNSOverrides> createState() => __DNSOverridesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __DNSOverridesState extends State<_DNSOverrides> {
|
||||||
|
var overrides = <(String, String)>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
for (var entry in (appdata.settings['dnsOverrides'] as Map).entries) {
|
||||||
|
if (entry.key is String && entry.value is String) {
|
||||||
|
overrides.add((entry.key, entry.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
var map = <String, String>{};
|
||||||
|
for (var entry in overrides) {
|
||||||
|
map[entry.$1] = entry.$2;
|
||||||
|
}
|
||||||
|
appdata.settings['dnsOverrides'] = map;
|
||||||
|
appdata.saveData();
|
||||||
|
JsEngine().resetDio();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: "DNS Overrides".tl,
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Enable DNS Overrides".tl,
|
||||||
|
settingKey: "enableDnsOverrides",
|
||||||
|
),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Server Name Indication",
|
||||||
|
settingKey: "sni",
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
height: 1,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
for (var i = 0; i < overrides.length; i++) buildOverride(i),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
overrides.add(('', ''));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text("Add".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildOverride(int index) {
|
||||||
|
var entry = overrides[index];
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
left: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
right: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: "Domain".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: entry.$1),
|
||||||
|
onChanged: (v) {
|
||||||
|
overrides[index] = (v, entry.$2);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: "IP".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: entry.$2),
|
||||||
|
onChanged: (v) {
|
||||||
|
overrides[index] = (entry.$1, v);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
overrides.removeAt(index);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -61,9 +61,17 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
).toSliver(),
|
).toSliver(),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
|
absorbing: (appdata.settings['readerMode']
|
||||||
|
?.toLowerCase()
|
||||||
|
.startsWith('continuous') ??
|
||||||
|
false),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false) ? 0.5 : 1.0,
|
opacity: (appdata.settings['readerMode']
|
||||||
|
?.toLowerCase()
|
||||||
|
.startsWith('continuous') ??
|
||||||
|
false)
|
||||||
|
? 0.5
|
||||||
|
: 1.0,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
child: _SliderSetting(
|
child: _SliderSetting(
|
||||||
title: "The number of pic in screen (Only Gallery Mode)".tl,
|
title: "The number of pic in screen (Only Gallery Mode)".tl,
|
||||||
@@ -93,7 +101,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call('limitImageWidth');
|
widget.onChanged?.call('limitImageWidth');
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
if(App.isAndroid)
|
if (App.isAndroid)
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Turn page by volume keys'.tl,
|
title: 'Turn page by volume keys'.tl,
|
||||||
settingKey: 'enableTurnPageByVolumeKey',
|
settingKey: 'enableTurnPageByVolumeKey',
|
||||||
@@ -108,7 +116,67 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Custom Image Processing".tl,
|
||||||
|
builder: () => _CustomImageProcessing(),
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CustomImageProcessing extends StatefulWidget {
|
||||||
|
const _CustomImageProcessing();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CustomImageProcessing> createState() => __CustomImageProcessingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __CustomImageProcessingState extends State<_CustomImageProcessing> {
|
||||||
|
var current = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
current = appdata.settings['customImageProcessing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
appdata.settings['customImageProcessing'] = current;
|
||||||
|
appdata.saveData();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: "Custom Image Processing".tl,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Enable".tl,
|
||||||
|
settingKey: "enableCustomImageProcessing",
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: context.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: CodeEditor(
|
||||||
|
initialValue: appdata.settings['customImageProcessing'],
|
||||||
|
onChanged: (value) {
|
||||||
|
current = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import 'package:venera/foundation/appdata.dart';
|
|||||||
import 'package:venera/foundation/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
|
@@ -8,6 +8,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
@@ -83,6 +84,33 @@ class _AppWebviewState extends State<AppWebview> {
|
|||||||
|
|
||||||
double _progress = 0;
|
double _progress = 0;
|
||||||
|
|
||||||
|
late var future = _createWebviewEnvironment();
|
||||||
|
|
||||||
|
Future<WebViewEnvironment> _createWebviewEnvironment() async {
|
||||||
|
var proxy = appdata.settings['proxy'].toString();
|
||||||
|
if (proxy != "system" && proxy != "direct") {
|
||||||
|
var proxyAvailable = await WebViewFeature.isFeatureSupported(
|
||||||
|
WebViewFeature.PROXY_OVERRIDE);
|
||||||
|
if (proxyAvailable) {
|
||||||
|
ProxyController proxyController = ProxyController.instance();
|
||||||
|
await proxyController.clearProxyOverride();
|
||||||
|
if (!proxy.contains("://")) {
|
||||||
|
proxy = "http://$proxy";
|
||||||
|
}
|
||||||
|
await proxyController.setProxyOverride(
|
||||||
|
settings: ProxySettings(
|
||||||
|
proxyRules: [ProxyRule(url: proxy)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WebViewEnvironment.create(
|
||||||
|
settings: WebViewEnvironmentSettings(
|
||||||
|
userDataFolder: "${App.dataPath}\\webview",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final actions = [
|
final actions = [
|
||||||
@@ -121,20 +149,17 @@ class _AppWebviewState extends State<AppWebview> {
|
|||||||
|
|
||||||
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
|
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
|
||||||
? FutureBuilder(
|
? FutureBuilder(
|
||||||
future: WebViewEnvironment.create(
|
future: future,
|
||||||
settings: WebViewEnvironmentSettings(
|
|
||||||
userDataFolder: "${App.dataPath}\\webview",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
builder: (context, e) {
|
builder: (context, e) {
|
||||||
if(e.error != null) {
|
if (e.error != null) {
|
||||||
return Center(child: Text("Error: ${e.error}"));
|
return Center(child: Text("Error: ${e.error}"));
|
||||||
}
|
}
|
||||||
if(e.data == null) {
|
if (e.data == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
AppWebview.webViewEnvironment = e.data;
|
AppWebview.webViewEnvironment = e.data;
|
||||||
return createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
return createWebviewWithEnvironment(
|
||||||
|
AppWebview.webViewEnvironment);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
@@ -63,7 +62,7 @@ abstract class CBZ {
|
|||||||
var cache = Directory(FilePath.join(App.cachePath, 'cbz_import'));
|
var cache = Directory(FilePath.join(App.cachePath, 'cbz_import'));
|
||||||
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
||||||
cache.createSync();
|
cache.createSync();
|
||||||
await Isolate.run(() => ZipFile.openAndExtract(file.path, cache.path));
|
await ZipFile.openAndExtractAsync(file.path, cache.path, 4);
|
||||||
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
|
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
|
||||||
ComicMetaData? metaData;
|
ComicMetaData? metaData;
|
||||||
if (metaDataFile.existsSync()) {
|
if (metaDataFile.existsSync()) {
|
||||||
@@ -208,13 +207,14 @@ abstract class CBZ {
|
|||||||
).toJson(),
|
).toJson(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
var cbz = File(FilePath.join(App.cachePath, '${comic.title}.cbz'));
|
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
|
||||||
|
if (cbz.existsSync()) cbz.deleteSync();
|
||||||
await _compress(cache.path, cbz.path);
|
await _compress(cache.path, cbz.path);
|
||||||
cache.deleteSync(recursive: true);
|
cache.deleteSync(recursive: true);
|
||||||
return cbz;
|
return cbz;
|
||||||
}
|
}
|
||||||
|
|
||||||
static _compress(String src, String dst) async {
|
static _compress(String src, String dst) async {
|
||||||
await Isolate.run(() => ZipFile.compressFolder(src, dst));
|
await ZipFile.compressFolderAsync(src, dst, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -80,15 +80,9 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
LocalFavoritesManager().init();
|
LocalFavoritesManager().init();
|
||||||
}
|
}
|
||||||
if (await appdataFile.exists()) {
|
if (await appdataFile.exists()) {
|
||||||
// proxy settings & authorization setting should be kept
|
var content = await appdataFile.readAsString();
|
||||||
var proxySettings = appdata.settings["proxy"];
|
var data = jsonDecode(content);
|
||||||
var authSettings = appdata.settings["authorizationRequired"];
|
appdata.syncData(data);
|
||||||
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
|
|
||||||
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
|
|
||||||
await appdata.init();
|
|
||||||
appdata.settings["proxy"] = proxySettings;
|
|
||||||
appdata.settings["authorizationRequired"] = authSettings;
|
|
||||||
appdata.saveData();
|
|
||||||
}
|
}
|
||||||
if (await cookieFile.exists()) {
|
if (await cookieFile.exists()) {
|
||||||
SingleInstanceCookieJar.instance?.dispose();
|
SingleInstanceCookieJar.instance?.dispose();
|
||||||
|
@@ -99,7 +99,7 @@ class DataSync with ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
appdata.settings['dataVersion']++;
|
appdata.settings['dataVersion']++;
|
||||||
await appdata.saveData();
|
await appdata.saveData(false);
|
||||||
var data = await exportAppData();
|
var data = await exportAppData();
|
||||||
var time =
|
var time =
|
||||||
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
|
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
|
||||||
|
@@ -37,6 +37,33 @@ class ImportComic {
|
|||||||
return registerComics(imported, false);
|
return registerComics(imported, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> multipleCbz() async {
|
||||||
|
var picker = DirectoryPicker();
|
||||||
|
var dir = await picker.pickDirectory(directAccess: true);
|
||||||
|
if (dir != null) {
|
||||||
|
var files = (await dir.list().toList()).whereType<File>().toList();
|
||||||
|
files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip');
|
||||||
|
Map<String?, List<LocalComic>> imported = {};
|
||||||
|
var controller = showLoadingDialog(App.rootContext, allowCancel: false);
|
||||||
|
var comics = <LocalComic>[];
|
||||||
|
for (var file in files) {
|
||||||
|
try {
|
||||||
|
var comic = await CBZ.import(file);
|
||||||
|
comics.add(comic);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Import Comic", e.toString(), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (comics.isEmpty) {
|
||||||
|
App.rootContext.showMessage(message: "No valid comics found".tl);
|
||||||
|
}
|
||||||
|
imported[selectedFolder] = comics;
|
||||||
|
controller.close();
|
||||||
|
return registerComics(imported, false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> ehViewer() async {
|
Future<bool> ehViewer() async {
|
||||||
var dbFile = await selectFile(ext: ['db']);
|
var dbFile = await selectFile(ext: ['db']);
|
||||||
final picker = DirectoryPicker();
|
final picker = DirectoryPicker();
|
||||||
|
@@ -72,6 +72,7 @@ extension FileSystemEntityExt on FileSystemEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension FileExtension on File {
|
extension FileExtension on File {
|
||||||
|
/// Get the file extension, not including the dot.
|
||||||
String get extension => path.split('.').last;
|
String get extension => path.split('.').last;
|
||||||
|
|
||||||
/// Copy the file to the specified path using memory.
|
/// Copy the file to the specified path using memory.
|
||||||
@@ -196,7 +197,7 @@ class DirectoryPicker {
|
|||||||
|
|
||||||
static const _methodChannel = MethodChannel("venera/method_channel");
|
static const _methodChannel = MethodChannel("venera/method_channel");
|
||||||
|
|
||||||
Future<Directory?> pickDirectory() async {
|
Future<Directory?> pickDirectory({bool directAccess = false}) async {
|
||||||
IO._isSelectingFiles = true;
|
IO._isSelectingFiles = true;
|
||||||
try {
|
try {
|
||||||
String? directory;
|
String? directory;
|
||||||
@@ -204,6 +205,16 @@ class DirectoryPicker {
|
|||||||
directory = await file_selector.getDirectoryPath();
|
directory = await file_selector.getDirectoryPath();
|
||||||
} else if (App.isAndroid) {
|
} else if (App.isAndroid) {
|
||||||
directory = (await AndroidDirectory.pickDirectory())?.path;
|
directory = (await AndroidDirectory.pickDirectory())?.path;
|
||||||
|
if (directory != null && directAccess) {
|
||||||
|
// Native library does not have access to the directory. Copy it to cache.
|
||||||
|
var cache = FilePath.join(App.cachePath, "selected_directory");
|
||||||
|
if (Directory(cache).existsSync()) {
|
||||||
|
Directory(cache).deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
Directory(cache).createSync();
|
||||||
|
await copyDirectoryIsolate(Directory(directory), Directory(cache));
|
||||||
|
directory = cache;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// ios, macos
|
// ios, macos
|
||||||
directory =
|
directory =
|
||||||
|
43
macos/Podfile
Normal file
43
macos/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
platform :osx, '12.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_macos_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_macos_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
@@ -557,7 +557,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -639,7 +639,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -689,7 +689,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
80
pubspec.lock
80
pubspec.lock
@@ -5,10 +5,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
|
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
app_links_linux:
|
app_links_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: battery_plus
|
name: battery_plus
|
||||||
sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7"
|
sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.0"
|
version: "6.2.1"
|
||||||
battery_plus_platform_interface:
|
battery_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -408,8 +408,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: ade0b9d
|
ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
||||||
resolved-ref: ade0b9d67331118c13a2b836684858e251512373
|
resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
||||||
url: "https://github.com/wgh136/flutter_qjs"
|
url: "https://github.com/wgh136/flutter_qjs"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.7"
|
version: "0.3.7"
|
||||||
@@ -425,16 +425,16 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_rust_bridge
|
name: flutter_rust_bridge
|
||||||
sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d
|
sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.7.0"
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "3315082b9f7055655610e4f6f136b69e48228c05"
|
ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
|
||||||
resolved-ref: "3315082b9f7055655610e4f6f136b69e48228c05"
|
resolved-ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
|
||||||
url: "https://github.com/pkuislm/flutter_saf.git"
|
url: "https://github.com/pkuislm/flutter_saf.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -446,12 +446,11 @@ packages:
|
|||||||
flutter_to_arch:
|
flutter_to_arch:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: flutter_to_arch
|
||||||
ref: HEAD
|
sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040
|
||||||
resolved-ref: "15bfead0380fda79b0256b37c73b886b0882f1bf"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/wgh136/flutter_to_arch"
|
source: hosted
|
||||||
source: git
|
version: "1.0.1"
|
||||||
version: "1.0.0"
|
|
||||||
flutter_to_debian:
|
flutter_to_debian:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -629,8 +628,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
|
ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
|
||||||
resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
|
resolved-ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
|
||||||
url: "https://github.com/venera-app/lodepng_flutter"
|
url: "https://github.com/venera-app/lodepng_flutter"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -799,10 +798,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rhttp
|
name: rhttp
|
||||||
sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5"
|
sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.6"
|
version: "0.9.8"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -856,18 +855,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8"
|
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.2"
|
version: "10.1.3"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: share_plus_platform_interface
|
name: share_plus_platform_interface
|
||||||
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
|
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.2"
|
||||||
shimmer:
|
shimmer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -917,10 +916,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9"
|
sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.27"
|
version: "0.5.28"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -945,6 +944,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
syntax_highlight:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: syntax_highlight
|
||||||
|
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -961,6 +968,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.3"
|
||||||
|
text_scroll:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: text_scroll
|
||||||
|
sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1125,12 +1140,11 @@ packages:
|
|||||||
zip_flutter:
|
zip_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: zip_flutter
|
||||||
ref: HEAD
|
sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646
|
||||||
resolved-ref: d5721f1fd8179ee4a5db59f932ae7c89d94e12a0
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/wgh136/zip_flutter"
|
source: hosted
|
||||||
source: git
|
version: "0.0.6"
|
||||||
version: "0.0.1"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.1"
|
||||||
|
31
pubspec.yaml
31
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.0+110
|
version: 1.1.4+114
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
flutter: 3.27.0
|
flutter: 3.27.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -17,11 +17,11 @@ dependencies:
|
|||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
window_manager: ^0.4.3
|
window_manager: ^0.4.3
|
||||||
sqlite3: ^2.4.7
|
sqlite3: ^2.4.7
|
||||||
sqlite3_flutter_libs: any
|
sqlite3_flutter_libs: ^0.5.28
|
||||||
flutter_qjs:
|
flutter_qjs:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/flutter_qjs
|
url: https://github.com/wgh136/flutter_qjs
|
||||||
ref: ade0b9d
|
ref: 9c99ac258a11f8e91761a5466a190efba3ca64af
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
dio: ^5.7.0
|
dio: ^5.7.0
|
||||||
html: ^0.15.5
|
html: ^0.15.5
|
||||||
@@ -33,7 +33,7 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/photo_view
|
url: https://github.com/wgh136/photo_view
|
||||||
ref: 94724a0b
|
ref: 94724a0b
|
||||||
mime: ^2.0.0
|
mime: ^2.0.0
|
||||||
share_plus: ^10.0.2
|
share_plus: ^10.1.3
|
||||||
scrollable_positioned_list:
|
scrollable_positioned_list:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/flutter.widgets
|
url: https://github.com/venera-app/flutter.widgets
|
||||||
@@ -47,39 +47,38 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/flutter_desktop_webview
|
url: https://github.com/wgh136/flutter_desktop_webview
|
||||||
path: packages/desktop_webview_window
|
path: packages/desktop_webview_window
|
||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
app_links: ^6.3.2
|
app_links: ^6.3.3
|
||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter:
|
zip_flutter: ^0.0.6
|
||||||
git:
|
|
||||||
url: https://github.com/wgh136/zip_flutter
|
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
|
ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
|
||||||
rhttp: 0.9.6
|
rhttp: 0.9.8
|
||||||
webdav_client:
|
webdav_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
||||||
battery_plus: ^6.2.0
|
battery_plus: ^6.2.1
|
||||||
local_auth: ^2.3.0
|
local_auth: ^2.3.0
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/pkuislm/flutter_saf.git
|
url: https://github.com/pkuislm/flutter_saf.git
|
||||||
ref: 3315082b9f7055655610e4f6f136b69e48228c05
|
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
|
||||||
pdf: ^3.11.1
|
pdf: ^3.11.1
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
flutter_memory_info: ^0.0.1
|
flutter_memory_info: ^0.0.1
|
||||||
|
syntax_highlight: ^0.4.0
|
||||||
|
text_scroll: ^0.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_to_arch:
|
flutter_to_arch: ^1.0.1
|
||||||
git: https://github.com/wgh136/flutter_to_arch
|
|
||||||
flutter_to_debian:
|
flutter_to_debian:
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
@@ -99,4 +98,4 @@ flutter_to_arch:
|
|||||||
url: https://github.com/venera-app/venera
|
url: https://github.com/venera-app/venera
|
||||||
depends:
|
depends:
|
||||||
- gtk3
|
- gtk3
|
||||||
- webkit2gtk-4.1
|
- webkit2gtk-4.1
|
||||||
|
@@ -58,6 +58,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.
|
|||||||
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\x64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
@@ -66,4 +67,4 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
|||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
||||||
|
Reference in New Issue
Block a user