added support for epubs

embedded images not supported yet
This commit is contained in:
Schnitzel5 2025-05-07 22:17:23 +02:00
parent d6d674c270
commit 3677938bf8
19 changed files with 149 additions and 89 deletions

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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 "";
}
}
}

View file

@ -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"

View file

@ -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 =>

View file

@ -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

View file

@ -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);

View file

@ -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,

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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';

View file

@ -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 {

View file

@ -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

View file

@ -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(

View file

@ -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');

View file

@ -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", "")

View file

@ -445,7 +445,7 @@ packages:
path: epubx
relative: true
source: path
version: "4.0.2"
version: "4.0.3"
exception_templates:
dependency: transitive
description: