* Add performance and stylus debugging logs

* Refactor and decouple UI models from `MainViewModel`

* Refactor library state management and projection logic

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Introduce ReaderEngine and enhance EPUB reader features in windows app

* Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support.

* Implement PDF rendering and text extraction for desktop using Pdfium

* Add `NonReaderScreens.kt` and UI dependencies

* Refactor and centralize library state management and models to improve cross-platform consistency

* Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing

* Implement PDF annotation system and enhanced zoom controls for the desktop viewer

* Implement WebView-based EPUB rendering for desktop using CEF and embedded resources

* Optimize UI state projection, navigation state handling, and main screen pager performance

* Implement Bring Your Own Key (BYOK) support for AI features in OSS version

* Support Gemini-based Cloud TTS with BYOK support for OSS builds

* Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling.

* crash fixes

* Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6

* Implement dynamic bottom padding for the page info bar to account for device rounded corners

* Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar`

* Optimize PDF tiling performance and refine pan-and-fling gesture handling

* Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers

* Updated UI for customize toolbar

* Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls

* restructure PDF viewer UI component hierarchy to fix verifier crash

* Implement separate text dimming factors for light and dark themes

* Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers

* Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning

* Centralize file type resolution logic and implement HTML sanitization during import

* Introduce vertical margin customization and configurable progress bar positioning

* texture support in epub reader

* Enhance TTS session management, progress tracking, and diagnostic logging

* Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic.

* Refine TTS page mapping for PDF and overhaul TTS control UI

* Implement natural session completion logic in `TtsPlaybackManager` for cloud tts

* Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen`

* Refine TTS playback continuity across PDF pages and improve state management for session transitions

* Implement global texture transparency and enhance textured theme support across PDF and EPUB readers.

* Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode

* Add Support Project screen

* Optimize library performance via projection caching, batch database updates, and scoped folder synchronization.

* Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic

* Bump version to 1.0.47 (51)
This commit is contained in:
Aryan 2026-05-04 21:55:38 +05:30 committed by GitHub
parent f42de6b462
commit d7a9cae9e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
126 changed files with 15287 additions and 3154 deletions

View file

@ -30,6 +30,9 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.aryan.reader.GEMINI_CLOUD_TTS_MODEL
import com.aryan.reader.isByokCloudTtsAvailable
import com.aryan.reader.loadAiByokSettings
import com.aryan.reader.tts.TtsPlaybackManager.TtsMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -236,16 +239,31 @@ class TtsService : MediaSessionService() {
private lateinit var cacheManager: TtsCacheManager
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
val hasNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
val playerState = if (::player.isInitialized) {
"playbackState=${player.playbackState}, isPlaying=${player.isPlaying}, playWhenReady=${player.playWhenReady}, mediaItems=${player.mediaItemCount}, currentIndex=${player.currentMediaItemIndex}"
} else {
"player=uninitialized"
}
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"onUpdateNotification called. startInForegroundRequired=$startInForegroundRequired, hasPostNotifications=$hasNotificationPermission, $playerState"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
if (startInForegroundRequired) {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Notification permission missing while foreground is required. Calling stopSelf().")
stopSelf()
} else {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Notification permission missing. Skipping notification update.")
}
return
}
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("Delegating notification update to MediaSessionService.")
super.onUpdateNotification(session, startInForegroundRequired)
}
@ -279,7 +297,12 @@ class TtsService : MediaSessionService() {
data class Error(val message: String) : GeminiWsEvent()
}
suspend fun ensureConnected(serverUrl: String, speaker: String, authToken: String?) = connectionMutex.withLock {
suspend fun ensureConnected(
serverUrl: String,
speaker: String,
authToken: String?,
directGeminiApiKey: String? = null
) = connectionMutex.withLock {
if (webSocket != null) {
if (connectedSpeaker == speaker) {
val isSetup = try { setupDeferred.await() } catch(_: Exception) { false }
@ -290,11 +313,15 @@ class TtsService : MediaSessionService() {
webSocket = null
}
val sanitizedUrl = serverUrl.removeSuffix("/")
val wsUrlStr = sanitizedUrl.replace("https://", "wss://").replace("http://", "ws://")
val url = "$wsUrlStr/live?speaker=$speaker&token=${authToken ?: ""}"
val url = if (!directGeminiApiKey.isNullOrBlank()) {
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=$directGeminiApiKey"
} else {
val sanitizedUrl = serverUrl.removeSuffix("/")
val wsUrlStr = sanitizedUrl.replace("https://", "wss://").replace("http://", "ws://")
"$wsUrlStr/live?speaker=$speaker&token=${authToken ?: ""}"
}
Timber.tag("TTS_CLOUD_DIAG").d("Connecting to WS: $url")
Timber.tag("TTS_CLOUD_DIAG").d("Connecting to WS: ${if (!directGeminiApiKey.isNullOrBlank()) "Gemini BYOK" else url}")
val request = Request.Builder().url(url).build()
val connectedDeferred = CompletableDeferred<Boolean>()
@ -316,7 +343,7 @@ class TtsService : MediaSessionService() {
val setupMsg = JSONObject().apply {
put("setup", JSONObject().apply {
put("model", "models/gemini-3.1-flash-live-preview")
put("model", "models/$GEMINI_CLOUD_TTS_MODEL")
put("systemInstruction", JSONObject().apply {
put("parts", org.json.JSONArray().apply {
put(JSONObject().apply {
@ -552,8 +579,17 @@ class TtsService : MediaSessionService() {
TtsAudioData(audioFile = cachedFile, serverText = text, wordTimings = emptyList(), error = null, streamUri = null)
} else {
try {
liveClient.ensureConnected(googleCloudWorkerTtsUrl, speaker, authToken)
liveClient.generateChunk(text, cachedFile)
val directGeminiApiKey = if (isByokCloudTtsAvailable(this@TtsService)) {
loadAiByokSettings(this@TtsService).geminiKey
} else {
null
}
if (directGeminiApiKey.isNullOrBlank() && googleCloudWorkerTtsUrl.isBlank()) {
TtsAudioData(audioFile = null, serverText = null, wordTimings = null, error = "Cloud TTS is not configured.")
} else {
liveClient.ensureConnected(googleCloudWorkerTtsUrl, speaker, authToken, directGeminiApiKey)
liveClient.generateChunk(text, cachedFile)
}
} catch (e: Exception) {
Timber.tag("TTS_CLOUD_DIAG").e(e, "Cloud TTS generation failed")
TtsAudioData(audioFile = null, serverText = null, wordTimings = null, error = e.message ?: "Failed to connect to TTS service")
@ -567,6 +603,11 @@ class TtsService : MediaSessionService() {
override fun onCreate() {
super.onCreate()
Timber.d("TtsService created.")
val hasNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"TtsService onCreate. sdk=${Build.VERSION.SDK_INT}, hasPostNotifications=$hasNotificationPermission"
)
cacheManager = TtsCacheManager(this)
@ -622,6 +663,7 @@ class TtsService : MediaSessionService() {
.setHandleAudioBecomingNoisy(true)
.setMediaSourceFactory(androidx.media3.exoplayer.source.DefaultMediaSourceFactory(this).setDataSourceFactory(dataSourceFactory))
.build()
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("ExoPlayer created for TTS service.")
playbackManager = TtsPlaybackManager(
player = player,
@ -634,21 +676,30 @@ class TtsService : MediaSessionService() {
.build()
mediaSession?.let { playbackManager.setMediaSession(it) }
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("MediaSession created and attached to playback manager. sessionAvailable=${mediaSession != null}")
}
override fun onTaskRemoved(rootIntent: Intent?) {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"onTaskRemoved. playWhenReady=${if (::player.isInitialized) player.playWhenReady else null}, isPlaying=${if (::player.isInitialized) player.isPlaying else null}"
)
if (!player.playWhenReady) {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Task removed while player is not playWhenReady. Calling stopSelf().")
stopSelf()
}
Timber.d("onTaskRemoved called, stopping service.")
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"onGetSession. package=${controllerInfo.packageName}, sessionAvailable=${mediaSession != null}"
)
return mediaSession
}
override fun onDestroy() {
Timber.d("TtsService is being destroyed.")
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("TtsService onDestroy.")
baseTtsSynthesizer.shutdown()
playbackManager.release()
mediaSession?.run {
@ -658,4 +709,4 @@ class TtsService : MediaSessionService() {
}
super.onDestroy()
}
}
}