adding support for LNReader plugins

This commit is contained in:
Schnitzel5 2025-10-10 15:49:33 +02:00
parent 1fa7f3123b
commit 5ef46ec13d
12 changed files with 1143 additions and 4 deletions

View file

@ -4,11 +4,13 @@ import 'package:mangayomi/models/source.dart';
import 'dart/service.dart';
import 'javascript/service.dart';
import 'mihon/service.dart';
import 'lnreader/service.dart';
ExtensionService getExtensionService(Source source, String androidProxyServer) {
return switch (source.sourceCodeLanguage) {
SourceCodeLanguage.dart => DartExtensionService(source),
SourceCodeLanguage.javascript => JsExtensionService(source),
SourceCodeLanguage.mihon => MihonExtensionService(source, androidProxyServer),
SourceCodeLanguage.lnreader => LNReaderExtensionService(source),
};
}

178
lib/eval/lnreader/http.dart Normal file
View file

@ -0,0 +1,178 @@
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() {
return JSON.parse(this.body);
}
text() {
return this.body;
}
}
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()),
);
}
}

View file

@ -0,0 +1,269 @@
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.documentElement;
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('''
function \$(key) {
return {
_key: key,
_call: function(method, args) {
if (!args) {
args = [];
}
return sendMessage("element_call", JSON.stringify([method, this._key, args]));
},
text: function() {
return this._call("text");
},
html: function() {
return this._call("html");
},
outerHtml: function() {
return this._call("outerHtml");
},
addClass: function(cls) {
this._call("addClass", [cls]);
return this;
},
removeClass: function(cls) {
this._call("removeClass", [cls]);
return this;
},
hasClass: function(cls) {
return this._call("hasClass", [cls]);
},
attr: function(name) {
return this._call("attr", [name]);
},
setAttr: function(name, value) {
this._call("setAttr", [name, value]);
return this;
},
removeAttr: function(name) {
this._call("removeAttr", [name]);
return this;
},
val: function() {
return this._call("val");
},
setVal: function(value) {
this._call("setVal", [value]);
return this;
},
children: function() {
let result = this._call("children");
return JSON.parse(result).map(k => \$(k));
},
parent: function() {
let k = this._call("parent");
return \$(k);
},
find: function(selector) {
let result = this._call("find", [selector]);
return JSON.parse(result).map(k => \$(k));
},
first: function() {
let k = this._call("first");
return \$(k);
},
last: function() {
let k = this._call("last");
return \$(k);
},
next: function() {
let k = this._call("next");
return \$(k);
},
prev: function() {
let k = this._call("prev");
return \$(k);
},
append: function(html) {
this._call("append", [html]);
return this;
},
prepend: function(html) {
this._call("prepend", [html]);
return this;
},
empty: function() {
this._call("empty");
return this;
},
remove: function() {
this._call("remove");
return this;
},
each: function(fn) {
this.children().forEach((child, i) => fn(child, i));
return this;
},
map(fn) {
return this.children().map((child, i) => fn(child, i));
},
filter(fn) {
return this.children().filter((child, i) => fn(child, i));
}
};
}
function load(html) {
const key = sendMessage("load", JSON.stringify([html]));
return \$(key);
}
''');
}
}

View file

