Update v1.0.49 (#330)
* Added PDF top tab strip visibility toggle and fixed WebView hit test NPE * Refactored desktop reader screens and state management into specialized components * Added image gallery to reader sidebar and refactored desktop PDF UI components * Implemented EPUB image gallery * Refactored reader and library models to use shared common types, centralizing file type resolution and texture management while removing redundant mapping logic * Standardized UI styling and refactored app navigation layout * Implemented auto-hiding reader chrome and activity tracking in desktop * Refactored reader panels into distinct left and right modal layers with platform-specific sizing and keyboard navigation support. * Added PPTX support for desktop and refactored parsing into a shared module * Implement paid AI features and account management for the desktop application. * Implement AI Hub and enhanced Cloud TTS integration for Desktop * Implement streaming support for AI definition and summarization features * Implement support for password-protected PDFs and file actions in the desktop reader. * Implement cloud synchronization for desktop using Firestore and Google Drive * Implement PDF reflow and "Text View" for the desktop reader * Refactor OPDS logic to use SharedOpdsController * Optimize PDF tile rendering performance * Implement two-page spread support for PDF pagination * Implement two-page spread support for the PDF viewer * Improved shared spread zoom in PDF viewer * Improve PDF spread navigation with fling support and configurable page gaps * Add brightness control to PDF and EPUB readers * Refactor folder synchronization to use shared logic engine * Implement safe string formatting and validation for localized resources * Implement TTS chunk skip navigation * Implement deep-linking and playback controls for TTS media sessions * Implement start index for TTS playback * Improve TTS navigation, prefetching, and notification duration reporting * Implement TTS mini playback bar for background reading * Implement multi-window reader support for the desktop application * Improve desktop modal window management and visibility syncing * Implement localized string support for Desktop and shared UI * Implement language selection and persistence for Desktop * Implement plural string support for Desktop and migrate hardcoded counts to plurals.xml * Implement localized banner messages and UI strings using resource-backed SharedText * Implement compact badge styling for small book covers * Refactor PDF native interaction and improve HTML import memory safety * fix language persistence * Refactor reader overflow menus to use section-based logic * Refactor PDF layout remapping and improve text box interaction * Improve CFI resolution and TTS resume accuracy using dynamic chunk offsets * Centralize PDF annotation export mapping and improve metadata handling * Add support for threaded comments in PDF highlight annotations * Flatten highlight comments into a single thread for PDF export and allow author editing * Integrate page slider into reader chrome and persist toggle state * Handle fragments and queries in EPUB chapter paths * Implement dynamic, theme-aware coloring for the reader slider * Implement customizable app-wide font preference * Implement one-hand zoom gestures in the PDF viewer * Implement File Information dialog for PDF and EPUB readers * Bump version to 1.0.49 (53) * Refactor PDF reader logic into modular components * Add ProGuard rules to prevent R8 optimization issues in EPUB reader screens * Add option to use PDF filenames as display names * Fix preservation of PDF filename display preference in library projection
This commit is contained in:
parent
dc5196526f
commit
9510293ac3
245 changed files with 37538 additions and 12460 deletions
|
|
@ -0,0 +1,243 @@
|
|||
package com.aryan.reader.epubreader
|
||||
|
||||
import com.aryan.reader.epub.EpubBook
|
||||
import com.aryan.reader.epub.EpubChapter
|
||||
import com.aryan.reader.paginatedreader.AndroidHtmlResourceResolver
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Base64
|
||||
|
||||
data class EpubReaderImageReference(
|
||||
val id: String,
|
||||
val index: Int,
|
||||
val sourcePath: String,
|
||||
val originalSource: String,
|
||||
val altText: String?,
|
||||
val chapterIndex: Int,
|
||||
val chapterTitle: String,
|
||||
val elementId: String?,
|
||||
val ordinalInChapter: Int,
|
||||
val chunkIndex: Int?,
|
||||
val intrinsicWidth: Int?,
|
||||
val intrinsicHeight: Int?
|
||||
) {
|
||||
val displayTitle: String
|
||||
get() = altText?.trim()?.takeIf { it.isNotBlank() }
|
||||
?: sourceName()?.substringBeforeLast('.')?.takeIf { it.isNotBlank() }
|
||||
?: "Image ${index + 1}"
|
||||
|
||||
val dimensionLabel: String?
|
||||
get() {
|
||||
val width = intrinsicWidth?.takeIf { it > 0 }
|
||||
val height = intrinsicHeight?.takeIf { it > 0 }
|
||||
return if (width != null && height != null) "${width}x$height" else null
|
||||
}
|
||||
|
||||
fun sourceName(): String? {
|
||||
val source = originalSource.takeIf { it.isNotBlank() } ?: sourcePath
|
||||
if (source.startsWith("data:", ignoreCase = true)) return null
|
||||
return source
|
||||
.substringBefore('#')
|
||||
.substringBefore('?')
|
||||
.replace('\\', '/')
|
||||
.substringAfterLast('/')
|
||||
.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun suggestedDownloadFileName(): String {
|
||||
val extension = sourcePath.readerImageExtension()
|
||||
?: originalSource.readerImageExtension()
|
||||
?: "png"
|
||||
val base = altText?.trim()?.takeIf { it.isNotBlank() }
|
||||
?: sourceName()?.substringBeforeLast('.')?.takeIf { it.isNotBlank() }
|
||||
?: "image-${index + 1}"
|
||||
val safeBase = base.sanitizedReaderImageFileBase().ifBlank { "image-${index + 1}" }
|
||||
return "$safeBase.$extension"
|
||||
}
|
||||
|
||||
fun mimeType(): String {
|
||||
val dataMime = readerDataUriMimeType(sourcePath)
|
||||
if (dataMime != null) return dataMime
|
||||
return when (sourcePath.readerImageExtension() ?: originalSource.readerImageExtension()) {
|
||||
"jpg", "jpeg" -> "image/jpeg"
|
||||
"png" -> "image/png"
|
||||
"gif" -> "image/gif"
|
||||
"webp" -> "image/webp"
|
||||
"bmp" -> "image/bmp"
|
||||
"svg" -> "image/svg+xml"
|
||||
else -> "image/*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun EpubBook.readerImageReferencesForDrawer(): List<EpubReaderImageReference> {
|
||||
val references = mutableListOf<EpubReaderImageReference>()
|
||||
chapters.forEachIndexed { chapterIndex, chapter ->
|
||||
val html = chapter.readerImageHtml(extractionBasePath).takeIf { it.isNotBlank() }
|
||||
?: return@forEachIndexed
|
||||
val sourceOrdinalByKey = mutableMapOf<String, Int>()
|
||||
val document = Jsoup.parse(html, chapter.absPath)
|
||||
|
||||
document.select("img, image").forEach { element ->
|
||||
val originalSource = element.readerImageSource() ?: return@forEach
|
||||
val sourcePath = resolveReaderImageSource(chapter, extractionBasePath, originalSource)
|
||||
val sourceKey = sourcePath.readerImageLookupKey()
|
||||
val ordinal = sourceOrdinalByKey.getOrDefault(sourceKey, 0)
|
||||
sourceOrdinalByKey[sourceKey] = ordinal + 1
|
||||
val index = references.size
|
||||
|
||||
references += EpubReaderImageReference(
|
||||
id = "android-epub-image:$chapterIndex:$ordinal:${sourcePath.hashCode()}:$index",
|
||||
index = index,
|
||||
sourcePath = sourcePath,
|
||||
originalSource = originalSource,
|
||||
altText = element.attr("alt").ifBlank { element.attr("title") }.ifBlank { null },
|
||||
chapterIndex = chapterIndex,
|
||||
chapterTitle = chapter.title.ifBlank { "Chapter ${chapterIndex + 1}" },
|
||||
elementId = element.id().ifBlank { null },
|
||||
ordinalInChapter = ordinal,
|
||||
chunkIndex = element.readerTopLevelBodyChildIndex()?.let { it / 20 },
|
||||
intrinsicWidth = element.readerImageDimension("width"),
|
||||
intrinsicHeight = element.readerImageDimension("height")
|
||||
)
|
||||
}
|
||||
}
|
||||
return references
|
||||
}
|
||||
|
||||
fun EpubReaderImageReference.readDownloadBytes(): ByteArray? {
|
||||
if (sourcePath.startsWith("data:", ignoreCase = true)) {
|
||||
return sourcePath.readerDataUriBytes()
|
||||
}
|
||||
return runCatching {
|
||||
File(sourcePath).takeIf { it.isFile }?.readBytes()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun EpubChapter.readerImageHtml(extractionBasePath: String): String {
|
||||
if (htmlContent.isNotBlank()) return htmlContent
|
||||
return runCatching {
|
||||
File(extractionBasePath, htmlFilePath).takeIf { it.isFile }?.readText().orEmpty()
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
private fun Element.readerImageSource(): String? {
|
||||
return listOf("src", "href", "xlink:href", "data-src")
|
||||
.firstNotNullOfOrNull { attrName ->
|
||||
attr(attrName).trim().takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.readerImageDimension(attribute: String): Int? {
|
||||
val raw = attr(attribute).trim().takeIf { it.isNotBlank() } ?: return null
|
||||
return Regex("""\d+""").find(raw)?.value?.toIntOrNull()?.takeIf { it > 0 }
|
||||
}
|
||||
|
||||
private fun Element.readerTopLevelBodyChildIndex(): Int? {
|
||||
val body = ownerDocument()?.body() ?: return null
|
||||
var topLevel: Element = this
|
||||
while (topLevel.parent() != null && topLevel.parent() != body) {
|
||||
topLevel = topLevel.parent() ?: break
|
||||
}
|
||||
if (topLevel.parent() != body) return null
|
||||
return body.childNodes().indexOf(topLevel).takeIf { it >= 0 }
|
||||
}
|
||||
|
||||
private fun resolveReaderImageSource(
|
||||
chapter: EpubChapter,
|
||||
extractionBasePath: String,
|
||||
source: String
|
||||
): String {
|
||||
val withoutFragment = source.substringBefore('#').substringBefore('?')
|
||||
if (withoutFragment.startsWith("data:", ignoreCase = true)) return source
|
||||
|
||||
val fileUriPath = withoutFragment.readerFileUriPath()
|
||||
fileUriPath?.let { path ->
|
||||
val file = File(path)
|
||||
if (file.isFile) {
|
||||
return runCatching { file.canonicalFile.absolutePath }.getOrDefault(file.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
val sourceForResolve = fileUriPath ?: withoutFragment
|
||||
AndroidHtmlResourceResolver.resolvePath(chapter.absPath, extractionBasePath, sourceForResolve)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val fallbackCandidates = listOf(
|
||||
File(extractionBasePath, sourceForResolve),
|
||||
File(extractionBasePath, sourceForResolve.trimStart('/', '\\'))
|
||||
)
|
||||
return fallbackCandidates
|
||||
.firstOrNull { it.isFile }
|
||||
?.let { runCatching { it.canonicalFile.absolutePath }.getOrDefault(it.absolutePath) }
|
||||
?: source
|
||||
}
|
||||
|
||||
private fun String.readerFileUriPath(): String? {
|
||||
if (!startsWith("file:", ignoreCase = true)) return null
|
||||
return runCatching {
|
||||
URI(this).path?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun String.readerImageLookupKey(): String {
|
||||
return substringBefore('#')
|
||||
.substringBefore('?')
|
||||
.replace('\\', '/')
|
||||
.lowercase()
|
||||
}
|
||||
|
||||
private fun String.readerImageExtension(): String? {
|
||||
readerDataUriMimeType(this)?.let { mime ->
|
||||
return when (mime.lowercase()) {
|
||||
"image/jpeg" -> "jpg"
|
||||
"image/png" -> "png"
|
||||
"image/gif" -> "gif"
|
||||
"image/webp" -> "webp"
|
||||
"image/bmp" -> "bmp"
|
||||
"image/svg+xml" -> "svg"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return substringBefore('#')
|
||||
.substringBefore('?')
|
||||
.substringAfterLast('.', "")
|
||||
.lowercase()
|
||||
.takeIf { it in setOf("jpg", "jpeg", "png", "gif", "webp", "bmp", "svg") }
|
||||
}
|
||||
|
||||
private fun readerDataUriMimeType(source: String): String? {
|
||||
if (!source.startsWith("data:", ignoreCase = true)) return null
|
||||
return source
|
||||
.drop(5)
|
||||
.substringBefore(';')
|
||||
.substringBefore(',')
|
||||
.takeIf { it.startsWith("image/", ignoreCase = true) }
|
||||
}
|
||||
|
||||
private fun String.readerDataUriBytes(): ByteArray? {
|
||||
val commaIndex = indexOf(',')
|
||||
if (!startsWith("data:", ignoreCase = true) || commaIndex == -1) return null
|
||||
val metadata = substring(0, commaIndex)
|
||||
val data = substring(commaIndex + 1)
|
||||
return runCatching {
|
||||
if (metadata.contains(";base64", ignoreCase = true)) {
|
||||
Base64.getDecoder().decode(data)
|
||||
} else {
|
||||
URLDecoder.decode(data, StandardCharsets.UTF_8.name()).toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun String.sanitizedReaderImageFileBase(): String {
|
||||
return replace(Regex("""[\\/:*?"<>|]+"""), "_")
|
||||
.replace(Regex("""\s+"""), " ")
|
||||
.trim()
|
||||
.trim('.')
|
||||
.take(80)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue