Bug fixes (#319)

* Fix TTS speaker persistence

* optimized redundant WebView updates

* Bumped version to 1.0.48
This commit is contained in:
Aryan 2026-05-16 22:59:53 +05:30 committed by GitHub
parent 70c272baa7
commit bcf34af719
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 781 additions and 173 deletions

View file

@ -30,8 +30,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
@ -54,19 +52,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
private const val SETTINGS_PREFS_NAME = "epub_reader_settings"
private const val TTS_SPEAKER_KEY = "tts_speaker"
private fun saveSpeaker(context: Context, speakerId: String) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putString(TTS_SPEAKER_KEY, speakerId) }
}
private fun loadSpeaker(context: Context): String {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getString(TTS_SPEAKER_KEY, DEFAULT_SPEAKER_ID) ?: DEFAULT_SPEAKER_ID
}
@OptIn(UnstableApi::class)
fun loadTtsMode(context: Context): TtsPlaybackManager.TtsMode {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
@ -101,7 +86,7 @@ class TtsController(context: Context) : Player.Listener {
private var pollingJob: Job? = null
init {
val initialSpeakerId = loadSpeaker(this.context)
val initialSpeakerId = loadTtsSpeaker(this.context)
_ttsState.value = _ttsState.value.copy(speakerId = initialSpeakerId)
}
@ -236,11 +221,12 @@ class TtsController(context: Context) : Player.Listener {
@Suppress("unused")
fun changeSpeaker(speakerId: String) {
Timber.d("UI sending CHANGE_SPEAKER command.")
saveSpeaker(context, speakerId)
_ttsState.value = _ttsState.value.copy(speakerId = speakerId)
val safeSpeakerId = normalizeTtsSpeakerId(speakerId)
saveTtsSpeaker(context, safeSpeakerId)
_ttsState.value = _ttsState.value.copy(speakerId = safeSpeakerId)
val args = Bundle().apply {
putString(KEY_SPEAKER_ID, speakerId)
putString(KEY_SPEAKER_ID, safeSpeakerId)
}
mediaController?.sendCustomCommand(CHANGE_SPEAKER_COMMAND, args)
}

View file

@ -130,15 +130,22 @@ class TtsPlaybackManager(
val ttsMode: String = TtsMode.CLOUD.name
)
private val _ttsState = MutableStateFlow(TtsState())
private val initialSpeakerId = loadTtsSpeaker(appContext)
private val initialTtsMode = loadTtsMode(appContext)
private val _ttsState = MutableStateFlow(
TtsState(
speakerId = initialSpeakerId,
ttsMode = initialTtsMode.name
)
)
private var textChunks: List<TtsChunk> = emptyList()
private val audioFiles = java.util.concurrent.ConcurrentHashMap<Int, File>()
private var currentSpeakerId = DEFAULT_SPEAKER_ID
private var currentSpeakerId = initialSpeakerId
private var bookTitle: String? = null
private var chapterTitle: String? = null
private var coverImageUri: String? = null
private var currentTtsMode = TtsMode.CLOUD
private var currentTtsMode = initialTtsMode
private var chapterIndex: Int? = null
private var totalChapters: Int? = null
@ -157,6 +164,12 @@ class TtsPlaybackManager(
fun setMediaSession(session: MediaSession) {
this.mediaSession = session
session.setCustomLayout(
listOf(
createStateButton(_ttsState.value),
createStopCommandButton()
)
)
}
override fun onConnect(
@ -422,8 +435,10 @@ class TtsPlaybackManager(
onPlaybackSessionPreparing(bookTitle, chapterTitle)
val effectiveSpeakerId = normalizeTtsSpeakerId(speakerId)
textChunks = chunks
currentSpeakerId = speakerId
currentSpeakerId = effectiveSpeakerId
currentTtsMode = ttsMode
this.bookTitle = bookTitle
this.chapterTitle = chapterTitle
@ -443,7 +458,7 @@ class TtsPlaybackManager(
currentChunkIndex = -1,
totalChunks = chunks.size,
bookProgressPercent = calculateBookProgressPercent(-1),
speakerId = speakerId,
speakerId = effectiveSpeakerId,
playbackSource = playbackSource,
ttsMode = ttsMode.name,
currentText = if (continueSession) _ttsState.value.currentText else null
@ -480,10 +495,12 @@ class TtsPlaybackManager(
}
private fun handleChangeSpeaker(newSpeakerId: String) {
if (currentSpeakerId == newSpeakerId) return
currentSpeakerId = newSpeakerId
_ttsState.value = _ttsState.value.copy(speakerId = newSpeakerId)
Timber.d("Speaker changed to $newSpeakerId (pending next start)")
val safeSpeakerId = normalizeTtsSpeakerId(newSpeakerId)
if (currentSpeakerId == safeSpeakerId) return
currentSpeakerId = safeSpeakerId
saveTtsSpeaker(appContext, safeSpeakerId)
_ttsState.value = _ttsState.value.copy(speakerId = safeSpeakerId)
Timber.d("Speaker changed to $safeSpeakerId (pending next start)")
}
private fun currentChunkIndexFromPlayer(): Int {
@ -681,7 +698,11 @@ class TtsPlaybackManager(
preparationJob?.cancel()
wordTrackingJob?.cancel()
if (clearState) {
val finalState = TtsState(sessionEndedByStop = userInitiated)
val finalState = TtsState(
sessionEndedByStop = userInitiated,
speakerId = currentSpeakerId,
ttsMode = currentTtsMode.name
)
_ttsState.value = finalState
mediaSession?.let { session ->
val layout = listOf(

View file

@ -25,6 +25,7 @@ import androidx.annotation.OptIn
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.media3.common.util.UnstableApi
import com.aryan.reader.BuildConfig
import kotlinx.coroutines.CoroutineScope
@ -42,6 +43,8 @@ const val googleCloudWorkerTtsUrl = BuildConfig.TTS_WORKER_URL
const val TTS_CHUNK_MAX_LENGTH = 250
const val DEFAULT_SPEAKER_ID = "Aoede"
internal const val TTS_SETTINGS_PREFS_NAME = "epub_reader_settings"
internal const val TTS_SPEAKER_KEY = "tts_speaker"
data class GeminiVoice(val id: String, val name: String, val description: String)
@ -78,6 +81,21 @@ val GEMINI_TTS_SPEAKERS = listOf(
GeminiVoice("Sulafat", "Sulafat", "Warn, Middle pitch"),
)
internal fun normalizeTtsSpeakerId(speakerId: String?): String {
val cleanSpeakerId = speakerId?.takeIf { it.isNotBlank() } ?: return DEFAULT_SPEAKER_ID
return cleanSpeakerId.takeIf { id -> GEMINI_TTS_SPEAKERS.any { it.id == id } } ?: DEFAULT_SPEAKER_ID
}
internal fun saveTtsSpeaker(context: Context, speakerId: String) {
val prefs = context.getSharedPreferences(TTS_SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putString(TTS_SPEAKER_KEY, normalizeTtsSpeakerId(speakerId)) }
}
internal fun loadTtsSpeaker(context: Context): String {
val prefs = context.getSharedPreferences(TTS_SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return normalizeTtsSpeakerId(prefs.getString(TTS_SPEAKER_KEY, DEFAULT_SPEAKER_ID))
}
data class TtsChapterCacheInfo(
val chapterTitle: String,
val chunkCount: Int,