12 Commits

Author SHA1 Message Date
50c6bec4cd Disable minify 2025-09-04 00:30:01 +08:00
nyne
8c44f83d6c Update Xcode version in GitHub Actions workflow 2025-09-03 22:50:32 +08:00
nyne
103b6b2832 Merge pull request #497 from venera-app/v1.5.0-dev
V1.5.0
2025-09-03 22:12:00 +08:00
4129349c70 Improve js api onResponse 2025-09-03 22:09:07 +08:00
77a9aa5457 Update version code. 2025-09-03 22:05:04 +08:00
97940b9492 Refactor category options. 2025-09-03 22:03:54 +08:00
7945c0e54f Improve compute api. 2025-09-03 20:31:42 +08:00
dfee65c3af Add compute api to js engine. 2025-09-02 22:15:54 +08:00
fa2dbd79f6 Fix invalid js stacktrace. 2025-09-02 20:35:47 +08:00
9a9f539906 Disable cache when updating comic source. 2025-09-02 20:16:13 +08:00
d7331f36e9 flutter 3.35.2 2025-09-01 21:13:57 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
d0b76de465 Use badge from shields.io (#455)
* Use badge from shields.io

* AUR
2025-09-01 20:55:45 +08:00
19 changed files with 690 additions and 247 deletions

View File

@@ -15,7 +15,7 @@ jobs:
channel: "stable" channel: "stable"
flutter-version-file: pubspec.yaml flutter-version-file: pubspec.yaml
architecture: x64 architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app - run: sudo xcode-select --switch /Applications/Xcode_16.4.app
- run: flutter pub get - run: flutter pub get
# Step 1: Decode and install the certificate # Step 1: Decode and install the certificate
- name: Decode and install certificate - name: Decode and install certificate
@@ -63,7 +63,7 @@ jobs:
channel: "stable" channel: "stable"
flutter-version-file: pubspec.yaml flutter-version-file: pubspec.yaml
architecture: x64 architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app - run: sudo xcode-select --switch /Applications/Xcode_16.4.app
- run: flutter pub get - run: flutter pub get
- run: flutter build ios --release --no-codesign - run: flutter build ios --release --no-codesign
- run: | - run: |

View File

@@ -1,15 +1,14 @@
# venera # venera
[![flutter](https://img.shields.io/badge/flutter-3.27.1-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) [![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?style=flat)](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/venera_release) [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/venera_release)
A comic reader that support reading local and network comics. [![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![AUR Version](https://img.shields.io/aur/version/venera-bin)](https://aur.archlinux.org/packages/venera-bin)
[![F-Droid Version](https://img.shields.io/f-droid/v/com.github.wgh136.venera)](https://f-droid.org/packages/com.github.wgh136.venera/)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" A comic reader that support reading local and network comics.
alt="Get it on F-Droid"
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
## Features ## Features
- Read local comics - Read local comics

View File

@@ -78,6 +78,9 @@ android {
buildTypes { buildTypes {
release { release {
// Temporarily solution to fix crash
minifyEnabled false
shrinkResources false
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
} }

View File

@@ -19,7 +19,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.9.0' apply false id "com.android.application" version '8.9.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false
} }
include ":app" include ":app"

View File

@@ -4,6 +4,18 @@ Venera JavaScript Library
This library provides a set of APIs for interacting with the Venera app. This library provides a set of APIs for interacting with the Venera app.
*/ */
/**
* @function sendMessage
* @global
* @param {Object} message
* @returns {any}
*/
/**
* Set a timeout to execute a callback function after a specified delay.
* @param callback {Function}
* @param delay {number} - delay in milliseconds
*/
function setTimeout(callback, delay) { function setTimeout(callback, delay) {
sendMessage({ sendMessage({
method: 'delay', method: 'delay',
@@ -1412,3 +1424,18 @@ function getClipboard() {
method: 'getClipboard' method: 'getClipboard'
}) })
} }
/**
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
* @param args {any[]} - The arguments to pass to the function.
* @returns {Promise<any>} - The result of the function.
* @since 1.5.0
*/
function compute(func, ...args) {
return sendMessage({
method: 'compute',
function: func,
args: args
})
}

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.4.6"; final version = "1.5.0";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;
@@ -30,6 +30,10 @@ class _App {
bool get isMobile => Platform.isAndroid || Platform.isIOS; bool get isMobile => Platform.isAndroid || Platform.isIOS;
// Whether the app has been initialized.
// If current Isolate is main Isolate, this value is always true.
bool isInitialized = false;
Locale get locale { Locale get locale {
Locale deviceLocale = PlatformDispatcher.instance.locale; Locale deviceLocale = PlatformDispatcher.instance.locale;
if (deviceLocale.languageCode == "zh" && if (deviceLocale.languageCode == "zh" &&
@@ -81,6 +85,7 @@ class _App {
if (isAndroid) { if (isAndroid) {
externalStoragePath = (await getExternalStorageDirectory())!.path; externalStoragePath = (await getExternalStorageDirectory())!.path;
} }
isInitialized = true;
} }
Future<void> initComponents() async { Future<void> initComponents() async {

View File

@@ -401,9 +401,14 @@ class SearchOptions {
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function( typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
String category, String? param, List<String> options, int page); String category, String? param, List<String> options, int page);
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
String category, String? param);
class CategoryComicsData { class CategoryComicsData {
/// options /// options
final List<CategoryComicsOptions> options; final List<CategoryComicsOptions>? options;
final CategoryOptionsLoader? optionsLoader;
/// [category] is the one clicked by the user on the category page. /// [category] is the one clicked by the user on the category page.
/// ///
@@ -414,7 +419,7 @@ class CategoryComicsData {
final RankingData? rankingData; final RankingData? rankingData;
const CategoryComicsData(this.options, this.load, {this.rankingData}); const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
} }
class RankingData { class RankingData {
@@ -429,6 +434,9 @@ class RankingData {
} }
class CategoryComicsOptions { class CategoryComicsOptions {
// The label will not be displayed if it is empty.
final String label;
/// Use a [LinkedHashMap] to describe an option list. /// Use a [LinkedHashMap] to describe an option list.
/// key is for loading comics, value is the name displayed on screen. /// key is for loading comics, value is the name displayed on screen.
/// Default value will be the first of the Map. /// Default value will be the first of the Map.
@@ -439,7 +447,7 @@ class CategoryComicsOptions {
final List<String>? showWhen; final List<String>? showWhen;
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen); const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
} }
class LinkHandler { class LinkHandler {

View File

@@ -64,8 +64,13 @@ class ComicSourceParser {
if (file.existsSync()) { if (file.existsSync()) {
int i = 0; int i = 0;
while (file.existsSync()) { while (file.existsSync()) {
file = File(FilePath.join(App.dataPath, "comic_source", file = File(
"${fileName.split('.').first}($i).js")); FilePath.join(
App.dataPath,
"comic_source",
"${fileName.split('.').first}($i).js",
),
);
i++; i++;
} }
} }
@@ -80,8 +85,9 @@ class ComicSourceParser {
Future<ComicSource> parse(String js, String filePath) async { Future<ComicSource> parse(String js, String filePath) async {
js = js.replaceAll("\r\n", "\n"); js = js.replaceAll("\r\n", "\n");
var line1 = var line1 = js
js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class ")); .split('\n')
.firstWhereOrNull((e) => e.trim().startsWith("class "));
if (line1 == null || if (line1 == null ||
!line1.startsWith("class ") || !line1.startsWith("class ") ||
!line1.contains("extends ComicSource")) { !line1.contains("extends ComicSource")) {
@@ -89,24 +95,27 @@ class ComicSourceParser {
} }
var className = line1.split("class")[1].split("extends ComicSource").first; var className = line1.split("class")[1].split("extends ComicSource").first;
className = className.trim(); className = className.trim();
JsEngine().runCode(""" JsEngine().runCode("""(() => { $js
(() => { $js
this['temp'] = new $className() this['temp'] = new $className()
}).call() }).call()
""", className); """, className);
_name = JsEngine().runCode("this['temp'].name") ?? _name =
JsEngine().runCode("this['temp'].name") ??
(throw ComicSourceParseException('name is required')); (throw ComicSourceParseException('name is required'));
var key = JsEngine().runCode("this['temp'].key") ?? var key =
JsEngine().runCode("this['temp'].key") ??
(throw ComicSourceParseException('key is required')); (throw ComicSourceParseException('key is required'));
var version = JsEngine().runCode("this['temp'].version") ?? var version =
JsEngine().runCode("this['temp'].version") ??
(throw ComicSourceParseException('version is required')); (throw ComicSourceParseException('version is required'));
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion"); var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
var url = JsEngine().runCode("this['temp'].url"); var url = JsEngine().runCode("this['temp'].url");
if (minAppVersion != null) { if (minAppVersion != null) {
if (compareSemVer(minAppVersion, App.version.split('-').first)) { if (compareSemVer(minAppVersion, App.version.split('-').first)) {
throw ComicSourceParseException( throw ComicSourceParseException(
"minAppVersion @version is required" "minAppVersion @version is required".tlParams({
.tlParams({"version": minAppVersion}), "version": minAppVersion,
}),
); );
} }
} }
@@ -175,8 +184,10 @@ class ComicSourceParser {
} }
bool _checkExists(String index) { bool _checkExists(String index) {
return JsEngine().runCode("ComicSource.sources.$_key.$index !== null " return JsEngine().runCode(
"&& ComicSource.sources.$_key.$index !== undefined"); "ComicSource.sources.$_key.$index !== null "
"&& ComicSource.sources.$_key.$index !== undefined",
);
} }
dynamic _getValue(String index) { dynamic _getValue(String index) {
@@ -277,16 +288,24 @@ class ComicSourceParser {
if (type == "singlePageWithMultiPart") { if (type == "singlePageWithMultiPart") {
loadMultiPart = () async { loadMultiPart = () async {
try { try {
var res = await JsEngine() var res = await JsEngine().runCode(
.runCode("ComicSource.sources.$_key.explore[$i].load()"); "ComicSource.sources.$_key.explore[$i].load()",
return Res(List.from(res.keys );
.map((e) => ExplorePagePart( return Res(
List.from(
res.keys
.map(
(e) => ExplorePagePart(
e, e,
(res[e] as List) (res[e] as List)
.map<Comic>((e) => Comic.fromJson(e, _key!)) .map<Comic>((e) => Comic.fromJson(e, _key!))
.toList(), .toList(),
null)) null,
.toList())); ),
)
.toList(),
),
);
} catch (e, s) { } catch (e, s) {
Log.error("Data Analysis", "$e\n$s"); Log.error("Data Analysis", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -297,11 +316,15 @@ class ComicSourceParser {
loadPage = (int page) async { loadPage = (int page) async {
try { try {
var res = await JsEngine().runCode( var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})"); "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})",
);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
subData: res["maxPage"]); (index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -311,10 +334,13 @@ class ComicSourceParser {
loadNext = (next) async { loadNext = (next) async {
try { try {
var res = await JsEngine().runCode( var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})"); "ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})",
);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"], subData: res["next"],
); );
} catch (e, s) { } catch (e, s) {
@@ -326,8 +352,9 @@ class ComicSourceParser {
} else if (type == "multiPartPage") { } else if (type == "multiPartPage") {
loadMultiPart = () async { loadMultiPart = () async {
try { try {
var res = await JsEngine() var res = await JsEngine().runCode(
.runCode("ComicSource.sources.$_key.explore[$i].load()"); "ComicSource.sources.$_key.explore[$i].load()",
);
return Res( return Res(
List.from( List.from(
(res as List).map((e) { (res as List).map((e) {
@@ -350,19 +377,22 @@ class ComicSourceParser {
loadMixed = (index) async { loadMixed = (index) async {
try { try {
var res = await JsEngine().runCode( var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})"); "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})",
);
var list = <Object>[]; var list = <Object>[];
for (var data in (res['data'] as List)) { for (var data in (res['data'] as List)) {
if (data is List) { if (data is List) {
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList()); list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
} else if (data is Map) { } else if (data is Map) {
list.add(ExplorePagePart( list.add(
ExplorePagePart(
data['title'], data['title'],
(data['comics'] as List).map((e) { (data['comics'] as List).map((e) {
return Comic.fromJson(e, _key!); return Comic.fromJson(e, _key!);
}).toList(), }).toList(),
data['viewMore'], data['viewMore'],
)); ),
);
} }
} }
return Res(list, subData: res['maxPage']); return Res(list, subData: res['maxPage']);
@@ -372,21 +402,25 @@ class ComicSourceParser {
} }
}; };
} }
pages.add(ExplorePageData( pages.add(
ExplorePageData(
title, title,
switch (type) { switch (type) {
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart, "singlePageWithMultiPart" =>
ExplorePageType.singlePageWithMultiPart,
"multiPartPage" => ExplorePageType.singlePageWithMultiPart, "multiPartPage" => ExplorePageType.singlePageWithMultiPart,
"multiPageComicList" => ExplorePageType.multiPageComicList, "multiPageComicList" => ExplorePageType.multiPageComicList,
"mixed" => ExplorePageType.mixed, "mixed" => ExplorePageType.mixed,
_ => _ => throw ComicSourceParseException(
throw ComicSourceParseException("Unknown explore page type $type") "Unknown explore page type $type",
),
}, },
loadPage, loadPage,
loadNext, loadNext,
loadMultiPart, loadMultiPart,
loadMixed, loadMixed,
)); ),
);
} }
return pages; return pages;
} }
@@ -426,18 +460,17 @@ class ComicSourceParser {
if (type == "fixed") { if (type == "fixed") {
categoryParts.add(FixedCategoryPart(name, cs!)); categoryParts.add(FixedCategoryPart(name, cs!));
} else if (type == "random") { } else if (type == "random") {
categoryParts categoryParts.add(
.add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1)); RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1),
);
} else if (type == "dynamic" && categories == null) { } else if (type == "dynamic" && categories == null) {
var loader = c["loader"]; var loader = c["loader"];
if (loader is! JSInvokable) { if (loader is! JSInvokable) {
throw "DynamicCategoryPart loader must be a function"; throw "DynamicCategoryPart loader must be a function";
} }
categoryParts.add(DynamicCategoryPart( categoryParts.add(
name, DynamicCategoryPart(name, JSAutoFreeFunction(loader), _key!),
JSAutoFreeFunction(loader), );
_key!,
));
} }
} else { } else {
// old format // old format
@@ -454,30 +487,16 @@ class ComicSourceParser {
for (int i = 0; i < tags.length; i++) { for (int i = 0; i < tags.length; i++) {
PageJumpTarget target; PageJumpTarget target;
if (itemType == 'category') { if (itemType == 'category') {
target = PageJumpTarget( target = PageJumpTarget(_key!, 'category', {
_key!,
'category',
{
"category": tags[i], "category": tags[i],
"param": categoryParams?.elementAtOrNull(i), "param": categoryParams?.elementAtOrNull(i),
}, });
);
} else if (itemType == 'search') { } else if (itemType == 'search') {
target = PageJumpTarget( target = PageJumpTarget(_key!, 'search', {"keyword": tags[i]});
_key!,
'search',
{
"keyword": tags[i],
},
);
} else if (itemType == 'search_with_namespace') { } else if (itemType == 'search_with_namespace') {
target = PageJumpTarget( target = PageJumpTarget(_key!, 'search', {
_key!,
'search',
{
"keyword": "$name:$tags[i]", "keyword": "$name:$tags[i]",
}, });
);
} else { } else {
target = PageJumpTarget(_key!, itemType, null); target = PageJumpTarget(_key!, itemType, null);
} }
@@ -486,8 +505,9 @@ class ComicSourceParser {
if (type == "fixed") { if (type == "fixed") {
categoryParts.add(FixedCategoryPart(name, cs)); categoryParts.add(FixedCategoryPart(name, cs));
} else if (type == "random") { } else if (type == "random") {
categoryParts categoryParts.add(
.add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1)); RandomCategoryPart(name, cs, c["randomNumber"] ?? 1),
);
} }
} }
} }
@@ -496,12 +516,16 @@ class ComicSourceParser {
title: title, title: title,
categories: categoryParts, categories: categoryParts,
enableRankingPage: enableRankingPage ?? false, enableRankingPage: enableRankingPage ?? false,
key: title); key: title,
);
} }
CategoryComicsData? _loadCategoryComicsData() { CategoryComicsData? _loadCategoryComicsData() {
if (!_checkExists("categoryComics")) return null; if (!_checkExists("categoryComics")) return null;
var options = <CategoryComicsOptions>[];
List<CategoryComicsOptions>? options;
if (_checkExists("categoryComics.optionList")) {
options = <CategoryComicsOptions>[];
for (var element in _getValue("categoryComics.optionList") ?? []) { for (var element in _getValue("categoryComics.optionList") ?? []) {
LinkedHashMap<String, String> map = LinkedHashMap<String, String>(); LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
for (var option in element["options"]) { for (var option in element["options"]) {
@@ -513,11 +537,64 @@ class ComicSourceParser {
var value = split.join("-"); var value = split.join("-");
map[key] = value; map[key] = value;
} }
options.add(CategoryComicsOptions( options.add(
CategoryComicsOptions(
element["label"] ?? "",
map, map,
List.from(element["notShowWhen"] ?? []), List.from(element["notShowWhen"] ?? []),
element["showWhen"] == null ? null : List.from(element["showWhen"]))); element["showWhen"] == null ? null : List.from(element["showWhen"]),
),
);
} }
}
CategoryOptionsLoader? optionLoader;
if (_checkExists("categoryComics.optionLoader")) {
optionLoader = (category, param) async {
try {
dynamic res = JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.optionLoader(
${jsonEncode(category)}, ${jsonEncode(param)})
""");
if (res is Future) {
res = await res;
}
if (res is! List) {
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
}
var options = <CategoryComicsOptions>[];
for (var element in res) {
if (element is! Map) {
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
}
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
for (var option in element["options"] ?? []) {
if (option.isEmpty || !option.contains("-")) {
continue;
}
var split = option.split("-");
var key = split.removeAt(0);
var value = split.join("-");
map[key] = value;
}
options.add(
CategoryComicsOptions(
element["label"] ?? "",
map,
List.from(element["notShowWhen"] ?? []),
element["showWhen"] == null ? null : List.from(element["showWhen"]),
),
);
}
return Res(options);
}
catch(e) {
Log.error("Data Analysis", "Failed to load category options.\n$e");
return Res.error(e.toString());
}
};
}
RankingData? rankingData; RankingData? rankingData;
if (_checkExists("categoryComics.ranking")) { if (_checkExists("categoryComics.ranking")) {
var options = <String, String>{}; var options = <String, String>{};
@@ -541,9 +618,12 @@ class ComicSourceParser {
${jsonEncode(option)}, ${jsonEncode(page)}) ${jsonEncode(option)}, ${jsonEncode(page)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
subData: res["maxPage"]); (index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -557,8 +637,10 @@ class ComicSourceParser {
${jsonEncode(option)}, ${jsonEncode(next)}) ${jsonEncode(option)}, ${jsonEncode(next)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"], subData: res["next"],
); );
} catch (e, s) { } catch (e, s) {
@@ -569,7 +651,15 @@ class ComicSourceParser {
} }
rankingData = RankingData(options, load, loadWithNext); rankingData = RankingData(options, load, loadWithNext);
} }
return CategoryComicsData(options, (category, param, options, page) async {
if (options == null && optionLoader == null) {
options = [];
}
return CategoryComicsData(
options: options,
optionsLoader: optionLoader,
load: (category, param, options, page) async {
try { try {
var res = await JsEngine().runCode(""" var res = await JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.load( ComicSource.sources.$_key.categoryComics.load(
@@ -580,14 +670,19 @@ class ComicSourceParser {
) )
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
subData: res["maxPage"]); (index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
} }
}, rankingData: rankingData); },
rankingData: rankingData,
);
} }
SearchPageData? _loadSearchData() { SearchPageData? _loadSearchData() {
@@ -604,12 +699,14 @@ class ComicSourceParser {
var value = split.join("-"); var value = split.join("-");
map[key] = value; map[key] = value;
} }
options.add(SearchOptions( options.add(
SearchOptions(
map, map,
element["label"], element["label"],
element['type'] ?? 'select', element['type'] ?? 'select',
element['default'] == null ? null : jsonEncode(element['default']), element['default'] == null ? null : jsonEncode(element['default']),
)); ),
);
} }
SearchFunction? loadPage; SearchFunction? loadPage;
@@ -624,9 +721,12 @@ class ComicSourceParser {
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)}) ${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
subData: res["maxPage"]); (index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -640,8 +740,10 @@ class ComicSourceParser {
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)}) ${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"], subData: res["next"],
); );
} catch (e, s) { } catch (e, s) {
@@ -690,8 +792,9 @@ class ComicSourceParser {
final bool multiFolder = _getValue("favorites.multiFolder"); final bool multiFolder = _getValue("favorites.multiFolder");
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
final bool? singleFolderForSingleComic = final bool? singleFolderForSingleComic = _getValue(
_getValue("favorites.singleFolderForSingleComic"); "favorites.singleFolderForSingleComic",
);
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async { Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
if (!ComicSource.find(_key!)!.isLogged) { if (!ComicSource.find(_key!)!.isLogged) {
@@ -744,9 +847,12 @@ class ComicSourceParser {
${jsonEncode(page)}, ${jsonEncode(folder)}) ${jsonEncode(page)}, ${jsonEncode(folder)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
subData: res["maxPage"]); (index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -766,8 +872,10 @@ class ComicSourceParser {
${jsonEncode(next)}, ${jsonEncode(folder)}) ${jsonEncode(next)}, ${jsonEncode(folder)})
"""); """);
return Res( return Res(
List.generate(res["comics"].length, List.generate(
(index) => Comic.fromJson(res["comics"][index], _key!)), res["comics"].length,
(index) => Comic.fromJson(res["comics"][index], _key!),
),
subData: res["next"], subData: res["next"],
); );
} catch (e, s) { } catch (e, s) {
@@ -859,7 +967,8 @@ class ComicSourceParser {
"""); """);
return Res( return Res(
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(), (res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
subData: res["maxPage"]); subData: res["maxPage"],
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());
@@ -1114,7 +1223,8 @@ class ComicSourceParser {
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)}) ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
"""); """);
return Res( return Res(
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList()); (res as List).map((e) => ArchiveInfo.fromJson(e)).toList(),
);
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e.toString()); return Res.error(e.toString());

View File

@@ -24,6 +24,7 @@ import 'package:pointycastle/block/modes/ofb.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:venera/components/js_ui.dart'; import 'package:venera/components/js_ui.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/js_pool.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';
import 'package:venera/network/proxy.dart'; import 'package:venera/network/proxy.dart';
@@ -68,6 +69,12 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
responseType: ResponseType.plain, validateStatus: (status) => true)); responseType: ResponseType.plain, validateStatus: (status) => true));
} }
static Uint8List? _jsInitCache;
static void cacheJsInit(Uint8List jsInit) {
_jsInitCache = jsInit;
}
@override @override
@protected @protected
Future<void> doInit() async { Future<void> doInit() async {
@@ -75,9 +82,11 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
return; return;
} }
try { try {
if (App.isInitialized) {
_cookieJar ??= await SingleInstanceCookieJar.createInstance();
}
_dio ??= AppDio(BaseOptions( _dio ??= AppDio(BaseOptions(
responseType: ResponseType.plain, validateStatus: (status) => true)); responseType: ResponseType.plain, validateStatus: (status) => true));
_cookieJar ??= SingleInstanceCookieJar.instance!;
_closed = false; _closed = false;
_engine = FlutterQjs(); _engine = FlutterQjs();
_engine!.dispatch(); _engine!.dispatch();
@@ -86,9 +95,15 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]); (setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
setGlobalFunc(["appVersion", App.version]); setGlobalFunc(["appVersion", App.version]);
setGlobalFunc.free(); setGlobalFunc.free();
var jsInit = await rootBundle.load("assets/init.js"); Uint8List jsInit;
if (_jsInitCache != null) {
jsInit = _jsInitCache!;
} else {
var buffer = await rootBundle.load("assets/init.js");
jsInit = buffer.buffer.asUint8List();
}
_engine! _engine!
.evaluate(utf8.decode(jsInit.buffer.asUint8List()), name: "<init>"); .evaluate(utf8.decode(jsInit), name: "<init>");
} catch (e, s) { } catch (e, s) {
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s'); Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
} }
@@ -97,6 +112,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
Object? _messageReceiver(dynamic message) { Object? _messageReceiver(dynamic message) {
try { try {
if (message is Map<dynamic, dynamic>) { if (message is Map<dynamic, dynamic>) {
if (message["method"] == null) return null;
String method = message["method"] as String; String method = message["method"] as String;
switch (method) { switch (method) {
case "log": case "log":
@@ -172,6 +188,20 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
var res = await Clipboard.getData(Clipboard.kTextPlain); var res = await Clipboard.getData(Clipboard.kTextPlain);
return res?.text; return res?.text;
}); });
case "compute":
final func = message["function"];
final args = message["args"];
if (func is JSInvokable) {
func.free();
throw "Function must be a string";
}
if (func is! String) {
throw "Function must be a string";
}
if (args != null && args is! List) {
throw "Args must be a list";
}
return JSPool().execute(func, args ?? []);
} }
} }
return null; return null;

163
lib/foundation/js_pool.dart Normal file
View File

@@ -0,0 +1,163 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:venera/foundation/js_engine.dart';
import 'package:venera/foundation/log.dart';
class JSPool {
static final int _maxInstances = 4;
final List<IsolateJsEngine> _instances = [];
bool _isInitializing = false;
static final JSPool _singleton = JSPool._internal();
factory JSPool() {
return _singleton;
}
JSPool._internal();
Future<void> init() async {
if (_isInitializing) return;
_isInitializing = true;
var jsInitBuffer = await rootBundle.load("assets/init.js");
var jsInit = jsInitBuffer.buffer.asUint8List();
for (int i = 0; i < _maxInstances; i++) {
_instances.add(IsolateJsEngine(jsInit));
}
_isInitializing = false;
}
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
await init();
var selectedInstance = _instances[0];
for (var instance in _instances) {
if (instance.pendingTasks < selectedInstance.pendingTasks) {
selectedInstance = instance;
}
}
return selectedInstance.execute(jsFunction, args);
}
}
class _IsolateJsEngineInitParam {
final SendPort sendPort;
final Uint8List jsInit;
_IsolateJsEngineInitParam(this.sendPort, this.jsInit);
}
class IsolateJsEngine {
Isolate? _isolate;
SendPort? _sendPort;
ReceivePort? _receivePort;
int _counter = 0;
final Map<int, Completer<dynamic>> _tasks = {};
bool _isClosed = false;
int get pendingTasks => _tasks.length;
IsolateJsEngine(Uint8List jsInit) {
_receivePort = ReceivePort();
_receivePort!.listen(_onMessage);
Isolate.spawn(_run, _IsolateJsEngineInitParam(_receivePort!.sendPort, jsInit));
}
void _onMessage(dynamic message) {
if (message is SendPort) {
_sendPort = message;
} else if (message is TaskResult) {
final completer = _tasks.remove(message.id);
if (completer != null) {
if (message.error != null) {
completer.completeError(message.error!);
} else {
completer.complete(message.result);
}
}
} else if (message is Exception) {
Log.error("IsolateJsEngine", message.toString());
for (var completer in _tasks.values) {
completer.completeError(message);
}
_tasks.clear();
close();
}
}
static void _run(_IsolateJsEngineInitParam params) async {
var sendPort = params.sendPort;
final port = ReceivePort();
sendPort.send(port.sendPort);
final engine = JsEngine();
try {
JsEngine.cacheJsInit(params.jsInit);
await engine.init();
}
catch(e, s) {
sendPort.send(Exception("Failed to initialize JS engine: $e\n$s"));
return;
}
await for (final message in port) {
if (message is Task) {
try {
final jsFunc = engine.runCode(message.jsFunction);
if (jsFunc is! JSInvokable) {
throw Exception("The provided code does not evaluate to a function.");
}
final result = jsFunc.invoke(message.args);
jsFunc.free();
sendPort.send(TaskResult(message.id, result, null));
} catch (e) {
sendPort.send(TaskResult(message.id, null, e.toString()));
}
}
}
}
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
if (_isClosed) {
throw Exception("IsolateJsEngine is closed.");
}
while (_sendPort == null) {
await Future.delayed(const Duration(milliseconds: 10));
}
final completer = Completer<dynamic>();
final taskId = _counter++;
_tasks[taskId] = completer;
final task = Task(taskId, jsFunction, args);
_sendPort?.send(task);
return completer.future;
}
void close() async {
if (!_isClosed) {
_isClosed = true;
while (_tasks.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 100));
}
_receivePort?.close();
_isolate?.kill(priority: Isolate.immediate);
_isolate = null;
}
}
}
class Task {
final int id;
final String jsFunction;
final List<dynamic> args;
const Task(this.id, this.jsFunction, this.args);
}
class TaskResult {
final int id;
final Object? result;
final String? error;
const TaskResult(this.id, this.result, this.error);
}

View File

@@ -42,7 +42,7 @@ class Log {
static void addLog(LogLevel level, String title, String content) { static void addLog(LogLevel level, String title, String content) {
if (isMuted) return; if (isMuted) return;
if (_file == null) { if (_file == null && App.isInitialized) {
Directory dir; Directory dir;
if (App.isAndroid) { if (App.isAndroid) {
dir = Directory(App.externalStoragePath!); dir = Directory(App.externalStoragePath!);

View File

@@ -112,11 +112,13 @@ class AppDio with DioMixin {
AppDio([BaseOptions? options]) { AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions(); this.options = options ?? BaseOptions();
httpClientAdapter = RHttpAdapter(); httpClientAdapter = RHttpAdapter();
if (App.isInitialized) {
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager()); interceptors.add(NetworkCacheManager());
interceptors.add(CloudflareInterceptor()); interceptors.add(CloudflareInterceptor());
interceptors.add(MyLogInterceptor()); interceptors.add(MyLogInterceptor());
} }
}
static final Map<String, bool> _requests = {}; static final Map<String, bool> _requests = {};

View File

@@ -202,9 +202,13 @@ class SingleInstanceCookieJar extends CookieJarSql {
static SingleInstanceCookieJar? instance; static SingleInstanceCookieJar? instance;
static Future<void> createInstance() async { static Future<SingleInstanceCookieJar> createInstance() async {
if (instance != null) {
return instance!;
}
var dataPath = (await getApplicationSupportDirectory()).path; var dataPath = (await getApplicationSupportDirectory()).path;
instance = SingleInstanceCookieJar("$dataPath/cookie.db"); instance = SingleInstanceCookieJar("$dataPath/cookie.db");
return instance!;
} }
} }

View File

@@ -181,12 +181,17 @@ abstract class ImageDownloader {
} }
if (configs['onResponse'] is JSInvokable) { if (configs['onResponse'] is JSInvokable) {
buffer = (configs['onResponse'] as JSInvokable)([buffer]); buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
(configs['onResponse'] as JSInvokable).free(); (configs['onResponse'] as JSInvokable).free();
} }
var data = Uint8List.fromList(buffer); Uint8List data;
if (buffer is Uint8List) {
data = buffer;
} else {
data = Uint8List.fromList(buffer);
buffer.clear(); buffer.clear();
}
if (configs['modifyImage'] != null) { if (configs['modifyImage'] != null) {
var newData = await modifyImageWithScript( var newData = await modifyImageWithScript(

View File

@@ -27,9 +27,11 @@ class CategoryComicsPage extends StatefulWidget {
class _CategoryComicsPageState extends State<CategoryComicsPage> { class _CategoryComicsPageState extends State<CategoryComicsPage> {
late final CategoryComicsData data; late final CategoryComicsData data;
late final List<CategoryComicsOptions> options; late List<CategoryComicsOptions>? options;
late final CategoryOptionsLoader? optionsLoader;
late List<String> optionsValue; late List<String> optionsValue;
late String sourceKey; late String sourceKey;
String? error;
void findData() { void findData() {
for (final source in ComicSource.all()) { for (final source in ComicSource.all()) {
@@ -38,7 +40,8 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
throw "The comic source ${source.name} does not support category comics"; throw "The comic source ${source.name} does not support category comics";
} }
data = source.categoryComicsData!; data = source.categoryComicsData!;
options = data.options.where((element) { if (data.options != null) {
options = data.options!.where((element) {
if (element.notShowWhen.contains(widget.category)) { if (element.notShowWhen.contains(widget.category)) {
return false; return false;
} else if (element.showWhen != null) { } else if (element.showWhen != null) {
@@ -46,16 +49,14 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
} }
return true; return true;
}).toList(); }).toList();
var defaultOptionsValue = } else {
options.map((e) => e.options.keys.first).toList(); options = null;
if (optionsValue.length != options.length) {
var newOptionsValue = List<String>.filled(options.length, "");
for (var i = 0; i < options.length; i++) {
newOptionsValue[i] =
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
} }
optionsValue = newOptionsValue; if (data.optionsLoader != null) {
optionsLoader = data.optionsLoader;
loadOptions();
} }
resetOptionsValue();
sourceKey = source.key; sourceKey = source.key;
return; return;
} }
@@ -63,6 +64,36 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
throw "${widget.categoryKey} Not found"; throw "${widget.categoryKey} Not found";
} }
void resetOptionsValue() {
if (options == null) return;
var defaultOptionsValue = options!
.map((e) => e.options.keys.first)
.toList();
if (optionsValue.length != options!.length) {
var newOptionsValue = List<String>.filled(options!.length, "");
for (var i = 0; i < options!.length; i++) {
newOptionsValue[i] =
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
}
optionsValue = newOptionsValue;
}
}
void loadOptions() async {
final res = await optionsLoader!(widget.category, widget.param);
if (res.error) {
setState(() {
error = res.errorMessage;
});
} else {
setState(() {
options = res.data;
resetOptionsValue();
error = null;
});
}
}
@override @override
void initState() { void initState() {
if (widget.options != null) { if (widget.options != null) {
@@ -77,27 +108,44 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var topPadding = context.padding.top + 56.0; var topPadding = context.padding.top + 56.0;
Widget body;
if (options == null) {
body = Center(child: CircularProgressIndicator());
} else if (error != null) {
body = NetworkError(
message: error!,
retry: () {
setState(() {
error = null;
});
loadOptions();
},
);
} else {
body = ComicList(
key: Key(widget.category + optionsValue.toString()),
errorLeading: buildOptions().paddingTop(topPadding),
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
loadPage: (i) =>
data.load(widget.category, widget.param, optionsValue, i),
);
}
return Scaffold( return Scaffold(
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
appBar: Appbar( appBar: Appbar(title: Text(widget.category)),
title: Text(widget.category), body: body,
),
body: ComicList(
key: Key(widget.category + optionsValue.toString()),
errorLeading: SizedBox(height: topPadding),
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
loadPage: (i) => data.load(
widget.category,
widget.param,
optionsValue,
i,
),
),
); );
} }
Widget buildOptionItem( Widget buildOptionItem(
String text, String value, int group, BuildContext context) { String text,
String value,
int group,
BuildContext context,
) {
return OptionChip( return OptionChip(
text: text.ts(sourceKey), text: text.ts(sourceKey),
isSelected: value == optionsValue[group], isSelected: value == optionsValue[group],
@@ -112,8 +160,26 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
Widget buildOptions() { Widget buildOptions() {
List<Widget> children = []; List<Widget> children = [];
for (var optionList in options) { var group = 0;
children.add(Wrap( for (var optionList in options!) {
if (optionList.label.isNotEmpty) {
children.add(Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 4.0,
),
child: Text(
optionList.label.ts(sourceKey),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
));
}
if (optionList.options.length <= 8) {
children.add(
Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
@@ -121,14 +187,30 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
buildOptionItem( buildOptionItem(
option.value.tl, option.value.tl,
option.key, option.key,
options.indexOf(optionList), group,
context, context,
) ),
], ],
),
);
} else {
var g = group;
children.add(Select(
current: optionList.options[optionsValue[g]],
values: optionList.options.values.toList(),
onTap: (i) {
var key = optionList.options.keys.elementAt(i);
if (key == optionsValue[g]) return;
setState(() {
optionsValue[g] = key;
});
},
)); ));
if (options.last != optionList) { }
if (options!.last != optionList) {
children.add(const SizedBox(height: 8)); children.add(const SizedBox(height: 8));
} }
group++;
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@@ -43,7 +43,10 @@ class ComicSourcePage extends StatelessWidget {
try { try {
var res = await AppDio().get<String>( var res = await AppDio().get<String>(
source.url, source.url,
options: Options(responseType: ResponseType.plain), options: Options(
responseType: ResponseType.plain,
headers: {"cache-time": "no"},
),
); );
if (cancel) return; if (cancel) return;
controller?.close(); controller?.close();
@@ -200,10 +203,7 @@ class _BodyState extends State<_Body> {
); );
} }
void update( void update(ComicSource source, [bool showLoading = true]) {
ComicSource source, [
bool showLoading = true,
]) {
ComicSourcePage.update(source, showLoading); ComicSourcePage.update(source, showLoading);
} }
@@ -304,7 +304,10 @@ class _BodyState extends State<_Body> {
try { try {
var res = await AppDio().get<String>( var res = await AppDio().get<String>(
url, url,
options: Options(responseType: ResponseType.plain), options: Options(
responseType: ResponseType.plain,
headers: {"cache-time": "no"},
),
); );
if (cancel) return; if (cancel) return;
controller.close(); controller.close();
@@ -710,11 +713,13 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FilledButton.tonalIcon( return FilledButton.tonalIcon(
icon: isLoading ? SizedBox( icon: isLoading
? SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) : Icon(Icons.update), )
: Icon(Icons.update),
label: Text("Check updates".tl), label: Text("Check updates".tl),
onPressed: check, onPressed: check,
); );

View File

@@ -62,7 +62,7 @@ class DebugPageState extends State<DebugPage> {
TextButton( TextButton(
onPressed: () { onPressed: () {
try { try {
var res = JsEngine().runCode(controller.text); var res = JsEngine().runCode(controller.text, "<debug>");
setState(() { setState(() {
result = res.toString(); result = res.toString();
}); });

View File

@@ -556,26 +556,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.9" version: "11.0.1"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -941,10 +941,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.6"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1037,10 +1037,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1116,4 +1116,4 @@ packages:
version: "0.0.12" version: "0.0.12"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.6" flutter: ">=3.35.2"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.4.6+146 version: 1.5.0+150
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
flutter: 3.32.6 flutter: 3.35.2
dependencies: dependencies:
flutter: flutter: