code refactor , deobfuscator method , extension preferences
This commit is contained in:
parent
282400baed
commit
3ff647bb01
32 changed files with 457 additions and 141 deletions
|
|
@ -41,6 +41,11 @@ class $VideoModel implements VideoModel, $Instance {
|
|||
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.list,
|
||||
[BridgeTypeRef.type(RuntimeTypes.dynamicType)])),
|
||||
false),
|
||||
BridgeParameter(
|
||||
'audios',
|
||||
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.list,
|
||||
[BridgeTypeRef.type(RuntimeTypes.dynamicType)])),
|
||||
false),
|
||||
]))
|
||||
},
|
||||
// Specify class fields
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import 'package:mangayomi/services/anime_extractors/your_upload_extractor.dart';
|
|||
import 'package:mangayomi/services/http_service/cloudflare/cloudflare_bypass.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/utils/cryptoaes/crypto_aes.dart';
|
||||
import 'package:mangayomi/utils/cryptoaes/deobfuscator.dart';
|
||||
import 'package:mangayomi/utils/extensions.dart';
|
||||
import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
||||
import 'package:mangayomi/utils/xpath_selector.dart';
|
||||
|
|
@ -110,7 +111,7 @@ class MBridge {
|
|||
.trimLeft()
|
||||
.trimRight();
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +199,7 @@ class MBridge {
|
|||
//return last element of the resRegExp list
|
||||
return resRegExp.last.trim().trimLeft().trimRight();
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +226,7 @@ class MBridge {
|
|||
return attr;
|
||||
}
|
||||
} catch (e) {
|
||||
// _botToast(e.toString());
|
||||
// botToast(e.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -296,9 +297,11 @@ class MBridge {
|
|||
..launch(url);
|
||||
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
html = await decodeHtml(webview);
|
||||
if (xpathSelector(html!).query(rule).attrs.isEmpty) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
html = await decodeHtml(
|
||||
webview,
|
||||
);
|
||||
if (html == null || xpathSelector(html!).query(rule).attrs.isEmpty) {
|
||||
html = await decodeHtml(webview);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -374,7 +377,7 @@ class MBridge {
|
|||
final jsPacker = JSPacker(code);
|
||||
return jsPacker.unpack() ?? "";
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -423,7 +426,7 @@ class MBridge {
|
|||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -484,7 +487,7 @@ class MBridge {
|
|||
}
|
||||
return listRg.first;
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -608,7 +611,7 @@ class MBridge {
|
|||
|
||||
return result;
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -682,7 +685,7 @@ class MBridge {
|
|||
|
||||
return result;
|
||||
} catch (e) {
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -778,7 +781,7 @@ class MBridge {
|
|||
["menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق"])
|
||||
.anyWordIn(date)) {
|
||||
return cal.subtract(Duration(minutes: number)).millisecondsSinceEpoch;
|
||||
} else if (WordSet(["detik", "segundo", "second", "วินาที"])
|
||||
} else if (WordSet(["detik", "segundo", "second", "วินาที", "sec"])
|
||||
.anyWordIn(date)) {
|
||||
return cal.subtract(Duration(seconds: number)).millisecondsSinceEpoch;
|
||||
} else if (WordSet(["week", "semana"]).anyWordIn(date)) {
|
||||
|
|
@ -872,11 +875,15 @@ class MBridge {
|
|||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
_botToast(e.toString());
|
||||
botToast(e.toString());
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
static String deobfuscateJsPassword(String inputString) {
|
||||
return Deobfuscator.deobfuscateJsPassword(inputString);
|
||||
}
|
||||
|
||||
static Future<List<Video>> sibnetExtractor(String url) async {
|
||||
return await SibnetExtractor().videosFromUrl(
|
||||
url,
|
||||
|
|
@ -1097,6 +1104,19 @@ class $MBridge extends MBridge with $Bridge {
|
|||
],
|
||||
namedParams: []),
|
||||
isStatic: true),
|
||||
'deobfuscateJsPassword': BridgeMethodDef(
|
||||
BridgeFunctionDef(
|
||||
returns: BridgeTypeAnnotation(
|
||||
BridgeTypeRef.type(RuntimeTypes.stringType)),
|
||||
params: [
|
||||
BridgeParameter(
|
||||
'inputString',
|
||||
BridgeTypeAnnotation(
|
||||
BridgeTypeRef.type(RuntimeTypes.stringType)),
|
||||
false),
|
||||
],
|
||||
namedParams: []),
|
||||
isStatic: true),
|
||||
'sibnetExtractor': BridgeMethodDef(
|
||||
BridgeFunctionDef(
|
||||
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.future,
|
||||
|
|
@ -2033,6 +2053,10 @@ class $MBridge extends MBridge with $Bridge {
|
|||
$String(MBridge.parseChapterDate(
|
||||
args[0]!.$value, args[1]!.$value, args[2]!.$value));
|
||||
|
||||
static $String $deobfuscateJsPassword(
|
||||
Runtime runtime, $Value? target, List<$Value?> args) =>
|
||||
$String(MBridge.deobfuscateJsPassword(args[0]!.$value));
|
||||
|
||||
static $String $querySelectorAll(
|
||||
Runtime runtime, $Value? target, List<$Value?> args) =>
|
||||
$String(MBridge.querySelectorAll(
|
||||
|
|
@ -2160,7 +2184,7 @@ class $MBridge extends MBridge with $Bridge {
|
|||
void $bridgeSet(String identifier, $Value value) {}
|
||||
}
|
||||
|
||||
void _botToast(String title) {
|
||||
void botToast(String title) {
|
||||
BotToast.showSimpleNotification(
|
||||
onlyOne: true,
|
||||
dismissDirections: [DismissDirection.horizontal, DismissDirection.down],
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ Runtime runtimeEval(Uint8List bytecode) {
|
|||
'package:bridge_lib/bridge_lib.dart', 'MBridge.xpath', $MBridge.$xpath);
|
||||
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
|
||||
'MBridge.querySelector', $MBridge.$querySelector);
|
||||
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
|
||||
'MBridge.deobfuscateJsPassword', $MBridge.$deobfuscateJsPassword);
|
||||
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
|
||||
'MBridge.querySelectorAll', $MBridge.$querySelectorAll);
|
||||
runtime.setup();
|
||||
|
|
|
|||
|
|
@ -3,17 +3,24 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/browse/extension/providers/extension_preferences_providers.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/colors.dart';
|
||||
import 'package:mangayomi/utils/language.dart';
|
||||
import 'package:mangayomi/utils/media_query.dart';
|
||||
|
||||
class ExtensionDetail extends ConsumerWidget {
|
||||
class ExtensionDetail extends ConsumerStatefulWidget {
|
||||
final Source source;
|
||||
const ExtensionDetail({super.key, required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<ExtensionDetail> createState() => _ExtensionDetailState();
|
||||
}
|
||||
|
||||
class _ExtensionDetailState extends ConsumerState<ExtensionDetail> {
|
||||
late Source source = widget.source;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -28,10 +35,10 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
color:
|
||||
Theme.of(context).secondaryHeaderColor.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: source.iconUrl!.isEmpty
|
||||
child: widget.source.iconUrl!.isEmpty
|
||||
? const Icon(Icons.source_outlined, size: 140)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: source.iconUrl!,
|
||||
imageUrl: widget.source.iconUrl!,
|
||||
fit: BoxFit.contain,
|
||||
width: 140,
|
||||
height: 140,
|
||||
|
|
@ -50,7 +57,7 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
source.name!,
|
||||
widget.source.name!,
|
||||
style: const TextStyle(fontSize: 23, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -69,7 +76,7 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
Column(
|
||||
children: [
|
||||
Text(
|
||||
source.version!,
|
||||
widget.source.version!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
|
@ -79,7 +86,7 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (source.isNsfw!)
|
||||
if (widget.source.isNsfw!)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.7),
|
||||
|
|
@ -96,7 +103,7 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
Column(
|
||||
children: [
|
||||
Text(
|
||||
completeLanguageName(source.lang!),
|
||||
completeLanguageName(widget.source.lang!),
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
|
@ -131,10 +138,10 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
source.name!,
|
||||
widget.source.name!,
|
||||
),
|
||||
content:
|
||||
Text(l10n.uninstall_extension(source.name!)),
|
||||
content: Text(
|
||||
l10n.uninstall_extension(widget.source.name!)),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
|
|
@ -149,8 +156,8 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
isar.writeTxnSync(
|
||||
() => isar.sources.putSync(source
|
||||
isar.writeTxnSync(() =>
|
||||
isar.sources.putSync(widget.source
|
||||
..sourceCode = ""
|
||||
..isAdded = false
|
||||
..isPinned = false));
|
||||
|
|
@ -170,7 +177,86 @@ class ExtensionDetail extends ConsumerWidget {
|
|||
fontSize: 20, fontWeight: FontWeight.bold),
|
||||
)),
|
||||
),
|
||||
)
|
||||
),
|
||||
ref.watch(getMirrorPrefProvider(widget.source.sourceCode!)).when(
|
||||
data: (data) => data != null
|
||||
? ListTile(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
l10n.relative_timestamp,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: mediaWidth(context, 0.8),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: data.entries.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RadioListTile(
|
||||
dense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.all(0),
|
||||
value: data.entries
|
||||
.toList()[index]
|
||||
.value,
|
||||
groupValue: widget.source.baseUrl!,
|
||||
onChanged: (value) {
|
||||
isar.writeTxnSync(() => isar
|
||||
.sources
|
||||
.putSync(widget.source
|
||||
..baseUrl = data.entries
|
||||
.toList()[index]
|
||||
.value));
|
||||
setState(() {
|
||||
source = isar.sources
|
||||
.getSync(source.id!)!;
|
||||
});
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
title: Row(
|
||||
children: [
|
||||
Text(data.entries
|
||||
.toList()[index]
|
||||
.key)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
l10n.cancel,
|
||||
style: TextStyle(
|
||||
color: primaryColor(context)),
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
title: Text(l10n.relative_timestamp),
|
||||
subtitle: Text(
|
||||
widget.source.baseUrl!,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: secondaryColor(context)),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
error: (error, stackTrace) => Text(error.toString()),
|
||||
loading: () => Container(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:dart_eval/stdlib/core.dart';
|
||||
import 'package:mangayomi/eval/compiler/compiler.dart';
|
||||
import 'package:mangayomi/eval/runtime/runtime.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'extension_preferences_providers.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, String>?> getMirrorPref(
|
||||
GetMirrorPrefRef ref, String codeSource) async {
|
||||
try {
|
||||
final bytecode = compilerEval(codeSource);
|
||||
final runtime = runtimeEval(bytecode);
|
||||
var res = await runtime.executeLib(
|
||||
'package:mangayomi/source_code.dart',
|
||||
'getMirrorPref',
|
||||
);
|
||||
Map<String, String> headers = {};
|
||||
if (res is $Map) {
|
||||
headers = res.$reified
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
return headers;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'extension_preferences_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getMirrorPrefHash() => r'87d8329eabbe702d2e612a04cfe6fc719519194c';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef GetMirrorPrefRef = AutoDisposeFutureProviderRef<Map<String, String>?>;
|
||||
|
||||
/// See also [getMirrorPref].
|
||||
@ProviderFor(getMirrorPref)
|
||||
const getMirrorPrefProvider = GetMirrorPrefFamily();
|
||||
|
||||
/// See also [getMirrorPref].
|
||||
class GetMirrorPrefFamily extends Family<AsyncValue<Map<String, String>?>> {
|
||||
/// See also [getMirrorPref].
|
||||
const GetMirrorPrefFamily();
|
||||
|
||||
/// See also [getMirrorPref].
|
||||
GetMirrorPrefProvider call(
|
||||
String codeSource,
|
||||
) {
|
||||
return GetMirrorPrefProvider(
|
||||
codeSource,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
GetMirrorPrefProvider getProviderOverride(
|
||||
covariant GetMirrorPrefProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.codeSource,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'getMirrorPrefProvider';
|
||||
}
|
||||
|
||||
/// See also [getMirrorPref].
|
||||
class GetMirrorPrefProvider
|
||||
extends AutoDisposeFutureProvider<Map<String, String>?> {
|
||||
/// See also [getMirrorPref].
|
||||
GetMirrorPrefProvider(
|
||||
this.codeSource,
|
||||
) : super.internal(
|
||||
(ref) => getMirrorPref(
|
||||
ref,
|
||||
codeSource,
|
||||
),
|
||||
from: getMirrorPrefProvider,
|
||||
name: r'getMirrorPrefProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$getMirrorPrefHash,
|
||||
dependencies: GetMirrorPrefFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
GetMirrorPrefFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String codeSource;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is GetMirrorPrefProvider && other.codeSource == codeSource;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, codeSource.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member
|
||||
|
|
@ -3,8 +3,8 @@ import 'package:file_picker/file_picker.dart';
|
|||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/archive_reader/models/models.dart';
|
||||
import 'package:mangayomi/modules/archive_reader/providers/archive_reader_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/archive_reader/models/models.dart';
|
||||
import 'package:mangayomi/modules/manga/archive_reader/providers/archive_reader_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'local_archive.g.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'local_archive.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$importArchivesFromFileHash() =>
|
||||
r'25ff2ca889a31d482a95af7cb9be8ebd9cf0dc89';
|
||||
r'f911dfd50c20ebbfec97322dd0bf44261a025341';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -76,17 +76,19 @@ class MainScreen extends ConsumerWidget {
|
|||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 0),
|
||||
width: isLongPressed
|
||||
? 0
|
||||
: location == null
|
||||
? 100
|
||||
: location != '/MangaLibrary' &&
|
||||
location != '/AnimeLibrary' &&
|
||||
location != '/history' &&
|
||||
location != '/browse' &&
|
||||
location != '/more'
|
||||
? 0
|
||||
: 100,
|
||||
width: switch (isLongPressed) {
|
||||
true => 0,
|
||||
_ => switch (location) {
|
||||
null => 100,
|
||||
!= '/MangaLibrary' &&
|
||||
!= '/AnimeLibrary' &&
|
||||
!= '/history' &&
|
||||
!= '/browse' &&
|
||||
!= '/more' =>
|
||||
0,
|
||||
_ => 100,
|
||||
},
|
||||
},
|
||||
child: NavigationRailTheme(
|
||||
data: NavigationRailThemeData(
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
|
|
@ -176,17 +178,19 @@ class MainScreen extends ConsumerWidget {
|
|||
: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 0),
|
||||
width: mediaWidth(context, 1),
|
||||
height: isLongPressed
|
||||
? 0
|
||||
: location == null
|
||||
? null
|
||||
: location != '/MangaLibrary' &&
|
||||
location != '/AnimeLibrary' &&
|
||||
location != '/history' &&
|
||||
location != '/browse' &&
|
||||
location != '/more'
|
||||
? 0
|
||||
: null,
|
||||
height: switch (isLongPressed) {
|
||||
true => 0,
|
||||
_ => switch (location) {
|
||||
null => null,
|
||||
!= '/MangaLibrary' &&
|
||||
!= '/AnimeLibrary' &&
|
||||
!= '/history' &&
|
||||
!= '/browse' &&
|
||||
!= '/more' =>
|
||||
0,
|
||||
_ => null,
|
||||
},
|
||||
},
|
||||
child: NavigationBarTheme(
|
||||
data: NavigationBarThemeData(
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mangayomi/modules/archive_reader/models/models.dart';
|
||||
import 'package:mangayomi/modules/manga/archive_reader/models/models.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'archive_reader_providers.g.dart';
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ import 'package:mangayomi/main.dart';
|
|||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/services/http_service/cloudflare/cookie.dart';
|
||||
import 'package:mangayomi/services/http_service/cloudflare/providers/cookie_providers.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
|
@ -182,7 +183,6 @@ class _MangaWebViewState extends ConsumerState<MangaWebView> {
|
|||
_url = url.toString();
|
||||
});
|
||||
},
|
||||
|
||||
shouldOverrideUrlLoading:
|
||||
(controller, navigationAction) async {
|
||||
var uri = navigationAction.request.url!;
|
||||
|
|
@ -213,7 +213,6 @@ class _MangaWebViewState extends ConsumerState<MangaWebView> {
|
|||
_url = url.toString();
|
||||
});
|
||||
},
|
||||
|
||||
onProgressChanged: (controller, progress) async {
|
||||
setState(() {
|
||||
this.progress = progress / 100;
|
||||
|
|
@ -233,12 +232,12 @@ class _MangaWebViewState extends ConsumerState<MangaWebView> {
|
|||
_canGoback = canGoback;
|
||||
_canGoForward = canGoForward;
|
||||
});
|
||||
},initialOptions: InAppWebViewGroupOptions(
|
||||
crossPlatform: InAppWebViewOptions(userAgent: isar.settings.getSync(227)!.userAgent!),
|
||||
),
|
||||
|
||||
initialUrlRequest:
|
||||
URLRequest(url: Uri.parse(widget.url)),
|
||||
},
|
||||
initialOptions: InAppWebViewGroupOptions(
|
||||
crossPlatform: InAppWebViewOptions(
|
||||
userAgent: isar.settings.getSync(227)!.userAgent!),
|
||||
),
|
||||
initialUrlRequest: URLRequest(url: Uri.parse(widget.url)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -256,10 +255,17 @@ Future<String> getWebViewPath() async {
|
|||
);
|
||||
}
|
||||
|
||||
decodeHtml(Webview webview) async {
|
||||
Future<String?> decodeHtml(Webview webview, {String? sourceId}) async {
|
||||
final html = await webview
|
||||
.evaluateJavaScript("window.document.documentElement.outerHTML;");
|
||||
// final cookie = await webview.evaluateJavaScript("window.document.cookie;");
|
||||
// log(cookie!);
|
||||
return jsonDecode(html!) as String;
|
||||
final cookie = await webview.evaluateJavaScript("window.document.cookie;");
|
||||
if (cookie != null && sourceId != null) {
|
||||
setCookieBA(cookie, sourceId);
|
||||
}
|
||||
|
||||
final res = jsonDecode(html!) as String;
|
||||
|
||||
return res == "<html><head></head><body></body></html>" || res.isEmpty
|
||||
? null
|
||||
: res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ Future<(List<Video>, bool)> getAnimeServers(
|
|||
];
|
||||
var res = await runtime.executeLib(
|
||||
'package:mangayomi/source_code.dart', 'getVideoList');
|
||||
// await Future.delayed(Duration(days: 1));
|
||||
if (res is $List) {
|
||||
video = res.$reified.map(
|
||||
(e) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:mangayomi/eval/compiler/compiler.dart';
|
|||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/archive_reader/providers/archive_reader_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/archive_reader/providers/archive_reader_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/eval/runtime/runtime.dart';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_chapter_url.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getChapterUrlHash() => r'330c495774b22fc1fdedb0fa64969cd17e4635f8';
|
||||
String _$getChapterUrlHash() => r'6a8b0eb3869519b75787d8d4a876f536a517e849';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'get_latest_updates_manga.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$getLatestUpdatesMangaHash() =>
|
||||
r'b4de1c71b935893780b02be6a4fa1980651a833d';
|
||||
r'5bae855778ae63dd954705adab9b9bdb3432065e';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_manga_detail.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getMangaDetailHash() => r'025ccc11f94f9b69bd85d86833bc261f66b74f7f';
|
||||
String _$getMangaDetailHash() => r'9ac46d56d3492b3a61ff159873762f579415fb36';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_popular_manga.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getPopularMangaHash() => r'f5a1f5a66bad652cb461ef0ed9d30f60ff7a000a';
|
||||
String _$getPopularMangaHash() => r'6c9face3040220071430e814a12da83201692310';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ Future<String> cloudflareBypass(
|
|||
..launch(url);
|
||||
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
if (html == null) {
|
||||
html = await decodeHtml(webview);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
html = await decodeHtml(webview, sourceId: sourceId);
|
||||
if (html == null ||
|
||||
html!.contains("Just a moment") ||
|
||||
html!.contains("challenges.cloudflare.com")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -8,22 +8,14 @@ Future setCookie(SetCookieRef ref, String sourceId, String url) async {
|
|||
CookieManager cookie = CookieManager.instance();
|
||||
|
||||
final cookies = await cookie.getCookies(url: Uri.parse(url.toString()));
|
||||
final newCookie =
|
||||
cookies.where((element) => element.name == "cf_clearance").toList();
|
||||
if (newCookie.isNotEmpty) {
|
||||
ref
|
||||
.read(cookieStateProvider(sourceId).notifier)
|
||||
.setCookie("cf_clearance=${newCookie.first.value};");
|
||||
}
|
||||
final newCookie = cookies.join("; ");
|
||||
setCookieBA(newCookie, sourceId);
|
||||
}
|
||||
|
||||
Future setCookieB(String sourceId, String url) async {
|
||||
CookieManager cookie = CookieManager.instance();
|
||||
|
||||
final cookies = await cookie.getCookies(url: Uri.parse(url.toString()));
|
||||
final newCookie =
|
||||
cookies.where((element) => element.name == "cf_clearance").toList();
|
||||
if (newCookie.isNotEmpty) {
|
||||
setCookieB("cf_clearance=${newCookie.first.value};", sourceId);
|
||||
}
|
||||
final newCookie = cookies.join("; ");
|
||||
setCookieBA(newCookie, sourceId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'cookie.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$setCookieHash() => r'4beff910b58df98a7b5019a5375f9fb5c84a580c';
|
||||
String _$setCookieHash() => r'55073d2aceae6ce9bf03d61d390dfb84150d272e';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class CookieState extends _$CookieState {
|
|||
}
|
||||
}
|
||||
|
||||
void setCookieB(String newCookie, String idSource) {
|
||||
void setCookieBA(String newCookie, String idSource) {
|
||||
final settings = isar.settings.getSync(227);
|
||||
List<Cookie>? cookieList = [];
|
||||
for (var cookie in settings!.cookiesList ?? []) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'cookie_providers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$cookieStateHash() => r'42286f51989b6f65eed9787ca2390a96854395a8';
|
||||
String _$cookieStateHash() => r'a3e0c52a396c7b9072c2b1e948399f2e4e4728d5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'search_manga.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$searchMangaHash() => r'9bbf3d7b3cb8f1aa92201200e6a4616721cd3037';
|
||||
String _$searchMangaHash() => r'0c16725a47f5ad9b7ec7f128a5c1c03e4868fcf0';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
|||
import 'dart:io';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mangayomi/eval/m_bridge.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
|
|
@ -72,8 +73,7 @@ class Anilist extends _$Anilist {
|
|||
Future<Track> addLibManga(Track track) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
mutation AddManga(\$mangaId: Int, \$progress: Int, \$status: MediaListStatus) {
|
||||
SaveMediaListEntry(mediaId: \$mangaId, progress: \$progress, status: \$status) {
|
||||
id
|
||||
|
|
@ -107,8 +107,7 @@ class Anilist extends _$Anilist {
|
|||
Future<Track> addLibAnime(Track track) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
mutation AddAnime(\$animeId: Int, \$progress: Int, \$status: MediaListStatus) {
|
||||
SaveMediaListEntry(mediaId: \$animeId, progress: \$progress, status: \$status) {
|
||||
id
|
||||
|
|
@ -141,8 +140,7 @@ class Anilist extends _$Anilist {
|
|||
|
||||
Future<Track> updateLibManga(Track track) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
mutation UpdateManga(\$listId: Int, \$progress: Int, \$status: MediaListStatus, \$score: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) {
|
||||
SaveMediaListEntry(
|
||||
id: \$listId,
|
||||
|
|
@ -185,8 +183,7 @@ class Anilist extends _$Anilist {
|
|||
|
||||
Future<Track> updateLibAnime(Track track) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
mutation UpdateAnime(\$listId: Int, \$progress: Int, \$status: MediaListStatus, \$score: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) {
|
||||
SaveMediaListEntry(
|
||||
id: \$listId,
|
||||
|
|
@ -227,8 +224,7 @@ class Anilist extends _$Anilist {
|
|||
|
||||
Future<List<TrackSearch>> search(String search) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
query Search(\$query: String) {
|
||||
Page(perPage: 50) {
|
||||
media(search: \$query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|
|
@ -294,8 +290,7 @@ class Anilist extends _$Anilist {
|
|||
|
||||
Future<List<TrackSearch>> searchAnime(String search) async {
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
query Search(\$query: String) {
|
||||
Page(perPage: 50) {
|
||||
media(search: \$query, type: ANIME) {
|
||||
|
|
@ -365,8 +360,7 @@ class Anilist extends _$Anilist {
|
|||
final userId = ref.watch(tracksProvider(syncId: syncId))!.username;
|
||||
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
query(\$id: Int!, \$manga_id: Int!) {
|
||||
Page {
|
||||
mediaList(userId: \$id, type: MANGA, mediaId: \$manga_id) {
|
||||
|
|
@ -448,8 +442,7 @@ class Anilist extends _$Anilist {
|
|||
final userId = ref.watch(tracksProvider(syncId: syncId))!.username;
|
||||
|
||||
final accessToken = await _getAccesToken();
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
query(\$id: Int!, \$anime_id: Int!) {
|
||||
Page {
|
||||
mediaList(userId: \$id, type: ANIME, mediaId: \$anime_id) {
|
||||
|
|
@ -526,8 +519,7 @@ class Anilist extends _$Anilist {
|
|||
}
|
||||
|
||||
Future<(String, String)> _getCurrentUser(String accessToken) async {
|
||||
const query =
|
||||
'''
|
||||
const query = '''
|
||||
query User {
|
||||
Viewer {
|
||||
id
|
||||
|
|
@ -566,6 +558,7 @@ class Anilist extends _$Anilist {
|
|||
final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!);
|
||||
if (DateTime.now().isAfter(expiresIn)) {
|
||||
ref.read(tracksProvider(syncId: syncId).notifier).logout();
|
||||
botToast("Anilist Token expired");
|
||||
throw Exception("Token expired");
|
||||
}
|
||||
return mALOAuth.accessToken!;
|
||||
|
|
@ -577,24 +570,20 @@ class Anilist extends _$Anilist {
|
|||
return switch (scoreFormat) {
|
||||
"POINT_10" => (score / 10).toString(),
|
||||
"POINT_100" => score.toString(),
|
||||
"POINT_5" => score == 0
|
||||
? "0"
|
||||
: score < 30
|
||||
? "1"
|
||||
: score < 50
|
||||
? "2"
|
||||
: score < 70
|
||||
? "3"
|
||||
: score < 90
|
||||
? "4"
|
||||
: "5",
|
||||
"POINT_3" => score == 0
|
||||
? "0"
|
||||
: score <= 35
|
||||
? ":("
|
||||
: score <= 60
|
||||
? ":|"
|
||||
: ":)",
|
||||
"POINT_5" => switch (score) {
|
||||
0 => "0",
|
||||
< 30 => "1",
|
||||
< 50 => "2",
|
||||
< 70 => "3",
|
||||
< 90 => "4",
|
||||
_ => "5"
|
||||
},
|
||||
"POINT_3" => switch (score) {
|
||||
0 => "0",
|
||||
<= 35 => ":(",
|
||||
<= 60 => ":|",
|
||||
_ => ":)"
|
||||
},
|
||||
"POINT_10_DECIMAL" => (score / 10).toString(),
|
||||
_ => throw ("Unknown score type")
|
||||
};
|
||||
|
|
@ -683,14 +672,16 @@ class Anilist extends _$Anilist {
|
|||
final prefs = isar.trackPreferences.getSync(syncId)!.prefs;
|
||||
final scoreFormat = jsonDecode(prefs!)['scoreFormat'];
|
||||
return switch (scoreFormat) {
|
||||
'POINT_5' => score == 0 ? "0 ★" : "${(score + 10) ~/ 20} ★",
|
||||
'POINT_3' => score == 0
|
||||
? "-"
|
||||
: score <= 35
|
||||
? "😦"
|
||||
: score <= 60
|
||||
? "😐"
|
||||
: "😊",
|
||||
'POINT_5' => switch (score) {
|
||||
0 => "0 ★",
|
||||
_ => "${(score + 10) ~/ 20} ★"
|
||||
},
|
||||
'POINT_3' => switch (score) {
|
||||
0 => "-",
|
||||
<= 35 => "😦",
|
||||
<= 60 => "😐",
|
||||
_ => "😊"
|
||||
},
|
||||
_ => _toAnilistScore(score),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class CryptoAES {
|
|||
|
||||
static String decryptAESCryptoJS(String encrypted, String passphrase) {
|
||||
try {
|
||||
Uint8List encryptedBytesWithSalt = base64.decode(encrypted);
|
||||
Uint8List encryptedBytesWithSalt = base64.decode(encrypted.trim());
|
||||
|
||||
Uint8List encryptedBytes =
|
||||
encryptedBytesWithSalt.sublist(16, encryptedBytesWithSalt.length);
|
||||
|
|
|
|||
65
lib/utils/cryptoaes/deobfuscator.dart
Normal file
65
lib/utils/cryptoaes/deobfuscator.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
class Deobfuscator {
|
||||
static String deobfuscateJsPassword(String inputString) {
|
||||
int idx = 0;
|
||||
final brackets = ['[', '('];
|
||||
final evaluatedString = StringBuffer();
|
||||
|
||||
while (idx < inputString.length) {
|
||||
final chr = inputString[idx];
|
||||
|
||||
if (!brackets.contains(chr)) {
|
||||
idx++;
|
||||
continue;
|
||||
}
|
||||
|
||||
final closingIndex = getMatchingBracketIndex(idx, inputString);
|
||||
|
||||
if (chr == '[') {
|
||||
final digit = calculateDigit(inputString.substring(idx, closingIndex));
|
||||
evaluatedString.write(digit);
|
||||
} else {
|
||||
evaluatedString.write('.');
|
||||
|
||||
if (inputString[closingIndex + 1] == '[') {
|
||||
final skippingIndex = getMatchingBracketIndex(closingIndex + 1, inputString);
|
||||
idx = skippingIndex + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
idx = closingIndex + 1;
|
||||
}
|
||||
|
||||
return evaluatedString.toString();
|
||||
}
|
||||
|
||||
static int getMatchingBracketIndex(int openingIndex, String inputString) {
|
||||
final openingBracket = inputString[openingIndex];
|
||||
final closingBracket = openingBracket == '[' ? ']' : ')';
|
||||
var counter = 0;
|
||||
|
||||
for (var idx = openingIndex; idx < inputString.length; idx++) {
|
||||
if (inputString[idx] == openingBracket) counter++;
|
||||
if (inputString[idx] == closingBracket) counter--;
|
||||
|
||||
if (counter == 0) return idx; // found matching bracket
|
||||
if (counter < 0) return -1; // unbalanced brackets
|
||||
}
|
||||
|
||||
return -1; // matching bracket not found
|
||||
}
|
||||
|
||||
static String calculateDigit(String inputSubString) {
|
||||
final digit = RegExp(r"\!\+\[\]").allMatches(inputSubString).length;
|
||||
|
||||
if (digit == 0) {
|
||||
if (RegExp(r"\+\[\]").allMatches(inputSubString).length == 1) {
|
||||
return '0';
|
||||
}
|
||||
} else if (digit >= 1 && digit <= 9) {
|
||||
return digit.toString();
|
||||
}
|
||||
|
||||
return '-'; // Illegal digit
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ Map<String, String> headers(HeadersRef ref,
|
|||
|
||||
if (sourceM.hasCloudflare!) {
|
||||
final userAgent = isar.settings.getSync(227)!.userAgent!;
|
||||
final cookie = ref.watch(cookieStateProvider(source));
|
||||
final cookie = ref.watch(cookieStateProvider(sourceM.id.toString()));
|
||||
|
||||
newHeaders.addAll({'User-Agent': userAgent, "Cookie": cookie});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'headers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$headersHash() => r'072a5ed2f4253378506a5fc1f9095531f252450e';
|
||||
String _$headersHash() => r'36e19ced66a55af45ef31bb3b342505246745ebe';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1579,5 +1579,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
dart: ">=3.1.1 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
name: mangayomi
|
||||
description: Free and open source manga reader multi plateform app inspired by Tachiyomi.
|
||||
|
||||
version: 0.0.42+16
|
||||
version: 0.0.43+17
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.0 <4.0.0'
|
||||
sdk: '>=3.1.1 <4.0.0'
|
||||
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue