mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 16:01:58 +00:00
Merge pull request #587 from Schnitzel5/integration/lnreader
added support for LNReader plugins
This commit is contained in:
commit
84330d2a3d
12 changed files with 1506 additions and 4 deletions
|
|
@ -4,11 +4,13 @@ import 'package:mangayomi/models/source.dart';
|
||||||
import 'dart/service.dart';
|
import 'dart/service.dart';
|
||||||
import 'javascript/service.dart';
|
import 'javascript/service.dart';
|
||||||
import 'mihon/service.dart';
|
import 'mihon/service.dart';
|
||||||
|
import 'lnreader/service.dart';
|
||||||
|
|
||||||
ExtensionService getExtensionService(Source source, String androidProxyServer) {
|
ExtensionService getExtensionService(Source source, String androidProxyServer) {
|
||||||
return switch (source.sourceCodeLanguage) {
|
return switch (source.sourceCodeLanguage) {
|
||||||
SourceCodeLanguage.dart => DartExtensionService(source),
|
SourceCodeLanguage.dart => DartExtensionService(source),
|
||||||
SourceCodeLanguage.javascript => JsExtensionService(source),
|
SourceCodeLanguage.javascript => JsExtensionService(source),
|
||||||
SourceCodeLanguage.mihon => MihonExtensionService(source, androidProxyServer),
|
SourceCodeLanguage.mihon => MihonExtensionService(source, androidProxyServer),
|
||||||
|
SourceCodeLanguage.lnreader => LNReaderExtensionService(source),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
lib/eval/lnreader/http.dart
Normal file
184
lib/eval/lnreader/http.dart
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:d4rt/d4rt.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:http_interceptor/http_interceptor.dart';
|
||||||
|
import 'package:mangayomi/services/http/m_client.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
class JsHttpClient {
|
||||||
|
late JavascriptRuntime runtime;
|
||||||
|
JsHttpClient(this.runtime);
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
InterceptedClient client() {
|
||||||
|
return MClient.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.onMessage('http_head', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "HEAD", args);
|
||||||
|
});
|
||||||
|
runtime.onMessage('http_get', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "GET", args);
|
||||||
|
});
|
||||||
|
runtime.onMessage('http_post', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "POST", args);
|
||||||
|
});
|
||||||
|
runtime.onMessage('http_put', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "PUT", args);
|
||||||
|
});
|
||||||
|
runtime.onMessage('http_delete', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "DELETE", args);
|
||||||
|
});
|
||||||
|
runtime.onMessage('http_patch', (dynamic args) async {
|
||||||
|
return await _toHttpResponse(client(), "PATCH", args);
|
||||||
|
});
|
||||||
|
runtime.evaluate('''
|
||||||
|
class Response {
|
||||||
|
constructor(url, result) {
|
||||||
|
this.url = url;
|
||||||
|
this.response = JSON.parse(result);
|
||||||
|
}
|
||||||
|
get status() {
|
||||||
|
return this.response.statusCode;
|
||||||
|
}
|
||||||
|
get statusText() {
|
||||||
|
return this.response.reasonPhrase;
|
||||||
|
}
|
||||||
|
get ok() {
|
||||||
|
return this.status >= 200 && this.status <= 299;
|
||||||
|
}
|
||||||
|
get redirected() {
|
||||||
|
return this.response.isRedirect;
|
||||||
|
}
|
||||||
|
get headers() {
|
||||||
|
return this.response.headers;
|
||||||
|
}
|
||||||
|
get body() {
|
||||||
|
return this.response.body;
|
||||||
|
}
|
||||||
|
json() {
|
||||||
|
const val = JSON.parse(this.body);
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
resolve(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
text() {
|
||||||
|
const val = this.body;
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
resolve(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchApi(url, init) {
|
||||||
|
const method = init?.method ? init.method.toLowerCase() : "get";
|
||||||
|
const result = await sendMessage(
|
||||||
|
"http_" + method,
|
||||||
|
JSON.stringify([url, init?.headers, init?.body])
|
||||||
|
);
|
||||||
|
return new Response(url, result);
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _toHttpResponse(Client client, String method, List args) async {
|
||||||
|
final url = args[0] as String;
|
||||||
|
final headers = (args[1] as Map?)?.toMapStringString ?? {};
|
||||||
|
final body = args.length >= 3
|
||||||
|
? args[2] is List
|
||||||
|
? args[2] as List
|
||||||
|
: args[2] is String
|
||||||
|
? args[2] as String
|
||||||
|
: (args[2] as Map?)?.toMapStringDynamic
|
||||||
|
: null;
|
||||||
|
var request = http.Request(method, Uri.parse(url));
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
if ((request.headers[HttpHeaders.contentTypeHeader]?.contains(
|
||||||
|
"application/json",
|
||||||
|
)) ??
|
||||||
|
false) {
|
||||||
|
request.body = json.encode(body);
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
http.StreamedResponse response = await client.send(request);
|
||||||
|
final res = Response(
|
||||||
|
"",
|
||||||
|
response.statusCode,
|
||||||
|
request: response.request,
|
||||||
|
headers: response.headers,
|
||||||
|
isRedirect: response.isRedirect,
|
||||||
|
persistentConnection: response.persistentConnection,
|
||||||
|
reasonPhrase: response.reasonPhrase,
|
||||||
|
);
|
||||||
|
Map<String, dynamic> resMap = res.toJson();
|
||||||
|
resMap["body"] = await response.stream.bytesToString();
|
||||||
|
return jsonEncode(resMap);
|
||||||
|
}
|
||||||
|
String? formData;
|
||||||
|
if (body is Map && body.containsKey("_data")) {
|
||||||
|
formData = (body.get("_data") as List<dynamic>)
|
||||||
|
.map(
|
||||||
|
(e) =>
|
||||||
|
"${Uri.encodeQueryComponent(e[0])}"
|
||||||
|
"=${Uri.encodeQueryComponent(e[1])}",
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
headers["content-type"] =
|
||||||
|
"application/x-www-form-urlencoded; charset=UTF-8";
|
||||||
|
}
|
||||||
|
final future = switch (method) {
|
||||||
|
"HEAD" => client.head(Uri.parse(url), headers: headers),
|
||||||
|
"GET" => client.get(Uri.parse(url), headers: headers),
|
||||||
|
"POST" => client.post(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: headers,
|
||||||
|
body: formData ?? body,
|
||||||
|
),
|
||||||
|
"PUT" => client.put(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: headers,
|
||||||
|
body: formData ?? body,
|
||||||
|
),
|
||||||
|
"DELETE" => client.delete(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: headers,
|
||||||
|
body: formData ?? body,
|
||||||
|
),
|
||||||
|
_ => client.patch(Uri.parse(url), headers: headers, body: formData ?? body),
|
||||||
|
};
|
||||||
|
return jsonEncode((await future).toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ResponseExtexsion on Response {
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'body': body,
|
||||||
|
'headers': headers,
|
||||||
|
'isRedirect': isRedirect,
|
||||||
|
'persistentConnection': persistentConnection,
|
||||||
|
'reasonPhrase': reasonPhrase,
|
||||||
|
'statusCode': statusCode,
|
||||||
|
'request': {
|
||||||
|
'contentLength': request?.contentLength,
|
||||||
|
'finalized': request?.finalized,
|
||||||
|
'followRedirects': request?.followRedirects,
|
||||||
|
'headers': request?.headers,
|
||||||
|
'maxRedirects': request?.maxRedirects,
|
||||||
|
'method': request?.method,
|
||||||
|
'persistentConnection': request?.persistentConnection,
|
||||||
|
'url': request?.url.toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ToMapExtension on Map? {
|
||||||
|
Map<String, dynamic>? get toMapStringDynamic {
|
||||||
|
return this?.map((key, value) => MapEntry(key.toString(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String>? get toMapStringString {
|
||||||
|
return this?.map(
|
||||||
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
414
lib/eval/lnreader/js_cheerio.dart
Normal file
414
lib/eval/lnreader/js_cheerio.dart
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
|
||||||
|
|
||||||
|
class JsCheerio {
|
||||||
|
late JavascriptRuntime runtime;
|
||||||
|
final Map<int, Element?> _elements = {};
|
||||||
|
int _elementKey = 0;
|
||||||
|
|
||||||
|
JsCheerio(this.runtime);
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
runtime.onMessage('load', (dynamic args) {
|
||||||
|
final html = args[0];
|
||||||
|
final doc = parse(html);
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = doc.body;
|
||||||
|
return _elementKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.onMessage('element_call', (dynamic args) {
|
||||||
|
final method = args[0] as String;
|
||||||
|
final key = args[1] as int;
|
||||||
|
final element = _elements[key];
|
||||||
|
|
||||||
|
if (element == null) return null;
|
||||||
|
|
||||||
|
final methodArgs = args.length > 2 ? args[2] : [];
|
||||||
|
|
||||||
|
dynamic result;
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'text':
|
||||||
|
result = element.text;
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
case 'innerHtml':
|
||||||
|
result = element.innerHtml;
|
||||||
|
break;
|
||||||
|
case 'outerHtml':
|
||||||
|
result = element.outerHtml;
|
||||||
|
break;
|
||||||
|
case 'addClass':
|
||||||
|
element.classes.add(methodArgs[0]);
|
||||||
|
break;
|
||||||
|
case 'removeClass':
|
||||||
|
element.classes.remove(methodArgs[0]);
|
||||||
|
break;
|
||||||
|
case 'hasClass':
|
||||||
|
result = element.classes.contains(methodArgs[0]);
|
||||||
|
break;
|
||||||
|
case 'attr':
|
||||||
|
result = element.attributes[methodArgs[0]] ?? '';
|
||||||
|
break;
|
||||||
|
case 'setAttr':
|
||||||
|
element.attributes[methodArgs[0]] = methodArgs[1];
|
||||||
|
break;
|
||||||
|
case 'removeAttr':
|
||||||
|
element.attributes.remove(methodArgs[0]);
|
||||||
|
break;
|
||||||
|
case 'val':
|
||||||
|
result = element.attributes['value'] ?? '';
|
||||||
|
break;
|
||||||
|
case 'setVal':
|
||||||
|
element.attributes['value'] = methodArgs[0];
|
||||||
|
break;
|
||||||
|
case 'children':
|
||||||
|
final children = element.children;
|
||||||
|
List<int> keys = [];
|
||||||
|
for (var child in children) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = child;
|
||||||
|
keys.add(_elementKey);
|
||||||
|
}
|
||||||
|
result = jsonEncode(keys);
|
||||||
|
break;
|
||||||
|
case 'parent':
|
||||||
|
final parent = element.parent;
|
||||||
|
if (parent != null) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = parent;
|
||||||
|
result = _elementKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'find':
|
||||||
|
final selector = methodArgs[0];
|
||||||
|
final elements = element.select(selector);
|
||||||
|
List<int> keys = [];
|
||||||
|
for (var el in elements ?? []) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = el;
|
||||||
|
keys.add(_elementKey);
|
||||||
|
}
|
||||||
|
result = jsonEncode(keys);
|
||||||
|
break;
|
||||||
|
case 'first':
|
||||||
|
final first = element.children.firstOrNull;
|
||||||
|
if (first != null) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = first;
|
||||||
|
result = _elementKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
final last = element.children.lastOrNull;
|
||||||
|
if (last != null) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = last;
|
||||||
|
result = _elementKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
final next = element.nextElementSibling;
|
||||||
|
if (next != null) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = next;
|
||||||
|
result = _elementKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'prev':
|
||||||
|
final prev = element.previousElementSibling;
|
||||||
|
if (prev != null) {
|
||||||
|
_elementKey++;
|
||||||
|
_elements[_elementKey] = prev;
|
||||||
|
result = _elementKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'append':
|
||||||
|
final htmlToAppend = methodArgs[0];
|
||||||
|
final newNodes = parse(htmlToAppend).children;
|
||||||
|
for (var node in newNodes) {
|
||||||
|
element.append(node);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'prepend':
|
||||||
|
final htmlToPrepend = methodArgs[0];
|
||||||
|
final newNodes = parse(htmlToPrepend).children;
|
||||||
|
for (var node in newNodes.reversed) {
|
||||||
|
element.insertBefore(node, element.firstChild);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'empty':
|
||||||
|
element.children.clear();
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
element.remove();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = 'Unsupported method: $method';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.evaluate('''
|
||||||
|
class Element {
|
||||||
|
constructor(key) {
|
||||||
|
this._key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
_call(method, args = []) {
|
||||||
|
return sendMessage("element_call", JSON.stringify([method, this._key, args]));
|
||||||
|
}
|
||||||
|
|
||||||
|
text() { return this._call("text"); }
|
||||||
|
html() { return this._call("html"); }
|
||||||
|
outerHtml() { return this._call("outerHtml"); }
|
||||||
|
val() { return this._call("val"); }
|
||||||
|
attr(name) { return this._call("attr", [name]); }
|
||||||
|
hasClass(cls) { return this._call("hasClass", [cls]); }
|
||||||
|
|
||||||
|
addClass(cls) { this._call("addClass", [cls]); return this; }
|
||||||
|
removeClass(cls) { this._call("removeClass", [cls]); return this; }
|
||||||
|
setAttr(name, value) { this._call("setAttr", [name, value]); return this; }
|
||||||
|
removeAttr(name) { this._call("removeAttr", [name]); return this; }
|
||||||
|
setVal(value) { this._call("setVal", [value]); return this; }
|
||||||
|
|
||||||
|
append(html) { this._call("append", [html]); return this; }
|
||||||
|
prepend(html) { this._call("prepend", [html]); return this; }
|
||||||
|
empty() { this._call("empty"); return this; }
|
||||||
|
remove() { this._call("remove"); return this; }
|
||||||
|
|
||||||
|
children() {
|
||||||
|
const keys = JSON.parse(this._call("children"));
|
||||||
|
return new ElementCollection(keys.map(k => new Element(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
find(selector) {
|
||||||
|
const keys = JSON.parse(this._call("find", [selector]));
|
||||||
|
return new ElementCollection(keys.map(k => new Element(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
parent() {
|
||||||
|
return new Element(this._call("parent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
return new Element(this._call("next"));
|
||||||
|
}
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
return new Element(this._call("prev"));
|
||||||
|
}
|
||||||
|
|
||||||
|
first() {
|
||||||
|
return new Element(this._call("first"));
|
||||||
|
}
|
||||||
|
|
||||||
|
last() {
|
||||||
|
return new Element(this._call("last"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElementCollection {
|
||||||
|
constructor(elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
text() {
|
||||||
|
return this.map(function(i, el) {
|
||||||
|
return el.text();
|
||||||
|
}).toArray().join("\\n") ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
html() {
|
||||||
|
return this.first()?.html();
|
||||||
|
}
|
||||||
|
|
||||||
|
outerHtml() {
|
||||||
|
return this.first()?.outerHtml();
|
||||||
|
}
|
||||||
|
|
||||||
|
attr(name) {
|
||||||
|
return this.first()?.attr(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClass(cls) {
|
||||||
|
return this.first()?.hasClass(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
each(fn) {
|
||||||
|
this.elements.forEach((el, i) => fn(i, el));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
map(fn) {
|
||||||
|
return new ElementCollection(this.elements.map((el, i) => fn(i, el)));
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(fn) {
|
||||||
|
return new ElementCollection(this.elements.filter(function (el, i) {
|
||||||
|
try {
|
||||||
|
return fn(i, el);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
addClass(cls) {
|
||||||
|
this.elements.forEach(el => el.addClass(cls));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClass(cls) {
|
||||||
|
this.elements.forEach(el => el.removeClass(cls));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttr(name, value) {
|
||||||
|
this.elements.forEach(el => el.setAttr(name, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAttr(name) {
|
||||||
|
this.elements.forEach(el => el.removeAttr(name));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVal(value) {
|
||||||
|
this.elements.forEach(el => el.setVal(value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(html) {
|
||||||
|
this.elements.forEach(el => el.append(html));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepend(html) {
|
||||||
|
this.elements.forEach(el => el.prepend(html));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty() {
|
||||||
|
this.elements.forEach(el => el.empty());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.elements.forEach(el => el.remove());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(selector) {
|
||||||
|
const found = this.elements.flatMap(el => {
|
||||||
|
const keys = JSON.parse(el._call("find", [selector]));
|
||||||
|
return keys.map(k => new Element(k));
|
||||||
|
});
|
||||||
|
return new ElementCollection(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
children() {
|
||||||
|
const children = this.elements.flatMap(el => {
|
||||||
|
const keys = JSON.parse(el._call("children"));
|
||||||
|
return keys.map(k => new Element(k));
|
||||||
|
});
|
||||||
|
return new ElementCollection(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent() {
|
||||||
|
const parents = this.elements.map(el => new Element(el._call("parent")));
|
||||||
|
return new ElementCollection(parents);
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
const nextEls = this.elements.map(el => new Element(el._call("next")));
|
||||||
|
return new ElementCollection(nextEls);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
const prevEls = this.elements.map(el => new Element(el._call("prev")));
|
||||||
|
return new ElementCollection(prevEls);
|
||||||
|
}
|
||||||
|
|
||||||
|
first() {
|
||||||
|
return this.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
last() {
|
||||||
|
return this.get(this.elements.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index) {
|
||||||
|
return this.elements[index] || new Stub();
|
||||||
|
}
|
||||||
|
|
||||||
|
length() {
|
||||||
|
return this.elements.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray() {
|
||||||
|
return this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.elements[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stub {
|
||||||
|
text() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
html() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
outerHtml() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
val() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
attr(name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
hasClass(cls) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(html) {
|
||||||
|
const rootKey = sendMessage("load", JSON.stringify([html]));
|
||||||
|
const root = new Element(rootKey);
|
||||||
|
|
||||||
|
const \$ = function(input) {
|
||||||
|
if (input instanceof ElementCollection) {
|
||||||
|
return input;
|
||||||
|
} else if (input instanceof Element) {
|
||||||
|
return input;
|
||||||
|
} else if (typeof input === "string") {
|
||||||
|
return root.find(input); // returns ElementCollection
|
||||||
|
} else if (input && input._key) {
|
||||||
|
return new ElementCollection([new Element(input._key)]);
|
||||||
|
} else {
|
||||||
|
return new ElementCollection([new Element(input)]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
\$.html = function() {
|
||||||
|
return root.html();
|
||||||
|
};
|
||||||
|
\$.root = root;
|
||||||
|
\$.Element = Element;
|
||||||
|
\$.Collection = ElementCollection;
|
||||||
|
|
||||||
|
return \$;
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
202
lib/eval/lnreader/js_htmlparser.dart
Normal file
202
lib/eval/lnreader/js_htmlparser.dart
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
|
||||||
|
class JsHtmlParser {
|
||||||
|
late JavascriptRuntime runtime;
|
||||||
|
JsHtmlParser(this.runtime);
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
runtime.evaluate('''
|
||||||
|
class Parser {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = options;
|
||||||
|
this.buffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
isVoidElement(name) {
|
||||||
|
return [
|
||||||
|
"area",
|
||||||
|
"base",
|
||||||
|
"basefont",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"command",
|
||||||
|
"embed",
|
||||||
|
"frame",
|
||||||
|
"hr",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"isindex",
|
||||||
|
"keygen",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"param",
|
||||||
|
"source",
|
||||||
|
"track",
|
||||||
|
"wbr",
|
||||||
|
].includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(html) {
|
||||||
|
this.buffer += html;
|
||||||
|
let i = 0;
|
||||||
|
let textStart = 0;
|
||||||
|
const len = this.buffer.length;
|
||||||
|
let insideQuote = null;
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
const ch = this.buffer[i];
|
||||||
|
|
||||||
|
// Track string literals
|
||||||
|
if ((ch === '"' || ch === "'")) {
|
||||||
|
if (insideQuote === ch) {
|
||||||
|
insideQuote = null;
|
||||||
|
} else if (insideQuote === null) {
|
||||||
|
insideQuote = ch;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '<' && insideQuote === null) {
|
||||||
|
// Emit any text before the tag
|
||||||
|
if (i > textStart && this.options.ontext) {
|
||||||
|
const text = this.buffer.slice(textStart, i);
|
||||||
|
this.options.ontext(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagStart = i;
|
||||||
|
i++;
|
||||||
|
|
||||||
|
const isClosing = this.buffer[i] === '/';
|
||||||
|
if (isClosing) i++;
|
||||||
|
|
||||||
|
// Parse tag name
|
||||||
|
const nameStart = i;
|
||||||
|
while (i < len && /[a-zA-Z0-9:-]/.test(this.buffer[i])) i++;
|
||||||
|
const nameEnd = i;
|
||||||
|
const tagName = this.buffer.slice(nameStart, nameEnd);
|
||||||
|
|
||||||
|
if (isClosing) {
|
||||||
|
if (this.options.onclosetag) {
|
||||||
|
this.options.onclosetag(tagName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.options.onopentagname) {
|
||||||
|
this.options.onopentagname(tagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attributes
|
||||||
|
let attrs = {};
|
||||||
|
let attrName = '';
|
||||||
|
let attrValue = '';
|
||||||
|
let readingAttrName = true;
|
||||||
|
let inAttrQuote = null;
|
||||||
|
|
||||||
|
while (i < len && this.buffer[i] !== '>') {
|
||||||
|
const c = this.buffer[i];
|
||||||
|
|
||||||
|
// Skip over whitespace
|
||||||
|
if (/\\s/.test(c)) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle self-closing tag
|
||||||
|
if (c === '/' && this.buffer[i + 1] === '>') {
|
||||||
|
if (!isClosing && this.options.onselfclosingtag) {
|
||||||
|
this.options.onselfclosingtag();
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
textStart = i;
|
||||||
|
if (this.options.onopentag) {
|
||||||
|
this.options.onopentag(tagName, attrs);
|
||||||
|
}
|
||||||
|
if (this.options.onopentagend) {
|
||||||
|
this.options.onopentagend();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attribute name
|
||||||
|
let attrStart = i;
|
||||||
|
while (i < len && /[^\\s=>]/.test(this.buffer[i])) i++;
|
||||||
|
attrName = this.buffer.slice(attrStart, i);
|
||||||
|
|
||||||
|
// Skip whitespace after name
|
||||||
|
while (i < len && /\\s/.test(this.buffer[i])) i++;
|
||||||
|
|
||||||
|
// Expect '='
|
||||||
|
if (this.buffer[i] === '=') {
|
||||||
|
i++; // skip '='
|
||||||
|
|
||||||
|
// Skip whitespace after '='
|
||||||
|
while (i < len && /\\s/.test(this.buffer[i])) i++;
|
||||||
|
|
||||||
|
const quote = this.buffer[i];
|
||||||
|
if (quote === '"' || quote === "'") {
|
||||||
|
inAttrQuote = quote;
|
||||||
|
i++; // skip quote
|
||||||
|
|
||||||
|
const valStart = i;
|
||||||
|
while (i < len && this.buffer[i] !== inAttrQuote) i++;
|
||||||
|
attrValue = this.buffer.slice(valStart, i);
|
||||||
|
i++; // skip closing quote
|
||||||
|
} else {
|
||||||
|
// Unquoted value
|
||||||
|
const valStart = i;
|
||||||
|
while (i < len && /[^\\s>]/.test(this.buffer[i])) i++;
|
||||||
|
attrValue = this.buffer.slice(valStart, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit merged attribute callback
|
||||||
|
if (this.options.onattribute) {
|
||||||
|
this.options.onattribute(attrName, attrValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[attrName] = attrValue;
|
||||||
|
attrName = '';
|
||||||
|
attrValue = '';
|
||||||
|
} else {
|
||||||
|
// attribute without value (e.g. `disabled`)
|
||||||
|
if (this.options.onattribute) {
|
||||||
|
this.options.onattribute(attrName, null);
|
||||||
|
}
|
||||||
|
attrs[attrName] = null;
|
||||||
|
attrName = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing normal tag '>'
|
||||||
|
i++; // skip '>'
|
||||||
|
|
||||||
|
if (!isClosing && this.options.onopentag) {
|
||||||
|
this.options.onopentag(tagName, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.onopentagend) {
|
||||||
|
this.options.onopentagend();
|
||||||
|
}
|
||||||
|
|
||||||
|
textStart = i;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit any remaining text
|
||||||
|
if (textStart < len && this.options.ontext) {
|
||||||
|
const text = this.buffer.slice(textStart, len);
|
||||||
|
this.options.ontext(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
if (this.options.onend) {
|
||||||
|
this.options.onend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
159
lib/eval/lnreader/js_libs.dart
Normal file
159
lib/eval/lnreader/js_libs.dart
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:mangayomi/utils/log/log.dart';
|
||||||
|
|
||||||
|
class JsLibs {
|
||||||
|
late JavascriptRuntime runtime;
|
||||||
|
JsLibs(this.runtime);
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
runtime.onMessage('log', (dynamic args) {
|
||||||
|
Logger.add(LoggerLevel.warning, "${args[0]}");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
runtime.onMessage('urlencode', (dynamic args) {
|
||||||
|
return Uri.encodeComponent(args[0]);
|
||||||
|
});
|
||||||
|
runtime.onMessage('urldecode', (dynamic args) {
|
||||||
|
return Uri.decodeComponent(args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.evaluate('''
|
||||||
|
console.log = function (message) {
|
||||||
|
if (typeof message === "object") {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
sendMessage("log", JSON.stringify([message.toString()]));
|
||||||
|
};
|
||||||
|
console.warn = function (message) {
|
||||||
|
if (typeof message === "object") {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
sendMessage("log", JSON.stringify([message.toString()]));
|
||||||
|
};
|
||||||
|
console.error = function (message) {
|
||||||
|
if (typeof message === "object") {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
sendMessage("log", JSON.stringify([message.toString()]));
|
||||||
|
};
|
||||||
|
String.prototype.substringAfter = function(pattern) {
|
||||||
|
const startIndex = this.indexOf(pattern);
|
||||||
|
if (startIndex === -1) return this.substring(0);
|
||||||
|
|
||||||
|
const start = startIndex + pattern.length;
|
||||||
|
return this.substring(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.substringAfterLast = function(pattern) {
|
||||||
|
return this.split(pattern).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.substringBefore = function(pattern) {
|
||||||
|
const endIndex = this.indexOf(pattern);
|
||||||
|
if (endIndex === -1) return this.substring(0);
|
||||||
|
|
||||||
|
return this.substring(0, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.substringBeforeLast = function(pattern) {
|
||||||
|
const endIndex = this.lastIndexOf(pattern);
|
||||||
|
if (endIndex === -1) return this.substring(0);
|
||||||
|
return this.substring(0, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.substringBetween = function(left, right) {
|
||||||
|
let startIndex = 0;
|
||||||
|
let index = this.indexOf(left, startIndex);
|
||||||
|
if (index === -1) return "";
|
||||||
|
let leftIndex = index + left.length;
|
||||||
|
let rightIndex = this.indexOf(right, leftIndex);
|
||||||
|
if (rightIndex === -1) return "";
|
||||||
|
startIndex = rightIndex + right.length;
|
||||||
|
return this.substring(leftIndex, rightIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function jsonStringify(fn) {
|
||||||
|
return JSON.stringify(await fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUrlAbsolute = url => {
|
||||||
|
if (url) {
|
||||||
|
if (url.indexOf("//") === 0) {
|
||||||
|
return true
|
||||||
|
} // URL is protocol-relative (= absolute)
|
||||||
|
if (url.indexOf("://") === -1) {
|
||||||
|
return false
|
||||||
|
} // URL has no protocol (= relative)
|
||||||
|
if (url.indexOf(".") === -1) {
|
||||||
|
return false
|
||||||
|
} // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
|
||||||
|
if (url.indexOf("/") === -1) {
|
||||||
|
return false
|
||||||
|
} // URL does not contain a single slash (= relative)
|
||||||
|
if (url.indexOf(":") > url.indexOf("/")) {
|
||||||
|
return false
|
||||||
|
} // The first colon comes after the first slash (= relative)
|
||||||
|
if (url.indexOf("://") < url.indexOf(".")) {
|
||||||
|
return true
|
||||||
|
} // Protocol is defined before first dot (= absolute)
|
||||||
|
}
|
||||||
|
return false // Anything else must be relative
|
||||||
|
}
|
||||||
|
|
||||||
|
const NovelStatus = {
|
||||||
|
"Unknown": "Unknown",
|
||||||
|
"Ongoing": "Ongoing",
|
||||||
|
"Completed": "Completed",
|
||||||
|
"Licensed": "Licensed",
|
||||||
|
"PublishingFinished": "Publishing Finished",
|
||||||
|
"Cancelled": "Cancelled",
|
||||||
|
"OnHiatus": "On Hiatus"
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterTypes = {
|
||||||
|
"TextInput": "Text",
|
||||||
|
"Picker": "Picker",
|
||||||
|
"CheckboxGroup": "Checkbox",
|
||||||
|
"Switch": "Switch",
|
||||||
|
"ExcludableCheckboxGroup": "XCheckbox"
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPickerValue = q => {
|
||||||
|
return q.type === FilterTypes.Picker && typeof q.value === "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCheckboxValue = q => {
|
||||||
|
return q.type === FilterTypes.CheckboxGroup && Array.isArray(q.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSwitchValue = q => {
|
||||||
|
return q.type === FilterTypes.Switch && typeof q.value === "boolean"
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTextValue = q => {
|
||||||
|
return q.type === FilterTypes.TextInput && typeof q.value === "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
const isXCheckboxValue = q => {
|
||||||
|
return (
|
||||||
|
q.type === FilterTypes.ExcludableCheckboxGroup &&
|
||||||
|
typeof q.value === "object" &&
|
||||||
|
!Array.isArray(q.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlencode(input) {
|
||||||
|
return sendMessage(
|
||||||
|
"urlencode",
|
||||||
|
JSON.stringify([input])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function urldecode(input) {
|
||||||
|
return sendMessage(
|
||||||
|
"urldecode",
|
||||||
|
JSON.stringify([input])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/eval/lnreader/js_polyfills.dart
Normal file
39
lib/eval/lnreader/js_polyfills.dart
Normal file
File diff suppressed because one or more lines are too long
136
lib/eval/lnreader/m_plugin.dart
Normal file
136
lib/eval/lnreader/m_plugin.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
class ChapterItem {
|
||||||
|
String name;
|
||||||
|
String path;
|
||||||
|
String? releaseTime;
|
||||||
|
int? chapterNumber;
|
||||||
|
String? page;
|
||||||
|
|
||||||
|
ChapterItem({
|
||||||
|
required this.name,
|
||||||
|
required this.path,
|
||||||
|
this.releaseTime,
|
||||||
|
this.chapterNumber,
|
||||||
|
this.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChapterItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ChapterItem(
|
||||||
|
name: json['name'],
|
||||||
|
path: json['path'],
|
||||||
|
releaseTime: json['releaseTime'],
|
||||||
|
chapterNumber: json['chapterNumber'] != null
|
||||||
|
? (json['chapterNumber'] as num?)?.toInt() ??
|
||||||
|
int.tryParse(json['chapterNumber'])
|
||||||
|
: null,
|
||||||
|
page: json['page'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'path': path,
|
||||||
|
'releaseTime': releaseTime,
|
||||||
|
'chapterNumber': chapterNumber,
|
||||||
|
'page': page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NovelItem {
|
||||||
|
String name;
|
||||||
|
String path;
|
||||||
|
String? cover;
|
||||||
|
|
||||||
|
NovelItem({required this.name, required this.path, this.cover});
|
||||||
|
|
||||||
|
factory NovelItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return NovelItem(
|
||||||
|
name: json['name'],
|
||||||
|
path: json['path'],
|
||||||
|
cover: json['cover'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'name': name, 'path': path, 'cover': cover};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceNovel extends NovelItem {
|
||||||
|
String? genres;
|
||||||
|
String? summary;
|
||||||
|
String? author;
|
||||||
|
String? artist;
|
||||||
|
String? status;
|
||||||
|
double? rating;
|
||||||
|
List<ChapterItem>? chapters;
|
||||||
|
|
||||||
|
SourceNovel({
|
||||||
|
required super.name,
|
||||||
|
required super.path,
|
||||||
|
super.cover,
|
||||||
|
this.genres,
|
||||||
|
this.summary,
|
||||||
|
this.author,
|
||||||
|
this.artist,
|
||||||
|
this.status,
|
||||||
|
this.rating,
|
||||||
|
this.chapters,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SourceNovel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SourceNovel(
|
||||||
|
name: json['name'],
|
||||||
|
path: json['path'],
|
||||||
|
cover: json['cover'],
|
||||||
|
genres: json['genres'],
|
||||||
|
summary: json['summary'],
|
||||||
|
author: json['author'],
|
||||||
|
artist: json['artist'],
|
||||||
|
status: json['status'],
|
||||||
|
rating: json['rating'] is double
|
||||||
|
? json['rating']
|
||||||
|
: json['rating']?.toDouble(),
|
||||||
|
chapters: (json['chapters'] as List<dynamic>?)
|
||||||
|
?.map((item) => ChapterItem.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'path': path,
|
||||||
|
'cover': cover,
|
||||||
|
'genres': genres,
|
||||||
|
'summary': summary,
|
||||||
|
'author': author,
|
||||||
|
'artist': artist,
|
||||||
|
'status': status,
|
||||||
|
'rating': rating,
|
||||||
|
'chapters': chapters?.map((item) => item.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourcePage {
|
||||||
|
List<ChapterItem> chapters;
|
||||||
|
|
||||||
|
SourcePage({required this.chapters});
|
||||||
|
|
||||||
|
factory SourcePage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SourcePage(
|
||||||
|
chapters:
|
||||||
|
(json['chapters'] as List<dynamic>?)
|
||||||
|
?.map((item) => ChapterItem.fromJson(item))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'chapters': chapters.map((item) => item.toJson()).toList()};
|
||||||
|
}
|
||||||
|
}
|
||||||
279
lib/eval/lnreader/service.dart
Normal file
279
lib/eval/lnreader/service.dart
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:mangayomi/eval/lnreader/http.dart';
|
||||||
|
import 'package:mangayomi/eval/lnreader/m_plugin.dart';
|
||||||
|
import 'package:mangayomi/eval/model/filter.dart';
|
||||||
|
import 'package:mangayomi/eval/model/m_chapter.dart';
|
||||||
|
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||||
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
|
import 'package:mangayomi/eval/model/source_preference.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/page.dart';
|
||||||
|
import 'package:mangayomi/models/source.dart';
|
||||||
|
import 'package:mangayomi/models/video.dart';
|
||||||
|
|
||||||
|
import '../interface.dart';
|
||||||
|
import 'js_cheerio.dart';
|
||||||
|
import 'js_htmlparser.dart';
|
||||||
|
import 'js_libs.dart';
|
||||||
|
import 'js_polyfills.dart';
|
||||||
|
|
||||||
|
JavascriptRuntime getJavascriptRuntime({
|
||||||
|
Map<String, dynamic>? extraArgs = const {},
|
||||||
|
}) {
|
||||||
|
JavascriptRuntime runtime;
|
||||||
|
runtime = QuickJsRuntime2(stackSize: 1024 * 1024 * 4);
|
||||||
|
runtime.enableHandlePromises();
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LNReaderExtensionService implements ExtensionService {
|
||||||
|
late JavascriptRuntime runtime;
|
||||||
|
@override
|
||||||
|
late Source source;
|
||||||
|
|
||||||
|
LNReaderExtensionService(this.source);
|
||||||
|
|
||||||
|
void _init() {
|
||||||
|
runtime = getJavascriptRuntime();
|
||||||
|
runtime.evaluate('''
|
||||||
|
module={},exports=Function("return this")(),Object.defineProperties(module,{namespace:{set:function(a){exports=a}},exports:{set:function(a){for(var b in a)a.hasOwnProperty(b)&&(exports[b]=a[b])},get:function(){return exports}}});
|
||||||
|
''');
|
||||||
|
JsPolyfills(runtime).init();
|
||||||
|
JsHttpClient(runtime).init();
|
||||||
|
JsLibs(runtime).init();
|
||||||
|
JsHtmlParser(runtime).init();
|
||||||
|
JsCheerio(runtime).init();
|
||||||
|
runtime.evaluate('''
|
||||||
|
const require = (package) => {
|
||||||
|
switch (package) {
|
||||||
|
case "htmlparser2":
|
||||||
|
return {Parser: Parser};
|
||||||
|
case "cheerio":
|
||||||
|
return {load: load};
|
||||||
|
case "dayjs":
|
||||||
|
return module.exports.dayjs;
|
||||||
|
case "urlencode":
|
||||||
|
return {encode: urlencode, decode: urldecode};
|
||||||
|
case "@libs/fetch":
|
||||||
|
return {fetchApi: fetchApi};
|
||||||
|
case "@libs/novelStatus":
|
||||||
|
return {NovelStatus: NovelStatus};
|
||||||
|
case "@libs/isAbsoluteUrl":
|
||||||
|
return {isUrlAbsolute: isUrlAbsolute};
|
||||||
|
case "@libs/filterInputs":
|
||||||
|
return {
|
||||||
|
FilterTypes: FilterTypes,
|
||||||
|
isPickerValue: isPickerValue,
|
||||||
|
isCheckboxValue: isCheckboxValue,
|
||||||
|
isSwitchValue: isSwitchValue,
|
||||||
|
isTextValue: isTextValue,
|
||||||
|
isXCheckboxValue: isXCheckboxValue
|
||||||
|
};
|
||||||
|
case "@libs/defaultCover":
|
||||||
|
return {defaultCover: 'https://raw.githubusercontent.com/LNReader/lnreader-plugins/refs/heads/master/public/static/coverNotAvailable.webp'};
|
||||||
|
case "@libs/storage":
|
||||||
|
return {storage: {get: () => null}};
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
''');
|
||||||
|
runtime.evaluate('''
|
||||||
|
${source.sourceCode}
|
||||||
|
const extension = exports.default;
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> getHeaders() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get supportsLatest {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sourceBaseUrl {
|
||||||
|
return source.baseUrl!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MPages> getPopular(int page) async {
|
||||||
|
final items =
|
||||||
|
((await _extensionCallAsync(
|
||||||
|
'popularNovels($page, {showLatestNovels: false, filters: extension.filters})',
|
||||||
|
[],
|
||||||
|
)))
|
||||||
|
.map((e) => NovelItem.fromJson(e))
|
||||||
|
.map(
|
||||||
|
(e) => MManga(
|
||||||
|
name: e.name,
|
||||||
|
imageUrl: e.cover,
|
||||||
|
link: e.path,
|
||||||
|
chapters: [],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return MPages(list: items, hasNextPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MPages> getLatestUpdates(int page) async {
|
||||||
|
final items =
|
||||||
|
((await _extensionCallAsync(
|
||||||
|
'popularNovels($page, {showLatestNovels: true, filters: extension.filters})',
|
||||||
|
[],
|
||||||
|
)))
|
||||||
|
.map((e) => NovelItem.fromJson(e))
|
||||||
|
.map(
|
||||||
|
(e) => MManga(
|
||||||
|
name: e.name,
|
||||||
|
imageUrl: e.cover,
|
||||||
|
link: e.path,
|
||||||
|
chapters: [],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return MPages(list: items, hasNextPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MPages> search(String query, int page, List<dynamic> filters) async {
|
||||||
|
final items =
|
||||||
|
((await _extensionCallAsync('searchNovels("$query",$page)', [])))
|
||||||
|
.map((e) => NovelItem.fromJson(e))
|
||||||
|
.map(
|
||||||
|
(e) => MManga(
|
||||||
|
name: e.name,
|
||||||
|
imageUrl: e.cover,
|
||||||
|
link: e.path,
|
||||||
|
chapters: [],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return MPages(list: items, hasNextPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MManga> getDetail(String url) async {
|
||||||
|
final item = SourceNovel.fromJson(
|
||||||
|
await _extensionCallAsync('parseNovel(`$url`)', {}),
|
||||||
|
);
|
||||||
|
final chapters = SourcePage.fromJson(
|
||||||
|
await _extensionCallAsync('parsePage(`${item.path}`, `1`)', {}),
|
||||||
|
);
|
||||||
|
return MManga(
|
||||||
|
name: item.name,
|
||||||
|
imageUrl: item.cover,
|
||||||
|
link: item.path,
|
||||||
|
artist: item.artist,
|
||||||
|
author: item.author,
|
||||||
|
description: item.summary,
|
||||||
|
status: switch (item.status) {
|
||||||
|
"Ongoing" => Status.ongoing,
|
||||||
|
"Completed" => Status.completed,
|
||||||
|
_ => Status.unknown,
|
||||||
|
},
|
||||||
|
genre: item.genres?.split(","),
|
||||||
|
chapters:
|
||||||
|
(chapters.chapters.isNotEmpty ? chapters.chapters : item.chapters)
|
||||||
|
?.map(
|
||||||
|
(e) => MChapter(
|
||||||
|
name: e.name,
|
||||||
|
url: e.path,
|
||||||
|
dateUpload: e.releaseTime != null
|
||||||
|
? DateTime.tryParse(
|
||||||
|
e.releaseTime!,
|
||||||
|
)?.millisecondsSinceEpoch.toString() ??
|
||||||
|
int.tryParse(e.releaseTime!)?.toString() ??
|
||||||
|
DateTime.now().millisecondsSinceEpoch.toString()
|
||||||
|
: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PageUrl>> getPageList(String url) async {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Video>> getVideoList(String url) async {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getHtmlContent(String name, String url) async {
|
||||||
|
_init();
|
||||||
|
final res = (await runtime.handlePromise(
|
||||||
|
await runtime.evaluateAsync(
|
||||||
|
'jsonStringify(() => extension.parseChapter(`$url`))',
|
||||||
|
),
|
||||||
|
)).stringResult;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> cleanHtmlContent(String html) async {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FilterList getFilterList() {
|
||||||
|
List<dynamic> list;
|
||||||
|
|
||||||
|
try {
|
||||||
|
list = fromJsonFilterValuesToList(_extensionCall('filters', []));
|
||||||
|
} catch (_) {
|
||||||
|
list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<SourcePreference> getSourcePreferences() {
|
||||||
|
return _extensionCall(
|
||||||
|
'pluginSettings',
|
||||||
|
[],
|
||||||
|
).map((e) => SourcePreference.fromJson(e)..sourceId = source.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
T _extensionCall<T>(String call, T def) {
|
||||||
|
_init();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = runtime.evaluate('JSON.stringify(extension.$call)');
|
||||||
|
|
||||||
|
return jsonDecode(res.stringResult) as T;
|
||||||
|
} catch (_) {
|
||||||
|
if (def != null) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _extensionCallAsync<T>(String call, T def) async {
|
||||||
|
_init();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final promised = await runtime.handlePromise(
|
||||||
|
await runtime.evaluateAsync('jsonStringify(() => extension.$call)'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonDecode(promised.stringResult) as T;
|
||||||
|
} catch (e) {
|
||||||
|
if (def != null) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ class Source {
|
||||||
filterList = json['filterList'];
|
filterList = json['filterList'];
|
||||||
preferenceList = json['preferenceList'];
|
preferenceList = json['preferenceList'];
|
||||||
iconUrl = json['iconUrl'];
|
iconUrl = json['iconUrl'];
|
||||||
id = json['id'];
|
id = json['id'] is int ? json['id'] : null;
|
||||||
isActive = json['isActive'];
|
isActive = json['isActive'];
|
||||||
isAdded = json['isAdded'];
|
isAdded = json['isAdded'];
|
||||||
isFullData = json['isFullData'];
|
isFullData = json['isFullData'];
|
||||||
|
|
@ -216,4 +216,9 @@ class Source {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SourceCodeLanguage { dart, javascript, mihon }
|
enum SourceCodeLanguage {
|
||||||
|
dart,
|
||||||
|
javascript,
|
||||||
|
mihon,
|
||||||
|
lnreader
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -475,11 +475,13 @@ const _SourcesourceCodeLanguageEnumValueMap = {
|
||||||
'dart': 0,
|
'dart': 0,
|
||||||
'javascript': 1,
|
'javascript': 1,
|
||||||
'mihon': 2,
|
'mihon': 2,
|
||||||
|
'lnreader': 3,
|
||||||
};
|
};
|
||||||
const _SourcesourceCodeLanguageValueEnumMap = {
|
const _SourcesourceCodeLanguageValueEnumMap = {
|
||||||
0: SourceCodeLanguage.dart,
|
0: SourceCodeLanguage.dart,
|
||||||
1: SourceCodeLanguage.javascript,
|
1: SourceCodeLanguage.javascript,
|
||||||
2: SourceCodeLanguage.mihon,
|
2: SourceCodeLanguage.mihon,
|
||||||
|
3: SourceCodeLanguage.lnreader,
|
||||||
};
|
};
|
||||||
|
|
||||||
Id _sourceGetId(Source object) {
|
Id _sourceGetId(Source object) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ class _CreateExtensionState extends State<CreateExtension> {
|
||||||
int _languageIndex = 0;
|
int _languageIndex = 0;
|
||||||
final List<String> _sourceTypes = ["single", "multi", "torrent"];
|
final List<String> _sourceTypes = ["single", "multi", "torrent"];
|
||||||
final List<String> _itemTypes = ["Manga", "Anime", "Novel"];
|
final List<String> _itemTypes = ["Manga", "Anime", "Novel"];
|
||||||
final List<String> _languages = ["Dart", "JavaScript"];
|
final List<String> _languages = [
|
||||||
|
"Dart",
|
||||||
|
"JavaScript",
|
||||||
|
"LNReader compiled JS",
|
||||||
|
];
|
||||||
SourceCodeLanguage _sourceCodeLanguage = SourceCodeLanguage.dart;
|
SourceCodeLanguage _sourceCodeLanguage = SourceCodeLanguage.dart;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -67,9 +71,11 @@ class _CreateExtensionState extends State<CreateExtension> {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (v == 0) {
|
if (v == 0) {
|
||||||
_sourceCodeLanguage = SourceCodeLanguage.dart;
|
_sourceCodeLanguage = SourceCodeLanguage.dart;
|
||||||
} else {
|
} else if (v == 1) {
|
||||||
_sourceCodeLanguage =
|
_sourceCodeLanguage =
|
||||||
SourceCodeLanguage.javascript;
|
SourceCodeLanguage.javascript;
|
||||||
|
} else {
|
||||||
|
_sourceCodeLanguage = SourceCodeLanguage.lnreader;
|
||||||
}
|
}
|
||||||
_languageIndex = v!;
|
_languageIndex = v!;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,39 @@ Future<void> fetchSourcesList({
|
||||||
src.id = 'mihon-${source['id']}'.hashCode;
|
src.id = 'mihon-${source['id']}'.hashCode;
|
||||||
yield src;
|
yield src;
|
||||||
}
|
}
|
||||||
|
} else if (e['id'] is String &&
|
||||||
|
e['name'] != null &&
|
||||||
|
e['site'] != null &&
|
||||||
|
e['lang'] != null &&
|
||||||
|
e['version'] != null &&
|
||||||
|
e['url'] != null &&
|
||||||
|
e['iconUrl'] != null) {
|
||||||
|
final src = Source.fromJson(e)
|
||||||
|
..apiUrl = ''
|
||||||
|
..appMinVerReq = ''
|
||||||
|
..dateFormat = ''
|
||||||
|
..dateFormatLocale = ''
|
||||||
|
..hasCloudflare = false
|
||||||
|
..headers = ''
|
||||||
|
..isActive = true
|
||||||
|
..isAdded = false
|
||||||
|
..isFullData = false
|
||||||
|
..isNsfw = false
|
||||||
|
..isPinned = false
|
||||||
|
..lastUsed = false
|
||||||
|
..sourceCode = ''
|
||||||
|
..typeSource = ''
|
||||||
|
..versionLast = '0.0.1'
|
||||||
|
..isObsolete = false
|
||||||
|
..isLocal = false
|
||||||
|
..lang = _convertLang(e)
|
||||||
|
..baseUrl = e['site']
|
||||||
|
..sourceCodeUrl = e['url']
|
||||||
|
..sourceCodeLanguage = SourceCodeLanguage.lnreader
|
||||||
|
..itemType = ItemType.novel
|
||||||
|
..notes = "Performance might be poor due to limited engine";
|
||||||
|
src.id = 'lnreader-plugin-"${src.name}"."${src.lang}"'.hashCode;
|
||||||
|
yield src;
|
||||||
} else {
|
} else {
|
||||||
yield Source.fromJson(e);
|
yield Source.fromJson(e);
|
||||||
}
|
}
|
||||||
|
|
@ -447,3 +480,44 @@ Future<List<SourcePreference>?> fetchPreferencesDalvik(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _convertLang(dynamic e) {
|
||||||
|
final lang = e['lang'];
|
||||||
|
if (lang is String) {
|
||||||
|
switch (lang) {
|
||||||
|
case "العربية":
|
||||||
|
return "ar";
|
||||||
|
case "中文, 汉语, 漢語":
|
||||||
|
return "zh";
|
||||||
|
case "English":
|
||||||
|
return "en";
|
||||||
|
case "Français":
|
||||||
|
return "fr";
|
||||||
|
case "Bahasa Indonesia":
|
||||||
|
return "id";
|
||||||
|
case "日本語":
|
||||||
|
return "ja";
|
||||||
|
case "조선말, 한국어":
|
||||||
|
return "ko";
|
||||||
|
case "Polski":
|
||||||
|
return "pl";
|
||||||
|
case "Português":
|
||||||
|
return "pt";
|
||||||
|
case "Русский":
|
||||||
|
return "ru";
|
||||||
|
case "Español":
|
||||||
|
return "es";
|
||||||
|
case "ไทย":
|
||||||
|
return "th";
|
||||||
|
case "Türkçe":
|
||||||
|
return "tr";
|
||||||
|
case "Українська":
|
||||||
|
return "uk";
|
||||||
|
case "Tiếng Việt":
|
||||||
|
return "vi";
|
||||||
|
default:
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "all";
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue