book-reader/app/src/main/java/com/aryan/reader/opds/OpdsViewModel.kt
Aryan 9510293ac3
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
2026-05-20 22:14:01 +05:30

172 lines
6.8 KiB
Kotlin

package com.aryan.reader.opds
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.aryan.reader.R
import com.aryan.reader.shared.opds.SharedOpdsController
import com.aryan.reader.shared.opds.SharedOpdsDownloadNamer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Request
import okhttp3.Response
import timber.log.Timber
import java.io.File
import java.util.UUID
class OpdsViewModel(application: Application) : AndroidViewModel(application) {
private val repository = OpdsRepository(application)
private val controller = SharedOpdsController(
repository = repository,
feedLoadErrorMessage = { error ->
application.getString(R.string.opds_error_load_feed, error.message.orEmpty())
},
idFactory = { UUID.randomUUID().toString() }
)
private val _uiState = MutableStateFlow(controller.state)
val uiState: StateFlow<OpdsScreenState> = _uiState.asStateFlow()
fun loadNextPage() {
viewModelScope.launch {
controller.loadNextPage(::emitState)
}
}
fun downloadBook(entry: OpdsEntry, acquisition: OpdsAcquisition, context: Context, onDownloaded: (Uri) -> Unit) {
val downloadUrl = acquisition.url
val catalog = _uiState.value.currentCatalog
viewModelScope.launch {
updateDownloadState(entry.id, OpdsDownloadState(isDownloading = true, progress = 0f))
try {
val tempFile = withContext(Dispatchers.IO) {
val client = repository.getAuthenticatedClient(catalog?.username, catalog?.password)
val request = Request.Builder().url(downloadUrl).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw OpdsDownloadFailedException(
context.getString(R.string.opds_error_download_failed, response.message)
)
}
val body = response.body
?: throw IllegalStateException(context.getString(R.string.opds_error_empty_response))
val contentLength = body.contentLength()
val ext = resolveOpdsDownloadExtension(acquisition, response)
val safeTitle = SharedOpdsDownloadNamer.safeFileStem(entry.title).take(50)
val tempFile = File(context.cacheDir, "opds_dl_${safeTitle}$ext")
body.byteStream().use { input ->
tempFile.outputStream().use { output ->
val buffer = ByteArray(8 * 1024)
var totalRead = 0L
var lastProgressUpdate = System.currentTimeMillis()
while (true) {
val bytesRead = input.read(buffer)
if (bytesRead == -1) break
output.write(buffer, 0, bytesRead)
totalRead += bytesRead
if (contentLength > 0) {
val now = System.currentTimeMillis()
if (now - lastProgressUpdate > 200) {
val progress = (totalRead.toFloat() / contentLength.toFloat()).coerceIn(0f, 1f)
withContext(Dispatchers.Main) {
updateDownloadState(
entry.id,
OpdsDownloadState(isDownloading = true, progress = progress)
)
}
lastProgressUpdate = now
}
}
}
}
}
tempFile
}
}
onDownloaded(Uri.fromFile(tempFile))
} catch (e: Exception) {
Timber.e(e, "Download error")
val message = if (e is OpdsDownloadFailedException) {
e.message.orEmpty()
} else {
context.getString(R.string.opds_error_download_error, e.message.orEmpty())
}
emitState(controller.setErrorMessage(message))
} finally {
updateDownloadState(entry.id, null)
}
}
}
private fun resolveOpdsDownloadExtension(acquisition: OpdsAcquisition, response: Response): String {
return SharedOpdsDownloadNamer.resolveExtension(
acquisition = acquisition,
contentDisposition = response.header("Content-Disposition"),
urlPathSegment = Uri.parse(acquisition.url).lastPathSegment
)
}
fun addCatalog(title: String, url: String, username: String?, password: String?) {
emitState(controller.addCatalog(title, url, username, password))
}
fun removeCatalog(id: String) {
emitState(controller.removeCatalog(id))
}
fun openCatalog(catalog: OpdsCatalog) {
viewModelScope.launch {
controller.openCatalog(catalog, ::emitState)
}
}
fun openFeedUrl(url: String) {
viewModelScope.launch {
controller.openFeedUrl(url, ::emitState)
}
}
fun navigateBack(): Boolean {
val returnsToPreviousFeed = controller.hasFeedHistory()
viewModelScope.launch {
controller.navigateBack(::emitState)
}
return returnsToPreviousFeed
}
fun updateCatalog(id: String, title: String, url: String, username: String?, password: String?) {
emitState(controller.updateCatalog(id, title, url, username, password))
}
fun search(query: String) {
viewModelScope.launch {
controller.search(query, ::emitState)
}
}
fun clearError() {
emitState(controller.clearError())
}
private fun updateDownloadState(entryId: String, downloadState: OpdsDownloadState?) {
emitState(controller.updateDownloadState(entryId, downloadState))
}
private fun emitState(state: OpdsScreenState) {
_uiState.value = state
}
private class OpdsDownloadFailedException(message: String) : Exception(message)
}