84 Commits

Author SHA1 Message Date
nyne
b56f8d7398 Merge pull request #129 from UjuiUjuMandan/patch-2
Setting `checkUpdateOnStart` to false by default
2025-01-06 22:44:22 +08:00
nyne
8375fb721e Merge pull request #130 from venera-app/dev
v1.1.4
2025-01-06 22:42:09 +08:00
9876da85da Add "Check for updates on startup" setting. #129 2025-01-06 22:38:05 +08:00
4b19ab57d2 fix menu overflow 2025-01-06 22:10:01 +08:00
91ee48cc6c Improve comic display 2025-01-06 21:41:52 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
7495c11944 Setting checkUpdateOnStart to false by default 2025-01-06 19:50:22 +08:00
08e8a45236 Update version code 2025-01-06 11:06:02 +08:00
fb1b017bc9 fix #127 2025-01-06 11:05:20 +08:00
99a3788f4a fix #128 2025-01-06 10:55:26 +08:00
a747179cc4 sni 2025-01-06 10:15:23 +08:00
1ca8da1c83 improve editor 2024-12-31 15:50:28 +08:00
8eddab5e13 line numbers in editor 2024-12-31 15:05:11 +08:00
030007159d fix editor font 2024-12-31 13:56:11 +08:00
43a054c12a code highlight 2024-12-31 13:36:00 +08:00
51a6456dad Use the built-in editor to edit the config file if vscode is not installed. 2024-12-31 12:36:47 +08:00
3a320feda9 fix #107 2024-12-31 12:05:56 +08:00
nyne
a88bbe9ea6 Merge pull request #123 from Pacalini/cert
revert #46
2024-12-31 09:27:54 +08:00
Pacalini
5be2dbcfd7 revert #46 2024-12-31 08:54:01 +08:00
68a203a1c1 Sync data after changing settings 2024-12-30 23:12:46 +08:00
c06709aeb7 DNS overrides 2024-12-30 22:55:01 +08:00
95649ca9fe upgrade gradle 2024-12-30 21:59:24 +08:00
1e09d69507 improve cbz export 2024-12-30 21:58:23 +08:00
nyne
a5c745f40d Merge pull request #119 from venera-app/dev
v1.1.3
2024-12-26 19:31:21 +08:00
d27efb180a V1.1.3 2024-12-26 19:19:28 +08:00
1f5382ff8c Fixed the issue of not storing maxPage.
fix #112
2024-12-26 14:12:13 +08:00
2238fcc68f continue reading 2024-12-26 13:59:54 +08:00
df42cf320c add scrollbar to local favorites page 2024-12-26 13:43:28 +08:00
eb14f973e4 add "View Detail" option 2024-12-26 12:06:05 +08:00
99454041d3 fix pinch to zoom 2024-12-26 10:55:31 +08:00
1ae33c43b1 fix #115 2024-12-26 10:32:15 +08:00
bed30d3cea fix #116 2024-12-26 10:03:40 +08:00
06f953c1bc fix history length 2024-12-23 22:52:03 +08:00
0b96d01afb Improve local comics page 2024-12-22 18:07:13 +08:00
6023e462d7 Improve init 2024-12-22 11:31:57 +08:00
0e22574002 fix #111 2024-12-22 11:22:59 +08:00
e1b2f83c48 fix selecting image 2024-12-22 11:14:53 +08:00
e77424e00e fix #109 2024-12-21 18:08:32 +08:00
9f67cd0d07 fix #110 2024-12-21 17:51:54 +08:00
6a79f68909 fix color 2024-12-21 17:50:05 +08:00
aa66111f2c fix updating comic info 2024-12-21 17:38:59 +08:00
ddeaaf0856 fix desktop file 2024-12-21 17:27:17 +08:00
18f450a0db Update README.md 2024-12-19 10:46:15 +08:00
a217b86c08 typo 2024-12-19 10:11:42 +08:00
nyne
79d2c91723 Create analyse.dart 2024-12-19 10:09:20 +08:00
nyne
731510e11d Merge pull request #108 from UjuiUjuMandan/evil
Remove DependencyInfoBlock
2024-12-18 23:18:27 +08:00
UjuiUjuMandan
b3d3c141f9 Remove DependencyInfoBlock 2024-12-18 13:53:23 +00:00
nyne
bea861a83c Merge pull request #105 from UjuiUjuMandan/patch-2
Fix scope
2024-12-18 20:26:01 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
4a595a8aca Update build.gradle 2024-12-18 20:24:21 +08:00
nyne
bf634f8654 Merge pull request #104 from venera-app/dev
v1.1.2
2024-12-18 20:11:11 +08:00
nyne
bda215ebb7 Merge branch 'master' into dev 2024-12-18 20:10:41 +08:00
a70b690d3c Run dart fix 2024-12-18 20:07:35 +08:00
0b8ae2d377 Update version code 2024-12-18 20:05:59 +08:00
24c5a1bb01 Improve local comics page 2024-12-18 20:04:45 +08:00
ea973a2787 fix #92 2024-12-18 19:36:54 +08:00
nyne
17bce96143 Merge pull request #103 from UjuiUjuMandan/abivercode
Add abiVersionCode & Remove x86
2024-12-18 19:17:58 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
909c0014ac * 10 for universal 2024-12-18 19:15:45 +08:00
eb1abfc02a Fixed the issue where the images of multi-chapter comic are downloaded to invalid folder. 2024-12-18 19:13:35 +08:00
UjuiUjuMandan
788e41f584 Add abiVersionCode & Remove x86 2024-12-18 10:50:19 +00:00
929ec88e84 Fixed issue where deleting a download caused favourites to be deleted. 2024-12-18 17:58:18 +08:00
abaeaf4f77 improve mouse hover effects on click areas 2024-12-18 17:37:03 +08:00
nyne
a614e83470 Merge pull request #102 from UjuiUjuMandan/rb
update dependencies again
2024-12-18 17:22:30 +08:00
8b9fd0d03d improve pop_up_widget and side_bar in dark mode 2024-12-18 17:19:34 +08:00
UjuiUjuMandan
1964c4c0d5 update dependencies again 2024-12-18 09:16:28 +00:00
43d724dd27 fix #97 2024-12-18 17:08:03 +08:00
f9c42aef4b fix #98 2024-12-18 16:51:57 +08:00
06a6e5156a Fix minimum support platform version 2024-12-18 15:48:35 +08:00
deltamaya
be45a06981 update minimum support platform version 2024-12-18 15:25:32 +08:00
4763b9c7b4 test zip_flutter 2024-12-18 15:14:27 +08:00
7e608be70f test zip_flutter 2024-12-18 14:15:08 +08:00
211e6ab8c8 update dependencies 2024-12-18 13:29:43 +08:00
nyne
100dc6458b Merge pull request #100 from UjuiUjuMandan/master
F-Droid
2024-12-17 22:56:39 +08:00
UjuiUjuMandan
8dab5f9e88 test fastlane
add icon.png

add icon.png

scale to 512x512

metadata for zh-CN

Revert "metadata for zh-CN"

This reverts commit 77b30b9209dd1b082f050c55fa175fa96afbfcf6.
2024-12-17 14:18:41 +00:00
d08383e14b disable Impeller 2024-12-17 20:11:18 +08:00
a55e4eff67 Update to flutter 3.27.1 & Fix android build 2024-12-17 17:21:10 +08:00
ab3953292b fix https://github.com/venera-app/venera-configs/issues/28 2024-12-17 13:01:44 +08:00
b49e0974ab improve zip 2024-12-17 12:10:57 +08:00
nyne
b6cccb7749 update version code 2024-12-13 09:34:37 +08:00
nyne
dac07cfac4 Fix windows build script 2024-12-13 09:30:28 +08:00
nyne
da12b3bcca Fix favorites_page 2024-12-13 09:27:53 +08:00
nyne
017f964705 [Android] Disable Impeller 2024-12-13 09:25:09 +08:00
nyne
bed0f78e81 Merge pull request #96 from venera-app/dev
v1.1.0-patch
2024-12-12 23:30:44 +08:00
nyne
092eb59c10 fix #94 2024-12-12 23:28:54 +08:00
nyne
a5d3d160c8 fix #95 2024-12-12 23:22:19 +08:00
nyne
d3c3748ce5 Update app.dart 2024-12-12 22:07:42 +08:00
69 changed files with 2109 additions and 733 deletions

