* Centralize library management logic and introduce support for plain text and HTML formats

* Centralize library management logic and introduce support for plain text and HTML formats

* Expand unit test coverage for library state management, UI models, and MainViewModel features.

* Add comprehensive unit tests for PDF reader core logic, preferences, and data persistence

* Add unit tests for EPUB parsing, content loading, search functionality, and reader JavaScript bridges.

* Add unit tests for OPDS parsing and Smart Collection engine, and integrate Kover plugin

* Add comprehensive unit tests

* Centralize library snapshot serialization in the `shared` module and improve filtering and sorting logic.

* Implement text selection, highlighting, and reading state persistence for PDF and EPUB engines in desktop version

* Folder import support for desktop app

* Introduce Smart Shelves with rule-based filtering in desktop version

* Implement shared EPUB annotation serialization and highlight rendering

* Centralize file type capabilities and platform-specific support logic

* Refactor reader state management to use a central reducer

* Implement customizable reader toolbar and advanced formatting settings in shared

* Implement locator-based navigation and customizable highlight palette for desktop app

* Enhance reader customization and expand search functionality in desktop app

* Redesign reader settings and tools into a tabbed control panel in desktop app

* Enhance reader navigation and highlight precision in desktop app

* Implement bidirectional position synchronization and dynamic highlights in the desktop reader

* Implement shared state management and enhanced search for the PDF reader in desktop app

* Add vertical scroll support to the desktop PDF reader

* Implement ink, text, and eraser annotation support in desktop PDF viewer

* Implement PDF bookmarks, Table of Contents, and annotation editing in desktop app

* Implement link handling and navigation for PDF and EPUB readers in desktop app

* Implement PDF jump history for navigation in desktop app

* Enhance PDF ink rendering and annotation capabilities in desktop app

* Implement advanced PDF text annotations with inline editing and rich styling in desktop app

* Add move handle and movement logic for PDF text annotations in desktop app

* Implement local folder synchronization and metadata sidecar support in desktop app

* Implement book metadata extraction and drag-and-drop import for Desktop

* Implement dynamic and custom app theme management for desktop

* Introduce canonical PDF annotation codec and support for multi-segment highlights

* Implement rich text editing and pagination support for the PDF reader in desktop app

* Improve PDF rich text pagination, synchronization, and observability in desktop

* Hide trailing structural page breaks in rich text editor

* Implement a unified JVM book loader and expand supported formats on Desktop

* Add comic archive support for Desktop and enhance MOBI parsing

* Implement shared OPDS catalog support and UI for Android and Desktop

* Improve native WebView lifecycle and surface transition management on Desktop

* Enable Compose Swing interop blending and simplify Desktop WebView management

* Integrate BYOK AI features and Cloud TTS for desktop

* Enhance Desktop TTS with streaming audio and improved secure storage for AI key

* Implement scoped Cloud TTS with synchronized highlighting for EPUB and PDF in desktop app

* Implement custom font management and utility screens in desktop app

* Implement PDFium-based PDF annotation export

* Remove PdfBox dependency and standardize PDF export via Pdfium

* Implement local audio caching and playback controls for Gemini Cloud TTS in desktop app

* Implement reader themes and custom texture support in desktop app

* Redesign non-reader UI with responsive navigation and enhanced library management in desktop app

* Introduce ReaderWorkspaceShell to unify EPUB and PDF reader layouts in desktop app

* Exclude manual-only files from automated sync and import

* Implement customizable Text-to-Speech (TTS) word replacements

* Optimize reader performance with persistent layout caching and decoupled theme rendering

* Improve position restoration during reader reconfiguration in epub pagination

* Use independent thickness for eraser tool and stylus override
This commit is contained in:
Aryan 2026-05-10 10:07:37 +05:30 committed by GitHub
parent 88c7fa7b5c
commit 8366d76dcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
214 changed files with 53372 additions and 4702 deletions

View file

