mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
added support for epubs
embedded images not supported yet
This commit is contained in:
parent
d6d674c270
commit
3677938bf8
19 changed files with 149 additions and 89 deletions
|
|
@ -11,25 +11,32 @@ import '../schema/opf/epub_metadata_meta.dart';
|
|||
|
||||
class BookCoverReader {
|
||||
static Future<images.Image?> readBookCover(EpubBookRef bookRef) async {
|
||||
var metaItems = bookRef.Schema!.Package!.Metadata!.MetaItems;
|
||||
if (metaItems == null || metaItems.isEmpty) return null;
|
||||
EpubManifestItem? coverManifestItem =
|
||||
bookRef.Schema!.Package!.Manifest!.Items!.firstWhereOrNull(
|
||||
(i) => i.Properties == "cover-image",
|
||||
);
|
||||
|
||||
var coverMetaItem = metaItems.firstWhereOrNull(
|
||||
(EpubMetadataMeta metaItem) =>
|
||||
metaItem.Name != null && metaItem.Name!.toLowerCase() == 'cover');
|
||||
if (coverMetaItem == null) return null;
|
||||
if (coverMetaItem.Content == null || coverMetaItem.Content!.isEmpty) {
|
||||
throw Exception(
|
||||
'Incorrect EPUB metadata: cover item content is missing.');
|
||||
if (coverManifestItem == null) {
|
||||
var metaItems = bookRef.Schema!.Package!.Metadata!.MetaItems;
|
||||
if (metaItems == null || metaItems.isEmpty) return null;
|
||||
|
||||
var coverMetaItem = metaItems.firstWhereOrNull(
|
||||
(EpubMetadataMeta metaItem) =>
|
||||
metaItem.Name != null && metaItem.Name!.toLowerCase() == 'cover');
|
||||
if (coverMetaItem == null) return null;
|
||||
if (coverMetaItem.Content == null || coverMetaItem.Content!.isEmpty) {
|
||||
throw Exception(
|
||||
'Incorrect EPUB metadata: cover item content is missing.');
|
||||
}
|
||||
|
||||
coverManifestItem = bookRef.Schema!.Package!.Manifest!.Items!
|
||||
.firstWhereOrNull((EpubManifestItem manifestItem) =>
|
||||
manifestItem.Id!.toLowerCase() ==
|
||||
coverMetaItem.Content!.toLowerCase());
|
||||
}
|
||||
|
||||
var coverManifestItem = bookRef.Schema!.Package!.Manifest!.Items!
|
||||
.firstWhereOrNull((EpubManifestItem manifestItem) =>
|
||||
manifestItem.Id!.toLowerCase() ==
|
||||
coverMetaItem.Content!.toLowerCase());
|
||||
if (coverManifestItem == null) {
|
||||
throw Exception(
|
||||
'Incorrect EPUB manifest: item with ID = \"${coverMetaItem.Content}\" is missing.');
|
||||
throw Exception('Incorrect EPUB manifest');
|
||||
}
|
||||
|
||||
EpubByteContentFileRef? coverImageContentFileRef;
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ class ChapterReader {
|
|||
EpubBookRef bookRef, List<EpubNavigationPoint> navigationPoints) {
|
||||
var result = <EpubChapterRef>[];
|
||||
// navigationPoints.forEach((EpubNavigationPoint navigationPoint) {
|
||||
for (var navigationPoint in navigationPoints){
|
||||
for (var navigationPoint in navigationPoints) {
|
||||
String? contentFileName;
|
||||
String? anchor;
|
||||
if (navigationPoint.Content?.Source ==null) continue;
|
||||
if (navigationPoint.Content?.Source == null) continue;
|
||||
var contentSourceAnchorCharIndex =
|
||||
navigationPoint.Content!.Source!.indexOf('#');
|
||||
if (contentSourceAnchorCharIndex == -1) {
|
||||
|
|
@ -31,7 +31,7 @@ class ChapterReader {
|
|||
anchor = navigationPoint.Content!.Source!
|
||||
.substring(contentSourceAnchorCharIndex + 1);
|
||||
}
|
||||
contentFileName = Uri.decodeFull(contentFileName!);
|
||||
contentFileName = Uri.decodeFull(contentFileName!).replaceAll("\\", "/");
|
||||
EpubTextContentFileRef? htmlContentFileRef;
|
||||
if (!bookRef.Content!.Html!.containsKey(contentFileName)) {
|
||||
throw Exception(
|
||||
|
|
@ -45,21 +45,23 @@ class ChapterReader {
|
|||
chapterRef.Title = navigationPoint.NavigationLabels!.first.Text;
|
||||
chapterRef.SubChapters =
|
||||
getChaptersImpl(bookRef, navigationPoint.ChildNavigationPoints!);
|
||||
if(chapterRef.ContentFileName!.contains('_split_')) {
|
||||
if (chapterRef.ContentFileName!.contains('_split_')) {
|
||||
var fileNamePart = chapterRef.ContentFileName!.split('_split_')[0];
|
||||
for (var fileName in bookRef.Content!.Html!.keys) {
|
||||
if(fileName.contains(fileNamePart)) {
|
||||
if (fileName.contains(fileNamePart)) {
|
||||
if (fileName == contentFileName) {
|
||||
continue;
|
||||
}
|
||||
chapterRef.otherTextContentFileRefs.add(bookRef.Content!.Html![fileName]!);
|
||||
chapterRef.otherTextContentFileRefs
|
||||
.add(bookRef.Content!.Html![fileName]!);
|
||||
chapterRef.OtherContentFileNames.add(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.add(chapterRef);
|
||||
};
|
||||
}
|
||||
;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,9 +308,15 @@ class PackageReader {
|
|||
case 'scheme':
|
||||
result.Scheme = attributeValue;
|
||||
break;
|
||||
case 'content':
|
||||
result.Content = attributeValue;
|
||||
break;
|
||||
case 'name':
|
||||
result.Name = attributeValue;
|
||||
break;
|
||||
}
|
||||
});
|
||||
result.Content = metadataMetaNode.text;
|
||||
result.Content = result.Content ?? metadataMetaNode.text;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,14 +64,22 @@ abstract class EpubContentFileRef {
|
|||
}
|
||||
|
||||
Future<Uint8List> readContentAsBytes() async {
|
||||
var contentFileEntry = getContentFileEntry();
|
||||
var content = openContentStream(contentFileEntry);
|
||||
return Uint8List.fromList(content);
|
||||
try {
|
||||
var contentFileEntry = getContentFileEntry();
|
||||
var content = openContentStream(contentFileEntry);
|
||||
return Uint8List.fromList(content);
|
||||
} catch (_) {
|
||||
return Uint8List.fromList([]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> readContentAsText() async {
|
||||
var contentStream = getContentStream();
|
||||
var result = convert.utf8.decode(contentStream);
|
||||
return result;
|
||||
try {
|
||||
var contentStream = getContentStream();
|
||||
var result = convert.utf8.decode(contentStream);
|
||||
return result;
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: epubx
|
|||
description: Epub Parser for Dart. Epub package fork. Suitable for use on the Server, the Web, or in Flutter
|
||||
homepage: https://github.com/rbcprolabs/epubx.dart
|
||||
issue_tracker: https://github.com/rbcprolabs/epubx.dart
|
||||
version: 4.0.2
|
||||
version: 4.0.3
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
|
|
@ -164,6 +164,11 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
|||
BridgeTypeRef(CoreTypes.future, [BridgeTypeRef(CoreTypes.string)]),
|
||||
),
|
||||
params: [
|
||||
BridgeParameter(
|
||||
'name',
|
||||
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
|
||||
false,
|
||||
),
|
||||
BridgeParameter(
|
||||
'url',
|
||||
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
|
||||
|
|
@ -1673,8 +1678,8 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> getHtmlContent(String url) async =>
|
||||
await $_invoke('getHtmlContent', [$String(url)]);
|
||||
Future<String> getHtmlContent(String name, String url) async =>
|
||||
await $_invoke('getHtmlContent', [$String(name), $String(url)]);
|
||||
|
||||
@override
|
||||
Future<String> cleanHtmlContent(String html) async =>
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@ class DartExtensionService implements ExtensionService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> getHtmlContent(String url) async {
|
||||
return await _executeLib().getHtmlContent(url);
|
||||
Future<String> getHtmlContent(String name, String url) async {
|
||||
return await _executeLib().getHtmlContent(name, url);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ abstract interface class ExtensionService {
|
|||
|
||||
Future<List<Video>> getVideoList(String url);
|
||||
|
||||
Future<String> getHtmlContent(String url);
|
||||
Future<String> getHtmlContent(String name, String url);
|
||||
|
||||
Future<String> cleanHtmlContent(String html);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
|
|
@ -17,9 +16,6 @@ class JsHttpClient {
|
|||
);
|
||||
}
|
||||
|
||||
runtime.onMessage('bytes_get', (dynamic args) async {
|
||||
return await _toBytesResponse(client(args[1]), "GET", args);
|
||||
});
|
||||
runtime.onMessage('http_head', (dynamic args) async {
|
||||
return await _toHttpResponse(client(args[1]), "HEAD", args);
|
||||
});
|
||||
|
|
@ -43,14 +39,6 @@ class Client {
|
|||
constructor(reqcopyWith) {
|
||||
this.reqcopyWith = reqcopyWith;
|
||||
}
|
||||
async getBytes(url, headers) {
|
||||
headers = headers;
|
||||
const result = await sendMessage(
|
||||
"bytes_get",
|
||||
JSON.stringify([null, this.reqcopyWith, url, headers])
|
||||
);
|
||||
return result;
|
||||
}
|
||||
async head(url, headers) {
|
||||
headers = headers;
|
||||
const result = await sendMessage(
|
||||
|
|
@ -148,29 +136,6 @@ Future<String> _toHttpResponse(Client client, String method, List args) async {
|
|||
return jsonEncode((await future).toJson());
|
||||
}
|
||||
|
||||
Future<Uint8List> _toBytesResponse(Client client, String method, List args) async {
|
||||
final url = args[2] as String;
|
||||
final headers = (args[3] as Map?)?.toMapStringString;
|
||||
final body =
|
||||
args.length >= 5
|
||||
? args[4] is List
|
||||
? args[4] as List
|
||||
: args[4] is String
|
||||
? args[4] as String
|
||||
: (args[4] as Map?)?.toMapStringDynamic
|
||||
: null;
|
||||
var request = http.Request(method, Uri.parse(url));
|
||||
request.headers.addAll(headers ?? {});
|
||||
final future = switch (method) {
|
||||
"GET" => client.get(Uri.parse(url), headers: headers),
|
||||
"POST" => client.post(Uri.parse(url), headers: headers, body: body),
|
||||
"PUT" => client.put(Uri.parse(url), headers: headers, body: body),
|
||||
"DELETE" => client.delete(Uri.parse(url), headers: headers, body: body),
|
||||
_ => client.patch(Uri.parse(url), headers: headers, body: body),
|
||||
};
|
||||
return (await future).bodyBytes;
|
||||
}
|
||||
|
||||
extension ResponseExtexsion on Response {
|
||||
Map<String, dynamic> toJson() => {
|
||||
'body': body,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class MProvider {
|
|||
async getVideoList(url) {
|
||||
throw new Error("getVideoList not implemented");
|
||||
}
|
||||
async getHtmlContent(url) {
|
||||
async getHtmlContent(name, url) {
|
||||
throw new Error("getHtmlContent not implemented");
|
||||
}
|
||||
async cleanHtmlContent(html) {
|
||||
|
|
@ -150,12 +150,12 @@ var extention = new DefaultExtension();
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> getHtmlContent(String url) async {
|
||||
Future<String> getHtmlContent(String name, String url) async {
|
||||
_init();
|
||||
final res =
|
||||
(await runtime.handlePromise(
|
||||
await runtime.evaluateAsync(
|
||||
'jsonStringify(() => extention.getHtmlContent(`$url`))',
|
||||
'jsonStringify(() => extention.getHtmlContent(`$name`, `$url`))',
|
||||
),
|
||||
)).stringResult;
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:epubx/epubx.dart';
|
||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:http_interceptor/http/intercepted_client.dart';
|
||||
import 'package:js_packer/js_packer.dart';
|
||||
import 'package:mangayomi/eval/javascript/http.dart';
|
||||
import 'package:mangayomi/eval/model/m_bridge.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/utils/cryptoaes/js_unpacker.dart';
|
||||
import 'package:mangayomi/utils/log/log.dart';
|
||||
|
||||
|
|
@ -14,6 +20,10 @@ class JsUtils {
|
|||
JsUtils(this.runtime);
|
||||
|
||||
void init() {
|
||||
InterceptedClient client() {
|
||||
return MClient.init();
|
||||
}
|
||||
|
||||
runtime.onMessage('log', (dynamic args) {
|
||||
Logger.add(LoggerLevel.warning, "${args[0]}");
|
||||
return null;
|
||||
|
|
@ -44,10 +54,11 @@ class JsUtils {
|
|||
);
|
||||
});
|
||||
runtime.onMessage('parseEpub', (dynamic args) async {
|
||||
final book = await EpubReader.readBook(decodeBytes(args[0]));
|
||||
final bytes = await _toBytesResponse(client(), "GET", args);
|
||||
final book = await EpubReader.readBook(bytes);
|
||||
final List<String> chapters = [];
|
||||
for (var chapter in book.Chapters ?? []) {
|
||||
String chapterTitle = chapter.Title;
|
||||
final chapterTitle = chapter.Title;
|
||||
chapters.add(chapterTitle);
|
||||
}
|
||||
return jsonEncode({
|
||||
|
|
@ -57,10 +68,11 @@ class JsUtils {
|
|||
});
|
||||
});
|
||||
runtime.onMessage('parseEpubChapter', (dynamic args) async {
|
||||
final book = await EpubReader.readBook(decodeBytes(args[0]));
|
||||
final bytes = await _toBytesResponse(client(), "GET", args);
|
||||
final book = await EpubReader.readBook(bytes);
|
||||
final chapter =
|
||||
book.Chapters?.where(
|
||||
(element) => element.Title == args[1],
|
||||
(element) => element.Title == args[3],
|
||||
).firstOrNull;
|
||||
return chapter?.HtmlContent;
|
||||
});
|
||||
|
|
@ -168,23 +180,62 @@ async function evaluateJavascriptViaWebview(url, headers, scripts) {
|
|||
JSON.stringify([url, headers, scripts])
|
||||
);
|
||||
}
|
||||
async function parseEpub(bytes) {
|
||||
async function parseEpub(bookName, url, headers) {
|
||||
return JSON.parse(await sendMessage(
|
||||
"parseEpub",
|
||||
JSON.stringify([bytes])
|
||||
JSON.stringify([bookName, url, headers])
|
||||
));
|
||||
}
|
||||
async function parseEpubChapter(bytes, chapterTitle) {
|
||||
async function parseEpubChapter(bookName, url, headers, chapterTitle) {
|
||||
return await sendMessage(
|
||||
"parseEpubChapter",
|
||||
JSON.stringify([bytes, chapterTitle])
|
||||
JSON.stringify([bookName, url, headers, chapterTitle])
|
||||
);
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
Uint8List decodeBytes(String value) {
|
||||
var bytes = Uint8List.fromList(value.codeUnits);
|
||||
Future<Uint8List> _toBytesResponse(
|
||||
http.Client client,
|
||||
String method,
|
||||
List args,
|
||||
) async {
|
||||
final bookName = args[0] as String;
|
||||
final url = args[1] as String;
|
||||
final headers = (args[2] as Map?)?.toMapStringString;
|
||||
final body =
|
||||
args.length >= 4
|
||||
? args[3] is List
|
||||
? args[3] as List
|
||||
: args[3] is String
|
||||
? args[3] as String
|
||||
: (args[3] as Map?)?.toMapStringDynamic
|
||||
: null;
|
||||
|
||||
final tmpDirectory = (await StorageProvider().getTmpDirectory())!;
|
||||
if (Platform.isAndroid) {
|
||||
if (!(await File(p.join(tmpDirectory.path, ".nomedia")).exists())) {
|
||||
await File(p.join(tmpDirectory.path, ".nomedia")).create();
|
||||
}
|
||||
}
|
||||
final file = File(
|
||||
p.join(tmpDirectory.path, "$bookName.epub"),
|
||||
);
|
||||
if (await file.exists()) {
|
||||
return await file.readAsBytes();
|
||||
}
|
||||
|
||||
var request = http.Request(method, Uri.parse(url));
|
||||
request.headers.addAll(headers ?? {});
|
||||
final future = switch (method) {
|
||||
"GET" => client.get(Uri.parse(url), headers: headers),
|
||||
"POST" => client.post(Uri.parse(url), headers: headers, body: body),
|
||||
"PUT" => client.put(Uri.parse(url), headers: headers, body: body),
|
||||
"DELETE" => client.delete(Uri.parse(url), headers: headers, body: body),
|
||||
_ => client.patch(Uri.parse(url), headers: headers, body: body),
|
||||
};
|
||||
final bytes = (await future).bodyBytes;
|
||||
await file.writeAsBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ abstract class MProvider {
|
|||
|
||||
Future<List<Video>> getVideoList(String url);
|
||||
|
||||
Future<String> getHtmlContent(String url);
|
||||
Future<String> getHtmlContent(String name, String url);
|
||||
|
||||
Future<String> cleanHtmlContent(String html);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:bot_toast/bot_toast.dart';
|
|||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
|
|||
)).map((e) => e.toJson()).toList();
|
||||
} else if (_serviceIndex == 6) {
|
||||
result = (await service.getHtmlContent(
|
||||
"test",
|
||||
_url,
|
||||
));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ class TestSource extends MProvider {
|
|||
|
||||
// For novel html content
|
||||
@override
|
||||
Future<String> getHtmlContent(String url) async {
|
||||
Future<String> getHtmlContent(String name, String url) async {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +409,7 @@ class DefaultExtension extends MProvider {
|
|||
throw new Error("getDetail not implemented");
|
||||
}
|
||||
// For novel html content
|
||||
async getHtmlContent(url) {
|
||||
async getHtmlContent(name, url) {
|
||||
throw new Error("getHtmlContent not implemented");
|
||||
}
|
||||
// Clean html up for reader
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:mangayomi/eval/model/m_bridge.dart';
|
|||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/router/router.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -37,6 +38,9 @@ class TotalChapterCacheSizeState extends _$TotalChapterCacheSizeState {
|
|||
}
|
||||
msg = "0.00 B";
|
||||
} catch (_) {}
|
||||
try {
|
||||
await StorageProvider().deleteTmpDirectory();
|
||||
} catch (_) {}
|
||||
if (msg != null && showToast) {
|
||||
state = msg;
|
||||
botToast(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ class StorageProvider {
|
|||
await Directory(d!.path).delete(recursive: true);
|
||||
}
|
||||
|
||||
Future<void> deleteTmpDirectory() async {
|
||||
final d = await getTmpDirectory();
|
||||
await Directory(d!.path).delete(recursive: true);
|
||||
}
|
||||
|
||||
Future<Directory?> getDefaultDirectory() async {
|
||||
Directory? directory;
|
||||
if (Platform.isAndroid) {
|
||||
|
|
@ -60,6 +65,13 @@ class StorageProvider {
|
|||
return Directory(dbDir);
|
||||
}
|
||||
|
||||
Future<Directory?> getTmpDirectory() async {
|
||||
final gefaultDirectory = await getDirectory();
|
||||
String dbDir = path.join(gefaultDirectory!.path, 'tmp');
|
||||
await Directory(dbDir).create(recursive: true);
|
||||
return Directory(dbDir);
|
||||
}
|
||||
|
||||
Future<Directory?> getIosBackupDirectory() async {
|
||||
final gefaultDirectory = await getDefaultDirectory();
|
||||
String dbDir = path.join(gefaultDirectory!.path, 'backup');
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Future<String> getHtmlContent(Ref ref, {required Chapter chapter}) async {
|
|||
if (htmlContent != null) {
|
||||
html = await getExtensionService(source!).cleanHtmlContent(htmlContent);
|
||||
} else {
|
||||
html = await getExtensionService(source!).getHtmlContent(chapter.url!);
|
||||
html = await getExtensionService(source!).getHtmlContent(chapter.manga.value!.name!, chapter.url!);
|
||||
}
|
||||
return '''<div id="readerViewContent"><div style="padding: 2em;">${html.substring(1, html.length - 1)}</div></div>'''
|
||||
.replaceAll("\\n", "")
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ packages:
|
|||
path: epubx
|
||||
relative: true
|
||||
source: path
|
||||
version: "4.0.2"
|
||||
version: "4.0.3"
|
||||
exception_templates:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
Loading…
Reference in a new issue