@ -0,0 +1,12 @@
import 'package:flutter_qjs/flutter_qjs.dart';
class JsHtmlParser {
late JavascriptRuntime runtime;
JsHtmlParser(this.runtime);
void init() {
runtime.evaluate('''
class Parser{constructor(t={}){this.options=t,this.buffer=""}write(t){this.buffer+=t;let s=0,o=0;const i=this.buffer.length;let n=null;for(;s<i;){const t=this.buffer[s];if('"'!==t&&"'"!==t)if("<"===t&&null===n){if(s>o&&this.options.ontext){const t=this.buffer.slice(o,s);this.options.ontext(t)}s++;const t="/"===this.buffer[s];t&&s++;const n=s;for(;s<i&&/[a-zA-Z0-9:-]/.test(this.buffer[s]);)s++;const e=s,f=this.buffer.slice(n,e);t?this.options.onclosetag&&this.options.onclosetag(f):this.options.onopentagname&&this.options.onopentagname(f);let h={},r="",l="",u=null;for(;s<i&&">"!==this.buffer[s];){const n=this.buffer[s];if(/\\s/.test(n)){s++;continue}if("/"===n&&">"===this.buffer[s+1])return!t&&this.options.onselfclosingtag&&this.options.onselfclosingtag(),s+=2,o=s,this.options.onopentag&&this.options.onopentag(f,h),void(this.options.onopentagend&&this.options.onopentagend());let e=s;for(;s<i&&/[^\\s=>]/.test(this.buffer[s]);)s++;for(r=this.buffer.slice(e,s);s<i&&/\\s/.test(this.buffer[s]);)s++;if("="===this.buffer[s]){for(s++;s<i&&/\\s/.test(this.buffer[s]);)s++;const t=this.buffer[s];if('"'===t||"'"===t){u=t,s++;const o=s;for(;s<i&&this.buffer[s]!==u;)s++;l=this.buffer.slice(o,s),s++}else{const t=s;for(;s<i&&/[^\\s>]/.test(this.buffer[s]);)s++;l=this.buffer.slice(t,s)}this.options.onattribute&&this.options.onattribute(r,l),h[r]=l,r="",l=""}else this.options.onattribute&&this.options.onattribute(r,null),h[r]=null,r=""}s++,!t&&this.options.onopentag&&this.options.onopentag(f,h),this.options.onopentagend&&this.options.onopentagend(),o=s}else s++;else n===t?n=null:null===n&&(n=t),s++}if(o<i&&this.options.ontext){const t=this.buffer.slice(o,i);this.options.ontext(t)}}end(){this.options.onend&&this.options.onend()}}
''');
}
}

View file

@ -0,0 +1,155 @@
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);
}
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])
);
}
''');
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,129 @@
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'],
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']?.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()};
}
}

View file

@ -0,0 +1,280 @@
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('''
async function jsonStringify(fn) {
return JSON.stringify(await fn());
}
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})',
))
as List<dynamic>)
.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})',
))
as List<dynamic>)
.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)'))
as List<dynamic>)
.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`)'),
);
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:
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) async {
_init();
try {
final promised = await runtime.handlePromise(
await runtime.evaluateAsync('jsonStringify(() => extension.$call)'),
);
return jsonDecode(promised.stringResult) as T;
} catch (e) {
rethrow;
}
}
}

View file

@ -135,7 +135,7 @@ class Source {
filterList = json['filterList'];
preferenceList = json['preferenceList'];
iconUrl = json['iconUrl'];
id = json['id'];
id = json['id'] is int ? json['id'] : null;
isActive = json['isActive'];
isAdded = json['isAdded'];
isFullData = json['isFullData'];
@ -216,4 +216,9 @@ class Source {
}
}
enum SourceCodeLanguage { dart, javascript, mihon }
enum SourceCodeLanguage {
dart,
javascript,
mihon,
lnreader
}

View file

@ -475,11 +475,13 @@ const _SourcesourceCodeLanguageEnumValueMap = {
'dart': 0,
'javascript': 1,
'mihon': 2,
'lnreader': 3,
};
const _SourcesourceCodeLanguageValueEnumMap = {
0: SourceCodeLanguage.dart,
1: SourceCodeLanguage.javascript,
2: SourceCodeLanguage.mihon,
3: SourceCodeLanguage.lnreader,
};
Id _sourceGetId(Source object) {

View file

@ -26,7 +26,11 @@ class _CreateExtensionState extends State<CreateExtension> {
int _languageIndex = 0;
final List<String> _sourceTypes = ["single", "multi", "torrent"];
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;
@override
Widget build(BuildContext context) {
@ -67,9 +71,11 @@ class _CreateExtensionState extends State<CreateExtension> {
setState(() {
if (v == 0) {
_sourceCodeLanguage = SourceCodeLanguage.dart;
} else {
} else if (v == 1) {
_sourceCodeLanguage =
SourceCodeLanguage.javascript;
} else {
_sourceCodeLanguage = SourceCodeLanguage.lnreader;
}
_languageIndex = v!;
});

View file

@ -75,6 +75,39 @@ Future<void> fetchSourcesList({
src.id = 'mihon-${source['id']}'.hashCode;
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 {
yield Source.fromJson(e);
}
@ -447,3 +480,44 @@ Future<List<SourcePreference>?> fetchPreferencesDalvik(
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";
}