mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-20 15:02:07 +00:00
Merge pull request #527 from Schnitzel5/feature/local-epub
added support for local epubs and displaying its images
This commit is contained in:
commit
7bb3da36fc
8 changed files with 165 additions and 80 deletions
|
|
@ -2085,6 +2085,11 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
|
||||
void _importLocal(BuildContext context, ItemType itemType) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
final filesText = switch (itemType) {
|
||||
ItemType.manga => ".zip, .cbz",
|
||||
ItemType.anime => ".mp4, .mkv, .avi, and more",
|
||||
ItemType.novel => ".epub",
|
||||
};
|
||||
bool isLoading = false;
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -2133,7 +2138,7 @@ void _importLocal(BuildContext context, ItemType itemType) {
|
|||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
Text(
|
||||
"${l10n.import_files} ( ${itemType == ItemType.manga ? ".zip, .cbz" : ".mp4, .mkv, .avi, and more"} )",
|
||||
"${l10n.import_files} ( $filesText )",
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:epubx/epubx.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
|
|
@ -19,9 +21,11 @@ Future importArchivesFromFile(
|
|||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: true,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: itemType == ItemType.manga
|
||||
? ['cbz', 'zip']
|
||||
: ['mp4', 'mov', 'avi', 'flv', 'wmv', 'mpeg', 'mkv'],
|
||||
allowedExtensions: switch (itemType) {
|
||||
ItemType.manga => ['cbz', 'zip'],
|
||||
ItemType.anime => ['mp4', 'mov', 'avi', 'flv', 'wmv', 'mpeg', 'mkv'],
|
||||
ItemType.novel => ['epub'],
|
||||
},
|
||||
);
|
||||
if (result != null) {
|
||||
final dateNow = DateTime.now().millisecondsSinceEpoch;
|
||||
|
|
@ -57,16 +61,45 @@ Future importArchivesFromFile(
|
|||
manga.customCoverImage = itemType == ItemType.manga ? data!.$3 : null;
|
||||
}
|
||||
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(manga);
|
||||
final chapters = Chapter(
|
||||
name: itemType == ItemType.manga ? data!.$1 : name,
|
||||
archivePath: itemType == ItemType.manga ? data!.$4 : file.path,
|
||||
mangaId: manga.id,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
)..manga.value = manga;
|
||||
isar.chapters.putSync(chapters);
|
||||
chapters.manga.saveSync();
|
||||
await isar.writeTxn(() async {
|
||||
final mangaId = await isar.mangas.put(manga);
|
||||
final List<Chapter> chapters = [];
|
||||
if (itemType == ItemType.novel) {
|
||||
final bytes = await File(file.path!).readAsBytes();
|
||||
final book = await EpubReader.readBook(bytes);
|
||||
if (book.Content != null && book.Content!.Images != null) {
|
||||
final coverImage =
|
||||
book.Content!.Images!.containsKey("media/file0.png")
|
||||
? book.Content!.Images!["media/file0.png"]!.Content
|
||||
: book.Content!.Images!.values.first.Content;
|
||||
await isar.mangas.put(manga..customCoverImage = coverImage);
|
||||
}
|
||||
for (var chapter in book.Chapters ?? []) {
|
||||
chapters.add(
|
||||
Chapter(
|
||||
mangaId: mangaId,
|
||||
name: chapter.Title is String && chapter.Title.isEmpty
|
||||
? "Book"
|
||||
: chapter.Title,
|
||||
archivePath: file.path,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
)..manga.value = manga,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
chapters.add(
|
||||
Chapter(
|
||||
name: itemType == ItemType.manga ? data!.$1 : name,
|
||||
archivePath: itemType == ItemType.manga ? data!.$4 : file.path,
|
||||
mangaId: manga.id,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
)..manga.value = manga,
|
||||
);
|
||||
}
|
||||
for (final chapter in chapters) {
|
||||
await isar.chapters.put(chapter);
|
||||
await chapter.manga.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +113,7 @@ String _getName(String path) {
|
|||
.split("\\")
|
||||
.last
|
||||
.replaceAll(
|
||||
RegExp(r'\.(mp4|mov|avi|flv|wmv|mpeg|mkv|cbz|zip|cbt|tar)'),
|
||||
RegExp(r'\.(mp4|mov|avi|flv|wmv|mpeg|mkv|cbz|zip|cbt|tar|epub)'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'local_archive.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$importArchivesFromFileHash() =>
|
||||
r'e57fafc17833a24bccdd8f945a4c8e6dc50b49c0';
|
||||
r'4d92aaade0544f76214030364433f91d27570b5a';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:epubx/epubx.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_qjs/quickjs/ffi.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -17,6 +19,7 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr
|
|||
import 'package:mangayomi/modules/novel/novel_reader_controller_provider.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/services/get_html_content.dart';
|
||||
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
|
||||
import 'package:mangayomi/utils/utils.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/services/get_chapter_pages.dart';
|
||||
|
|
@ -25,6 +28,8 @@ import 'package:mangayomi/utils/global_style.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:flutter/widgets.dart' as widgets;
|
||||
|
||||
typedef DoubleClickAnimationListener = void Function();
|
||||
|
||||
|
|
@ -34,21 +39,17 @@ class NovelReaderView extends ConsumerWidget {
|
|||
late final Chapter chapter = isar.chapters.getSync(chapterId)!;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final htmlContent = ref.watch(getHtmlContentProvider(chapter: chapter));
|
||||
final result = ref.watch(getHtmlContentProvider(chapter: chapter));
|
||||
|
||||
return NovelWebView(chapter: chapter, htmlContent: htmlContent);
|
||||
return NovelWebView(chapter: chapter, result: result);
|
||||
}
|
||||
}
|
||||
|
||||
class NovelWebView extends ConsumerStatefulWidget {
|
||||
const NovelWebView({
|
||||
super.key,
|
||||
required this.chapter,
|
||||
required this.htmlContent,
|
||||
});
|
||||
const NovelWebView({super.key, required this.chapter, required this.result});
|
||||
|
||||
final Chapter chapter;
|
||||
final AsyncValue<String> htmlContent;
|
||||
final AsyncValue<(String, EpubBook?)> result;
|
||||
|
||||
@override
|
||||
ConsumerState createState() {
|
||||
|
|
@ -98,6 +99,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
}
|
||||
|
||||
late Chapter chapter = widget.chapter;
|
||||
EpubBook? epubBook;
|
||||
|
||||
final StreamController<double> _rebuildDetail =
|
||||
StreamController<double>.broadcast();
|
||||
|
|
@ -204,8 +206,9 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
widget.htmlContent.when(
|
||||
data: (htmlContent) {
|
||||
widget.result.when(
|
||||
data: (data) {
|
||||
epubBook = data.$2;
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
if (!scrolled && _scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
|
|
@ -221,48 +224,48 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
interactive: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
_isViewFunction();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
HtmlWidget(
|
||||
htmlContent,
|
||||
customStylesBuilder: (element) {
|
||||
switch (backgroundColor) {
|
||||
case BackgroundColor.black:
|
||||
return {
|
||||
'background-color': 'black',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
'background-color': '#F0F0F0',
|
||||
};
|
||||
}
|
||||
},
|
||||
onTapUrl: (url) {
|
||||
context.push(
|
||||
"/mangawebview",
|
||||
extra: {'url': url, 'title': url},
|
||||
);
|
||||
return true;
|
||||
},
|
||||
renderMode: RenderMode.column,
|
||||
textStyle: TextStyle(
|
||||
color:
|
||||
backgroundColor ==
|
||||
BackgroundColor.white
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontSize: fontSize.toDouble(),
|
||||
),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
_isViewFunction();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
HtmlWidget(
|
||||
data.$1,
|
||||
customWidgetBuilder: (element) =>
|
||||
_buildCustomWidgets(element),
|
||||
customStylesBuilder: (element) {
|
||||
switch (backgroundColor) {
|
||||
case BackgroundColor.black:
|
||||
return {'background-color': 'black'};
|
||||
default:
|
||||
return {
|
||||
'background-color': '#F0F0F0',
|
||||
};
|
||||
}
|
||||
},
|
||||
onTapUrl: (url) {
|
||||
context.push(
|
||||
"/mangawebview",
|
||||
extra: {'url': url, 'title': url},
|
||||
);
|
||||
return true;
|
||||
},
|
||||
renderMode: RenderMode.sliverList,
|
||||
textStyle: TextStyle(
|
||||
color:
|
||||
backgroundColor ==
|
||||
BackgroundColor.white
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontSize: fontSize.toDouble(),
|
||||
),
|
||||
Center(
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
heightFactor: 2,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
|
|
@ -308,8 +311,8 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -733,6 +736,26 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _buildCustomWidgets(dom.Element element) {
|
||||
if (element.localName == "img" &&
|
||||
element.getSrc != null &&
|
||||
epubBook != null) {
|
||||
final fileName = element.getSrc!.split("/").last;
|
||||
final image = epubBook!.Content!.Images!.entries
|
||||
.firstWhereOrNull((img) => img.key.endsWith(fileName))
|
||||
?.value
|
||||
.Content;
|
||||
return image != null
|
||||
? widgets.Image(
|
||||
errorBuilder: (context, error, stackTrace) => Text("❌"),
|
||||
fit: BoxFit.scaleDown,
|
||||
image: MemoryImage(image as Uint8List) as ImageProvider,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class UChapDataPreload {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'aniskip.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421';
|
||||
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
|
||||
|
||||
/// See also [AniSkip].
|
||||
@ProviderFor(AniSkip)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:epubx/epubx.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:mangayomi/eval/lib.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
|
|
@ -10,10 +11,27 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
part 'get_html_content.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<String> getHtmlContent(Ref ref, {required Chapter chapter}) async {
|
||||
Future<(String, EpubBook?)> getHtmlContent(
|
||||
Ref ref, {
|
||||
required Chapter chapter,
|
||||
}) async {
|
||||
if (!chapter.manga.isLoaded) {
|
||||
chapter.manga.loadSync();
|
||||
}
|
||||
if (chapter.archivePath != null && chapter.archivePath!.isNotEmpty) {
|
||||
final htmlFile = File(chapter.archivePath!);
|
||||
if (await htmlFile.exists()) {
|
||||
final bytes = await htmlFile.readAsBytes();
|
||||
final book = await EpubReader.readBook(bytes);
|
||||
final tempChapter = book.Chapters?.where(
|
||||
(element) => element.Title!.isNotEmpty
|
||||
? element.Title == chapter.name
|
||||
: "Book" == chapter.name,
|
||||
).firstOrNull;
|
||||
return (_buildHtml(tempChapter?.HtmlContent ?? "No content"), book);
|
||||
}
|
||||
return (_buildHtml("Local epub file not found!"), null);
|
||||
}
|
||||
final storageProvider = StorageProvider();
|
||||
final mangaDirectory = await storageProvider.getMangaMainDirectory(chapter);
|
||||
final htmlPath = "${mangaDirectory!.path}${chapter.name}.html";
|
||||
|
|
@ -37,7 +55,11 @@ Future<String> getHtmlContent(Ref ref, {required Chapter chapter}) async {
|
|||
source!,
|
||||
).getHtmlContent(chapter.manga.value!.name!, chapter.url!);
|
||||
}
|
||||
return '''<div id="readerViewContent"><div style="padding: 2em;">${html.substring(1, html.length - 1)}</div></div>'''
|
||||
return (_buildHtml(html.substring(1, html.length - 1)), null);
|
||||
}
|
||||
|
||||
String _buildHtml(String input) {
|
||||
return '''<div id="readerViewContent"><div style="padding: 2em;">$input</div></div>'''
|
||||
.replaceAll("\\n", "")
|
||||
.replaceAll("\\t", "")
|
||||
.replaceAll("\\\"", "\"");
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_html_content.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getHtmlContentHash() => r'6bdc17222f959cb5f91b56027d4f98e26571175d';
|
||||
String _$getHtmlContentHash() => r'19e6959d8fceb065b19c6c6d38cd1b5132a8ba94';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
@ -34,7 +34,7 @@ class _SystemHash {
|
|||
const getHtmlContentProvider = GetHtmlContentFamily();
|
||||
|
||||
/// See also [getHtmlContent].
|
||||
class GetHtmlContentFamily extends Family<AsyncValue<String>> {
|
||||
class GetHtmlContentFamily extends Family<AsyncValue<(String, EpubBook?)>> {
|
||||
/// See also [getHtmlContent].
|
||||
const GetHtmlContentFamily();
|
||||
|
||||
|
|
@ -72,7 +72,8 @@ class GetHtmlContentFamily extends Family<AsyncValue<String>> {
|
|||
}
|
||||
|
||||
/// See also [getHtmlContent].
|
||||
class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
|
||||
class GetHtmlContentProvider
|
||||
extends AutoDisposeFutureProvider<(String, EpubBook?)> {
|
||||
/// See also [getHtmlContent].
|
||||
GetHtmlContentProvider({
|
||||
required Chapter chapter,
|
||||
|
|
@ -107,7 +108,7 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
|
|||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<String> Function(GetHtmlContentRef provider) create,
|
||||
FutureOr<(String, EpubBook?)> Function(GetHtmlContentRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
|
|
@ -124,7 +125,7 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
|
|||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<String> createElement() {
|
||||
AutoDisposeFutureProviderElement<(String, EpubBook?)> createElement() {
|
||||
return _GetHtmlContentProviderElement(this);
|
||||
}
|
||||
|
||||
|
|
@ -144,13 +145,14 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
|
|||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin GetHtmlContentRef on AutoDisposeFutureProviderRef<String> {
|
||||
mixin GetHtmlContentRef on AutoDisposeFutureProviderRef<(String, EpubBook?)> {
|
||||
/// The parameter `chapter` of this provider.
|
||||
Chapter get chapter;
|
||||
}
|
||||
|
||||
class _GetHtmlContentProviderElement
|
||||
extends AutoDisposeFutureProviderElement<String> with GetHtmlContentRef {
|
||||
extends AutoDisposeFutureProviderElement<(String, EpubBook?)>
|
||||
with GetHtmlContentRef {
|
||||
_GetHtmlContentProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'anilist.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$anilistHash() => r'c786a526fdacc875e4a7e00886b2bda546eafeae';
|
||||
String _$anilistHash() => r'fafb964252b3a5741e981cb8c2f0f2090b3b86ae';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
Loading…
Reference in a new issue