* 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
172 lines
6.8 KiB
Kotlin
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)
|
|
}
|