Windows ga (#358)

* Enhance Cloud TTS with navigation controls and shared UI overlay in desktop app

* Refactor Cloud TTS voice settings and remove standalone settings overlay on desktop app

* Persist reader window state and improve slider interaction in desktop app

* Improve PDF page transitions and refine focus management in desktop app

* Refactor scrollbar interaction and adjust desktop modal focus handling

* Improve PDF sidecar synchronization and cross-platform metadata compatibility

* Refactor PDF annotation comment logic to shared module and implement Desktop UI

* Refactor reader screen to use tap-to-toggle and full-width styling in desktop app

* Refactor reader workspace layout and chrome-panel interactions

* Implement global search keyboard shortcuts and focusable chrome layers

* Implement flavor-specific legal links and update the About UI

* Refactor reader UI controls on desktop app

* Enhance desktop folder sync with background metadata extraction and improved error handling

* Refactor Library UI and remove redundant Home tab in desktop app

* Add custom tooltips to reader icon buttons in desktop app

* Integrate app theme controls into reader interfaces on desktop

* Add right-to-left pagination support and improve focus restoration on desktop app

* Improve EPUB pagination geometry and diagnostic logging for layout cutoffs on desktop app

* Update desktop reader defaults and implement settings migration

* Implement block-based position tracking in ReaderLocator

* Enhance EPUB highlighting reliability in desktop app

* Add support for custom reader themes and update highlight palette logic in desktop app

* Replace the Tools panel with a "More" dropdown menu and refactor account UI

* Implement account profile header in desktop sidebar

* Implement cloud sync reliability improvements and sidebar toggle on desktop app

* Improve EPUB annotation synchronization and highlight mapping accuracy in desktop app

* Integrate WebView2 for EPUB vertical rendering on Windows

* Refactor reader layout logic and enhance WebView2 diagnostics

* Improve vertical reading layout and WebView2 resizing on Desktop

* Refine vertical reading mode layout and margin handling

* Enhance reader locator precision and Desktop mode-switching reliability

* Implement chapter-level caching and warm-start pagination in desktop app

* Replace bundled KCEF with native system webviews via SWT

* Refactor EPUB page info bar visibility and layout logic

* Improve PDF toolbar persistence and fix tab reactivation logic

* Enable multi-selection and bulk operations for custom fonts

* Refactor instrumentation tests

* Add EPUB UI test fixture and initial instrumentation tests

* Expand EpubReader UI tests and improve accessibility

* Add instrumentation tests and test tags for library and reader screens

* Enhance OPDS parser logic and catalog integration

* Add support for toggling local synchronization on a per-folder basis.

* Implement tri-state sizing for the TTS overlay

* Persist TTS overlay size across sessions

* Refactor reader brightness control and add incremental step buttons

* Improve CSS support, pagination control, and style-aware semantic caching

* Improve link handling, interaction, and diagnostics in the paginated reader

* crash fixes

* Implement persistent pending removal for external files

* Implement book-specific word replacements

* Add native vertical reading mode with custom renderer

* Implement text selection and navigation improvements for the native vertical reader

* Implement locator-based navigation and improved vertical scrolling in native vertical mode in epub

* Implement lazy loading and chapter prefetching for native vertical reader

* Improve window lifecycle and disposal handling on Desktop

* Optimize vertical reading performance in desktop app

* Enhance TTS start accuracy and diagnostic logging on desktop

* Refactor AI settings visibility on desktop

* Improve pagination height measurement and enhance cutoff diagnostics

* Implement lifecycle management and improve justified text splitting for pagination

* Refine AI usage tracking and force AI feature visibility on Desktop

* Add descriptive context comments and usage examples to string and plural resources.

* Optimize performance and memory usage in search and state mapping

* Replace reader page sliders with minimal slider and navigation controls

* Add support for CBT comic archives

* Harden file path validation and XML parsing to prevent security vulnerabilities

* Implement local account profile caching and optimize desktop performance

* Improve desktop persistence reliability and add Linux secure storage support

* Improved PDF zoom stability and layout prediction during zoom commits

* Improved PDF spread layout prediction, reader focus restoration, and account profile caching

* Enhance highlight precision and scoping using block-local offsets and CFIs

* Enhance cloud book content synchronization and background downloads

* Implement granular timestamp tracking for reading positions and PDF annotations

* Restrict diagnostic logging and stack traces to debug builds

* Refine PDF page gaps and reader chrome interaction logic

* Refactor PDF highlight rendering and overhaul Desktop sidebar UI

* Implement a new interaction dock and undo/redo history for PDF annotations in desktop

* Enhance PDF color picker and improve navigation scroll restoration

* Add highlight palette customization and improve selection menu UI in desktop app epub reader

* Enhance desktop shelf management and library organization
This commit is contained in:
Aryan 2026-06-02 00:51:42 +05:30 committed by GitHub
parent 5971eaa571
commit 83dcafa4b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
444 changed files with 47279 additions and 8096 deletions

View file

@ -53,15 +53,64 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import java.io.File
import kotlin.math.max
import kotlin.math.min
private const val EPUB_SEARCH_WINDOW_CHARS = 32_768
private const val EPUB_SEARCH_SNIPPET_RADIUS = 35
private const val EPUB_SEARCH_MAX_OVERLAP_CHARS = 4_096
private val epubSearchSkippedTags = setOf("script", "style", "noscript")
private val epubSearchBlockBoundaryTags = setOf(
"address",
"article",
"aside",
"blockquote",
"br",
"caption",
"dd",
"div",
"dl",
"dt",
"figcaption",
"figure",
"footer",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"li",
"main",
"nav",
"ol",
"p",
"pre",
"section",
"table",
"td",
"th",
"tr",
"ul"
)
/**
* Creates the search implementation for EPUB chapters.
*/
fun createEpubSearcher(epubBook: EpubBook): suspend (String) -> List<SearchResult> = { query ->
withContext(Dispatchers.Default) {
val searchQuery = query.trim()
if (searchQuery.isBlank()) {
return@withContext emptyList()
}
val results = mutableListOf<SearchResult>()
epubBook.chapters.forEachIndexed { chapterIndex, chapter ->
try {
@ -69,54 +118,208 @@ fun createEpubSearcher(epubBook: EpubBook): suspend (String) -> List<SearchResul
if (!htmlFile.exists()) return@forEachIndexed
val doc = Jsoup.parse(htmlFile, "UTF-8")
val bodyChildren = doc.body().children().toList()
val chunks = bodyChildren.chunked(20)
doc.select("script, style, noscript").remove()
val bodyNodes = doc.body().childNodes().toList()
val chunks = bodyNodes.chunked(20)
var occurrenceIndexInChapter = 0
chunks.forEachIndexed { chunkIndex, chunkOfElements ->
val chunkHtml = chunkOfElements.joinToString(separator = "\n") { it.outerHtml() }
val content = Jsoup.parse(chunkHtml).text()
var lastIndex = -1
while (true) {
lastIndex = content.indexOf(query, startIndex = lastIndex + 1, ignoreCase = true)
if (lastIndex == -1) break
val isWordStart = lastIndex == 0 || !content[lastIndex - 1].isLetterOrDigit()
if (isWordStart) {
val snippetStart = max(0, lastIndex - 35)
val snippetEnd = min(content.length, lastIndex + query.length + 35)
val rawSnippet = content.substring(snippetStart, snippetEnd)
val annotatedSnippet = buildAnnotatedString {
append(rawSnippet)
val highlightStart = content.indexOf(query, lastIndex, ignoreCase = true) - snippetStart
val highlightEnd = highlightStart + query.length
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = highlightStart,
end = highlightEnd
)
}
results.add(
SearchResult(
locationInSource = chapterIndex,
locationTitle = chapter.title,
snippet = annotatedSnippet,
query = query,
occurrenceIndexInLocation = results.count { it.locationInSource == chapterIndex },
chunkIndex = chunkIndex
)
)
}
}
chunks.forEachIndexed { chunkIndex, chunkNodes ->
occurrenceIndexInChapter = appendSearchResultsFromNodes(
nodes = chunkNodes,
query = searchQuery,
chapterIndex = chapterIndex,
chapterTitle = chapter.title,
chunkIndex = chunkIndex,
occurrenceIndexInChapter = occurrenceIndexInChapter,
results = results
)
}
} catch (e: Exception) {
Timber.e("Failed to search in chapter $chapterIndex", e)
Timber.e(e, "Failed to search in chapter $chapterIndex")
} catch (e: OutOfMemoryError) {
Timber.e(e, "Skipping search in chapter $chapterIndex after running out of memory")
}
}
results
}
}
private fun appendSearchResultsFromNodes(
nodes: List<Node>,
query: String,
chapterIndex: Int,
chapterTitle: String,
chunkIndex: Int,
occurrenceIndexInChapter: Int,
results: MutableList<SearchResult>
): Int {
val searchWindow = EpubSearchWindow(
query = query,
chapterIndex = chapterIndex,
chapterTitle = chapterTitle,
chunkIndex = chunkIndex,
initialOccurrenceIndex = occurrenceIndexInChapter,
results = results
)
nodes.forEach { node ->
searchWindow.visit(node)
}
searchWindow.finish()
return searchWindow.occurrenceIndex
}
private class EpubSearchWindow(
private val query: String,
private val chapterIndex: Int,
private val chapterTitle: String,
private val chunkIndex: Int,
initialOccurrenceIndex: Int,
private val results: MutableList<SearchResult>
) {
private val buffer = StringBuilder()
private val overlapChars = (query.length + EPUB_SEARCH_SNIPPET_RADIUS)
.coerceIn(EPUB_SEARCH_SNIPPET_RADIUS * 2, EPUB_SEARCH_MAX_OVERLAP_CHARS)
private var lastAppendedWasWhitespace = true
private var previousCharBeforeBuffer: Char? = null
var occurrenceIndex: Int = initialOccurrenceIndex
private set
fun visit(node: Node) {
when (node) {
is TextNode -> appendNormalizedText(node.wholeText)
is Element -> {
val tagName = node.tagName().lowercase()
if (tagName in epubSearchSkippedTags) return
if (tagName == "br") {
appendNormalizedWhitespace()
return
}
node.childNodes().forEach(::visit)
if (tagName in epubSearchBlockBoundaryTags) {
appendNormalizedWhitespace()
}
}
else -> node.childNodes().forEach(::visit)
}
}
fun finish() {
scanBuffer(buffer.length)
buffer.clear()
previousCharBeforeBuffer = null
}
private fun appendNormalizedText(text: String) {
text.forEach { char ->
if (char.isWhitespace()) {
appendNormalizedWhitespace()
} else {
buffer.append(char)
lastAppendedWasWhitespace = false
trimScannedPrefixIfNeeded()
}
}
}
private fun appendNormalizedWhitespace() {
if (buffer.isEmpty() || lastAppendedWasWhitespace) {
lastAppendedWasWhitespace = true
return
}
buffer.append(' ')
lastAppendedWasWhitespace = true
trimScannedPrefixIfNeeded()
}
private fun trimScannedPrefixIfNeeded() {
if (buffer.length < EPUB_SEARCH_WINDOW_CHARS) return
val scanEndExclusive = (buffer.length - overlapChars).coerceAtLeast(0)
if (scanEndExclusive <= 0) return
scanBuffer(scanEndExclusive)
previousCharBeforeBuffer = buffer[scanEndExclusive - 1]
buffer.delete(0, scanEndExclusive)
}
private fun scanBuffer(scanEndExclusive: Int) {
var searchFrom = 0
while (searchFrom < scanEndExclusive) {
val matchStart = buffer.indexOfIgnoreCase(query, searchFrom, scanEndExclusive)
if (matchStart == -1) break
if (isWordStart(matchStart)) {
addSearchResult(matchStart)
}
searchFrom = matchStart + 1
}
}
private fun isWordStart(matchStart: Int): Boolean {
val previousChar = if (matchStart > 0) {
buffer[matchStart - 1]
} else {
previousCharBeforeBuffer
}
return previousChar == null || !previousChar.isLetterOrDigit()
}
private fun addSearchResult(matchStart: Int) {
val snippetStart = max(0, matchStart - EPUB_SEARCH_SNIPPET_RADIUS)
val snippetEnd = min(buffer.length, matchStart + query.length + EPUB_SEARCH_SNIPPET_RADIUS)
val rawSnippet = buffer.substring(snippetStart, snippetEnd)
val highlightStart = matchStart - snippetStart
val highlightEnd = highlightStart + query.length
val annotatedSnippet = buildAnnotatedString {
append(rawSnippet)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = highlightStart,
end = highlightEnd
)
}
results.add(
SearchResult(
locationInSource = chapterIndex,
locationTitle = chapterTitle,
snippet = annotatedSnippet,
query = query,
occurrenceIndexInLocation = occurrenceIndex,
chunkIndex = chunkIndex
)
)
occurrenceIndex++
}
}
private fun CharSequence.indexOfIgnoreCase(
query: String,
startIndex: Int,
matchStartLimitExclusive: Int
): Int {
if (query.isEmpty()) return -1
val lastStart = min(length - query.length, matchStartLimitExclusive - 1)
if (lastStart < startIndex) return -1
var index = startIndex.coerceAtLeast(0)
while (index <= lastStart) {
var queryIndex = 0
while (
queryIndex < query.length &&
this[index + queryIndex].equals(query[queryIndex], ignoreCase = true)
) {
queryIndex++
}
if (queryIndex == query.length) return index
index++
}
return -1
}
/**
* Handles the navigation to a specific search result.
*/