book-reader/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModel.kt
Aryan d16bdd70d4
V1.0.46 oss (#253)
* add a UI trigger for demo annotations

* Implement internal link navigation

* Simplify bottom padding logic in `EpubReaderScreen` for vertical scroll mode

* Add support for Calibre metadata

* Implement advanced library management with Room-backed collections, tags, and smart rules.

* Display book series information in book details and remove tags from home screen

* Implement zoom reset functionality in PDF viewer

* Add a "Pages" thumbnail view to the PDF navigation drawer

* Implement jump-back navigation in PDF viewer

* MainViewModel: disable FolderSyncWorker, add log export, and enhance PDF position logging

* Replace Gson with Kotlin Serialization in SmartCollectionEngine

* Implement ONNX-based speech bubble detection

* Implement speech bubble detection in PDF viewer

* Implement "Smart Comic Zoom" feature for PDF manga/comic reading

* Implement reader session persistence and restoration in `MainViewModel`

* Add tap-to-turn page feature to PDF viewer

* Improve book cache management and recovery

* Refactor top overlay padding logic in `PdfViewerScreen`

* Implement stylus eraser support in PDF reader

* Refactor ReaderTextFormatPanel to use ModalBottomSheet and add new formatting controls

* Introduce a customizable horizontal margin setting for the EPUB reader

* Refine folder sync and metadata handling for better conflict resolution and stability.

* Introduce user-adjustable image scaling for the EPUB reader

* Use maxWidthPx as default width fallback for blocks

* Improve cross-page text selection and header styling in the paginated reader

* Refactor speech bubble detection and UI to support segmentation masks and interactive scaling

* Improve speech bubble detection masking and rendering quality

* Update ONNX speech bubble detector to use `.ort` model and optimize inference

* Improve pagination accuracy

* Implement hierarchical folder navigation for the library.

* Refactor library item layout for improved space efficiency

* Add support for browsing and downloading Google Fonts

* Persist library landing state and add shelf search functionality

* Fix base tts sample.

* Move SpeechBubbleDetector to main source set and update ONNX dependency

* Implement a "locate" feature and improve synchronization for TTS (Text-to-Speech) playback across EPUB and PDF readers.

* Refine TTS synchronization and voice management in the EPUB reader

* Adjust OCR checks for OSS flavor and optimize external dictionary intent flags

* Add "Locate" button to PDF drawer's pages tab and move the page number to bottom right of thumbnail

* fix various crashes

* Refactor SpeechBubbleDetector into product flavors

* Implement speech bubble detection caching and background prefetching

* Implement on-demand download for Bubble Zoom ML model

* Add smooth animation for PDF speech bubble expansion

* fixes #252

* hide Google Fonts option, in FontsScreen, for offline variant

* Bump version to 1.0.46(46)
2026-04-29 16:37:53 +05:30

178 lines
6.7 KiB
Kotlin

/*
* Episteme Reader - A native Android document reader.
* Copyright (C) 2026 Episteme
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* mail: epistemereader@gmail.com
*/
package com.aryan.reader.paginatedreader
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aryan.reader.epub.EpubBook
import com.aryan.reader.paginatedreader.data.BookCacheDatabase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.protobuf.ProtoBuf
data class PaginatedReaderUiState(
val isLoading: Boolean = true,
val totalPageCount: Int = 0,
val generation: Int = 0
)
@OptIn(ExperimentalSerializationApi::class)
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class PaginatedReaderViewModel : ViewModel() {
@VisibleForTesting
internal var paginator: IPaginator? = null
private set
private val _uiState = MutableStateFlow(PaginatedReaderUiState())
val uiState: StateFlow<PaginatedReaderUiState> = _uiState.asStateFlow()
@VisibleForTesting
internal fun setPaginatorForTest(testPaginator: IPaginator) {
paginator = testPaginator
observePaginatorState()
}
companion object {
val proto = ProtoBuf { serializersModule = semanticBlockModule }
}
fun initialize(
book: EpubBook,
textMeasurer: TextMeasurer,
textConstraints: Constraints,
textStyle: TextStyle,
density: Density,
isDarkTheme: Boolean,
themeBackgroundColor: androidx.compose.ui.graphics.Color,
themeTextColor: androidx.compose.ui.graphics.Color,
context: Context,
initialChapterToPaginate: Int?,
mathMLRenderer: MathMLRenderer,
paragraphGapMultiplier: Float
) {
if (paginator != null) return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
// CSS Parsing and Font Loading
val userAgentStylesheet = UserAgentStylesheet.default
var allRules = OptimizedCssRules()
val allFontFaces = mutableListOf<FontFaceInfo>()
val uaResult = CssParser.parse(
cssContent = userAgentStylesheet,
cssPath = null,
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor
)
allRules = allRules.merge(uaResult.rules)
allFontFaces.addAll(uaResult.fontFaces)
book.css.forEach { (path, content) ->
val bookCssResult = CssParser.parse(
cssContent = content,
cssPath = path,
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor
)
allRules = allRules.merge(bookCssResult.rules)
allFontFaces.addAll(bookCssResult.fontFaces)
}
val fontFamilyMap = loadFontFamilies(
fontFaces = allFontFaces,
extractionPath = book.extractionBasePath
)
val bookId = book.title
val bookCacheDao = BookCacheDatabase.getDatabase(context.applicationContext).bookCacheDao()
val newPaginator = BookPaginator(
coroutineScope = viewModelScope,
chapters = book.chaptersForPagination,
textMeasurer = textMeasurer,
constraints = textConstraints,
textStyle = textStyle,
extractionBasePath = book.extractionBasePath,
density = density,
fontFamilyMap = fontFamilyMap,
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor,
bookId = bookId,
bookCacheDao = bookCacheDao,
proto = proto,
initialChapterToPaginate = initialChapterToPaginate ?: 0,
bookCss = book.css,
userAgentStylesheet = userAgentStylesheet,
allFontFaces = allFontFaces,
context = context.applicationContext,
mathMLRenderer = mathMLRenderer,
userTextAlign = null,
paragraphGapMultiplier = paragraphGapMultiplier,
imageSizeMultiplier = 1.0f
)
paginator = newPaginator
observePaginatorState()
}
}
private fun observePaginatorState() {
val p = paginator ?: return
viewModelScope.launch {
snapshotFlow { p.isLoading }.collect {
_uiState.value = _uiState.value.copy(isLoading = it)
}
}
viewModelScope.launch {
snapshotFlow { p.totalPageCount }.collect {
_uiState.value = _uiState.value.copy(totalPageCount = it)
}
}
viewModelScope.launch {
snapshotFlow { p.generation }.collect {
_uiState.value = _uiState.value.copy(generation = it)
}
}
}
fun onLinkClick(currentChapterPath: String, href: String, onNavigationComplete: (Int) -> Unit) {
paginator?.navigateToHref(currentChapterPath, href, onNavigationComplete)
}
}