19
.github/workflows/analyze.yml vendored Normal file
View 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
View File

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

View File

@@ -1,9 +1,9 @@
# venera
[![flutter](https://img.shields.io/badge/flutter-3.24.4-blue)](https://flutter.dev/)
[![flutter](https://img.shields.io/badge/flutter-3.27.1-blue)](https://flutter.dev/)
[![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![stars](https://img.shields.io/github/stars/venera-app/venera)](https://github.com/venera-app/venera/stargazers)
[![stars](https://img.shields.io/github/stars/venera-app/venera?style=flat)](https://github.com/venera-app/venera/stargazers)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/+Ws-IpmUutzkxMjhl)
A comic reader that support reading local and network comics.

View File

@@ -5,6 +5,8 @@ plugins {
id "dev.flutter.flutter-gradle-plugin"
}
ext.abiCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86_64": 3]
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
@@ -35,7 +37,7 @@ android {
splits{
abi {
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
enable true
universalApk true
}
@@ -78,7 +80,7 @@ android {
buildTypes {
release {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
}
signingConfig signingConfigs.release
applicationVariants.all { variant ->
@@ -86,13 +88,25 @@ android {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
outputFileName = "venera-${variant.versionName}-${abi}.apk"
def abiVersionCode = project.ext.abiCodes.get(abi)
if (abiVersionCode != null) {
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
} else {
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 {
@@ -102,4 +116,4 @@ flutter {
dependencies {
implementation "androidx.activity:activity-ktx:1.9.2"
implementation 'androidx.documentfile:documentfile:1.0.1'
}
}

View File

@@ -53,6 +53,8 @@
<meta-data
android:name="flutterEmbedding"
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>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
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
}

View File

@@ -218,7 +218,6 @@
"Create Folder": "新建文件夹",
"Select an image on screen": "选择屏幕上的图片",
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
"Ignore Certificate Errors": "忽略证书错误",
"Authorization Required": "需要身份验证",
"Sync": "同步",
"The folder is Linked to @source": "文件夹已关联到 @source",
@@ -251,7 +250,22 @@
"Aggregated Search": "聚合搜索",
"No search results found": "未找到搜索结果",
"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": {
"Home": "首頁",
@@ -472,7 +486,6 @@
"Create Folder": "新建文件夾",
"Select an image on screen": "選擇屏幕上的圖片",
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
"Ignore Certificate Errors": "忽略證書錯誤",
"Authorization Required": "需要身份驗證",
"Sync": "同步",
"The folder is Linked to @source": "文件夾已關聯到 @source",
@@ -505,6 +518,21 @@
"Aggregated Search": "聚合搜索",
"No search results found": "未找到搜索結果",
"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": "啟動時檢查更新"
}
}

View File

@@ -1,5 +1,4 @@
[Desktop Entry]
Version={{Version}}
Name=Venera
GenericName=Venera
Comment=venera

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

@@ -0,0 +1 @@
venera

View File

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

383
lib/components/code.dart Normal file
View 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": "\\}"
}
]
}
}
}
''';

View File

@@ -77,7 +77,7 @@ class ComicTile extends StatelessWidget {
icon: Icons.stars_outlined,
text: 'Add to favorites'.tl,
onClick: () {
addFavorite(comic);
addFavorite([comic]);
},
),
MenuEntry(
@@ -235,128 +235,145 @@ class ComicTile extends StatelessWidget {
}
Widget _buildBriefMode(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
child: LayoutBuilder(
builder: (context, constraints) {
return InkWell(
borderRadius: BorderRadius.circular(8),
onTap: _onTap,
onLongPress:
enableLongPressed ? () => _onLongPressed(context) : null,
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
child: Column(
children: [
Expanded(
child: SizedBox(
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.antiAlias,
child: buildImage(context),
return LayoutBuilder(
builder: (context, constraints) {
return InkWell(
borderRadius: BorderRadius.circular(8),
onTap: _onTap,
onLongPress: enableLongPressed ? () => _onLongPressed(context) : null,
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
child: Column(
children: [
Expanded(
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.toOpacity(0.2),
blurRadius: 2,
offset: const Offset(0, 2),
),
),
Align(
alignment: Alignment.bottomRight,
child: (() {
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,
),
),
),
),
),
);
})(),
),
],
],
),
clipBehavior: Clip.antiAlias,
child: buildImage(context),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
child: Text(
comic.title.replaceAll('\n', ''),
style: const TextStyle(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Align(
alignment: Alignment.bottomRight,
child: (() {
final subtitle =
comic.subtitle?.replaceAll('\n', '').trim();
final text = comic.description.isNotEmpty
? comic.description.split('|').join('\n')
: (subtitle?.isNotEmpty == true ? subtitle : null);
final fortSize = constraints.maxWidth < 80
? 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) {
// split text by space, comma. text in brackets will be kept together.
// split text by comma, brackets
var words = <String>[];
var buffer = StringBuffer();
var inBracket = false;
String? prevBracket;
for (var i = 0; i < text.length; i++) {
var c = text[i];
if (c == '[' || c == '(') {
inBracket = true;
} else if (c == ']' || c == ')') {
inBracket = false;
} else if (c == ' ' || c == ',') {
if (inBracket) {
buffer.write(c);
} 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();
}
} else {
@@ -364,8 +381,10 @@ class ComicTile extends StatelessWidget {
}
}
if (buffer.isNotEmpty) {
words.add(buffer.toString());
words.add(buffer.toString().trim());
}
words.removeWhere((element) => element == "");
words = words.toSet().toList();
return words;
}
@@ -383,26 +402,33 @@ class ComicTile extends StatelessWidget {
return StatefulBuilder(builder: (context, setState) {
return ContentDialog(
title: 'Block'.tl,
content: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (var word in all)
OptionChip(
text: word,
isSelected: words.contains(word),
onTap: () {
setState(() {
if (!words.contains(word)) {
words.add(word);
} else {
words.remove(word);
}
});
},
),
],
).paddingHorizontal(16),
content: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: math.min(400, context.height - 136),
),
child: SingleChildScrollView(
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (var word in all)
OptionChip(
text: word,
isSelected: words.contains(word),
onTap: () {
setState(() {
if (!words.contains(word)) {
words.add(word);
} else {
words.remove(word);
}
});
},
),
],
),
).paddingHorizontal(16),
),
actions: [
Button.filled(
onPressed: () {
@@ -780,7 +806,10 @@ class _SliverGridComics extends StatelessWidget {
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72)
? Theme.of(context)
.colorScheme
.secondaryContainer
.toOpacity(0.72)
: null,
borderRadius: BorderRadius.circular(12),
),
@@ -833,6 +862,7 @@ class ComicList extends StatefulWidget {
this.menuBuilder,
this.controller,
this.refreshHandlerCallback,
this.enablePageStorage = false,
});
final Future<Res<List<Comic>>> Function(int page)? loadPage;
@@ -851,6 +881,8 @@ class ComicList extends StatefulWidget {
final void Function(VoidCallback c)? refreshHandlerCallback;
final bool enablePageStorage;
@override
State<ComicList> createState() => ComicListState();
}
@@ -868,17 +900,19 @@ class ComicListState extends State<ComicList> {
String? _nextUrl;
late bool enablePageStorage = widget.enablePageStorage;
Map<String, dynamic> get state => {
'maxPage': _maxPage,
'data': _data,
'page': _page,
'error': _error,
'loading': _loading,
'nextUrl': _nextUrl,
};
'maxPage': _maxPage,
'data': _data,
'page': _page,
'error': _error,
'loading': _loading,
'nextUrl': _nextUrl,
};
void restoreState(Map<String, dynamic>? state) {
if (state == null) {
if (state == null || !enablePageStorage) {
return;
}
_maxPage = state['maxPage'];
@@ -892,7 +926,9 @@ class ComicListState extends State<ComicList> {
}
void storeState() {
PageStorage.of(context).writeState(context, state);
if (enablePageStorage) {
PageStorage.of(context).writeState(context, state);
}
}
void refresh() {
@@ -1060,11 +1096,11 @@ class ComicListState extends State<ComicList> {
while (_data[page] == null) {
await _fetchNext();
}
if(mounted) {
if (mounted) {
setState(() {});
}
} catch (e) {
if(mounted) {
if (mounted) {
setState(() {
_error = e.toString();
});
@@ -1122,7 +1158,7 @@ class ComicListState extends State<ComicList> {
);
}
return SmoothCustomScrollView(
key: const PageStorageKey('scroll'),
key: enablePageStorage ? PageStorageKey('scroll$_page') : null,
controller: widget.controller,
slivers: [
if (widget.leadingSliver != null) widget.leadingSliver!,

View File

@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.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_page_route.dart';
import 'package:venera/foundation/appdata.dart';
@@ -44,4 +46,5 @@ part 'select.dart';
part 'side_bar.dart';
part 'comic.dart';
part 'effects.dart';
part 'gesture.dart';
part 'gesture.dart';
part 'code.dart';

View File

@@ -41,13 +41,29 @@ class AnimatedTapRegion extends StatefulWidget {
}
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
bool isScaled = false;
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
onEnter: (_) {
isHovered = true;
if (!isScaled) {
Future.delayed(const Duration(milliseconds: 100), () {
if (isHovered) {
setState(() => isScaled = true);
}
});
}
},
onExit: (_) {
isHovered = false;
if(isScaled) {
setState(() => isScaled = false);
}
},
child: GestureDetector(
onTap: widget.onTap,
child: ClipRRect(
@@ -55,7 +71,7 @@ class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
clipBehavior: Clip.antiAlias,
child: AnimatedScale(
duration: _fastAnimationDuration,
scale: isHovered ? 1.1 : 1,
scale: isScaled ? 1.1 : 1,
child: widget.child,
),
),

View File

@@ -28,6 +28,9 @@ class _MenuRoute<T> extends PopupRoute<T> {
var width = entries.first.icon == null ? 216.0 : 242.0;
final size = MediaQuery.of(context).size;
var left = location.dx;
if (left < 10) {
left = 10;
}
if (left + width > size.width - 10) {
left = size.width - width - 10;
}

View File

@@ -290,6 +290,7 @@ class ContentDialog extends StatelessWidget {
: const EdgeInsets.symmetric(horizontal: 16),
elevation: 2,
shadowColor: context.colorScheme.shadow,
backgroundColor: context.colorScheme.surface,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topCenter,

View File

@@ -22,8 +22,15 @@ class PopUpWidget<T> extends PopupRoute<T> {
Widget body = PopupIndicatorWidget(
child: Container(
decoration: showPopUp
? const BoxDecoration(
? BoxDecoration(
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,
clipBehavior: showPopUp ? Clip.antiAlias : Clip.none,
@@ -86,7 +93,8 @@ class PopupIndicatorWidget extends InheritedWidget {
}
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 {
@@ -127,9 +135,8 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
message: "Back".tl,
child: IconButton(
icon: const Icon(Icons.arrow_back_sharp),
onPressed: () => context.canPop()
? context.pop()
: App.pop(),
onPressed: () =>
context.canPop() ? context.pop() : App.pop(),
),
),
const SizedBox(
@@ -148,6 +155,9 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
),
NotificationListener<ScrollNotification>(
onNotification: (notifications) {
if (notifications.metrics.axisDirection != AxisDirection.down) {
return false;
}
if (notifications.metrics.pixels ==
notifications.metrics.minScrollExtent &&
!top) {

View File

@@ -57,10 +57,18 @@ class SideBarRoute<T> extends PopupRoute<T> {
body = Container(
decoration: BoxDecoration(
borderRadius: showSideBar
? const BorderRadius.horizontal(left: Radius.circular(16))
: null,
color: Theme.of(context).colorScheme.surfaceTint),
borderRadius: showSideBar
? const BorderRadius.horizontal(left: Radius.circular(16))
: null,
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,
constraints: BoxConstraints(maxWidth: sideBarWidth),
height: MediaQuery.of(context).size.height,

View File

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

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/io.dart';
class _Appdata {
@@ -12,7 +13,7 @@ class _Appdata {
bool _isSavingData = false;
Future<void> saveData() async {
Future<void> saveData([bool sync = true]) async {
if (_isSavingData) {
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 20));
@@ -24,6 +25,9 @@ class _Appdata {
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
await file.writeAsString(data);
_isSavingData = false;
if (sync) {
DataSync().uploadData();
}
}
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>{};
void writeImplicitData() {
@@ -113,15 +136,21 @@ class _Settings with ChangeNotifier {
'cacheSize': 2048, // in MB
'downloadThreads': 5,
'enableLongPressToZoom': true,
'checkUpdateOnStart': true,
'checkUpdateOnStart': false,
'limitImageWidth': true,
'webdav': [], // empty means not configured
'dataVersion': 0,
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true,
'ignoreCertificateErrors': 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) {
@@ -138,3 +167,16 @@ class _Settings with ChangeNotifier {
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;
}
''';

View File

@@ -145,7 +145,7 @@ class ComicDetails with HistoryMixin {
final int? likesCount;
final int? commentsCount;
final int? commentCount;
final String? uploader;
@@ -189,7 +189,7 @@ class ComicDetails with HistoryMixin {
subId = json["subId"],
likesCount = json["likesCount"],
isLiked = json["isLiked"],
commentsCount = json["commentsCount"],
commentCount = json["commentCount"],
uploader = json["uploader"],
uploadTime = json["uploadTime"],
updateTime = json["updateTime"],
@@ -216,7 +216,7 @@ class ComicDetails with HistoryMixin {
"subId": subId,
"isLiked": isLiked,
"likesCount": likesCount,
"commentsCount": commentsCount,
"commentsCount": commentCount,
"uploader": uploader,
"uploadTime": uploadTime,
"updateTime": updateTime,

View File

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

View File

@@ -73,6 +73,7 @@ class FavoriteItem implements Comic {
@override
String get description {
var time = this.time.substring(0, 10);
return appdata.settings['comicDisplayMode'] == 'detailed'
? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"
: "${type.comicSource?.name ?? "Unknown"} | $time";

View File

@@ -201,8 +201,6 @@ class HistoryManager with ChangeNotifier {
Map<String, bool>? _cachedHistory;
static const _kMaxHistoryLength = 200;
Future<void> init() async {
_db = sqlite3.open("${App.dataPath}/history.db");
@@ -228,12 +226,6 @@ class HistoryManager with ChangeNotifier {
///
/// This function would be called when user start reading.
Future<void> addHistory(History newItem) async {
while(count() >= _kMaxHistoryLength) {
_db.execute("""
delete from history
where time == (select min(time) from history);
""");
}
_db.execute("""
insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

View File

@@ -27,10 +27,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
screen.size.height * _normalComicImageRatio,
);
} else {
_effectiveScreenWidth = max(
_effectiveScreenWidth ?? 0,
screen.size.width
);
_effectiveScreenWidth =
max(_effectiveScreenWidth ?? 0, screen.size.width);
}
}
if (_effectiveScreenWidth! < _minComicImageWidth) {
@@ -110,7 +108,10 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
try {
final buffer = await ImmutableBuffer.fromUint8List(data);
return await decode(buffer, getTargetSize: _getTargetSize);
return await decode(
buffer,
getTargetSize: enableResize ? _getTargetSize : null,
);
} catch (e) {
await CacheManager().delete(this.key);
if (data.length < 2 * 1024) {
@@ -151,6 +152,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
String toString() {
return "$runtimeType($key)";
}
bool get enableResize => false;
}
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);

View File

@@ -1,10 +1,13 @@
import 'dart:async' show Future, StreamController;
import 'package:flutter/foundation.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/utils/io.dart';
import 'base_image_provider.dart';
import 'reader_image.dart' as image_provider;
import 'package:venera/foundation/appdata.dart';
class ReaderImageProvider
extends BaseImageProvider<image_provider.ReaderImageProvider> {
@@ -21,25 +24,50 @@ class ReaderImageProvider
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Uint8List? imageBytes;
if (imageKey.startsWith('file://')) {
var file = File(imageKey);
if (await file.exists()) {
return file.readAsBytes();
imageBytes = await file.readAsBytes();
} else {
throw "Error: File not found.";
}
throw "Error: File not found.";
}
await for (var event
} else {
await for (var event
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes,
));
if (event.imageBytes != null) {
return event.imageBytes!;
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes,
));
if (event.imageBytes != null) {
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
@@ -49,4 +77,7 @@ class ReaderImageProvider
@override
String get key => "$imageKey@$sourceKey@$cid@$eid";
@override
bool get enableResize => true;
}

View File

@@ -58,6 +58,11 @@ class JsEngine with _JSEngineApi {
JsEngine().init();
}
void resetDio() {
_dio = AppDio(BaseOptions(
responseType: ResponseType.plain, validateStatus: (status) => true));
}
Future<void> init() async {
if (!_closed) {
return;
@@ -198,7 +203,8 @@ class JsEngine with _JSEngineApi {
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
},
);
dio.interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
dio.interceptors
.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
dio.interceptors.add(LogInterceptor());
}
response = await dio!.request(req["url"],

View File

@@ -76,15 +76,16 @@ class LocalComic with HistoryMixin implements Comic {
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
String get description => "";
@override
String get sourceKey => comicType == ComicType.local
? "local"
: comicType.sourceKey;
String get sourceKey =>
comicType == ComicType.local ? "local" : comicType.sourceKey;
@override
Map<String, dynamic> toJson() {
@@ -112,11 +113,12 @@ class LocalComic with HistoryMixin implements Comic {
chapters: chapters,
initialChapter: history?.ep,
initialPage: history?.page,
history: history ?? History.fromModel(
model: this,
ep: 0,
page: 0,
),
history: history ??
History.fromModel(
model: this,
ep: 0,
page: 0,
),
),
);
}
@@ -153,6 +155,15 @@ class LocalManager with ChangeNotifier {
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
Future<String?> setNewPath(String newPath) async {
var newDir = Directory(newPath);
@@ -167,13 +178,15 @@ class LocalManager with ChangeNotifier {
directory,
newDir,
);
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
await File(FilePath.join(App.dataPath, 'local_path'))
.writeAsString(newPath);
} catch (e, s) {
Log.error("IO", e, s);
return e.toString();
}
await directory.deleteContents(recursive: true);
path = newPath;
_checkNoMedia();
return null;
}
@@ -187,7 +200,8 @@ class LocalManager with ChangeNotifier {
}
} else if (App.isIOS) {
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;
} else {
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 {
_db = sqlite3.open(
'${App.dataPath}/local.db',
@@ -229,10 +255,11 @@ class LocalManager with ChangeNotifier {
if (!directory.existsSync()) {
await directory.create();
}
}
catch(e, s) {
} catch (e, s) {
Log.error("IO", "Failed to create local folder: $e", s);
}
_checkPathValidation();
_checkNoMedia();
restoreDownloadingTasks();
}
@@ -242,7 +269,8 @@ class LocalManager with ChangeNotifier {
SELECT id FROM comics WHERE comic_type = ?
ORDER BY CAST(id AS INTEGER) DESC
LIMIT 1;
''', [type.value],
''',
[type.value],
);
if (res.isEmpty) {
return '1';
@@ -352,15 +380,14 @@ class LocalManager with ChangeNotifier {
}
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";
}
var comic = find(id, type) ?? (throw "Comic Not Found");
var directory = Directory(comic.baseDir);
if (comic.chapters != null) {
var cid = ep is int
? comic.chapters!.keys.elementAt(ep - 1)
: (ep as String);
var cid =
ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String);
directory = Directory(FilePath.join(directory.path, cid));
}
var files = <File>[];
@@ -372,7 +399,7 @@ class LocalManager with ChangeNotifier {
continue;
}
//Hidden file in some file system
if(entity.name.startsWith('.')) {
if (entity.name.startsWith('.')) {
continue;
}
files.add(entity);
@@ -394,7 +421,7 @@ class LocalManager with ChangeNotifier {
if (comic == null) return false;
if (comic.chapters == null || ep == null) return true;
return comic.downloadedChapters
.contains(comic.chapters!.keys.elementAt(ep-1));
.contains(comic.chapters!.keys.elementAt(ep - 1));
}
List<DownloadTask> downloadingTasks = [];
@@ -451,12 +478,17 @@ class LocalManager with ChangeNotifier {
void restoreDownloadingTasks() {
var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json'));
if (file.existsSync()) {
var tasks = jsonDecode(file.readAsStringSync());
for (var e in tasks) {
var task = DownloadTask.fromJson(e);
if (task != null) {
downloadingTasks.add(task);
try {
var tasks = jsonDecode(file.readAsStringSync());
for (var e in tasks) {
var task = DownloadTask.fromJson(e);
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]) {
if(removeFileOnDisk) {
if (removeFileOnDisk) {
var dir = Directory(FilePath.join(path, c.directory));
dir.deleteIgnoreError(recursive: true);
}
//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) {
HistoryManager().remove(c.id, c.comicType);
}
var folders = LocalFavoritesManager().find(c.id, c.comicType);
for (var f in folders) {
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
// Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
if (c.comicType == ComicType.local) {
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) {
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
}
}
remove(c.id, c.comicType);
notifyListeners();
@@ -503,4 +537,4 @@ enum LocalSortType {
}
return name;
}
}
}

View File

@@ -6,23 +6,37 @@ import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/js_engine.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/cookie_jar.dart';
import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.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 {
await SAFTaskWorker().init();
await AppTranslation.init();
await appdata.init();
await App.init();
await HistoryManager().init();
await TagsTranslation.readData();
await LocalFavoritesManager().init();
await SAFTaskWorker().init().wait();
await AppTranslation.init().wait();
await appdata.init().wait();
await App.init().wait();
await HistoryManager().init().wait();
await TagsTranslation.readData().wait();
await LocalFavoritesManager().init().wait();
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
await JsEngine().init();
await ComicSource.init();
await LocalManager().init();
await JsEngine().init().wait();
await ComicSource.init().wait();
await LocalManager().init().wait();
CacheManager().setLimitSize(appdata.settings['cacheSize']);
}

View File

@@ -156,30 +156,40 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
home = const MainPage();
}
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();
light = ColorScheme.fromSeed(
seedColor: color,
surface: Colors.white,
);
dark = ColorScheme.fromSeed(
seedColor: color,
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(
home: home,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: light.copyWith(
surface: Colors.white,
),
colorScheme: light,
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
),
navigatorKey: App.rootNavigatorKey,
darkTheme: ThemeData(
colorScheme: dark.copyWith(
surface: Colors.black,
),
colorScheme: dark,
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
),
themeMode: switch (appdata.settings['theme_mode']) {
@@ -211,8 +221,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
],
builder: (context, widget) {
ErrorWidget.builder = (details) {
Log.error(
"Unhandled Exception", "${details.exception}\n${details.stack}");
Log.error("Unhandled Exception",
"${details.exception}\n${details.stack}");
return Material(
child: Center(
child: Text(details.exception.toString()),

View File

@@ -108,7 +108,6 @@ class MyLogInterceptor implements Interceptor {
class AppDio with DioMixin {
String? _proxy = proxy;
static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions();
@@ -116,9 +115,6 @@ class AppDio with DioMixin {
proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!),
tlsSettings: rhttp.TlsSettings(
verifyCertificates: !ignoreCertificateErrors,
),
));
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager());
@@ -196,9 +192,6 @@ class AppDio with DioMixin {
proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!),
tlsSettings: rhttp.TlsSettings(
verifyCertificates: !ignoreCertificateErrors,
),
));
}
try {
@@ -222,6 +215,22 @@ class AppDio with DioMixin {
class RHttpAdapter implements HttpClientAdapter {
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()]) {
settings = settings.copyWith(
redirectSettings: const rhttp.RedirectSettings.limited(5),
@@ -231,8 +240,9 @@ class RHttpAdapter implements HttpClientAdapter {
keepAlivePing: Duration(seconds: 30),
),
throwOnStatusCode: false,
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
tlsSettings: rhttp.TlsSettings(
verifyCertificates: !AppDio.ignoreCertificateErrors,
sni: appdata.settings['sni'] != false,
),
);
}

View File

@@ -146,14 +146,19 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
String? _cover;
/// All images to download, key is chapter name
Map<String, List<String>>? _images;
/// Downloaded image count
int _downloadedCount = 0;
/// Total image count
int _totalCount = 0;
/// Current downloading image index
int _index = 0;
/// Current downloading chapter, index of [_images]
int _chapter = 0;
var tasks = <int, _ImageDownloadWrapper>{};
@@ -180,10 +185,10 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
if (comic!.chapters != null) {
saveTo = Directory(FilePath.join(
path!,
comic!.chapters!.keys.elementAt(_chapter),
_images!.keys.elementAt(_chapter),
));
if (!saveTo.existsSync()) {
saveTo.createSync();
saveTo.createSync(recursive: true);
}
} else {
saveTo = Directory(path!);

View File

@@ -172,7 +172,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
isLiked = comic.isLiked ?? false;
isFavorite = comic.isFavorite ?? false;
if (comic.chapters == null) {
isDownloaded = await LocalManager().isDownloaded(
isDownloaded = LocalManager().isDownloaded(
comic.id,
comic.comicType,
0,
@@ -292,7 +292,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comicSource.commentsLoader != null)
_ActionButton(
icon: const Icon(Icons.comment),
text: (comic.commentsCount ?? 'Comments'.tl).toString(),
text: (comic.commentCount ?? 'Comments'.tl).toString(),
onPressed: showComments,
iconColor: context.useTextColor(Colors.green),
),
@@ -679,7 +679,7 @@ abstract mixin class _ComicPageActions {
return;
}
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);
return;
}

View File

@@ -111,13 +111,12 @@ class _BodyState extends State<_Body> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (App.isDesktop)
Tooltip(
message: "Edit".tl,
child: IconButton(
onPressed: () => edit(source),
icon: const Icon(Icons.edit_note)),
),
Tooltip(
message: "Edit".tl,
child: IconButton(
onPressed: () => edit(source),
icon: const Icon(Icons.edit_note)),
),
Tooltip(
message: "Update".tl,
child: IconButton(
@@ -163,6 +162,10 @@ class _BodyState extends State<_Body> {
break;
}
}
} else {
current = item.value['options']
.firstWhere((e) => e['value'] == current)['text'] ??
current;
}
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
@@ -246,27 +249,35 @@ class _BodyState extends State<_Body> {
}
void edit(ComicSource source) async {
try {
await Process.run("code", [source.filePath], runInShell: true);
await showDialog(
if (App.isDesktop) {
try {
await Process.run("code", [source.filePath], runInShell: true);
await showDialog(
context: App.rootContext,
builder: (context) => AlertDialog(
title: const Text("Reload Configs"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("cancel")),
TextButton(
onPressed: () async {
await ComicSource.reload();
App.forceRebuild();
},
child: const Text("continue")),
],
));
} catch (e) {
context.showMessage(message: "Failed to launch vscode");
title: const Text("Reload Configs"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("cancel")),
TextButton(
onPressed: () async {
await ComicSource.reload();
App.forceRebuild();
},
child: const Text("continue")),
],
),
);
return;
} catch (e) {
//
}
}
context.to(() => _EditFilePage(source.filePath)).then((value) async {
await ComicSource.reload();
setState(() {});
});
}
static Future<void> update(ComicSource source) async {
@@ -297,12 +308,14 @@ class _BodyState extends State<_Body> {
}
Widget buildCard(BuildContext context) {
Widget buildButton({required Widget child, required VoidCallback onPressed}) {
Widget buildButton(
{required Widget child, required VoidCallback onPressed}) {
return Button.normal(
onPressed: onPressed,
child: child,
).fixHeight(32);
}
return SliverToBoxAdapter(
child: SizedBox(
width: double.infinity,
@@ -558,3 +571,51 @@ void _addAllPagesWithComicSource(ComicSource source) {
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,
),
),
],
),
);
}
}

View File

@@ -295,6 +295,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
);
} else if (data.loadPage != null || data.loadNext != null) {
return ComicList(
enablePageStorage: true,
loadPage: data.loadPage,
loadNext: data.loadNext,
key: const PageStorageKey("comic_list"),

View File

@@ -77,7 +77,7 @@ String? validateFolderName(String newFolderName) {
return null;
}
void addFavorite(Comic comic) {
void addFavorite(List<Comic> comics) {
var folders = LocalFavoritesManager().folderNames;
showDialog(
@@ -105,19 +105,21 @@ void addFavorite(Comic comic) {
FilledButton(
onPressed: () {
if (selectedFolder != null) {
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle ?? '',
type: ComicType((comic.sourceKey == 'local'
? 0
: comic.sourceKey.hashCode)),
tags: comic.tags ?? [],
),
);
for (var comic in comics) {
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle ?? '',
type: ComicType((comic.sourceKey == 'local'
? 0
: comic.sourceKey.hashCode)),
tags: comic.tags ?? [],
),
);
}
context.pop();
}
},
@@ -144,6 +146,18 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
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(
id: c.id,
name: newInfo.title,
@@ -152,7 +166,7 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
newInfo.tags['author']?.firstOrNull ??
c.author,
type: c.type,
tags: c.tags,
tags: newTags,
);
LocalFavoritesManager().updateInfo(folder, comics[index]);

View File

@@ -14,6 +14,7 @@ import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/download.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/translations.dart';
@@ -152,14 +153,14 @@ class _FavoritesPageState extends State<FavoritesPage> {
);
}
if (!isNetwork) {
return _LocalFavoritesPage(folder: folder!, key: Key(folder!));
return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
} else {
var favoriteData = getFavoriteDataOrNull(folder!);
if (favoriteData == null) {
folder = null;
return buildBody();
} 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 updateFolders();
}
}

View File

@@ -102,10 +102,13 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
}
}
var scrollController = ScrollController();
@override
Widget build(BuildContext context) {
var body = Scaffold(
body: SmoothCustomScrollView(slivers: [
Widget body = SmoothCustomScrollView(
controller: scrollController,
slivers: [
if (!searchMode && !multiSelectMode)
SliverAppbar(
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
? (c) {
setState(() {
if (selectedComics.containsKey(c as FavoriteItem)) {
selectedComics.remove(c);
_checkExitSelectMode();
} else {
selectedComics[c] = true;
}
lastSelectedIndex = comics.indexOf(c);
});
onTap: (c) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as FavoriteItem)) {
selectedComics.remove(c);
_checkExitSelectMode();
} else {
selectedComics[c] = true;
}
: (c) {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
},
lastSelectedIndex = comics.indexOf(c);
});
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
} else {
App.mainNavigatorKey?.currentContext?.to(
() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
);
}
},
onLongPressed: (c) {
setState(() {
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(
canPop: !multiSelectMode && !searchMode,

View File

@@ -166,6 +166,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
),
];
},
enablePageStorage: true,
);
}
}
@@ -548,6 +549,7 @@ class _FavoriteFolder extends StatelessWidget {
Widget build(BuildContext context) {
return ComicList(
key: comicListKey,
enablePageStorage: true,
leadingSliver: SliverAppbar(
title: Text(title),
actions: [

View File

@@ -264,7 +264,8 @@ class _HistoryState extends State<_History> {
scrollDirection: Axis.horizontal,
itemCount: history.length,
itemBuilder: (context, index) {
return InkWell(
return AnimatedTapRegion(
borderRadius: 8,
onTap: () {
context.to(
() => ComicPage(
@@ -273,11 +274,9 @@ class _HistoryState extends State<_History> {
),
);
},
borderRadius: BorderRadius.circular(8),
child: Container(
width: 92,
height: 114,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context)
@@ -293,7 +292,7 @@ class _HistoryState extends State<_History> {
filterQuality: FilterQuality.medium,
),
),
);
).paddingHorizontal(8);
},
),
).paddingHorizontal(8).paddingBottom(16),
@@ -386,15 +385,14 @@ class _LocalState extends State<_Local> {
scrollDirection: Axis.horizontal,
itemCount: local.length,
itemBuilder: (context, index) {
return InkWell(
return AnimatedTapRegion(
onTap: () {
local[index].read();
},
borderRadius: BorderRadius.circular(8),
borderRadius: 8,
child: Container(
width: 92,
height: 114,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context)
@@ -412,7 +410,7 @@ class _LocalState extends State<_Local> {
filterQuality: FilterQuality.medium,
),
),
);
).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 directories.".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
][type];
List<String> importMethods = [
"Single Comic".tl,
"Multiple Comics".tl,
"A cbz file".tl,
"Multiple cbz files".tl,
"EhViewer downloads".tl
];
@@ -630,7 +630,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
0 => await importer.directory(true),
1 => await importer.directory(false),
2 => await importer.cbz(),
3 => await importer.ehViewer(),
3 => await importer.multipleCbz(),
4 => await importer.ehViewer(),
int() => true,
};
if(result) {

View File

@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/local.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/favorites/favorites_page.dart';
import 'package:venera/utils/cbz.dart';
import 'package:venera/utils/epub.dart';
import 'package:venera/utils/io.dart';
@@ -30,7 +31,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool multiSelectMode = false;
Map<Comic, bool> selectedComics = {};
Map<LocalComic, bool> selectedComics = {};
void update() {
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
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 = [
IconButton(
icon: const Icon(Icons.select_all),
@@ -172,78 +193,66 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: const Icon(Icons.flip),
tooltip: "Invert Selection".tl,
onPressed: invertSelection),
IconButton(
icon: const Icon(Icons.border_horizontal_outlined),
tooltip: "Select in range".tl,
onPressed: selectRange),
buildMultiSelectMenu(),
];
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(
body: SmoothCustomScrollView(
slivers: [
if (!searchMode && !multiSelectMode)
SliverAppbar(
title: Text("Local".tl),
actions: [
Tooltip(
message: "Search".tl,
child: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
searchMode = true;
});
},
),
),
Tooltip(
message: "Sort".tl,
child: IconButton(
icon: const Icon(Icons.sort),
onPressed: sort,
),
),
Tooltip(
message: "Downloading".tl,
child: IconButton(
icon: const Icon(Icons.download),
onPressed: () {
showPopUpWidget(context, const DownloadingPage());
},
),
),
Tooltip(
message: multiSelectMode
? "Exit Multi-Select".tl
: "Multi-Select".tl,
child: IconButton(
icon: const Icon(Icons.checklist),
onPressed: () {
setState(() {
multiSelectMode = !multiSelectMode;
});
},
),
),
],
)
else if (multiSelectMode)
if (!searchMode)
SliverAppbar(
leading: Tooltip(
message: "Cancel".tl,
message: multiSelectMode ? "Cancel".tl : "Back".tl,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
if (multiSelectMode) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
} else {
context.pop();
}
},
icon: multiSelectMode
? const Icon(Icons.close)
: const Icon(Icons.arrow_back),
),
),
title: Text(
"Selected @c comics".tlParams({"c": selectedComics.length})),
actions: selectActions,
title: multiSelectMode
? Text(selectedComics.length.toString())
: Text("Local".tl),
actions: multiSelectMode ? selectActions : normalActions,
)
else if (searchMode)
SliverAppbar(
@@ -275,149 +284,45 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
SliverGridComics(
comics: comics,
selections: selectedComics,
onTap: multiSelectMode
? (c) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c);
} else {
selectedComics[c] = true;
}
});
onLongPressed: (c) {
setState(() {
multiSelectMode = true;
selectedComics[c as LocalComic] = true;
});
},
onTap: (c) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c);
} else {
selectedComics[c] = true;
}
: (c) {
(c as LocalComic).read();
},
if (selectedComics.isEmpty) {
multiSelectMode = false;
}
});
} else {
(c as LocalComic).read();
}
},
menuBuilder: (c) {
return [
MenuEntry(
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
showDialog(
context: context,
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();
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());
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
deleteComics([c as LocalComic]).then((value) {
if (value && multiSelectMode) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
}
controller.close();
}),
if (!multiSelectMode)
MenuEntry(
icon: Icons.picture_as_pdf_outlined,
text: "Export as pdf".tl,
onClick: () async {
var cache = FilePath.join(App.cachePath, 'temp.pdf');
var controller = showLoadingDialog(
context,
allowCancel: false,
);
try {
await createPdfFromComicIsolate(
comic: c as LocalComic,
savePath: cache,
);
await saveFile(
file: File(cache),
filename: "${c.title}.pdf",
);
} catch (e, s) {
Log.error("PDF Export", e, s);
context.showMessage(message: e.toString());
} finally {
controller.close();
File(cache).deleteIgnoreError();
}
},
),
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();
}
},
)
});
},
),
...exportActions(c as LocalComic),
];
},
),
@@ -444,4 +349,120 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
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();
}
},
)
];
}
}

View File

@@ -62,9 +62,7 @@ class _MainPageState extends State<MainPage> {
}
final _pages = [
const HomePage(
key: PageStorageKey('home'),
),
const HomePage(),
const FavoritesPage(
key: PageStorageKey('favorites'),
),

View File

@@ -25,7 +25,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
if (inProgress) return;
inProgress = true;
if (reader.type == ComicType.local ||
(await LocalManager()
(LocalManager()
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
try {
var images = await LocalManager()
@@ -356,6 +356,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
var isCTRLPressed = false;
static var _isMouseScrolling = false;
var fingers = 0;
bool disableScroll = false;
@override
void initState() {
@@ -426,7 +427,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
? Axis.vertical
: Axis.horizontal,
reverse: reader.mode == ReaderMode.continuousRightToLeft,
physics: isCTRLPressed || _isMouseScrolling
physics: isCTRLPressed || _isMouseScrolling || disableScroll
? const NeverScrollableScrollPhysics()
: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
@@ -460,6 +461,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
widget = Listener(
onPointerDown: (event) {
fingers++;
if(fingers > 1 && !disableScroll) {
setState(() {
disableScroll = true;
});
}
futurePosition = null;
if (_isMouseScrolling) {
setState(() {
@@ -469,6 +475,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
},
onPointerUp: (event) {
fingers--;
if(fingers <= 1 && disableScroll) {
setState(() {
disableScroll = false;
});
}
},
onPointerPanZoomUpdate: (event) {
if (event.scale == 1.0) {
@@ -651,7 +662,7 @@ ImageProvider _createImageProviderFromKey(
var reader = context.reader;
return ReaderImageProvider(
imageKey,
reader.type.comicSource!.key,
reader.type.comicSource?.key,
reader.cid,
reader.eid,
);

View 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,
});
}

View File

@@ -18,11 +18,13 @@ import 'package:venera/components/custom_slider.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/image_provider/reader_image.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/file_type.dart';
@@ -36,6 +38,7 @@ part 'scaffold.dart';
part 'images.dart';
part 'gesture.dart';
part 'comic_image.dart';
part 'loading.dart';
extension _ReaderContext on BuildContext {
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
@@ -209,7 +212,11 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
if(history != null) {
history!.page = page;
history!.ep = chapter;
if (maxPage > 1) {
history!.maxPage = maxPage;
}
history!.readEpisode.add(chapter);
history!.time = DateTime.now();
HistoryManager().addHistory(history!);
}
}

View File

@@ -456,58 +456,63 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
var imagesOnScreen =
continuesState.itemPositionsListener.itemPositions.value;
var images = imagesOnScreen
.map((e) => context.reader.images![e.index - 1])
.map((e) => context.reader.images!.elementAtOrNull(e.index - 1))
.whereType<String>()
.toList();
String? selected;
await showPopUpWidget(
context,
PopUpWidgetScaffold(
title: "Select an image on screen".tl,
body: GridView.builder(
itemCount: images.length,
itemBuilder: (context, index) {
ImageProvider image;
var imageKey = images[index];
if (imageKey.startsWith('file://')) {
image = FileImage(File(imageKey.replaceFirst("file://", '')));
} else {
image = ReaderImageProvider(
imageKey,
reader.type.comicSource!.key,
reader.cid,
reader.eid,
);
}
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
selected = images[index];
App.rootContext.pop();
},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
if (images.length > 1) {
await showPopUpWidget(
context,
PopUpWidgetScaffold(
title: "Select an image on screen".tl,
body: GridView.builder(
itemCount: images.length,
itemBuilder: (context, index) {
ImageProvider image;
var imageKey = images[index];
if (imageKey.startsWith('file://')) {
image = FileImage(File(imageKey.replaceFirst("file://", '')));
} else {
image = ReaderImageProvider(
imageKey,
reader.type.comicSource!.key,
reader.cid,
reader.eid,
);
}
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
selected = images[index];
App.rootContext.pop();
},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
),
),
width: double.infinity,
height: double.infinity,
child: Image(
width: double.infinity,
height: double.infinity,
image: image,
child: Image(
width: double.infinity,
height: double.infinity,
image: image,
),
),
),
).padding(const EdgeInsets.all(8));
},
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 0.7,
).padding(const EdgeInsets.all(8));
},
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 0.7,
),
),
),
),
);
);
} else {
selected = images.first;
}
if (selected == null) {
return null;
} else {

View File

@@ -139,7 +139,9 @@ class _SearchPageState extends State<SearchPage> {
@override
void initState() {
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
if (defaultSearchTarget != null &&
if (defaultSearchTarget == "_aggregated_") {
aggregatedSearch = true;
} else if (defaultSearchTarget != null &&
ComicSource.find(defaultSearchTarget) != null) {
searchTarget = defaultSearchTarget;
} else {

View File

@@ -45,8 +45,9 @@ class _SearchResultPageState extends State<SearchResultPage> {
if (suggestionsController.entry != null) {
suggestionsController.remove();
}
text = checkAutoLanguage(text);
setState(() {
this.text = text;
this.text = text!;
});
appdata.addSearchHistory(text);
controller.currentText = text;
@@ -92,13 +93,33 @@ class _SearchResultPageState extends State<SearchResultPage> {
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
void initState() {
sourceKey = widget.sourceKey;
controller = SearchBarController(
currentText: widget.text,
currentText: checkAutoLanguage(widget.text),
onSearch: search,
);
sourceKey = widget.sourceKey;
options = widget.options ?? const [];
validateOptions();
text = widget.text;
@@ -162,6 +183,12 @@ class _SearchResultPageState extends State<SearchResultPage> {
child: IconButton(
icon: const Icon(Icons.tune),
onPressed: () async {
if (suggestionOverlay != null) {
suggestionsController.remove();
}
var previousOptions = options;
var previousSourceKey = sourceKey;
await showDialog(
context: context,
useRootNavigator: true,
@@ -169,7 +196,11 @@ class _SearchResultPageState extends State<SearchResultPage> {
return _SearchSettingsDialog(state: this);
},
);
setState(() {});
if (previousOptions != options || previousSourceKey != sourceKey) {
text = checkAutoLanguage(controller.text);
controller.currentText = text;
setState(() {});
}
},
),
);

View File

@@ -61,6 +61,10 @@ class _AboutSettingsState extends State<AboutSettings> {
},
).fixHeight(32),
).toSliver(),
_SwitchSetting(
title: "Check for updates on startup".tl,
settingKey: "checkUpdateOnStart",
).toSliver(),
ListTile(
title: const Text("Github"),
trailing: const Icon(Icons.open_in_new),
@@ -102,7 +106,9 @@ Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
return ContentDialog(
title: "New version available".tl,
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: [
Button.text(
onPressed: () {

View File

@@ -88,6 +88,30 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Keyword blocking".tl,
builder: () => const _ManageBlockingWordView(),
).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,
),
onChanged: (s) {
if(error != null){
if (error != null) {
setState(() {
error = null;
});
@@ -160,7 +184,8 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
actions: [
Button.filled(
onPressed: () {
if(appdata.settings["blockedWords"].contains(controller.text)){
if (appdata.settings["blockedWords"]
.contains(controller.text)) {
setState(() {
error = "Keyword already exists".tl;
});

View File

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

View File

@@ -17,6 +17,10 @@ class _NetworkSettingsState extends State<NetworkSettings> {
title: "Proxy".tl,
builder: () => const _ProxySettingView(),
).toSliver(),
_PopupWindowSetting(
title: "DNS Overrides".tl,
builder: () => const _DNSOverrides(),
).toSliver(),
_SliderSetting(
title: "Download Threads".tl,
settingsIndex: 'downloadThreads',
@@ -42,7 +46,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
String port = '';
String username = '';
String password = '';
bool ignoreCertificateErrors = false;
// USERNAME:PASSWORD@HOST:PORT
String toProxyStr() {
@@ -100,7 +103,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
void initState() {
var proxy = appdata.settings['proxy'];
parseProxyString(proxy);
ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
super.initState();
}
@@ -146,17 +148,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
},
),
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);
}
}
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);
});
},
),
],
),
);
}
}

View File

@@ -61,9 +61,17 @@ class _ReaderSettingsState extends State<ReaderSettings> {
).toSliver(),
SliverToBoxAdapter(
child: AbsorbPointer(
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
absorbing: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false),
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),
child: _SliderSetting(
title: "The number of pic in screen (Only Gallery Mode)".tl,
@@ -93,7 +101,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call('limitImageWidth');
},
).toSliver(),
if(App.isAndroid)
if (App.isAndroid)
_SwitchSetting(
title: 'Turn page by volume keys'.tl,
settingKey: 'enableTurnPageByVolumeKey',
@@ -108,7 +116,67 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
},
).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;
},
),
),
),
)
],
),
);
}
}

View File

@@ -12,6 +12,7 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/js_engine.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';

View File

@@ -8,6 +8,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart';
@@ -83,6 +84,33 @@ class _AppWebviewState extends State<AppWebview> {
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
Widget build(BuildContext context) {
final actions = [
@@ -121,20 +149,17 @@ class _AppWebviewState extends State<AppWebview> {
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
? FutureBuilder(
future: WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(
userDataFolder: "${App.dataPath}\\webview",
),
),
future: future,
builder: (context, e) {
if(e.error != null) {
if (e.error != null) {
return Center(child: Text("Error: ${e.error}"));
}
if(e.data == null) {
if (e.data == null) {
return const Center(child: CircularProgressIndicator());
}
AppWebview.webViewEnvironment = e.data;
return createWebviewWithEnvironment(AppWebview.webViewEnvironment);
return createWebviewWithEnvironment(
AppWebview.webViewEnvironment);
},
)
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_type.dart';
@@ -63,7 +62,7 @@ abstract class CBZ {
var cache = Directory(FilePath.join(App.cachePath, 'cbz_import'));
if (cache.existsSync()) cache.deleteSync(recursive: true);
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'));
ComicMetaData? metaData;
if (metaDataFile.existsSync()) {
@@ -208,13 +207,14 @@ abstract class CBZ {
).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);
cache.deleteSync(recursive: true);
return cbz;
}
static _compress(String src, String dst) async {
await Isolate.run(() => ZipFile.compressFolder(src, dst));
await ZipFile.compressFolderAsync(src, dst, 4);
}
}

View File

@@ -80,15 +80,9 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
LocalFavoritesManager().init();
}
if (await appdataFile.exists()) {
// proxy settings & authorization setting should be kept
var proxySettings = appdata.settings["proxy"];
var authSettings = appdata.settings["authorizationRequired"];
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
await appdata.init();
appdata.settings["proxy"] = proxySettings;
appdata.settings["authorizationRequired"] = authSettings;
appdata.saveData();
var content = await appdataFile.readAsString();
var data = jsonDecode(content);
appdata.syncData(data);
}
if (await cookieFile.exists()) {
SingleInstanceCookieJar.instance?.dispose();

View File

@@ -99,7 +99,7 @@ class DataSync with ChangeNotifier {
try {
appdata.settings['dataVersion']++;
await appdata.saveData();
await appdata.saveData(false);
var data = await exportAppData();
var time =
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();

View File

@@ -37,6 +37,33 @@ class ImportComic {
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 {
var dbFile = await selectFile(ext: ['db']);
final picker = DirectoryPicker();

View File

@@ -72,6 +72,7 @@ extension FileSystemEntityExt on FileSystemEntity {
}
extension FileExtension on File {
/// Get the file extension, not including the dot.
String get extension => path.split('.').last;
/// Copy the file to the specified path using memory.
@@ -196,7 +197,7 @@ class DirectoryPicker {
static const _methodChannel = MethodChannel("venera/method_channel");
Future<Directory?> pickDirectory() async {
Future<Directory?> pickDirectory({bool directAccess = false}) async {
IO._isSelectingFiles = true;
try {
String? directory;
@@ -204,6 +205,16 @@ class DirectoryPicker {
directory = await file_selector.getDirectoryPath();
} else if (App.isAndroid) {
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 {
// ios, macos
directory =

43
macos/Podfile Normal file
View File

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

View File

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

View File

@@ -5,10 +5,10 @@ packages:
dependency: "direct main"
description:
name: app_links
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
version: "6.3.3"
app_links_linux:
dependency: transitive
description:
@@ -69,10 +69,10 @@ packages:
dependency: "direct main"
description:
name: battery_plus
sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7"
sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "6.2.1"
battery_plus_platform_interface:
dependency: transitive
description:
@@ -408,8 +408,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: ade0b9d
resolved-ref: ade0b9d67331118c13a2b836684858e251512373
ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
url: "https://github.com/wgh136/flutter_qjs"
source: git
version: "0.3.7"
@@ -425,16 +425,16 @@ packages:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d
sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.7.0"
flutter_saf:
dependency: "direct main"
description:
path: "."
ref: "3315082b9f7055655610e4f6f136b69e48228c05"
resolved-ref: "3315082b9f7055655610e4f6f136b69e48228c05"
ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
resolved-ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
url: "https://github.com/pkuislm/flutter_saf.git"
source: git
version: "0.0.1"
@@ -446,12 +446,11 @@ packages:
flutter_to_arch:
dependency: "direct dev"
description:
path: "."
ref: HEAD
resolved-ref: "15bfead0380fda79b0256b37c73b886b0882f1bf"
url: "https://github.com/wgh136/flutter_to_arch"
source: git
version: "1.0.0"
name: flutter_to_arch
sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flutter_to_debian:
dependency: "direct dev"
description:
@@ -629,8 +628,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
resolved-ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
url: "https://github.com/venera-app/lodepng_flutter"
source: git
version: "0.0.1"
@@ -799,10 +798,10 @@ packages:
dependency: "direct main"
description:
name: rhttp
sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5"
sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e"
url: "https://pub.dev"
source: hosted
version: "0.9.6"
version: "0.9.8"
screen_retriever:
dependency: transitive
description:
@@ -856,18 +855,18 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8"
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
url: "https://pub.dev"
source: hosted
version: "10.1.2"
version: "10.1.3"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "5.0.2"
shimmer:
dependency: "direct main"
description:
@@ -917,10 +916,10 @@ packages:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9"
sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785"
url: "https://pub.dev"
source: hosted
version: "0.5.27"
version: "0.5.28"
stack_trace:
dependency: transitive
description:
@@ -945,6 +944,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -961,6 +968,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -1125,12 +1140,11 @@ packages:
zip_flutter:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: d5721f1fd8179ee4a5db59f932ae7c89d94e12a0
url: "https://github.com/wgh136/zip_flutter"
source: git
version: "0.0.1"
name: zip_flutter
sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646
url: "https://pub.dev"
source: hosted
version: "0.0.6"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.27.1"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.1.0+110
version: 1.1.4+114
environment:
sdk: '>=3.6.0 <4.0.0'
flutter: 3.27.0
flutter: 3.27.1
dependencies:
flutter:
@@ -17,11 +17,11 @@ dependencies:
intl: ^0.19.0
window_manager: ^0.4.3
sqlite3: ^2.4.7
sqlite3_flutter_libs: any
sqlite3_flutter_libs: ^0.5.28
flutter_qjs:
git:
url: https://github.com/wgh136/flutter_qjs
ref: ade0b9d
ref: 9c99ac258a11f8e91761a5466a190efba3ca64af
crypto: ^3.0.6
dio: ^5.7.0
html: ^0.15.5
@@ -33,7 +33,7 @@ dependencies:
url: https://github.com/wgh136/photo_view
ref: 94724a0b
mime: ^2.0.0
share_plus: ^10.0.2
share_plus: ^10.1.3
scrollable_positioned_list:
git:
url: https://github.com/venera-app/flutter.widgets
@@ -47,39 +47,38 @@ dependencies:
url: https://github.com/wgh136/flutter_desktop_webview
path: packages/desktop_webview_window
flutter_inappwebview: ^6.1.5
app_links: ^6.3.2
app_links: ^6.3.3
sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3
zip_flutter:
git:
url: https://github.com/wgh136/zip_flutter
zip_flutter: ^0.0.6
lodepng_flutter:
git:
url: https://github.com/venera-app/lodepng_flutter
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
rhttp: 0.9.6
ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
rhttp: 0.9.8
webdav_client:
git:
url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
battery_plus: ^6.2.0
battery_plus: ^6.2.1
local_auth: ^2.3.0
flutter_saf:
git:
url: https://github.com/pkuislm/flutter_saf.git
ref: 3315082b9f7055655610e4f6f136b69e48228c05
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
pdf: ^3.11.1
dynamic_color: ^1.7.0
shimmer: ^3.0.0
flutter_memory_info: ^0.0.1
syntax_highlight: ^0.4.0
text_scroll: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_to_arch:
git: https://github.com/wgh136/flutter_to_arch
flutter_to_arch: ^1.0.1
flutter_to_debian:
flutter:
@@ -99,4 +98,4 @@ flutter_to_arch:
url: https://github.com/venera-app/venera
depends:
- gtk3
- webkit2gtk-4.1
- webkit2gtk-4.1

View File

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