mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
v1.2.1
This commit is contained in:
101
assets/init.js
101
assets/init.js
@@ -1205,6 +1205,10 @@ class Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI related apis
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
let UI = {
|
let UI = {
|
||||||
/**
|
/**
|
||||||
* Show a message
|
* Show a message
|
||||||
@@ -1222,7 +1226,9 @@ let UI = {
|
|||||||
* Show a dialog. Any action will close the dialog.
|
* Show a dialog. Any action will close the dialog.
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param content {string}
|
* @param content {string}
|
||||||
* @param actions {{text:string, callback: () => void}[]}
|
* @param actions {{text:string, callback: () => void | Promise<void>, style: "text"|"filled"|"danger"}[]} - If callback returns a promise, the button will show a loading indicator until the promise is resolved.
|
||||||
|
* @returns {Promise<void>} - Resolved when the dialog is closed.
|
||||||
|
* @since 1.2.1
|
||||||
*/
|
*/
|
||||||
showDialog: (title, content, actions) => {
|
showDialog: (title, content, actions) => {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
@@ -1245,4 +1251,97 @@ let UI = {
|
|||||||
url: url,
|
url: url,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a loading dialog.
|
||||||
|
* @param onCancel {() => void | null | undefined} - Called when the loading dialog is canceled. If [onCancel] is null, the dialog cannot be canceled by the user.
|
||||||
|
* @returns {number} - A number that can be used to cancel the loading dialog.
|
||||||
|
* @since 1.2.1
|
||||||
|
*/
|
||||||
|
showLoading: (onCancel) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'UI',
|
||||||
|
function: 'showLoading',
|
||||||
|
onCancel: onCancel
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a loading dialog.
|
||||||
|
* @param id {number} - returned by [showLoading]
|
||||||
|
* @since 1.2.1
|
||||||
|
*/
|
||||||
|
cancelLoading: (id) => {
|
||||||
|
sendMessage({
|
||||||
|
method: 'UI',
|
||||||
|
function: 'cancelLoading',
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an input dialog
|
||||||
|
* @param title {string}
|
||||||
|
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
||||||
|
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
||||||
|
*/
|
||||||
|
showInputDialog: (title, validator) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'UI',
|
||||||
|
function: 'showInputDialog',
|
||||||
|
title: title,
|
||||||
|
validator: validator
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a select dialog
|
||||||
|
* @param title {string}
|
||||||
|
* @param options {string[]}
|
||||||
|
* @param initialIndex {number?}
|
||||||
|
* @returns {Promise<number | null>} - The selected index. If the dialog is canceled, return null.
|
||||||
|
*/
|
||||||
|
showSelectDialog: (title, options, initialIndex) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'UI',
|
||||||
|
function: 'showSelectDialog',
|
||||||
|
title: title,
|
||||||
|
options: options,
|
||||||
|
initialIndex: initialIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App related apis
|
||||||
|
* @since 1.2.1
|
||||||
|
*/
|
||||||
|
let APP = {
|
||||||
|
/**
|
||||||
|
* Get the app version
|
||||||
|
* @returns {string} - The app version
|
||||||
|
*/
|
||||||
|
get version() {
|
||||||
|
return appVersion // defined in the engine
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current app locale
|
||||||
|
* @returns {string} - The app locale, in the format of [languageCode]_[countryCode]
|
||||||
|
*/
|
||||||
|
get locale() {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'getLocale'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current running platform
|
||||||
|
* @returns {string} - The platform name, "android", "ios", "windows", "macos", "linux"
|
||||||
|
*/
|
||||||
|
get platform() {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'getPlatform'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
@@ -148,11 +148,6 @@
|
|||||||
"Size in MB": "大小(MB)",
|
"Size in MB": "大小(MB)",
|
||||||
"Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录",
|
"Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录",
|
||||||
"Help": "帮助",
|
"Help": "帮助",
|
||||||
"A directory is considered as a comic only if it matches one of the following conditions:" : "只有当目录满足以下条件之一时,才被视为漫画:",
|
|
||||||
"1. The directory only contains image files." : "1. 目录只包含图片文件。",
|
|
||||||
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
|
|
||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
|
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
|
|
||||||
"Export as cbz": "导出为cbz",
|
"Export as cbz": "导出为cbz",
|
||||||
"Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)",
|
"Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)",
|
||||||
"An archive file" : "一个归档文件",
|
"An archive file" : "一个归档文件",
|
||||||
@@ -318,7 +313,8 @@
|
|||||||
"Imported @a comics": "已导入 @a 本漫画",
|
"Imported @a comics": "已导入 @a 本漫画",
|
||||||
"New Version": "新版本",
|
"New Version": "新版本",
|
||||||
"@c updates": "@c 项更新",
|
"@c updates": "@c 项更新",
|
||||||
"No updates": "无更新"
|
"No updates": "无更新",
|
||||||
|
"Set comic source list url": "设置漫画源列表URL"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -468,11 +464,6 @@
|
|||||||
"Size in MB": "大小(MB)",
|
"Size in MB": "大小(MB)",
|
||||||
"Select a directory which contains the comic directories." : "選擇一個包含漫畫文件夾的目錄",
|
"Select a directory which contains the comic directories." : "選擇一個包含漫畫文件夾的目錄",
|
||||||
"Help": "幫助",
|
"Help": "幫助",
|
||||||
"A directory is considered as a comic only if it matches one of the following conditions:" : "只有當目錄滿足以下條件之一時,才被視為漫畫:",
|
|
||||||
"1. The directory only contains image files." : "1. 目錄只包含圖片文件。",
|
|
||||||
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
|
|
||||||
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
|
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
|
|
||||||
"Export as cbz": "匯出為cbz",
|
"Export as cbz": "匯出為cbz",
|
||||||
"Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)",
|
"Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)",
|
||||||
"An archive file" : "一個歸檔文件",
|
"An archive file" : "一個歸檔文件",
|
||||||
@@ -639,6 +630,7 @@
|
|||||||
"Imported @a comics": "已匯入 @a 部漫畫",
|
"Imported @a comics": "已匯入 @a 部漫畫",
|
||||||
"New Version": "新版本",
|
"New Version": "新版本",
|
||||||
"@c updates": "@c 項更新",
|
"@c updates": "@c 項更新",
|
||||||
"No updates": "無更新"
|
"No updates": "無更新",
|
||||||
|
"Set comic source list url": "設置漫畫源列表URL"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '15.0'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
242
lib/components/js_ui.dart
Normal file
242
lib/components/js_ui.dart
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
|
|
||||||
|
import 'components.dart';
|
||||||
|
|
||||||
|
mixin class JsUiApi {
|
||||||
|
final Map<int, LoadingDialogController> _loadingDialogControllers = {};
|
||||||
|
|
||||||
|
dynamic handleUIMessage(Map<String, dynamic> message) {
|
||||||
|
switch (message['function']) {
|
||||||
|
case 'showMessage':
|
||||||
|
var m = message['message'];
|
||||||
|
if (m.toString().isNotEmpty) {
|
||||||
|
App.rootContext.showMessage(message: m.toString());
|
||||||
|
}
|
||||||
|
case 'showDialog':
|
||||||
|
return _showDialog(message);
|
||||||
|
case 'launchUrl':
|
||||||
|
var url = message['url'];
|
||||||
|
if (url.toString().isNotEmpty) {
|
||||||
|
launchUrlString(url.toString());
|
||||||
|
}
|
||||||
|
case 'showLoading':
|
||||||
|
var onCancel = message['onCancel'];
|
||||||
|
if (onCancel != null && onCancel is! JSInvokable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return _showLoading(onCancel);
|
||||||
|
case 'cancelLoading':
|
||||||
|
var id = message['id'];
|
||||||
|
if (id is int) {
|
||||||
|
_cancelLoading(id);
|
||||||
|
}
|
||||||
|
case 'showInputDialog':
|
||||||
|
var title = message['title'];
|
||||||
|
var validator = message['validator'];
|
||||||
|
if (title is! String) return;
|
||||||
|
if (validator != null && validator is! JSInvokable) return;
|
||||||
|
return _showInputDialog(title, validator);
|
||||||
|
case 'showSelectDialog':
|
||||||
|
var title = message['title'];
|
||||||
|
var options = message['options'];
|
||||||
|
var initialIndex = message['initialIndex'];
|
||||||
|
if (title is! String) return;
|
||||||
|
if (options is! List) return;
|
||||||
|
if (initialIndex != null && initialIndex is! int) return;
|
||||||
|
return _showSelectDialog(
|
||||||
|
title,
|
||||||
|
options.whereType<String>().toList(),
|
||||||
|
initialIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDialog(Map<String, dynamic> message) {
|
||||||
|
BuildContext? dialogContext;
|
||||||
|
var title = message['title'];
|
||||||
|
var content = message['content'];
|
||||||
|
var actions = <Widget>[];
|
||||||
|
for (var action in message['actions']) {
|
||||||
|
if (action['callback'] is! JSInvokable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var callback = action['callback'] as JSInvokable;
|
||||||
|
var text = action['text'].toString();
|
||||||
|
var style = (action['style'] ?? 'text').toString();
|
||||||
|
actions.add(_JSCallbackButton(
|
||||||
|
text: text,
|
||||||
|
callback: JSAutoFreeFunction(callback),
|
||||||
|
style: style,
|
||||||
|
onCallbackFinished: () {
|
||||||
|
dialogContext?.pop();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (actions.isEmpty) {
|
||||||
|
actions.add(TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
dialogContext?.pop();
|
||||||
|
},
|
||||||
|
child: Text('OK'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (context) {
|
||||||
|
dialogContext = context;
|
||||||
|
return ContentDialog(
|
||||||
|
title: title,
|
||||||
|
content: Text(content).paddingHorizontal(16),
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
dialogContext = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int _showLoading(JSInvokable? onCancel) {
|
||||||
|
var func = onCancel == null ? null : JSAutoFreeFunction(onCancel);
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
barrierDismissible: onCancel != null,
|
||||||
|
allowCancel: onCancel != null,
|
||||||
|
onCancel: onCancel == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
func?.call([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
var i = 0;
|
||||||
|
while (_loadingDialogControllers.containsKey(i)) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
_loadingDialogControllers[i] = controller;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelLoading(int id) {
|
||||||
|
var controller = _loadingDialogControllers.remove(id);
|
||||||
|
controller?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _showInputDialog(String title, JSInvokable? validator) async {
|
||||||
|
String? result;
|
||||||
|
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
||||||
|
await showInputDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
title: title,
|
||||||
|
onConfirm: (v) {
|
||||||
|
if (func != null) {
|
||||||
|
var res = func.call([v]);
|
||||||
|
if (res != null) {
|
||||||
|
return res.toString();
|
||||||
|
} else {
|
||||||
|
result = v;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> _showSelectDialog(
|
||||||
|
String title,
|
||||||
|
List<String> options,
|
||||||
|
int? initialIndex,
|
||||||
|
) {
|
||||||
|
if (options.isEmpty) {
|
||||||
|
return Future.value(null);
|
||||||
|
}
|
||||||
|
if (initialIndex != null &&
|
||||||
|
(initialIndex >= options.length || initialIndex < 0)) {
|
||||||
|
initialIndex = null;
|
||||||
|
}
|
||||||
|
return showSelectDialog(
|
||||||
|
title: title,
|
||||||
|
options: options,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JSCallbackButton extends StatefulWidget {
|
||||||
|
const _JSCallbackButton({
|
||||||
|
required this.text,
|
||||||
|
required this.callback,
|
||||||
|
required this.style,
|
||||||
|
this.onCallbackFinished,
|
||||||
|
});
|
||||||
|
|
||||||
|
final JSAutoFreeFunction callback;
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
final String style;
|
||||||
|
|
||||||
|
final void Function()? onCallbackFinished;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_JSCallbackButton> createState() => _JSCallbackButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JSCallbackButtonState extends State<_JSCallbackButton> {
|
||||||
|
bool isLoading = false;
|
||||||
|
|
||||||
|
void onClick() async {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var res = widget.callback.call([]);
|
||||||
|
if (res is Future) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
await res;
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
widget.onCallbackFinished?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return switch (widget.style) {
|
||||||
|
"filled" => FilledButton(
|
||||||
|
onPressed: onClick,
|
||||||
|
child: isLoading
|
||||||
|
? CircularProgressIndicator(strokeWidth: 1.4)
|
||||||
|
.fixWidth(18)
|
||||||
|
.fixHeight(18)
|
||||||
|
: Text(widget.text),
|
||||||
|
),
|
||||||
|
"danger" => FilledButton(
|
||||||
|
onPressed: onClick,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(context.colorScheme.error),
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? CircularProgressIndicator(strokeWidth: 1.4)
|
||||||
|
.fixWidth(18)
|
||||||
|
.fixHeight(18)
|
||||||
|
: Text(widget.text),
|
||||||
|
),
|
||||||
|
_ => TextButton(
|
||||||
|
onPressed: onClick,
|
||||||
|
child: isLoading
|
||||||
|
? CircularProgressIndicator(strokeWidth: 1.4)
|
||||||
|
.fixWidth(18)
|
||||||
|
.fixHeight(18)
|
||||||
|
: Text(widget.text),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -402,3 +402,59 @@ void showInfoDialog({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int?> showSelectDialog({
|
||||||
|
required String title,
|
||||||
|
required List<String> options,
|
||||||
|
int? initialIndex,
|
||||||
|
}) async {
|
||||||
|
int? current = initialIndex;
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: title,
|
||||||
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Select(
|
||||||
|
current: current == null ? "" : options[current!],
|
||||||
|
values: options,
|
||||||
|
minWidth: 156,
|
||||||
|
onTap: (i) {
|
||||||
|
setState(() {
|
||||||
|
current = i;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
current = null;
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text('Cancel'.tl),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: current == null
|
||||||
|
? null
|
||||||
|
: context.pop,
|
||||||
|
child: Text('Confirm'.tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.2.0";
|
final version = "1.2.1";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -90,13 +90,15 @@ class _Appdata {
|
|||||||
|
|
||||||
/// Sync data from another device
|
/// Sync data from another device
|
||||||
void syncData(Map<String, dynamic> data) {
|
void syncData(Map<String, dynamic> data) {
|
||||||
for (var key in data.keys) {
|
if (data['settings'] is Map) {
|
||||||
if (_disableSync.contains(key)) {
|
var settings = data['settings'] as Map<String, dynamic>;
|
||||||
continue;
|
for (var key in settings.keys) {
|
||||||
|
if (!_disableSync.contains(key)) {
|
||||||
|
this.settings[key] = settings[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settings[key] = data[key];
|
|
||||||
}
|
}
|
||||||
searchHistory = List.from(data['searchHistory']);
|
searchHistory = List.from(data['searchHistory'] ?? []);
|
||||||
saveData();
|
saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'customImageProcessing': defaultCustomImageProcessing,
|
'customImageProcessing': defaultCustomImageProcessing,
|
||||||
'sni': true,
|
'sni': true,
|
||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
|
'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json",
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -63,7 +63,8 @@ class ReaderImageProvider
|
|||||||
})()
|
})()
|
||||||
''');
|
''');
|
||||||
if (func is JSInvokable) {
|
if (func is JSInvokable) {
|
||||||
var result = func.invoke([imageBytes, cid, eid, page, sourceKey]);
|
var autoFreeFunc = JSAutoFreeFunction(func);
|
||||||
|
var result = autoFreeFunc([imageBytes, cid, eid, page, sourceKey]);
|
||||||
if (result is Uint8List) {
|
if (result is Uint8List) {
|
||||||
imageBytes = result;
|
imageBytes = result;
|
||||||
} else if (result is Future) {
|
} else if (result is Future) {
|
||||||
@@ -76,9 +77,9 @@ class ReaderImageProvider
|
|||||||
if (image is Uint8List) {
|
if (image is Uint8List) {
|
||||||
imageBytes = image;
|
imageBytes = image;
|
||||||
} else if (image is Future) {
|
} else if (image is Future) {
|
||||||
JSInvokable? onCancel;
|
JSAutoFreeFunction? onCancel;
|
||||||
if (result['onCancel'] is JSInvokable) {
|
if (result['onCancel'] is JSInvokable) {
|
||||||
onCancel = result['onCancel'];
|
onCancel = JSAutoFreeFunction(result['onCancel']);
|
||||||
}
|
}
|
||||||
if (onCancel == null) {
|
if (onCancel == null) {
|
||||||
var futureImage = await image;
|
var futureImage = await image;
|
||||||
@@ -96,9 +97,7 @@ class ReaderImageProvider
|
|||||||
checkStop();
|
checkStop();
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch(e) {
|
||||||
onCancel.invoke([]);
|
onCancel([]);
|
||||||
onCancel.free();
|
|
||||||
func.free();
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
await Future.delayed(Duration(milliseconds: 50));
|
await Future.delayed(Duration(milliseconds: 50));
|
||||||
@@ -107,10 +106,8 @@ class ReaderImageProvider
|
|||||||
imageBytes = futureImage;
|
imageBytes = futureImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onCancel?.free();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func.free();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return imageBytes!;
|
return imageBytes!;
|
||||||
|
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:html/parser.dart' as html;
|
import 'package:html/parser.dart' as html;
|
||||||
import 'package:html/dom.dart' as dom;
|
import 'package:html/dom.dart' as dom;
|
||||||
@@ -20,9 +19,8 @@ import 'package:pointycastle/block/modes/cbc.dart';
|
|||||||
import 'package:pointycastle/block/modes/cfb.dart';
|
import 'package:pointycastle/block/modes/cfb.dart';
|
||||||
import 'package:pointycastle/block/modes/ecb.dart';
|
import 'package:pointycastle/block/modes/ecb.dart';
|
||||||
import 'package:pointycastle/block/modes/ofb.dart';
|
import 'package:pointycastle/block/modes/ofb.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/js_ui.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
@@ -42,7 +40,7 @@ class JavaScriptRuntimeException implements Exception {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsEngine with _JSEngineApi, _JsUiApi {
|
class JsEngine with _JSEngineApi, JsUiApi {
|
||||||
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
||||||
|
|
||||||
static JsEngine? _cache;
|
static JsEngine? _cache;
|
||||||
@@ -156,7 +154,11 @@ class JsEngine with _JSEngineApi, _JsUiApi {
|
|||||||
case "delay":
|
case "delay":
|
||||||
return Future.delayed(Duration(milliseconds: message["time"]));
|
return Future.delayed(Duration(milliseconds: message["time"]));
|
||||||
case "UI":
|
case "UI":
|
||||||
handleUIMessage(Map.from(message));
|
return handleUIMessage(Map.from(message));
|
||||||
|
case "getLocale":
|
||||||
|
return "${App.locale.languageCode}-${App.locale.countryCode}";
|
||||||
|
case "getPlatform":
|
||||||
|
return Platform.operatingSystem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -679,6 +681,7 @@ class JSAutoFreeFunction {
|
|||||||
|
|
||||||
/// Automatically free the function when it's not used anymore
|
/// Automatically free the function when it's not used anymore
|
||||||
JSAutoFreeFunction(this.func) {
|
JSAutoFreeFunction(this.func) {
|
||||||
|
func.dup();
|
||||||
finalizer.attach(this, func);
|
finalizer.attach(this, func);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,48 +690,6 @@ class JSAutoFreeFunction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static final finalizer = Finalizer<JSInvokable>((func) {
|
static final finalizer = Finalizer<JSInvokable>((func) {
|
||||||
func.free();
|
func.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin class _JsUiApi {
|
|
||||||
void handleUIMessage(Map<String, dynamic> message) {
|
|
||||||
switch (message['function']) {
|
|
||||||
case 'showMessage':
|
|
||||||
var m = message['message'];
|
|
||||||
if (m.toString().isNotEmpty) {
|
|
||||||
App.rootContext.showMessage(message: m.toString());
|
|
||||||
}
|
|
||||||
case 'showDialog':
|
|
||||||
_showDialog(message);
|
|
||||||
case 'launchUrl':
|
|
||||||
var url = message['url'];
|
|
||||||
if (url.toString().isNotEmpty) {
|
|
||||||
launchUrlString(url.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showDialog(Map<String, dynamic> message) {
|
|
||||||
var title = message['title'];
|
|
||||||
var content = message['content'];
|
|
||||||
var actions = <String, JSAutoFreeFunction>{};
|
|
||||||
for (var action in message['actions']) {
|
|
||||||
actions[action['text']] = JSAutoFreeFunction(action['callback']);
|
|
||||||
}
|
|
||||||
showDialog(context: App.rootContext, builder: (context) {
|
|
||||||
return ContentDialog(
|
|
||||||
title: title,
|
|
||||||
content: Text(content).paddingHorizontal(16),
|
|
||||||
actions: actions.entries.map((entry) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
entry.value.call([]);
|
|
||||||
},
|
|
||||||
child: Text(entry.key),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -135,6 +135,8 @@ class NetworkCacheManager implements Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) {
|
static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) {
|
||||||
|
a = Map.from(a);
|
||||||
|
b = Map.from(b);
|
||||||
const shouldIgnore = [
|
const shouldIgnore = [
|
||||||
'cache-time',
|
'cache-time',
|
||||||
'prevent-parallel',
|
'prevent-parallel',
|
||||||
|
@@ -246,7 +246,7 @@ class _BodyState extends State<_Body> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (type == "callback") {
|
} else if (type == "callback") {
|
||||||
yield _CallbackSetting(setting: item);
|
yield _CallbackSetting(setting: item, sourceKey: source.key);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
|
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
|
||||||
@@ -419,7 +419,7 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void help() {
|
void help() {
|
||||||
launchUrlString("https://github.com/venera-app/venera-configs");
|
launchUrlString("https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleAddSource(String url) async {
|
Future<void> handleAddSource(String url) async {
|
||||||
@@ -469,8 +469,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
|||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
var dio = AppDio();
|
var dio = AppDio();
|
||||||
var res = await dio.get<String>(
|
var res = await dio.get<String>(appdata.settings['comicSourceListUrl']);
|
||||||
"https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json");
|
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
context.showMessage(message: "Network error".tl);
|
context.showMessage(message: "Network error".tl);
|
||||||
return;
|
return;
|
||||||
@@ -485,6 +484,27 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopUpWidgetScaffold(
|
return PopUpWidgetScaffold(
|
||||||
title: "Comic Source".tl,
|
title: "Comic Source".tl,
|
||||||
|
tailing: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
onPressed: () async {
|
||||||
|
await showInputDialog(
|
||||||
|
context: context,
|
||||||
|
title: "Set comic source list url".tl,
|
||||||
|
initialValue: appdata.settings['comicSourceListUrl'],
|
||||||
|
onConfirm: (value) {
|
||||||
|
appdata.settings['comicSourceListUrl'] = value;
|
||||||
|
appdata.saveData();
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
json = null;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
body: buildBody(),
|
body: buildBody(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -682,10 +702,12 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CallbackSetting extends StatefulWidget {
|
class _CallbackSetting extends StatefulWidget {
|
||||||
const _CallbackSetting({required this.setting});
|
const _CallbackSetting({required this.setting, required this.sourceKey});
|
||||||
|
|
||||||
final MapEntry<String, Map<String, dynamic>> setting;
|
final MapEntry<String, Map<String, dynamic>> setting;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_CallbackSetting> createState() => _CallbackSettingState();
|
State<_CallbackSetting> createState() => _CallbackSettingState();
|
||||||
}
|
}
|
||||||
@@ -719,11 +741,11 @@ class _CallbackSettingState extends State<_CallbackSetting> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(title.ts(key)),
|
title: Text(title.ts(widget.sourceKey)),
|
||||||
trailing: Button.normal(
|
trailing: Button.normal(
|
||||||
onPressed: onClick,
|
onPressed: onClick,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
child: Text(buttonText.ts(key)),
|
child: Text(buttonText.ts(widget.sourceKey)),
|
||||||
).fixHeight(32),
|
).fixHeight(32),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -124,18 +124,7 @@ class _ExplorePageState extends State<ExplorePage>
|
|||||||
}
|
}
|
||||||
return NetworkError(
|
return NetworkError(
|
||||||
message: msg,
|
message: msg,
|
||||||
retry: () {
|
retry: onSettingsChanged,
|
||||||
setState(() {
|
|
||||||
pages = ComicSource.all()
|
|
||||||
.map((e) => e.explorePages)
|
|
||||||
.expand((e) => e.map((e) => e.title))
|
|
||||||
.toList();
|
|
||||||
controller = TabController(
|
|
||||||
length: pages.length,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
withAppbar: false,
|
withAppbar: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
@@ -535,38 +536,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
launchUrlString("https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
|
||||||
context: context,
|
|
||||||
barrierColor: Colors.black.toOpacity(0.2),
|
|
||||||
builder: (context) {
|
|
||||||
var help = '';
|
|
||||||
help +=
|
|
||||||
'${"A directory is considered as a comic only if it matches one of the following conditions:".tl}\n';
|
|
||||||
help += '${'1. The directory only contains image files.'.tl}\n';
|
|
||||||
help +=
|
|
||||||
'${'2. The directory contains directories which contain image files. Each directory is considered as a chapter.'.tl}\n\n';
|
|
||||||
help +=
|
|
||||||
'${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n';
|
|
||||||
help +=
|
|
||||||
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
|
|
||||||
.tl;
|
|
||||||
help +=
|
|
||||||
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database."
|
|
||||||
.tl;
|
|
||||||
return ContentDialog(
|
|
||||||
title: "Help".tl,
|
|
||||||
content: Text(help).paddingHorizontal(16),
|
|
||||||
actions: [
|
|
||||||
Button.filled(
|
|
||||||
child: Text("OK".tl),
|
|
||||||
onPressed: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
).fixWidth(90).paddingRight(8),
|
).fixWidth(90).paddingRight(8),
|
||||||
Button.filled(
|
Button.filled(
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@@ -417,8 +417,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
ref: "598d50572a658f8e04775566fe3789954d9a01e3"
|
||||||
resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
resolved-ref: "598d50572a658f8e04775566fe3789954d9a01e3"
|
||||||
url: "https://github.com/wgh136/flutter_qjs"
|
url: "https://github.com/wgh136/flutter_qjs"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.7"
|
version: "0.3.7"
|
||||||
@@ -1150,10 +1150,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zip_flutter
|
name: zip_flutter
|
||||||
sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646
|
sha256: ea7fdc86c988174ef3bb80dc26e8e8bfdf634c55930e2d18d7e77e991acf0483
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.6"
|
version: "0.0.8"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.1"
|
flutter: ">=3.27.2"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.2.0+120
|
version: 1.2.1+121
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
flutter: 3.27.1
|
flutter: 3.27.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -21,7 +21,7 @@ dependencies:
|
|||||||
flutter_qjs:
|
flutter_qjs:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/flutter_qjs
|
url: https://github.com/wgh136/flutter_qjs
|
||||||
ref: 9c99ac258a11f8e91761a5466a190efba3ca64af
|
ref: 598d50572a658f8e04775566fe3789954d9a01e3
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
dio: ^5.7.0
|
dio: ^5.7.0
|
||||||
html: ^0.15.5
|
html: ^0.15.5
|
||||||
@@ -51,7 +51,7 @@ dependencies:
|
|||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter: ^0.0.6
|
zip_flutter: ^0.0.8
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
|
Reference in New Issue
Block a user