@ -166,6 +166,7 @@ import com.aryan.reader.SearchResult
import com.aryan.reader.SummarizationResult
import com.aryan.reader.SummaryCacheManager
import com.aryan.reader.TtsSettingsSheet
import com.aryan.reader.TtsWordReplacementsSheet
import com.aryan.reader.areReaderAiFeaturesEnabled
import com.aryan.reader.countWords
import com.aryan.reader.isByokCloudTtsAvailable
@ -177,6 +178,7 @@ import com.aryan.reader.loadCustomThemes
import com.aryan.reader.loadGlobalTextureTransparency
import com.aryan.reader.loadReaderThemeId
import com.aryan.reader.loadReaderTextureBitmap
import com.aryan.reader.loadTtsReplacementPreferences
import com.aryan.reader.paginatedreader.BookPaginator
import com.aryan.reader.paginatedreader.CfiUtils
import com.aryan.reader.paginatedreader.HeaderBlock
@ -195,12 +197,16 @@ import com.aryan.reader.rememberSearchState
import com.aryan.reader.saveCustomThemes
import com.aryan.reader.saveGlobalTextureTransparency
import com.aryan.reader.saveReaderThemeId
import com.aryan.reader.saveTtsReplacementPreferences
import com.aryan.reader.shared.ReaderTtsReplacementPreferences
import com.aryan.reader.tts.SpeakerSamplePlayer
import com.aryan.reader.tts.TtsPlaybackManager
import com.aryan.reader.tts.loadTtsMode
import com.aryan.reader.tts.splitTextIntoChunks
import com.aryan.reader.withTtsReplacements
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
@ -570,6 +576,7 @@ fun EpubReaderScreen(
}
}
} else null,
stableBookId = uiState.selectedBookId,
viewModel = viewModel
)
}
@ -600,6 +607,7 @@ fun EpubReaderHost(
onImportFont: (Uri) -> Unit,
onToggleReflow: ((Int) -> Unit)? = null,
onDeleteReflow: (() -> Unit)? = null,
stableBookId: String? = null,
viewModel: MainViewModel
) {
val view = LocalView.current
@ -668,11 +676,16 @@ fun EpubReaderHost(
)
}
val locatorConverter = remember(context) {
val readerCacheBookId = remember(stableBookId, epubBook.title, epubBook.fileName) {
stableBookId ?: if (epubBook.fileName.length > 20) epubBook.fileName else getBookIdForPrefs(epubBook.title)
}
val locatorConverter = remember(context, readerCacheBookId) {
LocatorConverter(
bookCacheDao = BookCacheDatabase.getDatabase(context).bookCacheDao(),
proto = ProtoBuf { serializersModule = semanticBlockModule },
context = context
context = context,
stableBookId = readerCacheBookId
)
}
@ -698,9 +711,7 @@ fun EpubReaderHost(
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
var isTtsCollapsed by remember { mutableStateOf(false) }
val bookId = remember(epubBook.title, epubBook.fileName) {
if (epubBook.fileName.length > 20) epubBook.fileName else getBookIdForPrefs(epubBook.title)
}
val bookId = readerCacheBookId
var isAutoScrollLocal by remember { mutableStateOf(loadAutoScrollLocalMode(context, bookId)) }
val initialSettings = remember(isAutoScrollLocal) {
@ -895,6 +906,8 @@ fun EpubReaderHost(
var currentRenderMode by remember(renderMode) { mutableStateOf(renderMode) }
var chapterToLoadOnSwitch by remember { mutableStateOf<Int?>(null) }
var lastKnownLocator by remember(initialLocator) { mutableStateOf(initialLocator) }
var paginatedReconfigurationAnchor by remember { mutableStateOf<Locator?>(null) }
var isPaginatedReconfigurationRestoring by remember { mutableStateOf(false) }
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val roundedCornerBottomPadding = rememberBottomRoundedCornerPadding(view)
@ -910,18 +923,7 @@ fun EpubReaderHost(
LaunchedEffect(bookmarks) {
Timber.d("Bookmarks changed, saving...")
val stringSet = bookmarks.map { bookmark ->
JSONObject().apply {
put("cfi", bookmark.cfi)
put("chapterTitle", bookmark.chapterTitle)
put("label", bookmark.label)
put("snippet", bookmark.snippet)
bookmark.pageInChapter?.let { put("pageInChapter", it) }
bookmark.totalPagesInChapter?.let { put("totalPagesInChapter", it) }
put("chapterIndex", bookmark.chapterIndex)
}.toString()
}
onBookmarksChanged(JSONArray(stringSet).toString())
onBookmarksChanged(bookmarksToJson(bookmarks))
}
var activeBookmarkInVerticalView by remember { mutableStateOf<Bookmark?>(null) }
@ -1209,9 +1211,15 @@ fun EpubReaderHost(
var showPermissionRationaleDialog by remember { mutableStateOf(false) }
var showTtsSettingsSheet by remember { mutableStateOf(false) }
var showTtsReplacementsSheet by remember { mutableStateOf(false) }
var showTtsControlsSheet by remember { mutableStateOf(false) }
var showThemePanel by remember { mutableStateOf(false) }
var showPaletteManager by remember { mutableStateOf(false) }
var ttsReplacementPreferences by remember { mutableStateOf(loadTtsReplacementPreferences(context)) }
val updateTtsReplacementPreferences: (ReaderTtsReplacementPreferences) -> Unit = { next ->
ttsReplacementPreferences = next
saveTtsReplacementPreferences(context, next)
}
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
@ -1350,7 +1358,7 @@ fun EpubReaderHost(
}
Timber.tag("TTS_LOCATE")
.d("Saving locator from TTS. chapter=${locator.chapterIndex}, block=${locator.blockIndex}, progress=$progress")
.d("Saving resolved locator position. chapter=${locator.chapterIndex}, block=${locator.blockIndex}, progress=$progress")
onSavePosition(locator, cfiForWebView, progress)
}
@ -1535,7 +1543,7 @@ fun EpubReaderHost(
val coverUriString = coverImagePath?.let { Uri.fromFile(File(it)).toString() }
ttsChapterIndex = chapterIndex
ttsController.start(
chunks = ttsChunks,
chunks = ttsChunks.withTtsReplacements(ttsReplacementPreferences, bookId),
bookTitle = epubBook.title,
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
@ -1588,7 +1596,11 @@ fun EpubReaderHost(
val relativeOffset = startOffset - target.startOffsetInSource
val safeRelativeOffset = relativeOffset.coerceIn(0, target.text.length)
val slicedText = target.text.substring(safeRelativeOffset)
val newChunk = target.copy(text = slicedText, startOffsetInSource = startOffset)
val newChunk = target.copy(
text = slicedText,
startOffsetInSource = startOffset,
spokenText = slicedText,
)
val remainingChunks = mutableListOf(newChunk)
remainingChunks.addAll(chunks.subList(foundIdx + 1, chunks.size))
@ -1599,7 +1611,7 @@ fun EpubReaderHost(
val chapterTitle = chapters.getOrNull(chapterIndex)?.title
val coverUriString = coverImagePath?.let { Uri.fromFile(File(it)).toString() }
ttsController.start(
chunks = remainingChunks,
chunks = remainingChunks.withTtsReplacements(ttsReplacementPreferences, bookId),
bookTitle = epubBook.title,
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
@ -1662,7 +1674,9 @@ fun EpubReaderHost(
currentTtsMode = currentTtsMode,
getAuthToken = { viewModel.getAuthToken() },
locatorConverter = locatorConverter,
epubBook = epubBook
epubBook = epubBook,
ttsReplacementPreferences = ttsReplacementPreferences,
ttsReplacementBookId = bookId
)
TtsHighlightHandler(
@ -2045,8 +2059,26 @@ fun EpubReaderHost(
}
}
LaunchedEffect(paginatedPagerState.currentPage, paginator) {
if (currentRenderMode == RenderMode.PAGINATED && paginator != null && isPagerInitialized) {
LaunchedEffect(paginatedPagerState, paginator, currentRenderMode, isPagerInitialized, isPaginatedReconfigurationRestoring) {
if (currentRenderMode != RenderMode.PAGINATED || paginator == null || !isPagerInitialized) {
return@LaunchedEffect
}
snapshotFlow { paginatedPagerState.currentPage }
.collectLatest { page ->
if (!isPaginatedReconfigurationRestoring) {
(paginator as? BookPaginator)?.getLocatorForPage(page)?.let { locator ->
lastKnownLocator = locator
}
}
}
}
LaunchedEffect(paginatedPagerState.currentPage, paginator, isPaginatedReconfigurationRestoring) {
if (currentRenderMode == RenderMode.PAGINATED &&
paginator != null &&
isPagerInitialized &&
!isPaginatedReconfigurationRestoring
) {
delay(1500L)
val pageToSave = paginatedPagerState.currentPage
@ -2164,12 +2196,21 @@ fun EpubReaderHost(
RenderMode.PAGINATED -> {
scope.launch {
val pageToSave = paginatedPagerState.currentPage
val locator = (paginator as? BookPaginator)?.getLocatorForPage(pageToSave)
val pageLocator = if (isPaginatedReconfigurationRestoring) {
null
} else {
(paginator as? BookPaginator)?.getLocatorForPage(pageToSave)
}
val locator = pageLocator ?: paginatedReconfigurationAnchor ?: lastKnownLocator
val chapterIndex = paginator?.findChapterIndexForPage(pageToSave)
if (locator != null && chapterIndex != null) {
if (locator != null) {
val bookPaginator = paginator as? BookPaginator
val progress = if (totalBookLengthChars > 0 && bookPaginator != null) {
val progress = if (pageLocator == null || chapterIndex == null) {
saveResolvedLocatorPosition(locator, null)
onNavigateBack()
return@launch
} else if (totalBookLengthChars > 0 && bookPaginator != null) {
val completedCharsInPreviousChapters = chapters.take(chapterIndex).sumOf { it.plainTextContent.length.toLong() }
val currentPageInChapter = (bookPaginator.chapterStartPageIndices[chapterIndex] ?: 0).let { pageToSave - it }
val charsScrolledInCurrentChapter = bookPaginator.getCharactersScrolledInChapter(chapterIndex, currentPageInChapter)
@ -2186,7 +2227,7 @@ fun EpubReaderHost(
)
onSavePosition(locator, null, progress)
} else {
Timber.w("Final save for paginated view failed. Locator or chapter index is null."
Timber.w("Final save for paginated view failed. Locator is null."
)
}
onNavigateBack()
@ -3570,7 +3611,7 @@ fun EpubReaderHost(
ttsChapterIndex = targetChapterIndex
ttsController.start(
chunks = ttsChunks,
chunks = ttsChunks.withTtsReplacements(ttsReplacementPreferences, bookId),
bookTitle = epubBook.title,
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
@ -3886,6 +3927,7 @@ fun EpubReaderHost(
) {
PaginatedReaderScreen(
book = epubBook,
bookId = readerCacheBookId,
isDarkTheme = isDarkTheme,
effectiveBg = effectiveBg,
effectiveText = effectiveText,
@ -3910,7 +3952,18 @@ fun EpubReaderHost(
activeTextureId = activeTextureId,
activeTextureAlpha = activeTextureAlpha,
initialChapterIndexInBook = lastKnownLocator?.chapterIndex,
modifier = Modifier.alpha(if (isPagerInitialized) 1f else 0f),
fallbackLocatorForReconfiguration = paginatedReconfigurationAnchor ?: lastKnownLocator,
onReconfigurationAnchorCaptured = { locator ->
paginatedReconfigurationAnchor = locator
lastKnownLocator = locator
},
onReconfigurationRestoreActiveChanged = { isActive ->
isPaginatedReconfigurationRestoring = isActive
if (!isActive) {
paginatedReconfigurationAnchor = null
}
},
modifier = Modifier.alpha(if (isPagerInitialized && !isPaginatedReconfigurationRestoring) 1f else 0f),
onPaginatorReady = { newPaginator ->
paginator = newPaginator
},
@ -4624,6 +4677,7 @@ fun EpubReaderHost(
searchFocusRequester = searchFocusRequester,
modifier = Modifier.align(Alignment.TopCenter),
onOpenTtsSettings = { showTtsSettingsSheet = true },
onOpenTtsReplacements = { showTtsReplacementsSheet = true },
onOpenDictionarySettings = { showDictionarySettingsSheet = true },
onOpenThemeSettings = { showThemePanel = true },
onOpenVisualOptions = { showVisualOptionsSheet = true },
@ -5259,6 +5313,15 @@ fun EpubReaderHost(
)
}
TtsWordReplacementsSheet(
isVisible = showTtsReplacementsSheet,
bookId = bookId,
bookTitle = epubBook.title,
preferences = ttsReplacementPreferences,
onPreferencesChange = updateTtsReplacementPreferences,
onDismiss = { showTtsReplacementsSheet = false },
)
if (showCustomizeToolsSheet) {
CustomizeToolsSheet(
hiddenTools = hiddenTools,