Update v1.0.49 (#330)

* Added PDF top tab strip visibility toggle and fixed WebView hit test NPE

* Refactored desktop reader screens and state management into specialized components

* Added image gallery to reader sidebar and refactored desktop PDF UI components

* Implemented EPUB image gallery

* Refactored reader and library models to use shared common types, centralizing file type resolution and texture management while removing redundant mapping logic

* Standardized UI styling and refactored app navigation layout

* Implemented auto-hiding reader chrome and activity tracking in desktop

* Refactored reader panels into distinct left and right modal layers with platform-specific sizing and keyboard navigation support.

* Added PPTX support for desktop and refactored parsing into a shared module

* Implement paid AI features and account management for the desktop application.

* Implement AI Hub and enhanced Cloud TTS integration for Desktop

* Implement streaming support for AI definition and summarization features

* Implement support for password-protected PDFs and file actions in the desktop reader.

* Implement cloud synchronization for desktop using Firestore and Google Drive

* Implement PDF reflow and "Text View" for the desktop reader

* Refactor OPDS logic to use SharedOpdsController

* Optimize PDF tile rendering performance

* Implement two-page spread support for PDF pagination

* Implement two-page spread support for the PDF viewer

* Improved shared spread zoom in PDF viewer

* Improve PDF spread navigation with fling support and configurable page gaps

* Add brightness control to PDF and EPUB readers

* Refactor folder synchronization to use shared logic engine

* Implement safe string formatting and validation for localized resources

* Implement TTS chunk skip navigation

* Implement deep-linking and playback controls for TTS media sessions

* Implement start index for TTS playback

* Improve TTS navigation, prefetching, and notification duration reporting

* Implement TTS mini playback bar for background reading

* Implement multi-window reader support for the desktop application

* Improve desktop modal window management and visibility syncing

* Implement localized string support for Desktop and shared UI

* Implement language selection and persistence for Desktop

* Implement plural string support for Desktop and migrate hardcoded counts to plurals.xml

* Implement localized banner messages and UI strings using resource-backed SharedText

* Implement compact badge styling for small book covers

* Refactor PDF native interaction and improve HTML import memory safety

* fix language persistence

* Refactor reader overflow menus to use section-based logic

* Refactor PDF layout remapping and improve text box interaction

* Improve CFI resolution and TTS resume accuracy using dynamic chunk offsets

* Centralize PDF annotation export mapping and improve metadata handling

* Add support for threaded comments in PDF highlight annotations

* Flatten highlight comments into a single thread for PDF export and allow author editing

* Integrate page slider into reader chrome and persist toggle state

* Handle fragments and queries in EPUB chapter paths

* Implement dynamic, theme-aware coloring for the reader slider

* Implement customizable app-wide font preference

* Implement one-hand zoom gestures in the PDF viewer

* Implement File Information dialog for PDF and EPUB readers

* Bump version to 1.0.49 (53)

* Refactor PDF reader logic into modular components

* Add ProGuard rules to prevent R8 optimization issues in EPUB reader screens

* Add option to use PDF filenames as display names

* Fix preservation of PDF filename display preference in library projection
This commit is contained in:
Aryan 2026-05-20 22:14:01 +05:30 committed by GitHub
parent dc5196526f
commit 9510293ac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
245 changed files with 37538 additions and 12460 deletions

View file

@ -0,0 +1,142 @@
package com.aryan.reader.tts
import androidx.media3.common.util.UnstableApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
class TtsChunkNavigationTest {
@Test
fun `chunk skip target moves one chunk at a time`() {
assertEquals(1, resolveTtsChunkSkipTarget(currentChunkIndex = 2, totalChunks = 5, direction = -1))
assertEquals(3, resolveTtsChunkSkipTarget(currentChunkIndex = 2, totalChunks = 5, direction = 1))
}
@Test
fun `chunk skip target is absent at boundaries`() {
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 0, totalChunks = 5, direction = -1))
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 4, totalChunks = 5, direction = 1))
}
@Test
fun `chunk skip target is absent for invalid state`() {
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = -1, totalChunks = 5, direction = 1))
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 5, totalChunks = 5, direction = -1))
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 0, totalChunks = 0, direction = 1))
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 0, totalChunks = 5, direction = 0))
assertNull(resolveTtsChunkSkipTarget(currentChunkIndex = 0, totalChunks = 5, direction = 2))
}
@Test
fun `start chunk index is clamped to available chunks`() {
assertEquals(2, resolveTtsStartChunkIndex(requestedChunkIndex = 2, totalChunks = 5))
assertEquals(0, resolveTtsStartChunkIndex(requestedChunkIndex = -1, totalChunks = 5))
assertEquals(4, resolveTtsStartChunkIndex(requestedChunkIndex = 7, totalChunks = 5))
assertEquals(0, resolveTtsStartChunkIndex(requestedChunkIndex = 2, totalChunks = 0))
}
@Test
fun `only forward chunk skips can reuse an existing playlist item`() {
assertEquals(3, resolveReusableTtsPlaylistIndex(playlistIndex = 3, direction = 1))
assertNull(resolveReusableTtsPlaylistIndex(playlistIndex = 3, direction = -1))
assertNull(resolveReusableTtsPlaylistIndex(playlistIndex = null, direction = 1))
assertNull(resolveReusableTtsPlaylistIndex(playlistIndex = -1, direction = 1))
}
@Test
fun `automatic playlist advance must stay contiguous by chunk id`() {
assertEquals(true, shouldAdvanceToTtsPlaylistChunk(currentChunkIndex = 8, playlistChunkIndex = 9))
assertEquals(false, shouldAdvanceToTtsPlaylistChunk(currentChunkIndex = 8, playlistChunkIndex = 10))
assertEquals(false, shouldAdvanceToTtsPlaylistChunk(currentChunkIndex = 8, playlistChunkIndex = null))
}
@Test
fun `transition prefetch is deferred only for the rebuilding generation`() {
assertEquals(false, shouldStartTtsTransitionPrefetch(currentGeneration = 6, deferredGeneration = 6))
assertEquals(true, shouldStartTtsTransitionPrefetch(currentGeneration = 6, deferredGeneration = 5))
assertEquals(true, shouldStartTtsTransitionPrefetch(currentGeneration = 6, deferredGeneration = -1))
}
@androidx.annotation.OptIn(UnstableApi::class)
@Test
fun `reader tts mini bar is visible only for active reader playback outside reader routes`() {
val activeReaderState = TtsPlaybackManager.TtsState(
currentText = "Playing text",
playbackSource = "READER"
)
assertEquals(true, shouldShowReaderTtsMiniBar(activeReaderState, isOnReaderRoute = false))
assertEquals(false, shouldShowReaderTtsMiniBar(activeReaderState, isOnReaderRoute = true))
assertEquals(
false,
shouldShowReaderTtsMiniBar(activeReaderState.copy(playbackSource = "OTHER"), isOnReaderRoute = false)
)
assertEquals(
false,
shouldShowReaderTtsMiniBar(activeReaderState.copy(sessionFinished = true), isOnReaderRoute = false)
)
assertEquals(
false,
shouldShowReaderTtsMiniBar(activeReaderState.copy(sessionEndedByStop = true), isOnReaderRoute = false)
)
}
@Test
fun `reader tts mini bar clears main bottom navigation`() {
assertEquals(96, readerTtsMiniBarBottomPaddingDp(isOnMainRoute = true))
assertEquals(16, readerTtsMiniBarBottomPaddingDp(isOnMainRoute = false))
}
@Test
fun `stream pcm duration uses cloud tts audio format`() {
assertEquals(1_000L, resolveTtsStreamPcmDurationMs(totalBytes = 44L + 48_000L))
assertNull(resolveTtsStreamPcmDurationMs(totalBytes = 44L))
assertNull(resolveTtsStreamPcmDurationMs(totalBytes = 0L))
}
@Test
fun `notification duration estimate stays ahead of playback position`() {
assertEquals(1_500L, estimateTtsNotificationDurationMs(text = "one two"))
assertEquals(5_000L, estimateTtsNotificationDurationMs(text = "one", currentPositionMs = 3_000L))
assertNull(estimateTtsNotificationDurationMs(text = " "))
}
@Test
fun `wav file duration is read from pcm byte rate`() {
val file = createTempWavFile(pcmBytes = 48_000)
try {
assertEquals(1_000L, resolveWavFileDurationMs(file))
} finally {
file.delete()
}
}
private fun createTempWavFile(pcmBytes: Int): File {
val file = File.createTempFile("tts_chunk_navigation_", ".wav")
val header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN).apply {
put("RIFF".toByteArray(Charsets.US_ASCII))
putInt(36 + pcmBytes)
put("WAVE".toByteArray(Charsets.US_ASCII))
put("fmt ".toByteArray(Charsets.US_ASCII))
putInt(16)
putShort(1.toShort())
putShort(1.toShort())
putInt(24_000)
putInt(48_000)
putShort(2.toShort())
putShort(16.toShort())
put("data".toByteArray(Charsets.US_ASCII))
putInt(pcmBytes)
}.array()
file.outputStream().use { output ->
output.write(header)
output.write(ByteArray(pcmBytes))
}
return file
}
}