5 Commits

Author SHA1 Message Date
BookOasis Reader 5de6a8f110 Reconcile release artifact history
Build & Test / Build & Test App (push) Waiting to run
Release / Extract tag name (push) Waiting to run
Release / Build (push) Blocked by required conditions
Release / Build (FOSS) (push) Blocked by required conditions
Release / Create GitHub Release (push) Blocked by required conditions
Update website / update_website (release) Waiting to run
2026-09-21 16:41:28 +09:00
BookOasis Reader d4789c2373 Update release APKs for 0.20.5 2026-09-21 16:41:02 +09:00
BookOasis Reader 8dc5a0fc0b Update release APKs for 0.20.5
Build & Test / Build & Test App (push) Has been cancelled
Release / Extract tag name (push) Has been cancelled
Release / Build (push) Has been cancelled
Release / Build (FOSS) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2026-09-21 16:38:01 +09:00
BookOasis Reader 105551f658 Bump version to 0.20.5
Release / Extract tag name (push) Has been cancelled
Release / Build (push) Has been cancelled
Release / Build (FOSS) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2026-09-21 16:29:22 +09:00
BookOasis Reader 6a57bc601f Fix reading downloaded EPUB and TXT books
Build & Test / Build & Test App (push) Has been cancelled
2026-09-21 16:23:42 +09:00
10 changed files with 161 additions and 8 deletions
+3
View File
@@ -22,3 +22,6 @@
*.woff binary *.woff binary
*.pyc binary *.pyc binary
*.swp binary *.swp binary
# Android packages
*.apk binary
+2 -2
View File
@@ -33,8 +33,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "app.mihon" applicationId = "app.mihon"
versionCode = 30 versionCode = 31
versionName = "0.20.4" versionName = "0.20.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
@@ -117,6 +117,37 @@ class DownloadProvider(
.firstOrNull() .firstOrNull()
} }
/**
* Returns a downloaded EPUB/TXT document for a chapter if it exists.
*
* Document files are checked explicitly before the generic chapter path so
* a stale image directory with the same chapter name cannot shadow them.
*/
fun findChapterDocument(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
source: Source,
): UniFile? {
val mangaDir = findMangaDir(mangaTitle, source) ?: return null
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
return buildList {
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
add("$chapterDirName.$extension")
}
legacyChapterDirNames.forEach { legacyName ->
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
add("$legacyName.$extension")
}
}
}.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull { it.isFile }
}
/** /**
* Returns a list of downloaded directories for the chapters that exist. * Returns a list of downloaded directories for the chapters that exist.
* *
@@ -130,6 +130,33 @@ class ChapterLoader(
) )
val bookOasisRef = dbChapter.url.toBookOasisChapterRef() val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
val kavitaChapterId = dbChapter.url.toKavitaChapterId() val kavitaChapterId = dbChapter.url.toKavitaChapterId()
val httpSource = source as? HttpSource
val documentFallback: (() -> PageLoader)? = when {
httpSource != null && httpSource.isBookOasisSource() && bookOasisRef != null &&
(bookOasisRef.format == "epub" || bookOasisRef.format == "txt") -> {
{
BookOasisFilePageLoader(
context,
httpSource,
bookOasisRef,
readerPreferences.textReaderStyle(context),
)
}
}
httpSource != null && httpSource.isKavitaSource() && kavitaChapterId != null -> {
{
KavitaCompatPageLoader(
context = context,
chapter = chapter,
source = httpSource,
chapterCache = chapterCache,
chapterId = kavitaChapterId,
textStyle = readerPreferences.textReaderStyle(context),
)
}
}
else -> null
}
return when { return when {
isDownloaded -> DownloadPageLoader( isDownloaded -> DownloadPageLoader(
@@ -138,6 +165,7 @@ class ChapterLoader(
source, source,
downloadManager, downloadManager,
downloadProvider, downloadProvider,
documentFallback,
) )
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
@@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import kotlinx.coroutines.CancellationException
import mihon.core.archive.archiveReader import mihon.core.archive.archiveReader
import mihon.core.archive.epubReader import mihon.core.archive.epubReader
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
/** /**
* Loader used to load a chapter from the downloaded chapters. * Loader used to load a chapter from the downloaded chapters.
@@ -26,6 +28,7 @@ internal class DownloadPageLoader(
private val source: Source, private val source: Source,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider, private val downloadProvider: DownloadProvider,
private val documentFallback: (() -> PageLoader)? = null,
) : PageLoader() { ) : PageLoader() {
private val context: Context by injectLazy() private val context: Context by injectLazy()
@@ -35,9 +38,35 @@ internal class DownloadPageLoader(
override var isLocal: Boolean = true override var isLocal: Boolean = true
override val usesTextReaderStyle: Boolean override val usesTextReaderStyle: Boolean
get() = pageLoader?.usesTextReaderStyle == true get() = pageLoader?.usesTextReaderStyle == true || findDownloadedDocument() != null
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
check(!isRecycled)
return try {
val pages = getDownloadedPages()
if (pages.isNotEmpty() || documentFallback == null) {
pages
} else {
getFallbackPages()
}
} catch (error: Throwable) {
if (error is CancellationException || documentFallback == null) throw error
getFallbackPages()
}
}
private suspend fun getDownloadedPages(): List<ReaderPage> {
val downloadedDocument = findDownloadedDocument()
if (downloadedDocument != null) {
val readableDocument = prepareDownloadedDocument(downloadedDocument)
return when (readableDocument.extension?.lowercase()) {
"epub" -> getPagesFromEpub(readableDocument)
"txt" -> getPagesFromText(readableDocument)
else -> error("Unsupported downloaded document: ${downloadedDocument.name}")
}
}
val dbChapter = chapter.chapter val dbChapter = chapter.chapter
val chapterPath = downloadProvider.findChapterDir( val chapterPath = downloadProvider.findChapterDir(
dbChapter.name, dbChapter.name,
@@ -46,17 +75,79 @@ internal class DownloadPageLoader(
manga.title, manga.title,
source, source,
) )
return if (chapterPath?.isFile == true) { return if (chapterPath?.isFile == true) {
when (chapterPath.extension?.lowercase()) { getPagesFromArchive(chapterPath)
"epub" -> getPagesFromEpub(chapterPath)
"txt" -> getPagesFromText(chapterPath)
else -> getPagesFromArchive(chapterPath)
}
} else { } else {
getPagesFromDirectory() getPagesFromDirectory()
} }
} }
private fun findDownloadedDocument(): UniFile? {
val dbChapter = chapter.chapter
return downloadProvider.findChapterDocument(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
source,
)
}
/**
* Mirror SAF-backed documents into the app cache before opening them.
* EPUB uses mmap internally, which is not guaranteed to work with every
* document provider even though regular stream reads/writes succeed.
*/
private fun prepareDownloadedDocument(file: UniFile): UniFile {
val extension = file.extension?.lowercase()
?: error("Downloaded document has no extension: ${file.name}")
val sourceLength = file.length()
if (sourceLength <= 0L) {
error("Downloaded document is empty: ${file.name}")
}
val cacheDir = File(context.cacheDir, "downloaded_reader").apply { mkdirs() }
val prefix = "${source.id}-${chapter.chapter.id}-"
val target = File(
cacheDir,
"$prefix$sourceLength-${file.lastModified()}.$extension",
)
if (!target.isFile || target.length() != sourceLength) {
cacheDir.listFiles()
?.filter { it.name.startsWith(prefix) }
?.forEach { it.delete() }
val temp = File(cacheDir, "${target.name}.part")
if (temp.exists()) temp.delete()
file.openInputStream().use { input ->
temp.outputStream().use { output -> input.copyTo(output) }
}
if (temp.length() != sourceLength) {
temp.delete()
error("Downloaded document cache copy is incomplete: ${file.name}")
}
if (!temp.renameTo(target)) {
temp.copyTo(target, overwrite = true)
temp.delete()
}
}
return UniFile.fromFile(target)
?: error("Unable to open cached downloaded document: ${target.absolutePath}")
}
private suspend fun getFallbackPages(): List<ReaderPage> {
pageLoader?.recycle()
val fallback = documentFallback?.invoke()
?: error("Downloaded document fallback is unavailable")
pageLoader = fallback
val pages = fallback.getPages()
isLocal = fallback.isLocal
return pages
}
override fun recycle() { override fun recycle() {
super.recycle() super.recycle()
pageLoader?.recycle() pageLoader?.recycle()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.