* 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)
178 lines
6.7 KiB
Kotlin
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)
|
|
}
|
|
}
|