/*
* 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 .
*
* 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 = _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()
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)
}
}