General improvements (#164)

* Optimized PDF tile rendering performance and quality.

* Added support for adjusting TTS voice speed and pitch.

* Updated the TTS settings UI and playback logic to support real-time parameter adjustments.

* Added horizontal scrolling to the PDF viewer bottom toolbar and updated tool arrangement to use fixed spacing.

* Refined cross-page selection and text extraction in `PaginatedReader`

* Updated EPUB reader styling logic to refine typography and layout controls.

* Bump version to 1.0.42(43)
This commit is contained in:
Aryan 2026-04-10 20:23:39 +05:30 committed by GitHub
parent 12d50bb68d
commit 17a0097d4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 559 additions and 168 deletions

View file

@ -23,6 +23,8 @@ import android.content.Context
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import com.aryan.reader.epubreader.loadTtsPitch
import com.aryan.reader.epubreader.loadTtsSpeechRate
import com.aryan.reader.loadNativeVoice
import timber.log.Timber
import kotlinx.coroutines.CompletableDeferred
@ -198,6 +200,9 @@ class BaseTtsSynthesizer(private val context: Context) {
applyPreferredVoice()
tts?.setSpeechRate(loadTtsSpeechRate(context))
tts?.setPitch(loadTtsPitch(context))
Timber.d("BaseTts: Requesting synthesis (Attempt $attempt). ID: $utteranceId")
requests[utteranceId] = RequestContext(resultDeferred, startSignal, tempFile, text)

View file

@ -230,6 +230,16 @@ class TtsController(context: Context) : Player.Listener {
mediaController?.sendCustomCommand(CHANGE_TTS_MODE_COMMAND, args)
}
fun flushPrefetch() {
Timber.d("UI sending FLUSH_PREFETCH command.")
mediaController?.sendCustomCommand(FLUSH_PREFETCH_COMMAND, Bundle.EMPTY)
}
fun sliceAndRetainPosition() {
Timber.d("UI sending SLICE_CURRENT_AND_RELOAD command.")
mediaController?.sendCustomCommand(SLICE_CURRENT_AND_RELOAD_COMMAND, Bundle.EMPTY)
}
override fun onEvents(player: Player, events: Player.Events) {
updateStateFromController()
}

View file

@ -52,8 +52,10 @@ import kotlinx.coroutines.delay
val START_TTS_COMMAND = SessionCommand("com.aryan.reader.tts.START", Bundle.EMPTY)
val STOP_TTS_COMMAND = SessionCommand("com.aryan.reader.tts.STOP", Bundle.EMPTY)
val CHANGE_SPEAKER_COMMAND = SessionCommand("com.aryan.reader.tts.CHANGE_SPEAKER", Bundle.EMPTY)
val FLUSH_PREFETCH_COMMAND = SessionCommand("com.aryan.reader.tts.FLUSH_PREFETCH", Bundle.EMPTY)
private val STATE_UPDATE_COMMAND = SessionCommand("com.aryan.reader.tts.STATE_UPDATE", Bundle.EMPTY)
val CHANGE_TTS_MODE_COMMAND = SessionCommand("com.aryan.reader.tts.CHANGE_MODE", Bundle.EMPTY)
val SLICE_CURRENT_AND_RELOAD_COMMAND = SessionCommand("com.aryan.reader.tts.SLICE_AND_RELOAD", Bundle.EMPTY)
const val KEY_TEXT_CHUNKS = "KEY_TEXT_CHUNKS"
const val KEY_SOURCE_CFIS = "KEY_SOURCE_CFIS"
@ -137,6 +139,8 @@ class TtsPlaybackManager(
.add(STOP_TTS_COMMAND)
.add(CHANGE_SPEAKER_COMMAND)
.add(CHANGE_TTS_MODE_COMMAND)
.add(FLUSH_PREFETCH_COMMAND)
.add(SLICE_CURRENT_AND_RELOAD_COMMAND)
.build()
val availablePlayerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
.remove(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
@ -203,10 +207,71 @@ class TtsPlaybackManager(
val newMode = try { TtsMode.valueOf(newModeName) } catch (_: Exception) { TtsMode.CLOUD }
handleChangeTtsMode(newMode)
}
FLUSH_PREFETCH_COMMAND -> {
Timber.d("Flushing prefetched TTS chunks for new parameters.")
prefetchingJobs.values.forEach { it.cancel() }
prefetchingJobs.clear()
scope.launch(Dispatchers.IO) {
val currentIdx = withContext(Dispatchers.Main) { player.currentMediaItemIndex }
if (currentIdx == C.INDEX_UNSET) return@launch
val keysToRemove = audioFiles.keys.filter { it > currentIdx }
keysToRemove.forEach { key ->
audioFiles.remove(key)?.delete()
}
withContext(Dispatchers.Main) {
prefetchNextChunkAudio(currentIdx)
}
}
}
SLICE_CURRENT_AND_RELOAD_COMMAND -> {
handleSliceAndReload()
}
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
private fun handleSliceAndReload() {
val currentIdx = player.currentMediaItemIndex
if (currentIdx == C.INDEX_UNSET) return
val offset = _ttsState.value.currentWordStartOffset
val currentChunk = textChunks.getOrNull(currentIdx) ?: return
preparationJob?.cancel()
wordTrackingJob?.cancel()
player.stop()
player.clearMediaItems()
prefetchingJobs.values.forEach { it.cancel() }
prefetchingJobs.clear()
preparationJob = scope.launch {
clearAudioFiles()
if (offset == -1) {
prepareAndPlayFirstChunk(startAtIndex = currentIdx, playWhenReady = false)
return@launch
}
val relativeOffset = (offset - currentChunk.startOffsetInSource).coerceIn(0, currentChunk.text.length)
if (relativeOffset >= currentChunk.text.length) {
if (currentIdx + 1 < textChunks.size) {
prepareAndPlayFirstChunk(startAtIndex = currentIdx + 1, playWhenReady = false)
}
return@launch
}
val slicedText = currentChunk.text.substring(relativeOffset)
val newChunk = currentChunk.copy(text = slicedText, startOffsetInSource = offset)
val mutableChunks = textChunks.toMutableList()
mutableChunks[currentIdx] = newChunk
textChunks = mutableChunks.toList()
prepareAndPlayFirstChunk(startAtIndex = currentIdx, playWhenReady = false)
}
}
private fun handleChangeTtsMode(newMode: TtsMode) {
if (currentTtsMode == newMode) return
currentTtsMode = newMode