From 9458ae120b5f05bf7fb6f901a75c36dc5b9b82b4 Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:07:55 +0100 Subject: [PATCH] fix #637 --- .../library/providers/file_scanner.dart | 24 ++- .../library/providers/local_archive.dart | 20 +-- lib/modules/novel/novel_reader_view.dart | 137 +++++++++++++++--- lib/services/get_html_content.dart | 102 +++++++------ 4 files changed, 196 insertions(+), 87 deletions(-) diff --git a/lib/modules/library/providers/file_scanner.dart b/lib/modules/library/providers/file_scanner.dart index c7db7b14..361d115f 100644 --- a/lib/modules/library/providers/file_scanner.dart +++ b/lib/modules/library/providers/file_scanner.dart @@ -306,20 +306,16 @@ Future _scanDirectory(Ref ref, Directory? dir) async { : Uint8List.fromList(coverImage).getCoverImage; saveManga++; } - for (var chapter in book.Chapters ?? []) { - chaptersToSave.add( - Chapter( - mangaId: manga.id, - name: chapter.Title is String && chapter.Title.isEmpty - ? "Book" - : chapter.Title, - archivePath: chapterPath, - downloadSize: chapterFile.existsSync() - ? chapterFile.lengthSync().formattedFileSize() - : null, - )..manga.value = manga, - ); - } + chaptersToSave.add( + Chapter( + mangaId: manga.id, + name: book.Title, + archivePath: chapterPath, + downloadSize: chapterFile.existsSync() + ? chapterFile.lengthSync().formattedFileSize() + : null, + )..manga.value = manga, + ); } else { final chap = Chapter( mangaId: manga.id, diff --git a/lib/modules/library/providers/local_archive.dart b/lib/modules/library/providers/local_archive.dart index 9a36b44f..ddb6b2ad 100644 --- a/lib/modules/library/providers/local_archive.dart +++ b/lib/modules/library/providers/local_archive.dart @@ -85,18 +85,14 @@ Future importArchivesFromFile( : Uint8List.fromList(coverImage).getCoverImage, ); } - 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, - ); - } + chapters.add( + Chapter( + mangaId: mangaId, + name: book.Title, + archivePath: file.path, + updatedAt: DateTime.now().millisecondsSinceEpoch, + )..manga.value = manga, + ); } else { chapters.add( Chapter( diff --git a/lib/modules/novel/novel_reader_view.dart b/lib/modules/novel/novel_reader_view.dart index 8c7719f1..5070556e 100644 --- a/lib/modules/novel/novel_reader_view.dart +++ b/lib/modules/novel/novel_reader_view.dart @@ -268,11 +268,27 @@ class _NovelWebViewState extends ConsumerState novelReaderTextColorStateProvider, ); - Color parseColor(String hex) { - final hexColor = hex.replaceAll('#', ''); - return Color( - int.parse('FF$hexColor', radix: 16), - ); + Color parseColor(String hex, {Color? fallback}) { + try { + String hexColor = hex.trim().replaceAll( + '#', + '', + ); + // Ensure we have a valid 6-character hex color + if (hexColor.length == 6) { + return Color( + int.parse('FF$hexColor', radix: 16), + ); + } else if (hexColor.length == 8) { + // Already has alpha channel + return Color( + int.parse(hexColor, radix: 16), + ); + } + } catch (_) { + // If parsing fails, use fallback + } + return fallback ?? Colors.grey; } TextAlign getTextAlign() { @@ -289,7 +305,7 @@ class _NovelWebViewState extends ConsumerState } Future.delayed( - const Duration(milliseconds: 10), + const Duration(milliseconds: 100), () { if (!scrolled && _scrollController.hasClients) { @@ -339,9 +355,13 @@ class _NovelWebViewState extends ConsumerState ), color: parseColor( customTextColor, + fallback: Colors.white, ), backgroundColor: parseColor( customBackgroundColor, + fallback: const Color( + 0xFF292832, + ), ), margin: Margins.zero, padding: HtmlPaddings.all( @@ -384,6 +404,7 @@ class _NovelWebViewState extends ConsumerState "h1, h2, h3, h4, h5, h6": Style( color: parseColor( customTextColor, + fallback: Colors.white, ), lineHeight: LineHeight( lineHeight, @@ -402,10 +423,60 @@ class _NovelWebViewState extends ConsumerState ), height: Height.auto(), ), + "table": Style( + border: Border.all( + color: Colors.grey, + width: 1, + ), + margin: Margins.symmetric( + vertical: 10, + ), + ), + "td, th": Style( + border: Border.all( + color: Colors.grey, + width: 0.5, + ), + padding: HtmlPaddings.all(8), + ), + "th": Style( + fontWeight: FontWeight.bold, + backgroundColor: Colors.grey + .withValues(alpha: 0.2), + ), + "blockquote": Style( + border: Border( + left: BorderSide( + color: Colors.grey, + width: 4, + ), + ), + padding: HtmlPaddings.only( + left: 15, + ), + margin: Margins.symmetric( + vertical: 10, + ), + fontStyle: FontStyle.italic, + ), + "pre, code": Style( + backgroundColor: Colors.grey + .withValues(alpha: 0.2), + padding: HtmlPaddings.all(8), + fontFamily: 'monospace', + ), + "hr": Style( + margin: Margins.symmetric( + vertical: 20, + ), + ), }, extensions: [ TagExtension( - tagsToExtend: {"img"}, + tagsToExtend: { + "img", + "source", + }, builder: (extensionContext) { final element = extensionContext.node @@ -1125,22 +1196,48 @@ class _NovelWebViewState extends ConsumerState } 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)) + if (epubBook == null) return null; + + if (element.localName == "img" && element.getSrc != null) { + final src = element.getSrc!; + final fileName = src.split("/").last; + final image = epubBook!.Content?.Images?.entries + .firstWhereOrNull( + (img) => + img.key.endsWith(fileName) || + img.key.contains(fileName.replaceAll('%20', ' ')), + ) ?.value .Content; - return image != null - ? widgets.Image( - errorBuilder: (context, error, stackTrace) => Text("❌"), - fit: BoxFit.scaleDown, - image: MemoryImage(image as Uint8List) as ImageProvider, - ) - : null; + + if (image != null) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: widgets.Image( + errorBuilder: (context, error, stackTrace) => Container( + padding: const EdgeInsets.all(8), + color: Colors.red.withValues(alpha: 0.1), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.broken_image, color: Colors.red), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Image not loaded: $fileName', + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + fit: BoxFit.contain, + image: MemoryImage(image as Uint8List) as ImageProvider, + ), + ); + } } + return null; } } diff --git a/lib/services/get_html_content.dart b/lib/services/get_html_content.dart index 8d2d7b28..fee98031 100644 --- a/lib/services/get_html_content.dart +++ b/lib/services/get_html_content.dart @@ -16,7 +16,7 @@ Future<(String, EpubBook?)> getHtmlContent( required Chapter chapter, }) async { final keepAlive = ref.keepAlive(); - (String, EpubBook?) result; + (String, EpubBook?)? result; try { if (!chapter.manga.isLoaded) { chapter.manga.loadSync(); @@ -26,50 +26,53 @@ Future<(String, EpubBook?)> getHtmlContent( 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; - result = (_buildHtml(tempChapter?.HtmlContent ?? "No content"), book); + String htmlContent = ""; + for (var subChapter in book.Content!.Html!.values) { + htmlContent += "\n
\n${subChapter.Content}"; + } + + result = (_buildHtml(htmlContent), book); } - result = (_buildHtml("Local epub file not found!"), null); + result ??= (_buildHtml("Local epub file not found!"), null); } - final storageProvider = StorageProvider(); - final mangaMainDirectory = await storageProvider.getMangaMainDirectory( - chapter, - ); - final chapterDirectory = (await storageProvider.getMangaChapterDirectory( - chapter, - mangaMainDirectory: mangaMainDirectory, - ))!; + if (result == null) { + final storageProvider = StorageProvider(); + final mangaMainDirectory = await storageProvider.getMangaMainDirectory( + chapter, + ); + final chapterDirectory = (await storageProvider.getMangaChapterDirectory( + chapter, + mangaMainDirectory: mangaMainDirectory, + ))!; - final htmlPath = p.join(chapterDirectory.path, "${chapter.name}.html"); + final htmlPath = p.join(chapterDirectory.path, "${chapter.name}.html"); - final htmlFile = File(htmlPath); - String? htmlContent; - if (await htmlFile.exists()) { - htmlContent = await htmlFile.readAsString(); + final htmlFile = File(htmlPath); + String? htmlContent; + if (await htmlFile.exists()) { + htmlContent = await htmlFile.readAsString(); + } + final source = getSource( + chapter.manga.value!.lang!, + chapter.manga.value!.source!, + chapter.manga.value!.sourceId, + ); + String? html; + final proxyServer = ref.read(androidProxyServerStateProvider); + if (htmlContent != null) { + html = await getExtensionService( + source!, + proxyServer, + ).cleanHtmlContent(htmlContent); + } else { + html = await getExtensionService( + source!, + proxyServer, + ).getHtmlContent(chapter.manga.value!.name!, chapter.url!); + } + result = (_buildHtml(html.substring(1, html.length - 1)), null); } - final source = getSource( - chapter.manga.value!.lang!, - chapter.manga.value!.source!, - chapter.manga.value!.sourceId, - ); - String? html; - final proxyServer = ref.read(androidProxyServerStateProvider); - if (htmlContent != null) { - html = await getExtensionService( - source!, - proxyServer, - ).cleanHtmlContent(htmlContent); - } else { - html = await getExtensionService( - source!, - proxyServer, - ).getHtmlContent(chapter.manga.value!.name!, chapter.url!); - } - result = (_buildHtml(html.substring(1, html.length - 1)), null); + keepAlive.close(); return result; } catch (e) { @@ -91,11 +94,28 @@ String _buildHtml(String input) { // Parse HTML to clean it final document = parse(cleaned); - // Remove unwanted elements + // Remove unwanted elements (ads, tracking, etc.) document.querySelectorAll('iframe').forEach((el) => el.remove()); document.querySelectorAll('script').forEach((el) => el.remove()); document.querySelectorAll('[data-aa]').forEach((el) => el.remove()); + // Improve styles for EPUB tables + document.querySelectorAll('table').forEach((table) { + table.attributes['style'] = + '${table.attributes['style'] ?? ''} border-collapse: collapse; width: 100%; margin: 10px 0;'; + }); + + document.querySelectorAll('td, th').forEach((cell) { + cell.attributes['style'] = + '${cell.attributes['style'] ?? ''} border: 1px solid #ddd; padding: 8px;'; + }); + + // Improve citations/blockquotes + document.querySelectorAll('blockquote').forEach((quote) { + quote.attributes['style'] = + '${quote.attributes['style'] ?? ''} border-left: 4px solid #ccc; padding-left: 15px; margin: 10px 0; font-style: italic;'; + }); + // Get cleaned HTML String htmlContent = document.body?.innerHtml ?? cleaned;