Bug fixes (#319)
* Fix TTS speaker persistence * optimized redundant WebView updates * Bumped version to 1.0.48
This commit is contained in:
parent
70c272baa7
commit
bcf34af719
11 changed files with 781 additions and 173 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue