This commit is contained in:
Moustapha Kodjo Amadou 2026-01-07 13:07:55 +01:00
parent 701a696820
commit 9458ae120b
4 changed files with 196 additions and 87 deletions

View file

@ -306,20 +306,16 @@ Future<void> _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,

View file

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

View file

@ -268,11 +268,27 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
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<NovelWebView>
}
Future.delayed(
const Duration(milliseconds: 10),
const Duration(milliseconds: 100),
() {
if (!scrolled &&
_scrollController.hasClients) {
@ -339,9 +355,13 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
),
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<NovelWebView>
"h1, h2, h3, h4, h5, h6": Style(
color: parseColor(
customTextColor,
fallback: Colors.white,
),
lineHeight: LineHeight(
lineHeight,
@ -402,10 +423,60 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
),
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<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))
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;
}
}

View file

@ -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<hr/>\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;