* Enhance Cloud TTS with navigation controls and shared UI overlay in desktop app * Refactor Cloud TTS voice settings and remove standalone settings overlay on desktop app * Persist reader window state and improve slider interaction in desktop app * Improve PDF page transitions and refine focus management in desktop app * Refactor scrollbar interaction and adjust desktop modal focus handling * Improve PDF sidecar synchronization and cross-platform metadata compatibility * Refactor PDF annotation comment logic to shared module and implement Desktop UI * Refactor reader screen to use tap-to-toggle and full-width styling in desktop app * Refactor reader workspace layout and chrome-panel interactions * Implement global search keyboard shortcuts and focusable chrome layers * Implement flavor-specific legal links and update the About UI * Refactor reader UI controls on desktop app * Enhance desktop folder sync with background metadata extraction and improved error handling * Refactor Library UI and remove redundant Home tab in desktop app * Add custom tooltips to reader icon buttons in desktop app * Integrate app theme controls into reader interfaces on desktop * Add right-to-left pagination support and improve focus restoration on desktop app * Improve EPUB pagination geometry and diagnostic logging for layout cutoffs on desktop app * Update desktop reader defaults and implement settings migration * Implement block-based position tracking in ReaderLocator * Enhance EPUB highlighting reliability in desktop app * Add support for custom reader themes and update highlight palette logic in desktop app * Replace the Tools panel with a "More" dropdown menu and refactor account UI * Implement account profile header in desktop sidebar * Implement cloud sync reliability improvements and sidebar toggle on desktop app * Improve EPUB annotation synchronization and highlight mapping accuracy in desktop app * Integrate WebView2 for EPUB vertical rendering on Windows * Refactor reader layout logic and enhance WebView2 diagnostics * Improve vertical reading layout and WebView2 resizing on Desktop * Refine vertical reading mode layout and margin handling * Enhance reader locator precision and Desktop mode-switching reliability * Implement chapter-level caching and warm-start pagination in desktop app * Replace bundled KCEF with native system webviews via SWT * Refactor EPUB page info bar visibility and layout logic * Improve PDF toolbar persistence and fix tab reactivation logic * Enable multi-selection and bulk operations for custom fonts * Refactor instrumentation tests * Add EPUB UI test fixture and initial instrumentation tests * Expand EpubReader UI tests and improve accessibility * Add instrumentation tests and test tags for library and reader screens * Enhance OPDS parser logic and catalog integration * Add support for toggling local synchronization on a per-folder basis. * Implement tri-state sizing for the TTS overlay * Persist TTS overlay size across sessions * Refactor reader brightness control and add incremental step buttons * Improve CSS support, pagination control, and style-aware semantic caching * Improve link handling, interaction, and diagnostics in the paginated reader * crash fixes * Implement persistent pending removal for external files * Implement book-specific word replacements * Add native vertical reading mode with custom renderer * Implement text selection and navigation improvements for the native vertical reader * Implement locator-based navigation and improved vertical scrolling in native vertical mode in epub * Implement lazy loading and chapter prefetching for native vertical reader * Improve window lifecycle and disposal handling on Desktop * Optimize vertical reading performance in desktop app * Enhance TTS start accuracy and diagnostic logging on desktop * Refactor AI settings visibility on desktop * Improve pagination height measurement and enhance cutoff diagnostics * Implement lifecycle management and improve justified text splitting for pagination * Refine AI usage tracking and force AI feature visibility on Desktop * Add descriptive context comments and usage examples to string and plural resources. * Optimize performance and memory usage in search and state mapping * Replace reader page sliders with minimal slider and navigation controls * Add support for CBT comic archives * Harden file path validation and XML parsing to prevent security vulnerabilities * Implement local account profile caching and optimize desktop performance * Improve desktop persistence reliability and add Linux secure storage support * Improved PDF zoom stability and layout prediction during zoom commits * Improved PDF spread layout prediction, reader focus restoration, and account profile caching * Enhance highlight precision and scoping using block-local offsets and CFIs * Enhance cloud book content synchronization and background downloads * Implement granular timestamp tracking for reading positions and PDF annotations * Restrict diagnostic logging and stack traces to debug builds * Refine PDF page gaps and reader chrome interaction logic * Refactor PDF highlight rendering and overhaul Desktop sidebar UI * Implement a new interaction dock and undo/redo history for PDF annotations in desktop * Enhance PDF color picker and improve navigation scroll restoration * Add highlight palette customization and improve selection menu UI in desktop app epub reader * Enhance desktop shelf management and library organization
453 lines
19 KiB
Kotlin
453 lines
19 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
|
|
|
|
import android.os.Build
|
|
import timber.log.Timber
|
|
import androidx.activity.compose.BackHandler
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
import androidx.compose.animation.fadeIn
|
|
import androidx.compose.animation.fadeOut
|
|
import androidx.compose.animation.slideInVertically
|
|
import androidx.compose.animation.slideOutVertically
|
|
import androidx.compose.animation.core.tween
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import androidx.media3.common.util.UnstableApi
|
|
import androidx.navigation.NavHostController
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.composable
|
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
import com.aryan.reader.epubreader.EpubReaderScreen
|
|
import com.aryan.reader.feedback.FeedbackScreen
|
|
import com.aryan.reader.feedback.SupportProjectScreen
|
|
import com.aryan.reader.pdf.PdfViewerScreen
|
|
import com.aryan.reader.shared.ReaderFeatureSurface
|
|
import com.aryan.reader.tts.ReaderTtsMiniBar
|
|
import com.aryan.reader.tts.readerTtsMiniBarBottomPaddingDp
|
|
import com.aryan.reader.tts.shouldShowReaderTtsMiniBar
|
|
import kotlinx.coroutines.delay
|
|
|
|
object AppDestinations {
|
|
const val MAIN_ROUTE = "main"
|
|
const val PDF_VIEWER_ROUTE = "pdf_viewer"
|
|
const val EPUB_READER_ROUTE = "epub_reader"
|
|
const val PRO_SCREEN_ROUTE = "pro_screen"
|
|
const val FEEDBACK_SCREEN_ROUTE = "feedback_screen_route"
|
|
const val SUPPORT_PROJECT_SCREEN_ROUTE = "support_project_screen_route"
|
|
const val FONTS_SCREEN_ROUTE = "fonts_screen_route"
|
|
const val AI_SETTINGS_SCREEN_ROUTE = "ai_settings_screen_route"
|
|
const val SETTINGS_SCREEN_ROUTE = "settings_screen_route"
|
|
}
|
|
|
|
fun shouldInterceptAppNavBack(
|
|
currentRoute: String?,
|
|
hasPreviousBackStackEntry: Boolean,
|
|
isCurrentEntryResumed: Boolean
|
|
): Boolean {
|
|
if (!hasPreviousBackStackEntry || !isCurrentEntryResumed) return false
|
|
return currentRoute != null &&
|
|
currentRoute != AppDestinations.MAIN_ROUTE &&
|
|
currentRoute != AppDestinations.PDF_VIEWER_ROUTE &&
|
|
currentRoute != AppDestinations.EPUB_READER_ROUTE
|
|
}
|
|
|
|
private fun NavHostController.isReadyForBackStackChange(): Boolean {
|
|
return currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
|
|
}
|
|
|
|
private suspend fun NavHostController.awaitReadyForBackStackChange() {
|
|
while (!isReadyForBackStackChange()) {
|
|
delay(32)
|
|
}
|
|
}
|
|
|
|
private fun NavHostController.navigateSingleTopTo(route: String) {
|
|
if (!isReadyForBackStackChange()) {
|
|
Timber.d("Skipping navigation to $route because the current entry is not resumed yet.")
|
|
return
|
|
}
|
|
|
|
try {
|
|
navigate(route) {
|
|
launchSingleTop = true
|
|
popUpTo(graph.startDestinationId) {
|
|
saveState = false
|
|
}
|
|
}
|
|
} catch (e: IllegalStateException) {
|
|
Timber.w(e, "Navigation to $route ignored because the back stack is mid-transition.")
|
|
}
|
|
}
|
|
|
|
private fun NavHostController.navigateToMain() {
|
|
navigateSingleTopTo(AppDestinations.MAIN_ROUTE)
|
|
}
|
|
|
|
private fun NavHostController.navigateIfReady(route: String) {
|
|
if (currentDestination?.route == route) return
|
|
navigateSingleTopTo(route)
|
|
}
|
|
|
|
private fun NavHostController.popBackStackIfReady(): Boolean {
|
|
if (!isReadyForBackStackChange()) {
|
|
Timber.d("Skipping popBackStack because the current entry is not resumed yet.")
|
|
return false
|
|
}
|
|
|
|
return try {
|
|
popBackStack()
|
|
} catch (e: IllegalStateException) {
|
|
Timber.w(e, "popBackStack ignored because the back stack is mid-transition.")
|
|
false
|
|
}
|
|
}
|
|
|
|
private suspend fun NavHostController.syncRouteTo(route: String) {
|
|
awaitReadyForBackStackChange()
|
|
if (currentDestination?.route != route) {
|
|
if (route == AppDestinations.MAIN_ROUTE) {
|
|
navigateToMain()
|
|
} else {
|
|
navigateSingleTopTo(route)
|
|
}
|
|
}
|
|
}
|
|
|
|
@androidx.annotation.OptIn(UnstableApi::class)
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
@Composable
|
|
fun AppNavigation(
|
|
navController: NavHostController,
|
|
windowSizeClass: WindowSizeClass,
|
|
viewModel: MainViewModel
|
|
) {
|
|
Timber.d("AppNavigation composable invoked.")
|
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
|
val currentRoute = currentBackStackEntry?.destination?.route
|
|
val ttsController = viewModel.ttsController
|
|
val ttsState by ttsController.ttsState.collectAsStateWithLifecycle()
|
|
val isOnReaderRoute = currentRoute == AppDestinations.PDF_VIEWER_ROUTE ||
|
|
currentRoute == AppDestinations.EPUB_READER_ROUTE
|
|
val showTtsMiniBar = shouldShowReaderTtsMiniBar(
|
|
ttsState = ttsState,
|
|
isOnReaderRoute = isOnReaderRoute
|
|
)
|
|
val miniBarBottomPadding = readerTtsMiniBarBottomPaddingDp(
|
|
isOnMainRoute = currentRoute == AppDestinations.MAIN_ROUTE
|
|
).dp
|
|
val shouldInterceptBack = shouldInterceptAppNavBack(
|
|
currentRoute = currentRoute,
|
|
hasPreviousBackStackEntry = navController.previousBackStackEntry != null,
|
|
isCurrentEntryResumed = currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
|
|
)
|
|
|
|
LaunchedEffect(currentRoute, uiState.selectedFileType, uiState.isLoading, uiState.selectedEpubBook, uiState.selectedPdfUri) {
|
|
if (!uiState.isLoading) {
|
|
when (uiState.selectedFileType?.readerSurfaceOnAndroid()) {
|
|
ReaderFeatureSurface.PDF_VIEWER -> {
|
|
if (uiState.selectedPdfUri != null) {
|
|
if (currentRoute != AppDestinations.PDF_VIEWER_ROUTE) {
|
|
navController.syncRouteTo(AppDestinations.PDF_VIEWER_ROUTE)
|
|
}
|
|
}
|
|
}
|
|
ReaderFeatureSurface.EPUB_READER,
|
|
ReaderFeatureSurface.TEXT_READER -> {
|
|
if (uiState.selectedEpubBook != null) {
|
|
if (currentRoute != AppDestinations.EPUB_READER_ROUTE) {
|
|
navController.syncRouteTo(AppDestinations.EPUB_READER_ROUTE)
|
|
}
|
|
}
|
|
}
|
|
null -> {
|
|
if (currentRoute == AppDestinations.PDF_VIEWER_ROUTE || currentRoute == AppDestinations.EPUB_READER_ROUTE) {
|
|
navController.syncRouteTo(AppDestinations.MAIN_ROUTE)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
BackHandler(enabled = shouldInterceptBack) {
|
|
navController.popBackStackIfReady()
|
|
}
|
|
|
|
NavHost(navController = navController, startDestination = AppDestinations.MAIN_ROUTE) {
|
|
composable(AppDestinations.MAIN_ROUTE) {
|
|
Timber.d("Navigating to Main Screen (${AppDestinations.MAIN_ROUTE}).")
|
|
MainScreen(
|
|
viewModel = viewModel,
|
|
windowSizeClass = windowSizeClass,
|
|
navController = navController
|
|
)
|
|
}
|
|
|
|
// PDF Viewer Screen Composable
|
|
composable(route = AppDestinations.PDF_VIEWER_ROUTE) {
|
|
Timber.d("Navigating to PDF Viewer Screen (${AppDestinations.PDF_VIEWER_ROUTE}).")
|
|
val pdfUri = uiState.selectedPdfUri
|
|
val initialPage = uiState.initialPageInBook
|
|
val initialBookmarksJson = uiState.initialBookmarksJson
|
|
|
|
val bookId =
|
|
uiState.recentFiles.find { it.uriString == uiState.selectedPdfUri.toString() }?.bookId
|
|
|
|
if (pdfUri != null) {
|
|
Timber.i("Displaying PDF Viewer for URI: $pdfUri, initialPage: $initialPage")
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
PdfViewerScreen(
|
|
pdfUri = pdfUri,
|
|
initialPage = initialPage,
|
|
initialBookmarksJson = initialBookmarksJson,
|
|
isProUser = uiState.isProUser,
|
|
onNavigateBack = {
|
|
Timber.d("Back action triggered from PDF Viewer.")
|
|
viewModel.clearSelectedFile()
|
|
},
|
|
onSavePosition = viewModel::savePdfReadingPosition,
|
|
onBookmarksChanged = { bookmarksJson ->
|
|
if (bookId != null) {
|
|
viewModel.saveBookmarks(bookId, bookmarksJson)
|
|
} else {
|
|
Timber.w("Could not find bookId to save PDF bookmarks for URI: ${uiState.selectedPdfUri}")
|
|
}
|
|
},
|
|
onNavigateToPro = {
|
|
navController.navigateIfReady(AppDestinations.PRO_SCREEN_ROUTE)
|
|
},
|
|
viewModel = viewModel
|
|
)
|
|
|
|
if (uiState.isLoading) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f)),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
|
|
CustomTopBanner(bannerMessage = uiState.bannerMessage)
|
|
}
|
|
} else if (uiState.isLoading) {
|
|
Timber.d("PDF URI is null but loading is in progress. Showing loading indicator.")
|
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
CircularProgressIndicator()
|
|
}
|
|
} else {
|
|
Timber.w("PDF URI is null in ViewModel state while on PDF screen. Navigating back to Main.")
|
|
}
|
|
}
|
|
|
|
// EPUB Reader Screen Composable
|
|
composable(route = AppDestinations.EPUB_READER_ROUTE) {
|
|
Timber.d("Navigating to EPUB Reader Screen (${AppDestinations.EPUB_READER_ROUTE}).")
|
|
val epubBook = uiState.selectedEpubBook
|
|
val isLoading = uiState.isLoading
|
|
val errorMessage = uiState.errorMessage
|
|
val initialLocator = uiState.initialLocator
|
|
val initialCfi = uiState.initialCfi
|
|
val initialBookmarksJson = uiState.initialBookmarksJson
|
|
val renderMode = uiState.renderMode
|
|
|
|
when {
|
|
epubBook != null -> {
|
|
Timber.i("Displaying EPUB Reader for Book: ${epubBook.title}, initialLocator: $initialLocator")
|
|
val coverPath = uiState.recentFiles.find { it.uriString == uiState.selectedEpubUri.toString() }?.coverImagePath
|
|
val epubUri = uiState.selectedEpubUri
|
|
uiState.recentFiles.find { it.uriString == uiState.selectedEpubUri.toString() }?.bookId
|
|
val customFonts by viewModel.customFonts.collectAsStateWithLifecycle()
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
EpubReaderScreen(
|
|
epubBook = epubBook,
|
|
renderMode = renderMode,
|
|
initialLocator = initialLocator,
|
|
initialCfi = initialCfi,
|
|
initialBookmarksJson = initialBookmarksJson,
|
|
isProUser = uiState.isProUser,
|
|
coverImagePath = coverPath,
|
|
onNavigateBack = {
|
|
Timber.d("Back action from EPUB Reader. Clearing selected file to navigate home.")
|
|
viewModel.clearSelectedFile()
|
|
},
|
|
onSavePosition = { locator, cfiForWebView, progress ->
|
|
Timber.d("Auto-saving EPUB position: Locator $locator, Progress $progress%")
|
|
epubUri?.let { uri ->
|
|
viewModel.saveEpubReadingPosition(uri, locator, cfiForWebView, progress)
|
|
}
|
|
},
|
|
onBookmarksChanged = { bookmarksJson ->
|
|
val bookId = uiState.recentFiles.find { it.uriString == uiState.selectedEpubUri.toString() }?.bookId
|
|
if (bookId != null) {
|
|
viewModel.saveBookmarks(bookId, bookmarksJson)
|
|
} else {
|
|
Timber.w("Could not find bookId to save bookmarks for URI: ${uiState.selectedEpubUri}")
|
|
}
|
|
},
|
|
onNavigateToPro = {
|
|
navController.navigateIfReady(AppDestinations.PRO_SCREEN_ROUTE)
|
|
},
|
|
onRenderModeChange = viewModel::setRenderMode,
|
|
customFonts = customFonts,
|
|
onImportFonts = viewModel::importFonts,
|
|
viewModel = viewModel
|
|
)
|
|
|
|
if (uiState.isLoading) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f)),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
|
|
CustomTopBanner(bannerMessage = uiState.bannerMessage)
|
|
}
|
|
}
|
|
isLoading -> {
|
|
Timber.d("EPUB Reader: Showing loading indicator.")
|
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
errorMessage != null -> {
|
|
Timber.e("EPUB Reader: Showing error message - $errorMessage")
|
|
Column(
|
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
Text(stringResource(R.string.error_message_format, errorMessage), color = MaterialTheme.colorScheme.error)
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
Button(onClick = {
|
|
viewModel.clearSelectedFile()
|
|
}) {
|
|
Text(stringResource(R.string.action_go_back))
|
|
}
|
|
}
|
|
}
|
|
else -> {
|
|
Timber.w("EPUB Book is null and not loading/error state on EPUB screen. Navigating back.")
|
|
}
|
|
}
|
|
}
|
|
composable(route = AppDestinations.PRO_SCREEN_ROUTE) {
|
|
ProScreen(
|
|
viewModel = viewModel,
|
|
onNavigateBack = { navController.popBackStackIfReady() }
|
|
)
|
|
}
|
|
|
|
composable(route = AppDestinations.FEEDBACK_SCREEN_ROUTE) {
|
|
FeedbackScreen(
|
|
navController = navController
|
|
)
|
|
}
|
|
|
|
composable(route = AppDestinations.SUPPORT_PROJECT_SCREEN_ROUTE) {
|
|
SupportProjectScreen(
|
|
navController = navController
|
|
)
|
|
}
|
|
|
|
composable(route = AppDestinations.FONTS_SCREEN_ROUTE) {
|
|
FontsScreen(
|
|
viewModel = viewModel,
|
|
onBackClick = { navController.popBackStackIfReady() }
|
|
)
|
|
}
|
|
|
|
composable(route = AppDestinations.AI_SETTINGS_SCREEN_ROUTE) {
|
|
AiSettingsScreen(
|
|
onBackClick = { navController.popBackStackIfReady() }
|
|
)
|
|
}
|
|
|
|
composable(route = AppDestinations.SETTINGS_SCREEN_ROUTE) {
|
|
SettingsScreen(
|
|
viewModel = viewModel,
|
|
navController = navController,
|
|
onBackClick = { navController.popBackStackIfReady() }
|
|
)
|
|
}
|
|
}
|
|
|
|
AnimatedVisibility(
|
|
visible = showTtsMiniBar,
|
|
enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)),
|
|
exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)),
|
|
modifier = Modifier
|
|
.align(Alignment.BottomCenter)
|
|
.navigationBarsPadding()
|
|
.padding(start = 16.dp, end = 16.dp, bottom = miniBarBottomPadding)
|
|
) {
|
|
ReaderTtsMiniBar(
|
|
ttsController = ttsController,
|
|
ttsState = ttsState,
|
|
onOpenReader = {
|
|
ttsState.bookId?.let { bookId ->
|
|
viewModel.openTtsNotificationTarget(
|
|
bookId = bookId,
|
|
sourceCfi = ttsState.sourceCfi,
|
|
startOffset = ttsState.startOffsetInSource.takeIf { it >= 0 },
|
|
chapterIndex = ttsState.chapterIndex,
|
|
pageIndex = ttsState.pageIndex
|
|
)
|
|
}
|
|
},
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
}
|
|
}
|
|
}
|