Initial commit
This commit is contained in:
commit
6072b2ba29
844 changed files with 220532 additions and 0 deletions
319
app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt
Normal file
319
app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
package com.aryan.reader.epubreader
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.content.edit
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.aryan.reader.RenderMode
|
||||
import com.aryan.reader.epub.EpubChapter
|
||||
import com.aryan.reader.paginatedreader.BookPaginator
|
||||
import com.aryan.reader.paginatedreader.IPaginator
|
||||
import com.aryan.reader.tts.TtsController
|
||||
import com.aryan.reader.tts.TtsPlaybackManager
|
||||
import com.aryan.reader.tts.TtsPlaybackManager.TtsMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
const val TAG_TTS_DIAGNOSIS = "TTS_DIAGNOSIS"
|
||||
private const val TTS_MODE_KEY = "tts_mode"
|
||||
|
||||
data class TtsHighlightInfo(
|
||||
val text: String,
|
||||
val cfi: String,
|
||||
val offset: Int
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun saveTtsMode(context: Context, mode: TtsMode) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(TTS_MODE_KEY, mode.name) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun loadTtsMode(): TtsMode {
|
||||
// For this release, Cloud TTS is disabled. Force BASE mode.
|
||||
return TtsMode.BASE
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to update the WebView auto-scroll state.
|
||||
*/
|
||||
fun updateAutoScrollJs(webView: WebView?, playing: Boolean, speed: Float) {
|
||||
if (playing) {
|
||||
val jsCommand = "javascript:window.autoScroll.start($speed);"
|
||||
webView?.evaluateJavascript(jsCommand, null)
|
||||
} else {
|
||||
webView?.evaluateJavascript("javascript:window.autoScroll.stop();", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic for triggering the actual TTS start based on the current mode.
|
||||
*/
|
||||
fun initiateTtsPlayback(
|
||||
renderMode: RenderMode,
|
||||
webView: WebView?,
|
||||
onPaginatedStart: () -> Unit
|
||||
) {
|
||||
when (renderMode) {
|
||||
RenderMode.VERTICAL_SCROLL -> {
|
||||
Timber.d("Vertical: requesting text extraction via JS.")
|
||||
webView?.evaluateJavascript("javascript:TtsBridgeHelper.extractAndRelayText();", null)
|
||||
}
|
||||
RenderMode.PAGINATED -> {
|
||||
onPaginatedStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun TtsSessionObserver(
|
||||
ttsState: TtsPlaybackManager.TtsState,
|
||||
ttsController: TtsController,
|
||||
currentRenderMode: RenderMode,
|
||||
chapters: List<EpubChapter>,
|
||||
epubBookTitle: String,
|
||||
coverImagePath: String?,
|
||||
// Vertical Mode Dependencies
|
||||
webViewRef: WebView?,
|
||||
loadedChunkCount: Int,
|
||||
totalChunksInChapter: Int,
|
||||
// Paginated Mode Dependencies
|
||||
paginator: IPaginator?,
|
||||
pagerState: PagerState,
|
||||
ttsChapterIndex: Int?,
|
||||
onTtsChapterIndexChange: (Int?) -> Unit,
|
||||
onNavigateToChapter: (Int) -> Unit,
|
||||
onToggleTtsStartOnLoad: (Boolean) -> Unit,
|
||||
userStoppedTts: Boolean,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val prevTtsState = remember { mutableStateOf(ttsState) }
|
||||
|
||||
LaunchedEffect(ttsState) {
|
||||
val wasPlaying = prevTtsState.value.isPlaying
|
||||
val isPlaying = ttsState.isPlaying
|
||||
val isChangingConfig = ttsState.isChangingConfig
|
||||
val sessionFinished = ttsState.sessionFinished
|
||||
val wasSessionFinished = prevTtsState.value.sessionFinished
|
||||
val sessionEndedByStop = ttsState.sessionEndedByStop
|
||||
val isReaderSource = ttsState.playbackSource == "READER"
|
||||
|
||||
if (!isChangingConfig && isReaderSource) {
|
||||
if (sessionFinished && !wasSessionFinished) {
|
||||
Timber.d("TTS finished naturally. Checking for next content.")
|
||||
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) {
|
||||
handleVerticalAutoAdvance(
|
||||
webViewRef = webViewRef,
|
||||
loadedChunkCount = loadedChunkCount,
|
||||
totalChunksInChapter = totalChunksInChapter,
|
||||
currentTtsChapterIndex = ttsChapterIndex,
|
||||
totalChapters = chapters.size,
|
||||
onNavigateToNextChapter = { nextIndex ->
|
||||
onToggleTtsStartOnLoad(true)
|
||||
onNavigateToChapter(nextIndex)
|
||||
},
|
||||
onStopTts = { onTtsChapterIndexChange(null) }
|
||||
)
|
||||
} else if (currentRenderMode == RenderMode.PAGINATED) {
|
||||
handlePaginatedAutoAdvance(
|
||||
ttsController = ttsController,
|
||||
paginator = paginator,
|
||||
pagerState = pagerState,
|
||||
chapters = chapters,
|
||||
currentTtsChapterIndex = ttsChapterIndex,
|
||||
epubBookTitle = epubBookTitle,
|
||||
coverImagePath = coverImagePath,
|
||||
onUpdateTtsChapter = onTtsChapterIndexChange,
|
||||
scope = scope
|
||||
)
|
||||
}
|
||||
} else if (wasPlaying && !isPlaying && !sessionFinished) {
|
||||
// Playback stopped/paused
|
||||
if (userStoppedTts || sessionEndedByStop) {
|
||||
Timber.d("TTS stopped by user/stop command.")
|
||||
onTtsChapterIndexChange(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
prevTtsState.value = ttsState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles highlighting text in WebView (Vertical) or turning pages (Paginated)
|
||||
* based on playback progress.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun TtsHighlightHandler(
|
||||
ttsState: TtsPlaybackManager.TtsState,
|
||||
currentRenderMode: RenderMode,
|
||||
webViewRef: WebView?,
|
||||
paginator: IPaginator?,
|
||||
pagerState: PagerState,
|
||||
ttsChapterIndex: Int?,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
// 1. Vertical & General Highlighting (WebView)
|
||||
LaunchedEffect(ttsState.currentText, ttsState.sourceCfi, ttsState.startOffsetInSource, webViewRef) {
|
||||
val text = ttsState.currentText
|
||||
val cfi = ttsState.sourceCfi
|
||||
val offset = ttsState.startOffsetInSource
|
||||
|
||||
if (!text.isNullOrBlank() && !cfi.isNullOrBlank() && offset != -1) {
|
||||
val escapedText = escapeJsString(text)
|
||||
val escapedCfi = escapeJsString(cfi)
|
||||
// Use window.highlightFromCfi defined in epub_reader.js
|
||||
val jsCommand = "javascript:window.highlightFromCfi('$escapedCfi', '$escapedText', $offset);"
|
||||
webViewRef?.evaluateJavascript(jsCommand, null)
|
||||
} else {
|
||||
if (!ttsState.isPlaying && !ttsState.isLoading) {
|
||||
webViewRef?.evaluateJavascript("javascript:window.removeHighlight();", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Paginated Page Turning (Sentence/Fragment level)
|
||||
LaunchedEffect(ttsState.sourceCfi, ttsState.startOffsetInSource, paginator, ttsChapterIndex) {
|
||||
if (currentRenderMode != RenderMode.PAGINATED) return@LaunchedEffect
|
||||
|
||||
val cfi = ttsState.sourceCfi ?: return@LaunchedEffect
|
||||
val offset = ttsState.startOffsetInSource.takeIf { it != -1 } ?: return@LaunchedEffect
|
||||
val chapterIdx = ttsChapterIndex ?: return@LaunchedEffect
|
||||
val pag = paginator ?: return@LaunchedEffect
|
||||
|
||||
val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset)
|
||||
|
||||
if (targetPage != null && targetPage != pagerState.currentPage) {
|
||||
// Prevent backward jumps during reading (unless significant) to avoid jitter
|
||||
if (targetPage >= pagerState.currentPage) {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal Helper Functions ---
|
||||
|
||||
private fun handleVerticalAutoAdvance(
|
||||
webViewRef: WebView?,
|
||||
loadedChunkCount: Int,
|
||||
totalChunksInChapter: Int,
|
||||
currentTtsChapterIndex: Int?,
|
||||
totalChapters: Int,
|
||||
onNavigateToNextChapter: (Int) -> Unit,
|
||||
onStopTts: () -> Unit
|
||||
) {
|
||||
if (loadedChunkCount < totalChunksInChapter) {
|
||||
Timber.d("Vertical: Loading next chunk for TTS.")
|
||||
webViewRef?.evaluateJavascript("javascript:window.virtualization.loadNextChunk();", null)
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
webViewRef?.evaluateJavascript("javascript:TtsBridgeHelper.extractAndRelayText();", null)
|
||||
}, 500)
|
||||
} else {
|
||||
if (currentTtsChapterIndex != null && currentTtsChapterIndex < totalChapters - 1) {
|
||||
Timber.d("Vertical: Chapter finished, moving to next.")
|
||||
onNavigateToNextChapter(currentTtsChapterIndex + 1)
|
||||
} else {
|
||||
Timber.d("Vertical: End of book.")
|
||||
onStopTts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun handlePaginatedAutoAdvance(
|
||||
ttsController: TtsController,
|
||||
paginator: IPaginator?,
|
||||
pagerState: PagerState,
|
||||
chapters: List<EpubChapter>,
|
||||
currentTtsChapterIndex: Int?,
|
||||
epubBookTitle: String,
|
||||
coverImagePath: String?,
|
||||
onUpdateTtsChapter: (Int?) -> Unit,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val lastPlayedChapter = currentTtsChapterIndex
|
||||
if (lastPlayedChapter != null && lastPlayedChapter < chapters.size - 1) {
|
||||
Timber.d("Paginated: Searching for next TTS content...")
|
||||
|
||||
scope.launch {
|
||||
var chapterToTry = lastPlayedChapter + 1
|
||||
var foundContent = false
|
||||
val bookPaginator = paginator as? BookPaginator
|
||||
|
||||
if (bookPaginator == null) {
|
||||
onUpdateTtsChapter(null)
|
||||
return@launch
|
||||
}
|
||||
|
||||
while (chapterToTry < chapters.size) {
|
||||
// Visually scroll to start of chapter
|
||||
val targetPage = bookPaginator.chapterStartPageIndices[chapterToTry]
|
||||
if (targetPage != null && pagerState.currentPage != targetPage) {
|
||||
pagerState.animateScrollToPage(targetPage)
|
||||
delay(300)
|
||||
}
|
||||
|
||||
val nextChapterChunks = bookPaginator.getTtsChunksForChapter(chapterToTry)
|
||||
|
||||
if (!nextChapterChunks.isNullOrEmpty()) {
|
||||
Timber.d("Paginated: Found content in chapter $chapterToTry. Starting.")
|
||||
onUpdateTtsChapter(chapterToTry)
|
||||
|
||||
val chapterTitle = chapters.getOrNull(chapterToTry)?.title
|
||||
val coverUriString = coverImagePath?.let { Uri.fromFile(File(it)).toString() }
|
||||
|
||||
ttsController.start(
|
||||
chunks = nextChapterChunks,
|
||||
bookTitle = epubBookTitle,
|
||||
chapterTitle = chapterTitle,
|
||||
coverImageUri = coverUriString,
|
||||
ttsMode = "BASE" // Defaulting to Base for safety
|
||||
)
|
||||
foundContent = true
|
||||
break
|
||||
} else {
|
||||
Timber.d("Paginated: Chapter $chapterToTry is empty. Skipping.")
|
||||
// Visually flip through empty pages if needed
|
||||
val pageCount = bookPaginator.chapterPageCounts[chapterToTry] ?: 0
|
||||
if (pageCount > 1) {
|
||||
for (i in 1 until pageCount) {
|
||||
pagerState.animateScrollToPage(targetPage!! + i)
|
||||
delay(400)
|
||||
}
|
||||
}
|
||||
chapterToTry++
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundContent) {
|
||||
Timber.d("Paginated: No more content found.")
|
||||
onUpdateTtsChapter(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onUpdateTtsChapter(null)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue