improve code

This commit is contained in:
nyne
2024-10-17 14:28:29 +08:00
parent c33f2c9918
commit e1e571052f
3 changed files with 148 additions and 90 deletions

View File

@@ -106,6 +106,22 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
Future<Res<S>> loadData(); Future<Res<S>> loadData();
Future<Res<S>> loadDataWithRetry() async {
int retry = 0;
while (true) {
var res = await loadData();
if (res.success) {
return res;
} else {
if (retry >= 3) {
return res;
}
retry++;
await Future.delayed(const Duration(milliseconds: 200));
}
}
}
FutureOr<void> onDataLoaded() {} FutureOr<void> onDataLoaded() {}
Widget buildContent(BuildContext context, S data); Widget buildContent(BuildContext context, S data);
@@ -125,7 +141,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
isLoading = true; isLoading = true;
error = null; error = null;
}); });
loadData().then((value) async { loadDataWithRetry().then((value) async {
if (value.success) { if (value.success) {
data = value.data; data = value.data;
await onDataLoaded(); await onDataLoaded();
@@ -153,7 +169,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
void initState() { void initState() {
isLoading = true; isLoading = true;
Future.microtask(() { Future.microtask(() {
loadData().then((value) async { loadDataWithRetry().then((value) async {
if (value.success) { if (value.success) {
data = value.data; data = value.data;
await onDataLoaded(); await onDataLoaded();

View File

@@ -483,7 +483,6 @@ class ComicSourceParser {
if (res is! Map<String, dynamic>) throw "Invalid data"; if (res is! Map<String, dynamic>) throw "Invalid data";
res['comicId'] = id; res['comicId'] = id;
res['sourceKey'] = _key; res['sourceKey'] = _key;
JsEngine().clearHtml();
return Res(ComicDetails.fromJson(res)); return Res(ComicDetails.fromJson(res));
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");

View File

@@ -28,7 +28,6 @@ import 'comic_source/comic_source.dart';
import 'consts.dart'; import 'consts.dart';
import 'log.dart'; import 'log.dart';
class JavaScriptRuntimeException implements Exception { class JavaScriptRuntimeException implements Exception {
final String message; final String message;
@@ -40,7 +39,7 @@ class JavaScriptRuntimeException implements Exception {
} }
} }
class JsEngine with _JSEngineApi{ class JsEngine with _JSEngineApi {
factory JsEngine() => _cache ?? (_cache = JsEngine._create()); factory JsEngine() => _cache ?? (_cache = JsEngine._create());
static JsEngine? _cache; static JsEngine? _cache;
@@ -53,13 +52,13 @@ class JsEngine with _JSEngineApi{
Dio? _dio; Dio? _dio;
static void reset(){ static void reset() {
_cache = null; _cache = null;
_cache?.dispose(); _cache?.dispose();
JsEngine().init(); JsEngine().init();
} }
Future<void> init() async{ Future<void> init() async {
if (!_closed) { if (!_closed) {
return; return;
} }
@@ -72,14 +71,14 @@ class JsEngine with _JSEngineApi{
_closed = false; _closed = false;
_engine = FlutterQjs(); _engine = FlutterQjs();
_engine!.dispatch(); _engine!.dispatch();
var setGlobalFunc = _engine!.evaluate( var setGlobalFunc =
"(key, value) => { this[key] = value; }"); _engine!.evaluate("(key, value) => { this[key] = value; }");
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]); (setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
setGlobalFunc.free(); setGlobalFunc.free();
var jsInit = await rootBundle.load("assets/init.js"); var jsInit = await rootBundle.load("assets/init.js");
_engine!.evaluate(utf8.decode(jsInit.buffer.asUint8List()), name: "<init>"); _engine!
} .evaluate(utf8.decode(jsInit.buffer.asUint8List()), 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');
} }
} }
@@ -106,14 +105,13 @@ class JsEngine with _JSEngineApi{
{ {
String key = message["key"]; String key = message["key"];
String dataKey = message["data_key"]; String dataKey = message["data_key"];
return ComicSource.find(key) return ComicSource.find(key)?.data[dataKey];
?.data[dataKey];
} }
case 'save_data': case 'save_data':
{ {
String key = message["key"]; String key = message["key"];
String dataKey = message["data_key"]; String dataKey = message["data_key"];
if(dataKey == 'setting'){ if (dataKey == 'setting') {
throw "setting is not allowed to be saved"; throw "setting is not allowed to be saved";
} }
var data = message["data"]; var data = message["data"];
@@ -158,9 +156,9 @@ class JsEngine with _JSEngineApi{
String key = message["key"]; String key = message["key"];
String settingKey = message["setting_key"]; String settingKey = message["setting_key"];
var source = ComicSource.find(key)!; var source = ComicSource.find(key)!;
return source.data["setting"]?[settingKey] return source.data["setting"]?[settingKey] ??
?? source.settings?[settingKey]['default'] source.settings?[settingKey]['default'] ??
?? (throw "Setting not found: $settingKey"); (throw "Setting not found: $settingKey");
} }
case "isLogged": case "isLogged":
{ {
@@ -168,37 +166,40 @@ class JsEngine with _JSEngineApi{
} }
} }
} }
} } catch (e, s) {
catch(e, s){
Log.error("Failed to handle message: $message\n$e\n$s", "JsEngine"); Log.error("Failed to handle message: $message\n$e\n$s", "JsEngine");
rethrow; rethrow;
} }
} }
Future<Map<String, dynamic>> _http(Map<String, dynamic> req) async{ Future<Map<String, dynamic>> _http(Map<String, dynamic> req) async {
Response? response; Response? response;
String? error; String? error;
try { try {
var headers = Map<String, dynamic>.from(req["headers"] ?? {}); var headers = Map<String, dynamic>.from(req["headers"] ?? {});
if(headers["user-agent"] == null && headers["User-Agent"] == null){ if (headers["user-agent"] == null && headers["User-Agent"] == null) {
headers["User-Agent"] = webUA; headers["User-Agent"] = webUA;
} }
response = await _dio!.request(req["url"], data: req["data"], options: Options( response = await _dio!.request(req["url"],
data: req["data"],
options: Options(
method: req['http_method'], method: req['http_method'],
responseType: req["bytes"] == true ? ResponseType.bytes : ResponseType.plain, responseType: req["bytes"] == true
headers: headers ? ResponseType.bytes
)); : ResponseType.plain,
headers: headers));
} catch (e) { } catch (e) {
error = e.toString(); error = e.toString();
} }
Map<String, String> headers = {}; Map<String, String> headers = {};
response?.headers.forEach((name, values) => headers[name] = values.join(',')); response?.headers
.forEach((name, values) => headers[name] = values.join(','));
dynamic body = response?.data; dynamic body = response?.data;
if(body is! Uint8List && body is List<int>) { if (body is! Uint8List && body is List<int>) {
body = Uint8List.fromList(body); body = Uint8List.fromList(body);
} }
@@ -222,28 +223,42 @@ class JsEngine with _JSEngineApi{
} }
} }
mixin class _JSEngineApi{ mixin class _JSEngineApi {
final Map<int, dom.Document> _documents = {}; final Map<int, dom.Document> _documents = {};
final Map<int, List<int>> _documentElements = {};
final Map<int, List<int>> _documentNodes = {};
final Map<int, dom.Element> _elements = {}; final Map<int, dom.Element> _elements = {};
final Map<int, dom.Node> _nodes = {}; final Map<int, dom.Node> _nodes = {};
CookieJarSql? _cookieJar; CookieJarSql? _cookieJar;
int _elementKey = 0;
int _nodeKey = 0;
dynamic handleHtmlCallback(Map<String, dynamic> data) { dynamic handleHtmlCallback(Map<String, dynamic> data) {
switch (data["function"]) { switch (data["function"]) {
case "parse": case "parse":
_documents[data["key"]] = html.parse(data["data"]); _documents[data["key"]] = html.parse(data["data"]);
_documentElements[data["key"]] = [];
_documentNodes[data["key"]] = [];
Future.delayed(const Duration(seconds: 1), () {
handleHtmlCallback({"function": "dispose", "key": data["key"]});
});
return null; return null;
case "querySelector": case "querySelector":
var res = _documents[data["key"]]!.querySelector(data["query"]); var res = _documents[data["key"]]!.querySelector(data["query"]);
if(res == null) return null; if (res == null) return null;
_elements[_elements.length] = res; _elements[_elementKey] = res;
return _elements.length - 1; _elementKey++;
_documentElements[data["key"]]!.add(_elementKey - 1);
return _elementKey - 1;
case "querySelectorAll": case "querySelectorAll":
var res = _documents[data["key"]]!.querySelectorAll(data["query"]); var res = _documents[data["key"]]!.querySelectorAll(data["query"]);
var keys = <int>[]; var keys = <int>[];
for(var element in res){ for (var element in res) {
_elements[_elements.length] = element; _elements[_elementKey] = element;
keys.add(_elements.length - 1); keys.add(_elementKey);
_documentElements[data["key"]]!.add(_elementKey);
_elementKey++;
} }
return keys; return keys;
case "getText": case "getText":
@@ -252,44 +267,58 @@ mixin class _JSEngineApi{
return _elements[data["key"]]!.attributes; return _elements[data["key"]]!.attributes;
case "dom_querySelector": case "dom_querySelector":
var res = _elements[data["key"]]!.querySelector(data["query"]); var res = _elements[data["key"]]!.querySelector(data["query"]);
if(res == null) return null; if (res == null) return null;
_elements[_elements.length] = res; _elements[_elements.length] = res;
var docKey = _documentElements.keys
.firstWhere((key) => _documentElements[key]!.contains(data["key"]));
_documentElements[docKey]!.add(_elements.length - 1);
return _elements.length - 1; return _elements.length - 1;
case "dom_querySelectorAll": case "dom_querySelectorAll":
var res = _elements[data["key"]]!.querySelectorAll(data["query"]); var res = _elements[data["key"]]!.querySelectorAll(data["query"]);
var keys = <int>[]; var keys = <int>[];
for(var element in res){ var docKey = _documentElements.keys
.firstWhere((key) => _documentElements[key]!.contains(data["key"]));
for (var element in res) {
_elements[_elements.length] = element; _elements[_elements.length] = element;
keys.add(_elements.length - 1); keys.add(_elements.length - 1);
_documentElements[docKey]!.add(_elements.length - 1);
} }
return keys; return keys;
case "getChildren": case "getChildren":
var res = _elements[data["key"]]!.children; var res = _elements[data["key"]]!.children;
var keys = <int>[]; var keys = <int>[];
var docKey = _documentElements.keys
.firstWhere((key) => _documentElements[key]!.contains(data["key"]));
for (var element in res) { for (var element in res) {
_elements[_elements.length] = element; _elements[_elements.length] = element;
keys.add(_elements.length - 1); keys.add(_elements.length - 1);
_documentElements[docKey]!.add(_elements.length - 1);
} }
return keys; return keys;
case "getNodes": case "getNodes":
var res = _elements[data["key"]]!.nodes; var res = _elements[data["key"]]!.nodes;
var keys = <int>[]; var keys = <int>[];
var docKey = _documentElements.keys
.firstWhere((key) => _documentElements[key]!.contains(data["key"]));
for (var node in res) { for (var node in res) {
_nodes[_nodes.length] = node; _nodes[_nodeKey] = node;
keys.add(_nodes.length - 1); keys.add(_nodeKey);
_documentNodes[docKey]!.add(_nodeKey);
_nodeKey++;
} }
return keys; return keys;
case "getInnerHTML": case "getInnerHTML":
return _elements[data["key"]]!.innerHtml; return _elements[data["key"]]!.innerHtml;
case "getParent": case "getParent":
var res = _elements[data["key"]]!.parent; var res = _elements[data["key"]]!.parent;
if(res == null) return null; if (res == null) return null;
_elements[_elements.length] = res; _elements[_elementKey] = res;
return _elements.length - 1; _documentElements[data["key"]]!.add(_elementKey);
return _elementKey++;
case "node_text": case "node_text":
return _nodes[data["key"]]!.text; return _nodes[data["key"]]!.text;
case "node_type": case "node_type":
return switch(_nodes[data["key"]]!.nodeType) { return switch (_nodes[data["key"]]!.nodeType) {
dom.Node.ELEMENT_NODE => "element", dom.Node.ELEMENT_NODE => "element",
dom.Node.TEXT_NODE => "text", dom.Node.TEXT_NODE => "text",
dom.Node.COMMENT_NODE => "comment", dom.Node.COMMENT_NODE => "comment",
@@ -298,11 +327,26 @@ mixin class _JSEngineApi{
}; };
case "node_to_element": case "node_to_element":
var node = _nodes[data["key"]]!; var node = _nodes[data["key"]]!;
if(node is dom.Element){ if (node is dom.Element) {
_elements[_elements.length] = node; _elements[_elementKey] = node;
return _elements.length - 1; var docKey = _documentNodes.keys
.firstWhere((key) => _documentElements[key]!.contains(data["key"]));
_documentElements[docKey]!.add(_elementKey);
return _elementKey++;
} }
return null; return null;
case "dispose":
var docKey = data["key"];
_documents.remove(docKey);
for (var elementKey in _documentElements[docKey]!) {
_elements.remove(elementKey);
}
_documentElements.remove(docKey);
for (var nodeKey in _documentNodes[docKey]!) {
_nodes.remove(nodeKey);
}
_documentNodes.remove(docKey);
return null;
} }
} }
@@ -313,7 +357,7 @@ mixin class _JSEngineApi{
Uri.parse(data["url"]), Uri.parse(data["url"]),
(data["cookies"] as List).map((e) { (data["cookies"] as List).map((e) {
var c = Cookie(e["name"], e["value"]); var c = Cookie(e["name"], e["value"]);
if(e['domain'] != null){ if (e['domain'] != null) {
c.domain = e['domain']; c.domain = e['domain'];
} }
return c; return c;
@@ -321,7 +365,8 @@ mixin class _JSEngineApi{
return null; return null;
case "get": case "get":
var cookies = _cookieJar!.loadForRequest(Uri.parse(data["url"])); var cookies = _cookieJar!.loadForRequest(Uri.parse(data["url"]));
return cookies.map((e) => { return cookies
.map((e) => {
"name": e.name, "name": e.name,
"value": e.value, "value": e.value,
"domain": e.domain, "domain": e.domain,
@@ -331,23 +376,24 @@ mixin class _JSEngineApi{
"secure": e.secure, "secure": e.secure,
"httpOnly": e.httpOnly, "httpOnly": e.httpOnly,
"session": e.expires == null, "session": e.expires == null,
}).toList(); })
.toList();
case "delete": case "delete":
clearCookies([data["url"]]); clearCookies([data["url"]]);
return null; return null;
} }
} }
void clearHtml(){ void clearHtml() {
_documents.clear(); _documents.clear();
_elements.clear(); _elements.clear();
_nodes.clear(); _nodes.clear();
} }
void clearCookies(List<String> domains) async{ void clearCookies(List<String> domains) async {
for(var domain in domains){ for (var domain in domains) {
var uri = Uri.tryParse(domain); var uri = Uri.tryParse(domain);
if(uri == null) continue; if (uri == null) continue;
_cookieJar!.deleteUri(uri); _cookieJar!.deleteUri(uri);
} }
} }
@@ -359,16 +405,12 @@ mixin class _JSEngineApi{
try { try {
switch (type) { switch (type) {
case "utf8": case "utf8":
return isEncode return isEncode ? utf8.encode(value) : utf8.decode(value);
? utf8.encode(value)
: utf8.decode(value);
case "base64": case "base64":
if(value is String){ if (value is String) {
value = utf8.encode(value); value = utf8.encode(value);
} }
return isEncode return isEncode ? base64Encode(value) : base64Decode(value);
? base64Encode(value)
: base64Decode(value);
case "md5": case "md5":
return Uint8List.fromList(md5.convert(value).bytes); return Uint8List.fromList(md5.convert(value).bytes);
case "sha1": case "sha1":
@@ -380,20 +422,22 @@ mixin class _JSEngineApi{
case "hmac": case "hmac":
var key = data["key"]; var key = data["key"];
var hash = data["hash"]; var hash = data["hash"];
var hmac = Hmac(switch(hash) { var hmac = Hmac(
switch (hash) {
"md5" => md5, "md5" => md5,
"sha1" => sha1, "sha1" => sha1,
"sha256" => sha256, "sha256" => sha256,
"sha512" => sha512, "sha512" => sha512,
_ => throw "Unsupported hash: $hash" _ => throw "Unsupported hash: $hash"
}, key); },
if(data['isString'] == true){ key);
if (data['isString'] == true) {
return hmac.convert(value).toString(); return hmac.convert(value).toString();
} else { } else {
return Uint8List.fromList(hmac.convert(value).bytes); return Uint8List.fromList(hmac.convert(value).bytes);
} }
case "aes-ecb": case "aes-ecb":
if(!isEncode){ if (!isEncode) {
var key = data["key"]; var key = data["key"];
var cipher = ECBBlockCipher(AESEngine()); var cipher = ECBBlockCipher(AESEngine());
cipher.init(false, KeyParameter(key)); cipher.init(false, KeyParameter(key));
@@ -401,7 +445,7 @@ mixin class _JSEngineApi{
} }
return null; return null;
case "aes-cbc": case "aes-cbc":
if(!isEncode){ if (!isEncode) {
var key = data["key"]; var key = data["key"];
var iv = data["iv"]; var iv = data["iv"];
var cipher = CBCBlockCipher(AESEngine()); var cipher = CBCBlockCipher(AESEngine());
@@ -410,7 +454,7 @@ mixin class _JSEngineApi{
} }
return null; return null;
case "aes-cfb": case "aes-cfb":
if(!isEncode){ if (!isEncode) {
var key = data["key"]; var key = data["key"];
var blockSize = data["blockSize"]; var blockSize = data["blockSize"];
var cipher = CFBBlockCipher(AESEngine(), blockSize); var cipher = CFBBlockCipher(AESEngine(), blockSize);
@@ -419,7 +463,7 @@ mixin class _JSEngineApi{
} }
return null; return null;
case "aes-ofb": case "aes-ofb":
if(!isEncode){ if (!isEncode) {
var key = data["key"]; var key = data["key"];
var blockSize = data["blockSize"]; var blockSize = data["blockSize"];
var cipher = OFBBlockCipher(AESEngine(), blockSize); var cipher = OFBBlockCipher(AESEngine(), blockSize);
@@ -428,19 +472,18 @@ mixin class _JSEngineApi{
} }
return null; return null;
case "rsa": case "rsa":
if(!isEncode){ if (!isEncode) {
var key = data["key"]; var key = data["key"];
final cipher = PKCS1Encoding(RSAEngine()); final cipher = PKCS1Encoding(RSAEngine());
cipher.init( cipher.init(false,
false, PrivateKeyParameter<RSAPrivateKey>(_parsePrivateKey(key))); PrivateKeyParameter<RSAPrivateKey>(_parsePrivateKey(key)));
return _processInBlocks(cipher, value); return _processInBlocks(cipher, value);
} }
return null; return null;
default: default:
return value; return value;
} }
} } catch (e) {
catch(e) {
Log.error("JS Engine", "Failed to convert $type: $e"); Log.error("JS Engine", "Failed to convert $type: $e");
return null; return null;
} }
@@ -460,11 +503,11 @@ mixin class _JSEngineApi{
final p = pkSeq.elements![4] as ASN1Integer; final p = pkSeq.elements![4] as ASN1Integer;
final q = pkSeq.elements![5] as ASN1Integer; final q = pkSeq.elements![5] as ASN1Integer;
return RSAPrivateKey(modulus.integer!, privateExponent.integer!, p.integer!, q.integer!); return RSAPrivateKey(
modulus.integer!, privateExponent.integer!, p.integer!, q.integer!);
} }
Uint8List _processInBlocks( Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) {
AsymmetricBlockCipher engine, Uint8List input) {
final numBlocks = input.length ~/ engine.inputBlockSize + final numBlocks = input.length ~/ engine.inputBlockSize +
((input.length % engine.inputBlockSize != 0) ? 1 : 0); ((input.length % engine.inputBlockSize != 0) ? 1 : 0);