Windows (#291)
* 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:
parent
88c7fa7b5c
commit
8366d76dcd
214 changed files with 53372 additions and 4702 deletions
|
|
@ -70,58 +70,16 @@ import androidx.core.content.edit
|
|||
import androidx.core.text.HtmlCompat
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.epub.EpubChapter
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
import com.aryan.reader.shared.EpubAnnotationSerializer
|
||||
|
||||
private const val BOOKMARK_PREFS_NAME = "epub_reader_bookmarks"
|
||||
|
||||
data class Bookmark(
|
||||
val cfi: String,
|
||||
val chapterTitle: String,
|
||||
val label: String? = null,
|
||||
val snippet: String,
|
||||
val pageInChapter: Int?,
|
||||
val totalPagesInChapter: Int?,
|
||||
val chapterIndex: Int
|
||||
)
|
||||
|
||||
enum class HighlightColor(val id: String, val color: Color, val cssClass: String) {
|
||||
YELLOW("yellow", Color(0xFFFBC02D), "user-highlight-yellow"),
|
||||
GREEN("green", Color(0xFF388E3C), "user-highlight-green"),
|
||||
BLUE("blue", Color(0xFF1976D2), "user-highlight-blue"),
|
||||
RED("red", Color(0xFFD32F2F), "user-highlight-red"),
|
||||
PURPLE("purple", Color(0xFF7B1FA2), "user-highlight-purple"),
|
||||
ORANGE("orange", Color(0xFFF57C00), "user-highlight-orange"),
|
||||
CYAN("cyan", Color(0xFF0097A7), "user-highlight-cyan"),
|
||||
MAGENTA("magenta", Color(0xFFC2185B), "user-highlight-magenta"),
|
||||
LIME("lime", Color(0xFFAFB42B), "user-highlight-lime"),
|
||||
PINK("pink", Color(0xFFE91E63), "user-highlight-pink"),
|
||||
TEAL("teal", Color(0xFF00796B), "user-highlight-teal"),
|
||||
INDIGO("indigo", Color(0xFF303F9F), "user-highlight-indigo"),
|
||||
BLACK("black", Color(0xFF424242), "user-highlight-black"),
|
||||
WHITE("white", Color(0xFFF5F5F5), "user-highlight-white");
|
||||
}
|
||||
|
||||
data class UserHighlight(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val cfi: String,
|
||||
val text: String,
|
||||
val color: HighlightColor,
|
||||
val chapterIndex: Int,
|
||||
val note: String? = null
|
||||
)
|
||||
typealias Bookmark = com.aryan.reader.shared.EpubBookmark
|
||||
typealias HighlightColor = com.aryan.reader.shared.HighlightColor
|
||||
typealias UserHighlight = com.aryan.reader.shared.UserHighlight
|
||||
|
||||
fun escapeJsString(value: String): String {
|
||||
return value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\u2028", "\\u2028")
|
||||
.replace("\u2029", "\\u2029")
|
||||
return com.aryan.reader.shared.escapeJsString(value)
|
||||
}
|
||||
|
||||
fun saveHighlightPalette(context: Context, palette: List<HighlightColor>) {
|
||||
|
|
@ -146,60 +104,21 @@ fun loadHighlightPalette(context: Context): List<HighlightColor> {
|
|||
|
||||
fun loadBookmarks(context: Context, bookTitle: String, chapters: List<EpubChapter>, bookmarksJson: String?): Set<Bookmark> {
|
||||
val stringSetToParse: Collection<String> = if (bookmarksJson != null) {
|
||||
try {
|
||||
val jsonArray = JSONArray(bookmarksJson)
|
||||
(0 until jsonArray.length()).map { jsonArray.getString(it) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to parse bookmarks from ViewModel")
|
||||
emptyList()
|
||||
}
|
||||
return EpubAnnotationSerializer.parseBookmarksJson(bookmarksJson, chapters.map { it.title })
|
||||
} else {
|
||||
val prefs = context.getSharedPreferences(BOOKMARK_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val key = "bookmarks_cfi_${bookTitle.replace("[^a-zA-Z0-9]".toRegex(), "")}"
|
||||
prefs.getStringSet(key, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
return stringSetToParse.mapNotNull { jsonString ->
|
||||
try {
|
||||
val json = JSONObject(jsonString)
|
||||
val chapterIndex = if (json.has("chapterIndex")) {
|
||||
json.getInt("chapterIndex")
|
||||
} else {
|
||||
val chapterTitle = json.getString("chapterTitle")
|
||||
chapters.indexOfFirst { it.title == chapterTitle }.coerceAtLeast(0)
|
||||
}
|
||||
Bookmark(
|
||||
cfi = json.getString("cfi"),
|
||||
chapterTitle = json.getString("chapterTitle"),
|
||||
label = if (json.has("label")) json.getString("label") else null,
|
||||
snippet = json.getString("snippet"),
|
||||
pageInChapter = if (json.has("pageInChapter")) json.optInt("pageInChapter") else null,
|
||||
totalPagesInChapter = if (json.has("totalPagesInChapter")) json.optInt("totalPagesInChapter") else null,
|
||||
chapterIndex = chapterIndex
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}.toSet()
|
||||
return EpubAnnotationSerializer.parseBookmarkEntries(stringSetToParse, chapters.map { it.title })
|
||||
}
|
||||
|
||||
fun saveHighlightsToPrefs(context: Context, bookTitle: String, highlights: List<UserHighlight>) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val sanitizedTitle = bookTitle.replace("[^a-zA-Z0-9]".toRegex(), "")
|
||||
val key = "highlights_data_$sanitizedTitle"
|
||||
val jsonArray = JSONArray()
|
||||
highlights.forEach { h ->
|
||||
val obj = JSONObject().apply {
|
||||
put("id", h.id)
|
||||
put("cfi", h.cfi)
|
||||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
prefs.edit { putString(key, jsonArray.toString()) }
|
||||
prefs.edit { putString(key, EpubAnnotationSerializer.highlightsToJson(highlights)) }
|
||||
}
|
||||
|
||||
fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighlight> {
|
||||
|
|
@ -207,72 +126,19 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighl
|
|||
val sanitizedTitle = bookTitle.replace("[^a-zA-Z0-9]".toRegex(), "")
|
||||
val key = "highlights_data_$sanitizedTitle"
|
||||
val jsonString = prefs.getString(key, "[]") ?: "[]"
|
||||
val list = mutableListOf<UserHighlight>()
|
||||
try {
|
||||
val jsonArray = JSONArray(jsonString)
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error loading highlights")
|
||||
}
|
||||
return list
|
||||
return EpubAnnotationSerializer.parseHighlightsJson(jsonString)
|
||||
}
|
||||
|
||||
fun parseHighlightsJson(jsonString: String?): List<UserHighlight> {
|
||||
if (jsonString.isNullOrBlank()) return emptyList()
|
||||
val list = mutableListOf<UserHighlight>()
|
||||
try {
|
||||
val jsonArray = JSONArray(jsonString)
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val colorId = obj.getString("colorId")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val noteStr = obj.optString("note", "")
|
||||
list.add(
|
||||
UserHighlight(
|
||||
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||
cfi = obj.getString("cfi"),
|
||||
text = obj.getString("text"),
|
||||
color = color,
|
||||
chapterIndex = obj.getInt("chapterIndex"),
|
||||
note = noteStr.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error parsing highlights JSON")
|
||||
}
|
||||
return list
|
||||
return EpubAnnotationSerializer.parseHighlightsJson(jsonString)
|
||||
}
|
||||
|
||||
fun highlightsToJson(highlights: List<UserHighlight>): String {
|
||||
val jsonArray = JSONArray()
|
||||
highlights.forEach { h ->
|
||||
val obj = JSONObject().apply {
|
||||
put("id", h.id)
|
||||
put("cfi", h.cfi)
|
||||
put("text", h.text)
|
||||
put("colorId", h.color.id)
|
||||
put("chapterIndex", h.chapterIndex)
|
||||
put("note", h.note ?: "")
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
}
|
||||
return jsonArray.toString()
|
||||
return EpubAnnotationSerializer.highlightsToJson(highlights)
|
||||
}
|
||||
|
||||
fun bookmarksToJson(bookmarks: Collection<Bookmark>): String {
|
||||
return EpubAnnotationSerializer.bookmarksToJson(bookmarks)
|
||||
}
|
||||
|
||||
fun clearHighlightsFromPrefs(context: Context, bookTitle: String) {
|
||||
|
|
@ -291,28 +157,13 @@ fun processAndAddHighlight(
|
|||
chapterIndex: Int,
|
||||
currentList: MutableList<UserHighlight>
|
||||
): String {
|
||||
// Scenario: Exact match -> Update color and text instead of stacking identical spans
|
||||
val exactMatchIndex = currentList.indexOfFirst {
|
||||
it.chapterIndex == chapterIndex && it.cfi == newCfi
|
||||
}
|
||||
|
||||
if (exactMatchIndex != -1) {
|
||||
val existing = currentList[exactMatchIndex]
|
||||
currentList[exactMatchIndex] = existing.copy(color = newColor, text = newText)
|
||||
return existing.cfi
|
||||
}
|
||||
|
||||
// Scenarios: Partial overlap or subsumption -> Add independently
|
||||
currentList.add(
|
||||
UserHighlight(
|
||||
cfi = newCfi,
|
||||
text = newText,
|
||||
color = newColor,
|
||||
chapterIndex = chapterIndex,
|
||||
note = null
|
||||
)
|
||||
return EpubAnnotationSerializer.processAndAddHighlight(
|
||||
newCfi = newCfi,
|
||||
newText = newText,
|
||||
newColor = newColor,
|
||||
chapterIndex = chapterIndex,
|
||||
currentList = currentList
|
||||
)
|
||||
return newCfi
|
||||
}
|
||||
|
||||
// --- UI Components ---
|
||||
|
|
|
|||
|
|
@ -167,7 +167,8 @@ enum class ReaderTool(val title: String, val category: String) {
|
|||
KEEP_SCREEN_ON("Keep Screen On", "Overflow Menu"),
|
||||
VISUAL_OPTIONS("Visual Options", "Overflow Menu"),
|
||||
AUTO_SCROLL("Auto Scroll", "Overflow Menu"),
|
||||
TTS_SETTINGS("TTS Voice Settings", "Overflow Menu")
|
||||
TTS_SETTINGS("TTS Voice Settings", "Overflow Menu"),
|
||||
TTS_REPLACEMENTS("TTS Word Replacements", "Overflow Menu")
|
||||
}
|
||||
|
||||
enum class FlatItemType { SECTION_HEADER, TOOL, EMPTY_PLACEHOLDER, MORE_HEADER, MORE_TOOL }
|
||||
|
|
@ -282,6 +283,7 @@ fun EpubReaderTopBar(
|
|||
onTogglePageTurnAnimation: (Boolean) -> Unit,
|
||||
onStartAutoScroll: () -> Unit,
|
||||
onOpenTtsSettings: () -> Unit,
|
||||
onOpenTtsReplacements: () -> Unit,
|
||||
onOpenDictionarySettings: () -> Unit,
|
||||
onOpenThemeSettings: () -> Unit,
|
||||
onOpenVisualOptions: () -> Unit,
|
||||
|
|
@ -678,6 +680,23 @@ fun EpubReaderTopBar(
|
|||
)
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (!hiddenTools.contains(ReaderTool.TTS_REPLACEMENTS.name)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_tts_word_replacements)) },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onOpenTtsReplacements()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.GraphicEq,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -791,7 +791,8 @@ private fun HighlightsList(
|
|||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (!highlight.note.isNullOrBlank()) {
|
||||
val note = highlight.note
|
||||
if (!note.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
|
|
@ -799,7 +800,7 @@ private fun HighlightsList(
|
|||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = highlight.note,
|
||||
text = note,
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic),
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -37,9 +37,11 @@ import com.aryan.reader.RenderMode
|
|||
import com.aryan.reader.epub.EpubChapter
|
||||
import com.aryan.reader.paginatedreader.BookPaginator
|
||||
import com.aryan.reader.paginatedreader.IPaginator
|
||||
import com.aryan.reader.shared.ReaderTtsReplacementPreferences
|
||||
import com.aryan.reader.tts.TtsController
|
||||
import com.aryan.reader.tts.TtsPlaybackManager
|
||||
import com.aryan.reader.tts.TtsPlaybackManager.TtsMode
|
||||
import com.aryan.reader.withTtsReplacements
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -115,7 +117,9 @@ fun TtsSessionObserver(
|
|||
currentTtsMode: TtsMode,
|
||||
getAuthToken: suspend () -> String?,
|
||||
locatorConverter: com.aryan.reader.paginatedreader.LocatorConverter, // NEW
|
||||
epubBook: com.aryan.reader.epub.EpubBook // NEW
|
||||
epubBook: com.aryan.reader.epub.EpubBook, // NEW
|
||||
ttsReplacementPreferences: ReaderTtsReplacementPreferences,
|
||||
ttsReplacementBookId: String?
|
||||
) {
|
||||
val currentRenderModeState = rememberUpdatedState(currentRenderMode)
|
||||
val loadedChunkCountState = rememberUpdatedState(loadedChunkCount)
|
||||
|
|
@ -131,6 +135,8 @@ fun TtsSessionObserver(
|
|||
val onTtsChapterIndexChangeState = rememberUpdatedState(onTtsChapterIndexChange)
|
||||
val locatorConverterState = rememberUpdatedState(locatorConverter) // NEW
|
||||
val epubBookState = rememberUpdatedState(epubBook) // NEW
|
||||
val ttsReplacementPreferencesState = rememberUpdatedState(ttsReplacementPreferences)
|
||||
val ttsReplacementBookIdState = rememberUpdatedState(ttsReplacementBookId)
|
||||
|
||||
DisposableEffect(ttsController) {
|
||||
val job = scope.launch {
|
||||
|
|
@ -168,7 +174,9 @@ fun TtsSessionObserver(
|
|||
ttsController = ttsController,
|
||||
scope = this,
|
||||
locatorConverter = locatorConverterState.value,
|
||||
epubBook = epubBookState.value
|
||||
epubBook = epubBookState.value,
|
||||
ttsReplacementPreferences = ttsReplacementPreferencesState.value,
|
||||
ttsReplacementBookId = ttsReplacementBookIdState.value
|
||||
)
|
||||
} else if (currentRenderModeState.value == RenderMode.PAGINATED) {
|
||||
handlePaginatedAutoAdvance(
|
||||
|
|
@ -182,7 +190,9 @@ fun TtsSessionObserver(
|
|||
onUpdateTtsChapter = onTtsChapterIndexChangeState.value,
|
||||
scope = this,
|
||||
ttsMode = currentTtsMode,
|
||||
getAuthToken = getAuthToken
|
||||
getAuthToken = getAuthToken,
|
||||
ttsReplacementPreferences = ttsReplacementPreferencesState.value,
|
||||
ttsReplacementBookId = ttsReplacementBookIdState.value
|
||||
)
|
||||
}
|
||||
} else if (wasPlaying && !isPlaying && !sessionFinished) {
|
||||
|
|
@ -303,7 +313,9 @@ private fun handleVerticalAutoAdvance(
|
|||
ttsController: TtsController,
|
||||
scope: CoroutineScope,
|
||||
locatorConverter: com.aryan.reader.paginatedreader.LocatorConverter,
|
||||
epubBook: com.aryan.reader.epub.EpubBook
|
||||
epubBook: com.aryan.reader.epub.EpubBook,
|
||||
ttsReplacementPreferences: ReaderTtsReplacementPreferences,
|
||||
ttsReplacementBookId: String?
|
||||
) {
|
||||
if (currentTtsChapterIndex == null) return
|
||||
|
||||
|
|
@ -323,7 +335,7 @@ private fun handleVerticalAutoAdvance(
|
|||
val remainingChunks = nativeChunks.subList(resumeIdx + 1, nativeChunks.size)
|
||||
val token = getAuthToken()
|
||||
ttsController.start(
|
||||
chunks = remainingChunks,
|
||||
chunks = remainingChunks.withTtsReplacements(ttsReplacementPreferences, ttsReplacementBookId),
|
||||
bookTitle = epubBookTitle,
|
||||
chapterTitle = chapters.getOrNull(currentTtsChapterIndex)?.title,
|
||||
coverImageUri = coverImagePath?.let { android.net.Uri.fromFile(File(it)).toString() },
|
||||
|
|
@ -355,7 +367,7 @@ private fun handleVerticalAutoAdvance(
|
|||
onUpdateTtsChapter(nextIdx)
|
||||
|
||||
ttsController.start(
|
||||
chunks = nativeChunks,
|
||||
chunks = nativeChunks.withTtsReplacements(ttsReplacementPreferences, ttsReplacementBookId),
|
||||
bookTitle = epubBookTitle,
|
||||
chapterTitle = chapters.getOrNull(nextIdx)?.title,
|
||||
coverImageUri = coverImagePath?.let { Uri.fromFile(File(it)).toString() },
|
||||
|
|
@ -399,7 +411,9 @@ private fun handlePaginatedAutoAdvance(
|
|||
onUpdateTtsChapter: (Int?) -> Unit,
|
||||
scope: CoroutineScope,
|
||||
ttsMode: TtsMode,
|
||||
getAuthToken: suspend () -> String?
|
||||
getAuthToken: suspend () -> String?,
|
||||
ttsReplacementPreferences: ReaderTtsReplacementPreferences,
|
||||
ttsReplacementBookId: String?
|
||||
) {
|
||||
if (currentTtsChapterIndex != null && currentTtsChapterIndex < chapters.size - 1) {
|
||||
Timber.tag("TTS_CHAPTER_CHANGE_DIAG").d("Paginated: Searching for next TTS content...")
|
||||
|
|
@ -432,7 +446,7 @@ private fun handlePaginatedAutoAdvance(
|
|||
val token = getAuthToken()
|
||||
|
||||
ttsController.start(
|
||||
chunks = nextChapterChunks,
|
||||
chunks = nextChapterChunks.withTtsReplacements(ttsReplacementPreferences, ttsReplacementBookId),
|
||||
bookTitle = epubBookTitle,
|
||||
chapterTitle = chapterTitle,
|
||||
coverImageUri = coverUriString,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue