String extract (#148)

* FolderSyncWorker: update supported file types in getFileType

* Extract hardcoded strings to `strings.xml` and `plurals.xml`

* Extract hardcoded strings to `strings.xml` and `plurals.xml`

* Extract hardcoded strings to `strings.xml` and `plurals.xml` in MainViewModel

* Extract hardcoded strings to `strings.xml` and `plurals.xml` in Common.kt

* Extract hardcoded strings to `strings.xml` and `plurals.xml` in Epub Reader
This commit is contained in:
Aryan 2026-04-05 09:32:59 +05:30 committed by GitHub
parent da1c5bce1e
commit 381193d774
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1388 additions and 855 deletions

View file

@ -1,5 +1,5 @@
// Common.kt // Common.kt
@file:kotlin.OptIn(ExperimentalMaterial3Api::class) @file:OptIn(ExperimentalMaterial3Api::class)
package com.aryan.reader package com.aryan.reader
@ -389,14 +389,14 @@ fun SearchTopBar(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Close Search" contentDescription = stringResource(R.string.content_desc_close_search)
) )
} }
TextField( TextField(
value = searchState.searchQuery, value = searchState.searchQuery,
onValueChange = { searchState.onQueryChange(it) }, onValueChange = { searchState.onQueryChange(it) },
placeholder = { Text("Search in book...") }, placeholder = { Text(stringResource(R.string.search_in_book)) },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.focusRequester(focusRequester) .focusRequester(focusRequester)
@ -425,7 +425,7 @@ fun SearchTopBar(
) { ) {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,
contentDescription = "Clear Search" contentDescription = stringResource(R.string.content_desc_clear_search)
) )
} }
} }
@ -446,7 +446,10 @@ fun SearchTopBar(
) { ) {
Icon( Icon(
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results" contentDescription = stringResource(
if (searchState.showSearchResultsPanel) R.string.content_desc_hide_results
else R.string.content_desc_show_results
)
) )
} }
} }
@ -474,7 +477,7 @@ fun SearchNavigationControls(
onClick = { onNavigate(searchState.currentSearchResultIndex - 1) }, onClick = { onNavigate(searchState.currentSearchResultIndex - 1) },
enabled = searchState.currentSearchResultIndex > 0 enabled = searchState.currentSearchResultIndex > 0
) { ) {
Icon(Icons.Default.ArrowDropUp, contentDescription = "Previous Search Result") Icon(Icons.Default.ArrowDropUp, contentDescription = stringResource(R.string.content_desc_prev_result))
} }
Text( Text(
@ -489,7 +492,7 @@ fun SearchNavigationControls(
onClick = { onNavigate(searchState.currentSearchResultIndex + 1) }, onClick = { onNavigate(searchState.currentSearchResultIndex + 1) },
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1 enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1
) { ) {
Icon(Icons.Default.ArrowDropDown, contentDescription = "Next Search Result") Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.content_desc_next_result))
} }
} }
} }
@ -549,7 +552,7 @@ fun SummarizationPopup(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
Text("Generating summary...", modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.generating_summary), modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge)
} }
} else if (result != null) { } else if (result != null) {
val summaryText = result.summary val summaryText = result.summary
@ -596,7 +599,7 @@ fun SummarizationPopup(
) { ) {
Icon( Icon(
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow, imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = if (isTtsSessionActive) "Stop" else "Read aloud" contentDescription = stringResource(if (isTtsSessionActive) R.string.action_stop else R.string.action_read_aloud)
) )
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@ -605,7 +608,7 @@ fun SummarizationPopup(
}) { }) {
Icon( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy" contentDescription = stringResource(R.string.action_copy)
) )
} }
} }
@ -656,7 +659,7 @@ fun SummarizationPopup(
onTextLayout = { textLayoutResult = it } onTextLayout = { textLayoutResult = it }
) )
} else { } else {
Text("No summary could be generated.", style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.no_summary_generated), style = MaterialTheme.typography.bodyLarge)
} }
} }
} }
@ -711,7 +714,7 @@ fun AiDefinitionPopup(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
Text("Thinking...", modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.ai_thinking), modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge)
} }
} else if (result != null) { } else if (result != null) {
word?.let { word?.let {
@ -771,7 +774,7 @@ fun AiDefinitionPopup(
) { ) {
Icon( Icon(
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow, imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = if (isTtsSessionActive) "Stop" else "Read aloud" contentDescription = stringResource(if (isTtsSessionActive) R.string.action_stop else R.string.action_read_aloud)
) )
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@ -780,14 +783,14 @@ fun AiDefinitionPopup(
}) { }) {
Icon( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy" contentDescription = stringResource(R.string.action_copy)
) )
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = onOpenExternalDictionary) { IconButton(onClick = onOpenExternalDictionary) {
Icon( Icon(
painter = painterResource(id = R.drawable.dictionary), painter = painterResource(id = R.drawable.dictionary),
contentDescription = "Open in Dictionary App" contentDescription = stringResource(R.string.content_desc_open_dictionary)
) )
} }
} }
@ -839,11 +842,11 @@ fun AiDefinitionPopup(
onTextLayout = { textLayoutResult = it } onTextLayout = { textLayoutResult = it }
) )
} else { } else {
Text("AI could not provide a definition.", style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.ai_no_definition), style = MaterialTheme.typography.bodyLarge)
} }
} else if (word != null) { } else if (word != null) {
Text( Text(
text = "Asking AI about '$word'...", text = stringResource(R.string.ai_asking_about, word),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(vertical = 24.dp), modifier = Modifier.padding(vertical = 24.dp),
maxLines = 1, maxLines = 1,
@ -874,13 +877,13 @@ fun SearchResultsPanel(
} }
results.isEmpty() -> { results.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No results found.", style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.search_no_results_simple), style = MaterialTheme.typography.bodyLarge)
} }
} }
else -> { else -> {
Column { Column {
Text( Text(
text = "${results.size} " + if (results.size == 1) "result found" else "results found", text = LocalContext.current.resources.getQuantityString(R.plurals.search_results_count, results.size, results.size),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp, vertical = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
@ -907,12 +910,13 @@ fun SearchResultsPanel(
suspend fun fetchAiDefinition( suspend fun fetchAiDefinition(
text: String, text: String,
context: Context,
onUpdate: (String) -> Unit, onUpdate: (String) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
if (text.isBlank()) { if (text.isBlank()) {
onError("Text is empty.") onError(context.getString(R.string.error_text_empty))
onFinish() onFinish()
return return
} }
@ -961,16 +965,16 @@ suspend fun fetchAiDefinition(
} }
Timber.d("Definition: Finished reading stream.") Timber.d("Definition: Finished reading stream.")
if (!hasReceivedData) { if (!hasReceivedData) {
onError("AI returned an empty definition.") onError(context.getString(R.string.error_ai_empty_definition))
} }
} else { } else {
val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null } val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null }
val errorDetail = try { errorBody?.let { JSONObject(it).getString("detail") } } catch (_: Exception) { "Could not get definition." } val errorDetail = try { errorBody?.let { JSONObject(it).getString("detail") } } catch (_: Exception) { "Could not get definition." }
onError("Error: $responseCode. ${errorDetail ?: "An unknown server error occurred."}") onError("${responseCode}. ${errorDetail ?: context.getString(R.string.error_unknown_server)}")
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Network error fetching AI definition: ${e.message}") Timber.e(e, "Network error fetching AI definition: ${e.message}")
onError("Network error. Check connection.") onError(context.getString(R.string.error_network_check_connection))
} finally { } finally {
connection?.disconnect() connection?.disconnect()
onFinish() onFinish()
@ -1091,12 +1095,13 @@ class SummaryCacheManager(context: Context) {
suspend fun fetchRecap( suspend fun fetchRecap(
pastSummaries: List<String>, pastSummaries: List<String>,
currentText: String, currentText: String,
context: Context,
onUpdate: (String) -> Unit, onUpdate: (String) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
if (pastSummaries.isEmpty() && currentText.isBlank()) { if (pastSummaries.isEmpty() && currentText.isBlank()) {
onError("Not enough context for a recap.") onError(context.getString(R.string.error_not_enough_context))
onFinish() onFinish()
return return
} }
@ -1143,14 +1148,14 @@ suspend fun fetchRecap(
} }
} }
} }
if (!hasReceivedData) onError("Failed to parse recap.") if (!hasReceivedData) onError(context.getString(R.string.error_parse_recap))
} else { } else {
val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null } val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null }
onError("Error: $responseCode. ${errorBody ?: ""}") onError("${responseCode}. ${errorBody ?: ""}")
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Recap error: ${e.message}") Timber.e(e, "Recap error: ${e.message}")
onError("Network error during recap generation.") onError(context.getString(R.string.error_network_recap))
} finally { } finally {
connection?.disconnect() connection?.disconnect()
onFinish() onFinish()
@ -1194,7 +1199,7 @@ fun TtsSettingsSheet(
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
Text( Text(
text = "Text-to-Speech Settings", text = stringResource(R.string.tts_settings),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
@ -1213,7 +1218,7 @@ fun TtsSettingsSheet(
Icon(Icons.Default.Stop, contentDescription = null, tint = MaterialTheme.colorScheme.onErrorContainer) Icon(Icons.Default.Stop, contentDescription = null, tint = MaterialTheme.colorScheme.onErrorContainer)
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Text( Text(
"Please stop playback to change settings.", stringResource(R.string.tts_stop_to_change_settings),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer color = MaterialTheme.colorScheme.onErrorContainer
) )
@ -1222,7 +1227,7 @@ fun TtsSettingsSheet(
} }
Text( Text(
text = "Synthesis Mode", text = stringResource(R.string.tts_synthesis_mode),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -1239,7 +1244,7 @@ fun TtsSettingsSheet(
) { ) {
TtsPlaybackManager.TtsMode.entries.forEach { mode -> TtsPlaybackManager.TtsMode.entries.forEach { mode ->
val isSelected = currentMode == mode val isSelected = currentMode == mode
val label = if (mode == TtsPlaybackManager.TtsMode.BASE) "On-Device" else "Cloud (HQ)" val label = if (mode == TtsPlaybackManager.TtsMode.BASE) stringResource(R.string.tts_mode_on_device) else stringResource(R.string.tts_mode_cloud_hq)
val icon = if (mode == TtsPlaybackManager.TtsMode.BASE) Icons.Default.Smartphone else Icons.Default.Cloud val icon = if (mode == TtsPlaybackManager.TtsMode.BASE) Icons.Default.Smartphone else Icons.Default.Cloud
Surface( Surface(
@ -1271,7 +1276,7 @@ fun TtsSettingsSheet(
if (currentMode == TtsPlaybackManager.TtsMode.CLOUD) { if (currentMode == TtsPlaybackManager.TtsMode.CLOUD) {
Text( Text(
text = "Voice Selection", text = stringResource(R.string.tts_voice_selection),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -1306,7 +1311,7 @@ fun TtsSettingsSheet(
} else { } else {
Icon( Icon(
imageVector = if (isPlaying) Icons.Default.Stop else Icons.Default.PlayArrow, imageVector = if (isPlaying) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = "Play Sample", contentDescription = stringResource(R.string.tts_play_sample),
tint = if (isPlaying) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = if (isPlaying) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -1503,13 +1508,13 @@ fun DeviceVoiceSettingsSheet(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "On-Device Voice Settings", text = stringResource(R.string.tts_device_voice_settings),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
IconButton(onClick = onDismiss) { IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Close Settings") Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_close_settings))
} }
} }
@ -1534,19 +1539,19 @@ fun DeviceVoiceSettingsSheet(
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = "System Default", text = stringResource(R.string.tts_system_default),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = "Matches your Android system settings", text = stringResource(R.string.tts_system_default_desc),
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
if (savedVoiceName == null) { if (savedVoiceName == null) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
@ -1562,7 +1567,7 @@ fun DeviceVoiceSettingsSheet(
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text("Loading voices...", modifier = Modifier.padding(top = 48.dp)) Text(stringResource(R.string.tts_loading_voices), modifier = Modifier.padding(top = 48.dp))
} }
} else if (allVoices.isEmpty()) { } else if (allVoices.isEmpty()) {
Box( Box(
@ -1570,7 +1575,7 @@ fun DeviceVoiceSettingsSheet(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
"No voices available on this device.", stringResource(R.string.tts_no_voices),
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
} }
@ -1583,7 +1588,7 @@ fun DeviceVoiceSettingsSheet(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Specific Voices", text = stringResource(R.string.tts_specific_voices),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
@ -1597,7 +1602,7 @@ fun DeviceVoiceSettingsSheet(
.clickable { expandedLanguageMenu = true }, .clickable { expandedLanguageMenu = true },
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
border = androidx.compose.foundation.BorderStroke( border = BorderStroke(
1.dp, MaterialTheme.colorScheme.outlineVariant 1.dp, MaterialTheme.colorScheme.outlineVariant
) )
) { ) {
@ -1644,7 +1649,7 @@ fun DeviceVoiceSettingsSheet(
if (filteredVoices.isNotEmpty()) { if (filteredVoices.isNotEmpty()) {
Text( Text(
text = "Available Voices (${filteredVoices.size})", text = stringResource(R.string.tts_available_voices_count, filteredVoices.size),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp, start = 4.dp) modifier = Modifier.padding(bottom = 8.dp, start = 4.dp)
@ -1676,13 +1681,13 @@ fun DeviceVoiceSettingsSheet(
) )
}, },
supportingContent = if (voice.locale.variant.isNotEmpty()) { supportingContent = if (voice.locale.variant.isNotEmpty()) {
{ Text("Variant: ${voice.locale.variant}") } { Text(stringResource(R.string.tts_voice_variant, voice.locale.variant)) }
} else null, } else null,
leadingContent = { leadingContent = {
if (isSelected) { if (isSelected) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} else { } else {
@ -1698,8 +1703,7 @@ fun DeviceVoiceSettingsSheet(
Timber.e(e, "Failed to set language for sample") Timber.e(e, "Failed to set language for sample")
} }
ttsEngine?.voice = voice ttsEngine?.voice = voice
val sampleText = val sampleText = context.getString(R.string.tts_voice_sample_text, voice.locale.displayLanguage)
"This is a sample of ${voice.locale.displayLanguage}."
ttsEngine?.speak( ttsEngine?.speak(
sampleText, sampleText,
TextToSpeech.QUEUE_FLUSH, TextToSpeech.QUEUE_FLUSH,
@ -1709,7 +1713,7 @@ fun DeviceVoiceSettingsSheet(
}) { }) {
Icon( Icon(
imageVector = Icons.Default.PlayArrow, imageVector = Icons.Default.PlayArrow,
contentDescription = "Play Sample", contentDescription = stringResource(R.string.tts_play_sample),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
@ -1736,7 +1740,7 @@ fun DeviceVoiceSettingsSheet(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
"No voices found for this language.", stringResource(R.string.tts_no_voices_for_language),
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -2164,14 +2168,13 @@ fun ReaderThemePanel(
.padding(16.dp) .padding(16.dp)
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
Text( Text(stringResource(R.string.reading_themes),
"Reading Themes",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
Text("Presets", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.theme_presets), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
ThemeGrid(themes = builtInThemes, currentThemeId = currentThemeId, onThemeSelected = onThemeSelected) ThemeGrid(themes = builtInThemes, currentThemeId = currentThemeId, onThemeSelected = onThemeSelected)
@ -2181,7 +2184,7 @@ fun ReaderThemePanel(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("My Themes", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.theme_my_themes), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
IconButton(onClick = { editingTheme = null; showBuilder = true }, modifier = Modifier.size(24.dp)) { IconButton(onClick = { editingTheme = null; showBuilder = true }, modifier = Modifier.size(24.dp)) {
Icon(Icons.Default.Add, contentDescription = "Create Theme", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Add, contentDescription = "Create Theme", tint = MaterialTheme.colorScheme.primary)
} }
@ -2189,7 +2192,7 @@ fun ReaderThemePanel(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
if (customThemes.isEmpty()) { if (customThemes.isEmpty()) {
Text("No custom themes yet. Tap '+' to create one.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.theme_no_custom), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} else { } else {
ThemeGrid( ThemeGrid(
themes = customThemes, themes = customThemes,
@ -2290,7 +2293,7 @@ fun ThemeBuilderView(
.padding(16.dp) .padding(16.dp)
) { ) {
Text( Text(
text = if (initialTheme == null) "New Theme" else "Edit Theme", text = stringResource(if (initialTheme == null) R.string.theme_new else R.string.theme_edit),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
@ -2302,7 +2305,7 @@ fun ThemeBuilderView(
androidx.compose.material3.OutlinedTextField( androidx.compose.material3.OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("Theme Name") }, label = { Text(stringResource(R.string.theme_name)) },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
singleLine = true singleLine = true
) )
@ -2325,14 +2328,12 @@ fun ThemeBuilderView(
} else this } else this
}) { }) {
Column(Modifier.padding(16.dp).fillMaxWidth()) { Column(Modifier.padding(16.dp).fillMaxWidth()) {
Text( Text(text = stringResource(R.string.theme_preview_quote),
text = "So many books, so little time.",
color = txtColor, color = txtColor,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(text = stringResource(R.string.theme_preview_author),
text = "- Frank Zappa",
color = txtColor, color = txtColor,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -2344,8 +2345,7 @@ fun ThemeBuilderView(
// Animated Contrast Warning // Animated Contrast Warning
AnimatedVisibility(visible = contrast < 4.5f) { AnimatedVisibility(visible = contrast < 4.5f) {
Text( Text(stringResource(R.string.theme_low_contrast_warning),
"⚠️ Low contrast! This might cause eye strain.",
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -2357,13 +2357,13 @@ fun ThemeBuilderView(
// Sleek Color Swatches // Sleek Color Swatches
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
ColorSwatchItem( ColorSwatchItem(
label = "Page Color", label = stringResource(R.string.theme_page_color),
color = bgColor, color = bgColor,
onClick = { editingColorType = "bg" }, onClick = { editingColorType = "bg" },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
ColorSwatchItem( ColorSwatchItem(
label = "Text Color", label = stringResource(R.string.theme_text_color),
color = txtColor, color = txtColor,
onClick = { editingColorType = "text" }, onClick = { editingColorType = "text" },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -2379,13 +2379,13 @@ fun ThemeBuilderView(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextButton(onClick = onCancel) { TextButton(onClick = onCancel) {
Text("Cancel", color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.action_cancel), color = MaterialTheme.colorScheme.primary)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Button(onClick = { Button(onClick = {
onSave(ReaderTheme(id = initialTheme?.id ?: System.currentTimeMillis().toString(), name = name, backgroundColor = bgColor, textColor = txtColor, isDark = isDark, textureId = textureId, isCustom = true)) onSave(ReaderTheme(id = initialTheme?.id ?: System.currentTimeMillis().toString(), name = name, backgroundColor = bgColor, textColor = txtColor, isDark = isDark, textureId = textureId, isCustom = true))
}) { }) {
Text("Save", color = MaterialTheme.colorScheme.onPrimary) Text(stringResource(R.string.action_save), color = MaterialTheme.colorScheme.onPrimary)
} }
} }
} }
@ -2393,7 +2393,7 @@ fun ThemeBuilderView(
editingColorType?.let { type -> editingColorType?.let { type ->
ThemeColorPickerDialog( ThemeColorPickerDialog(
initialColor = if (type == "bg") bgColor else txtColor, initialColor = if (type == "bg") bgColor else txtColor,
title = if (type == "bg") "Page Color" else "Text Color", title = if (type == "bg") stringResource(R.string.theme_page_color) else stringResource(R.string.theme_text_color),
bgColor = bgColor, bgColor = bgColor,
textColor = txtColor, textColor = txtColor,
editingColorType = type, editingColorType = type,
@ -2502,14 +2502,12 @@ fun ThemeColorPickerDialog(
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
) { ) {
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text( Text(text = stringResource(R.string.theme_color_live_preview),
text = "Live Preview",
color = liveTextColor, color = liveTextColor,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(text = stringResource(R.string.theme_color_preview_text),
text = "Reading is dreaming.",
color = liveTextColor, color = liveTextColor,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
@ -2553,7 +2551,7 @@ fun ThemeColorPickerDialog(
modifier = Modifier.weight(1.6f), modifier = Modifier.weight(1.6f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("Hex", color = Color.Gray, fontSize = 12.sp, maxLines = 1) Text(stringResource(R.string.theme_color_hex), color = Color.Gray, fontSize = 12.sp, maxLines = 1)
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) }) HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
} }
@ -2562,18 +2560,15 @@ fun ThemeColorPickerDialog(
modifier = Modifier.weight(2.4f), modifier = Modifier.weight(2.4f),
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
RgbInputColumn( RgbInputColumn(label = stringResource(R.string.color_r), value = currentColor.red,
label = "R", value = currentColor.red,
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) }, onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
RgbInputColumn( RgbInputColumn(label = stringResource(R.string.color_g), value = currentColor.green,
label = "G", value = currentColor.green,
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) }, onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
RgbInputColumn( RgbInputColumn(label = stringResource(R.string.color_b), value = currentColor.blue,
label = "B", value = currentColor.blue,
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) }, onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@ -2593,7 +2588,7 @@ fun ThemeColorPickerDialog(
containerColor = Color.White containerColor = Color.White
) )
) { ) {
Text("Save", color = Color.Black, fontWeight = FontWeight.Bold) Text(stringResource(R.string.action_save), color = Color.Black, fontWeight = FontWeight.Bold)
} }
} }
} }

View file

@ -315,14 +315,19 @@ class FolderSyncWorker(
} }
private fun getFileType(name: String, mimeType: String?): FileType? { private fun getFileType(name: String, mimeType: String?): FileType? {
val lowerName = name.lowercase()
return when { return when {
mimeType == "application/pdf" || name.endsWith(".pdf", true) -> FileType.PDF mimeType == "application/pdf" || lowerName.endsWith(".pdf") -> FileType.PDF
mimeType == "application/epub+zip" || name.endsWith(".epub", true) -> FileType.EPUB mimeType == "application/epub+zip" || lowerName.endsWith(".epub") -> FileType.EPUB
mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || name.endsWith(".docx", true) -> FileType.DOCX mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || lowerName.endsWith(".docx") -> FileType.DOCX
name.endsWith(".mobi", true) || name.endsWith(".azw3", true) -> FileType.MOBI lowerName.endsWith(".mobi") || lowerName.endsWith(".azw3") || lowerName.endsWith(".prc") -> FileType.MOBI
name.endsWith(".md", true) -> FileType.MD lowerName.endsWith(".fb2") || lowerName.endsWith(".fb2.zip") -> FileType.FB2
name.endsWith(".txt", true) -> FileType.TXT lowerName.endsWith(".cbz") -> FileType.CBZ
name.endsWith(".html", true) || name.endsWith(".xhtml", true) || name.endsWith(".htm", true) -> FileType.HTML lowerName.endsWith(".cbr") -> FileType.CBR
lowerName.endsWith(".cb7") -> FileType.CB7
lowerName.endsWith(".md") || lowerName.endsWith(".markdown") -> FileType.MD
lowerName.endsWith(".txt") -> FileType.TXT
mimeType == "text/html" || lowerName.endsWith(".html") || lowerName.endsWith(".xhtml") || lowerName.endsWith(".htm") -> FileType.HTML
else -> null else -> null
} }
} }

View file

@ -59,6 +59,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -97,10 +98,10 @@ fun FontsScreen(
) )
Scaffold( Scaffold(
modifier = Modifier.statusBarsPadding(), // Fixes content flowing under status bar modifier = Modifier.statusBarsPadding(),
topBar = { topBar = {
CustomTopAppBar( CustomTopAppBar(
title = { Text("Custom Fonts") }, title = { Text(stringResource(R.string.custom_fonts)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
@ -109,12 +110,11 @@ fun FontsScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
// Hide FAB when empty state is visible (list is empty)
if (fonts.isNotEmpty()) { if (fonts.isNotEmpty()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { pickFontLauncher.launch(fontMimeTypes) }, onClick = { pickFontLauncher.launch(fontMimeTypes) },
icon = { Icon(Icons.Default.Add, contentDescription = null) }, icon = { Icon(Icons.Default.Add, contentDescription = null) },
text = { Text("Import Font") } text = { Text(stringResource(R.string.import_font)) }
) )
} }
} }
@ -122,15 +122,14 @@ fun FontsScreen(
Box(modifier = Modifier.fillMaxSize().padding(padding)) { Box(modifier = Modifier.fillMaxSize().padding(padding)) {
if (fonts.isEmpty()) { if (fonts.isEmpty()) {
EmptyState( EmptyState(
title = "No Custom Fonts", title = stringResource(R.string.no_custom_fonts),
message = "Import TTF or OTF files to use them in your books.", message = stringResource(R.string.import_fonts_desc),
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) }, onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
// Padding bottom 88.dp allows scrolling past the FloatingActionButton
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 88.dp), contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
@ -223,14 +222,14 @@ fun FontListItem(
) { ) {
if (customTypeface != null) { if (customTypeface != null) {
Text( Text(
text = "Grumpy wizards make toxic brew for the evil queen! 1234567890 ?.,;:", text = stringResource(R.string.font_preview_text),
fontFamily = customTypeface, fontFamily = customTypeface,
fontSize = 18.sp, fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
} else { } else {
Text( Text(
text = "Preview unavailable (Invalid font file)", text = stringResource(R.string.font_preview_error),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
@ -255,18 +254,18 @@ fun DeleteFontConfirmationDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Delete Font?") }, title = { Text(stringResource(R.string.dialog_delete_font)) },
text = { Text("Are you sure you want to delete '$fontName'? This will remove it from all your devices if sync is on.") }, text = { Text(stringResource(R.string.dialog_delete_font_desc, fontName)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = onConfirm, onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { ) {
Text("Delete") Text(stringResource(R.string.action_delete))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }

View file

@ -114,6 +114,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -247,7 +248,7 @@ fun HomeScreen(
try { try {
fallbackFilePickerLauncher.launch("*/*") fallbackFilePickerLauncher.launch("*/*")
} catch (_: android.content.ActivityNotFoundException) { } catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true) viewModel.showBanner(context.getString(R.string.error_no_file_manager), isError = true)
} }
} }
} }
@ -336,17 +337,17 @@ fun HomeScreen(
if (recentFilesForHome.isEmpty()) { if (recentFilesForHome.isEmpty()) {
if (uiState.recentFiles.isEmpty()) { if (uiState.recentFiles.isEmpty()) {
EmptyState( EmptyState(
title = "Your Library is Empty", title = stringResource(R.string.your_library_empty),
message = "Select a file to read, or sync a local folder to automatically import books.", message = stringResource(R.string.your_library_empty_desc),
onSelectFileClick = onSelectFileClick, onSelectFileClick = onSelectFileClick,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
secondaryButtonText = "Setup Folder Sync", secondaryButtonText = stringResource(R.string.setup_folder_sync),
onSecondaryClick = { viewModel.navigateToFolderSync() } onSecondaryClick = { viewModel.navigateToFolderSync() }
) )
} else { } else {
EmptyState( EmptyState(
title = "No Recent Files", title = stringResource(R.string.no_recent_files),
message = "Open a file from your library to see it here.", message = stringResource(R.string.no_recent_files_desc),
onSelectFileClick = onSelectFileClick, onSelectFileClick = onSelectFileClick,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@ -409,8 +410,8 @@ fun HomeScreen(
if (showClearBookCacheDialog) { if (showClearBookCacheDialog) {
DangerousFolderActionDialog( DangerousFolderActionDialog(
title = "Clear Book Cache", title = stringResource(R.string.dialog_clear_book_cache),
message = "This will clear all processed page in pagination mode. This helps fix layout issues but will require books to be re-processed next time you open them.", message = stringResource(R.string.dialog_clear_book_cache_desc),
onConfirm = { onConfirm = {
viewModel.clearBookCache() viewModel.clearBookCache()
showClearBookCacheDialog = false showClearBookCacheDialog = false
@ -436,8 +437,8 @@ fun HomeScreen(
if (showClearReflowCacheDialog) { if (showClearReflowCacheDialog) {
DangerousFolderActionDialog( DangerousFolderActionDialog(
title = "Clear Reflow Cache", title = stringResource(R.string.dialog_clear_reflow_cache),
message = "This will delete all generated 'Text View' versions of your PDFs and clear their associated images/HTML cache. Your original PDFs will remain untouched.", message = stringResource(R.string.dialog_clear_reflow_cache_desc),
onConfirm = { onConfirm = {
viewModel.clearReflowCache() viewModel.clearReflowCache()
showClearReflowCacheDialog = false showClearReflowCacheDialog = false
@ -528,10 +529,10 @@ private fun RecentFilesContent(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
androidx.compose.material3.Button(onClick = onSelectFileClick) { androidx.compose.material3.Button(onClick = onSelectFileClick) {
Text("Select File") Text(stringResource(R.string.empty_select_file))
} }
androidx.compose.material3.Button(onClick = onNavigateToFolderSync) { androidx.compose.material3.Button(onClick = onNavigateToFolderSync) {
Text("Sync Folder") Text(stringResource(R.string.sync_folder))
} }
} }
} }
@ -570,7 +571,7 @@ private fun RecentFilesGrid(
Column(modifier = modifier) { Column(modifier = modifier) {
Text( Text(
text = "Recent Files", text = stringResource(R.string.recent_files),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp, top = 24.dp) modifier = Modifier.padding(bottom = 8.dp, top = 24.dp)
) )
@ -650,7 +651,7 @@ fun RecentFileCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Folder, imageVector = Icons.Default.Folder,
contentDescription = "Local Folder", contentDescription = stringResource(R.string.local_folder),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer tint = MaterialTheme.colorScheme.onSecondaryContainer
) )
@ -671,7 +672,7 @@ fun RecentFileCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Cloud, imageVector = Icons.Default.Cloud,
contentDescription = "OPDS Stream", contentDescription = stringResource(R.string.opds_stream),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer tint = MaterialTheme.colorScheme.onTertiaryContainer
) )
@ -691,7 +692,7 @@ fun RecentFileCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.PushPin, imageVector = Icons.Default.PushPin,
contentDescription = "Pinned", contentDescription = stringResource(R.string.pinned),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer tint = MaterialTheme.colorScheme.onPrimaryContainer
) )
@ -710,7 +711,7 @@ fun RecentFileCard(
} else { } else {
Icon( Icon(
imageVector = Icons.Filled.Info, imageVector = Icons.Filled.Info,
contentDescription = "Not available locally", contentDescription = stringResource(R.string.not_available_locally),
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
tint = Color.White tint = Color.White
) )
@ -745,7 +746,7 @@ fun RecentFileCard(
) { ) {
item.progressPercentage?.let { progress -> item.progressPercentage?.let { progress ->
Text( Text(
text = "${progress.toInt()}% complete", text = stringResource(R.string.progress_complete, progress.toInt()),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@ -788,7 +789,7 @@ fun DefaultTopAppBar(
// Recent Files Limit Menu // Recent Files Limit Menu
Box { Box {
IconButton(onClick = { showLimitMenu = true }) { IconButton(onClick = { showLimitMenu = true }) {
Icon(Icons.Default.FormatListNumbered, contentDescription = "Recent Files Limit") Icon(Icons.Default.FormatListNumbered, contentDescription = stringResource(R.string.options_recent_limit))
} }
DropdownMenu( DropdownMenu(
expanded = showLimitMenu, onDismissRequest = { showLimitMenu = false } expanded = showLimitMenu, onDismissRequest = { showLimitMenu = false }
@ -796,7 +797,7 @@ fun DefaultTopAppBar(
val limitOptions = listOf(0, 10, 20, 50, 100) val limitOptions = listOf(0, 10, 20, 50, 100)
limitOptions.forEach { limit -> limitOptions.forEach { limit ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(if (limit == 0) "No limit" else "$limit files") }, text = { Text(if (limit == 0) stringResource(R.string.options_no_limit) else stringResource(R.string.options_files_limit, limit)) },
onClick = { onClick = {
onRecentFilesLimitChange(limit) onRecentFilesLimitChange(limit)
showLimitMenu = false showLimitMenu = false
@ -816,29 +817,28 @@ fun DefaultTopAppBar(
} }
DropdownMenu( DropdownMenu(
expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) { expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) {
DropdownMenuItem(text = { Text("About") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.about_title)) }, onClick = {
onAboutClick() onAboutClick()
showOptionsMenu = false showOptionsMenu = false
}) })
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(text = { Text("Clear Book Cache") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = {
onClearCache() onClearCache()
showOptionsMenu = false showOptionsMenu = false
}) })
DropdownMenuItem(text = { Text("Clear Reflow Cache") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_reflow_cache)) }, onClick = {
onClearReflowCache() onClearReflowCache()
showOptionsMenu = false showOptionsMenu = false
}) })
if (BuildConfig.DEBUG && BuildConfig.FLAVOR != "oss") { if (BuildConfig.DEBUG && BuildConfig.FLAVOR != "oss") {
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(text = { Text("[Debug] Show Device Management") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.debug_show_device_management)) }, onClick = {
onShowDeviceManagement() onShowDeviceManagement()
showOptionsMenu = false showOptionsMenu = false
}) })
DropdownMenuItem( DropdownMenuItem(text = { Text(stringResource(R.string.debug_clear_cloud_local_data)) },
text = { Text("[Debug] Clear Cloud & Local Data") },
onClick = { onClick = {
onClearCloudData() onClearCloudData()
showOptionsMenu = false showOptionsMenu = false
@ -905,12 +905,8 @@ private fun AppDrawerContent(
// Signed-out: Show Sign In button at the top // Signed-out: Show Sign In button at the top
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(Icons.Outlined.AccountCircle, contentDescription = null) },
Icon( label = { Text(stringResource(R.string.drawer_sign_in)) },
Icons.Outlined.AccountCircle, contentDescription = "Sign In"
)
},
label = { Text("Sign in with Google") },
selected = false, selected = false,
onClick = onSignInClick, onClick = onSignInClick,
modifier = Modifier.padding(horizontal = 12.dp) modifier = Modifier.padding(horizontal = 12.dp)
@ -918,7 +914,7 @@ private fun AppDrawerContent(
// LegalText // LegalText
LegalText( LegalText(
prefixText = "By signing in,", prefixText = stringResource(R.string.drawer_by_signing_in),
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
@ -928,14 +924,9 @@ private fun AppDrawerContent(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(Icons.Default.VerifiedUser, contentDescription = null) },
Icon(
Icons.Default.VerifiedUser, contentDescription = "Episteme Pro"
)
},
label = { label = {
val text = val text = if (uiState.isProUser) stringResource(R.string.drawer_pro_unlocked) else stringResource(R.string.drawer_upgrade_pro)
if (uiState.isProUser) "Episteme Pro" else "Upgrade to Episteme Pro"
Text(text) Text(text)
}, },
selected = false, selected = false,
@ -946,12 +937,9 @@ private fun AppDrawerContent(
// Sync Toggle Item // Sync Toggle Item
if (uiState.currentUser != null) { if (uiState.currentUser != null) {
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(painterResource(id = R.drawable.sync), contentDescription = null) },
Icon( label = { Text(stringResource(R.string.drawer_sync_library)) },
painter = painterResource(id = R.drawable.sync), badge = {
contentDescription = "Sync Library"
)
}, label = { Text("Sync Library") }, badge = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (!uiState.isProUser) { if (!uiState.isProUser) {
Icon( Icon(
@ -979,17 +967,12 @@ private fun AppDrawerContent(
} }
if (uiState.currentUser != null && uiState.isSyncEnabled) { if (uiState.currentUser != null && uiState.isSyncEnabled) {
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(imageVector = Icons.Default.FolderSpecial, contentDescription = null) },
Icon(
imageVector = Icons.Default.FolderSpecial,
contentDescription = "Backup Local Folders"
)
},
label = { label = {
Column { Column {
Text("Cloud sync for Local Folders") Text(stringResource(R.string.drawer_backup_local_folders))
Text( Text(
"Upload books from your synced folders to Google Drive).", stringResource(R.string.drawer_backup_desc),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1020,32 +1003,22 @@ private fun AppDrawerContent(
modifier = Modifier.size(64.dp) modifier = Modifier.size(64.dp)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = "Episteme OSS", style = MaterialTheme.typography.titleMedium) Text(text = stringResource(R.string.app_name_oss), style = MaterialTheme.typography.titleMedium)
} }
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
} }
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(painterResource(id = R.drawable.fonts), contentDescription = null) },
Icon( label = { Text(stringResource(R.string.drawer_custom_fonts)) },
painter = painterResource(id = R.drawable.fonts),
contentDescription = "Custom Fonts"
)
},
label = { Text("Custom Fonts") },
selected = false, selected = false,
onClick = onFontsClick, onClick = onFontsClick,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
) )
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(painterResource(id = R.drawable.feedback), contentDescription = null) },
Icon( label = { Text(stringResource(R.string.drawer_help_feedback)) },
painter = painterResource(id = R.drawable.feedback),
contentDescription = "Feedback"
)
},
label = { Text("Help & Feedback") },
selected = false, selected = false,
onClick = { navController.navigate("feedback_screen_route") }, onClick = { navController.navigate("feedback_screen_route") },
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
@ -1054,13 +1027,8 @@ private fun AppDrawerContent(
if (!isOss) { if (!isOss) {
if (uiState.currentUser != null) { if (uiState.currentUser != null) {
NavigationDrawerItem( NavigationDrawerItem(
icon = { icon = { Icon(painterResource(id = R.drawable.logout), contentDescription = null) },
Icon( label = { Text(stringResource(R.string.drawer_sign_out)) },
painter = painterResource(id = R.drawable.logout),
contentDescription = "Sign Out"
)
},
label = { Text("Sign Out") },
selected = false, selected = false,
onClick = onSignOutClick, onClick = onSignOutClick,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
@ -1077,7 +1045,7 @@ private fun AppDrawerContent(
var scaledTextStyle by remember { mutableStateOf(baseStyle) } var scaledTextStyle by remember { mutableStateOf(baseStyle) }
Text( Text(
text = "Privacy Policy • Terms of Service • Licenses", text = stringResource(R.string.legal_footer_combined),
style = scaledTextStyle, style = scaledTextStyle,
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
@ -1098,29 +1066,21 @@ private fun AppDrawerContent(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Privacy Policy", text = stringResource(R.string.legal_privacy_policy),
style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary),
modifier = Modifier.clickable { uriHandler.openUri(PRIVACY_POLICY_URL) }, modifier = Modifier.clickable { uriHandler.openUri(PRIVACY_POLICY_URL) },
softWrap = false softWrap = false
) )
Text("", style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant))
Text( Text(
"", text = stringResource(R.string.legal_terms_of_service),
style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant),
softWrap = false
)
Text(
text = "Terms of Service",
style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary),
modifier = Modifier.clickable { uriHandler.openUri(TERMS_URL) }, modifier = Modifier.clickable { uriHandler.openUri(TERMS_URL) },
softWrap = false softWrap = false
) )
Text("", style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant))
Text( Text(
"", text = stringResource(R.string.legal_licenses),
style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant),
softWrap = false
)
Text(
text = "Licenses",
style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary),
modifier = Modifier.clickable { uriHandler.openUri(LICENSES_URL) }, modifier = Modifier.clickable { uriHandler.openUri(LICENSES_URL) },
softWrap = false softWrap = false
@ -1136,13 +1096,13 @@ fun UpgradeDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { Icon(Icons.Default.VerifiedUser, contentDescription = null) }, icon = { Icon(Icons.Default.VerifiedUser, contentDescription = null) },
title = { Text("Unlock Episteme Pro") }, title = { Text(stringResource(R.string.dialog_unlock_pro)) },
text = { Text("Sync across devices is a Pro feature. Unlock all pro features with a single, one-time purchase.") }, text = { Text(stringResource(R.string.dialog_unlock_pro_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { Text("Upgrade") } TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_upgrade)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
@ -1150,18 +1110,18 @@ fun UpgradeDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
fun SignOutConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { fun SignOutConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Confirm Sign Out") }, title = { Text(stringResource(R.string.dialog_confirm_sign_out)) },
text = { Text("Are you sure you want to sign out?") }, text = { Text(stringResource(R.string.dialog_confirm_sign_out_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = onConfirm, onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { ) {
Text("Sign Out") Text(stringResource(R.string.drawer_sign_out))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
@ -1185,13 +1145,13 @@ fun DeviceManagementScreen(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = "Device Limit Reached", text = stringResource(R.string.device_limit_reached),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "To use Episteme Pro on this device, please remove one of your existing registered devices.", text = stringResource(R.string.device_limit_reached_desc),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -1218,14 +1178,14 @@ fun DeviceManagementScreen(
Text(device.deviceName, fontWeight = FontWeight.SemiBold) Text(device.deviceName, fontWeight = FontWeight.SemiBold)
device.lastSeen?.let { device.lastSeen?.let {
Text( Text(
"Last seen: ${dateFormatter.format(it)}", stringResource(R.string.last_seen, dateFormatter.format(it)),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
TextButton(onClick = { onRemoveDevice(device.deviceId) }) { TextButton(onClick = { onRemoveDevice(device.deviceId) }) {
Text("Remove") Text(stringResource(R.string.action_remove))
} }
} }
} }
@ -1241,18 +1201,18 @@ fun ClearAllDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit)
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Confirm Destructive Action") }, title = { Text(stringResource(R.string.dialog_destructive_action)) },
text = { Text("This will permanently delete all your books and reading progress from this device AND from your Google Drive account. This action cannot be undone. Are you sure?") }, text = { Text(stringResource(R.string.dialog_destructive_action_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = onConfirm, onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { ) {
Text("Delete Everything") Text(stringResource(R.string.action_delete))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
@ -1276,7 +1236,7 @@ fun FpsMonitor(modifier: Modifier = Modifier) {
} }
Text( Text(
text = "FPS: $fps", text = stringResource(R.string.debug_fps, fps),
color = Color.Green, color = Color.Green,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = modifier modifier = modifier
@ -1315,12 +1275,12 @@ fun DangerousFolderActionDialog(
contentColor = MaterialTheme.colorScheme.error contentColor = MaterialTheme.colorScheme.error
) )
) { ) {
Text("Confirm & Clear") Text(stringResource(R.string.action_confirm_clear))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )

View file

@ -115,6 +115,8 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -138,14 +140,16 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@Composable
private fun getBookCountString(count: Int): String { private fun getBookCountString(count: Int): String {
return if (count == 1) "1 book" else "$count books" return pluralStringResource(id = R.plurals.book_count, count, count)
} }
@Composable @Composable
fun LibraryScreen( fun LibraryScreen(
viewModel: MainViewModel, viewModel: MainViewModel,
) { ) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val selectedItems = uiState.contextualActionItems val selectedItems = uiState.contextualActionItems
val isContextualModeActive = selectedItems.isNotEmpty() val isContextualModeActive = selectedItems.isNotEmpty()
@ -187,7 +191,7 @@ fun LibraryScreen(
try { try {
pickFolderLauncher.launch(null) pickFolderLauncher.launch(null)
} catch (_: android.content.ActivityNotFoundException) { } catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("Your device doesn't support folder selection. You can still import files individually.", isError = true) viewModel.showBanner(context.getString(R.string.error_folder_selection_unsupported), isError = true)
} }
} }
@ -222,7 +226,7 @@ fun LibraryScreen(
try { try {
fallbackFilePickerLauncher.launch("*/*") fallbackFilePickerLauncher.launch("*/*")
} catch (_: android.content.ActivityNotFoundException) { } catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true) viewModel.showBanner(context.getString(R.string.error_no_file_manager), isError = true)
} }
} }
} }
@ -301,7 +305,7 @@ fun LibraryScreen(
isLoading = uiState.isLoading, isLoading = uiState.isLoading,
isRefreshing = uiState.isRefreshing, isRefreshing = uiState.isRefreshing,
onOpdsBookDownloaded = { uri, title -> onOpdsBookDownloaded = { uri, title ->
viewModel.showBanner("Downloaded $title") viewModel.showBanner(context.getString(R.string.banner_downloaded, title))
viewModel.onFileSelected(uri, isFromRecent = false) viewModel.onFileSelected(uri, isFromRecent = false)
}, },
onStreamOpdsBook = { entry, catalog -> onStreamOpdsBook = { entry, catalog ->
@ -542,7 +546,12 @@ fun LibraryScreenContent(
val isBookContextualModeActive = selectedItems.isNotEmpty() val isBookContextualModeActive = selectedItems.isNotEmpty()
val isShelfContextualModeActive = selectedShelves.isNotEmpty() val isShelfContextualModeActive = selectedShelves.isNotEmpty()
var showSortMenu by remember { mutableStateOf(false) } var showSortMenu by remember { mutableStateOf(false) }
val tabTitles = listOf("All Books", "Shelves", "Folders", "Catalogs") val tabTitles = listOf(
stringResource(R.string.tab_all_books),
stringResource(R.string.tab_shelves),
stringResource(R.string.tab_folders),
stringResource(R.string.tab_catalogs)
)
val searchFocusRequester = remember { FocusRequester() } val searchFocusRequester = remember { FocusRequester() }
var textFieldValue by remember(isSearchActive) { var textFieldValue by remember(isSearchActive) {
@ -603,7 +612,7 @@ fun LibraryScreenContent(
textFieldValue = it textFieldValue = it
onSearchQueryChange(it.text) onSearchQueryChange(it.text)
}, },
placeholder = { Text("Search title or author...") }, placeholder = { Text(stringResource(R.string.search_placeholder)) },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 4.dp) .padding(vertical = 4.dp)
@ -627,8 +636,7 @@ fun LibraryScreenContent(
} }
} }
} else { } else {
CustomTopAppBar( CustomTopAppBar(title = { Text(stringResource(R.string.library_title)) },
title = { Text("Library") },
actions = { actions = {
if (pagerState.currentPage == 0) { if (pagerState.currentPage == 0) {
IconButton(onClick = onFilterClick) { IconButton(onClick = onFilterClick) {
@ -698,21 +706,21 @@ fun LibraryScreenContent(
if (libraryFilters.fileTypes.isNotEmpty()) { if (libraryFilters.fileTypes.isNotEmpty()) {
AssistChip( AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) }, onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
label = { Text("Types: ${libraryFilters.fileTypes.joinToString { it.name }}") }, label = { Text(stringResource(R.string.filter_types, libraryFilters.fileTypes.joinToString { it.name })) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
) )
} }
if (libraryFilters.sourceFolders.isNotEmpty()) { if (libraryFilters.sourceFolders.isNotEmpty()) {
AssistChip( AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) }, onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) },
label = { Text("Folders: ${libraryFilters.sourceFolders.size}") }, label = { Text(stringResource(R.string.filter_folders, libraryFilters.sourceFolders.size)) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
) )
} }
if (libraryFilters.readStatus != ReadStatusFilter.ALL) { if (libraryFilters.readStatus != ReadStatusFilter.ALL) {
AssistChip( AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) }, onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) },
label = { Text("Status: ${libraryFilters.readStatus.displayName}") }, label = { Text(stringResource(R.string.filter_status, libraryFilters.readStatus.displayName)) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
) )
} }
@ -727,7 +735,7 @@ fun LibraryScreenContent(
0 -> { 0 -> {
if (recentFiles.isNotEmpty()) { if (recentFiles.isNotEmpty()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("Add file") }, text = { Text(stringResource(R.string.fab_add_file)) },
icon = { Icon(Icons.Default.Add, contentDescription = "Add file") }, icon = { Icon(Icons.Default.Add, contentDescription = "Add file") },
onClick = onSelectFileClick, onClick = onSelectFileClick,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
@ -736,7 +744,7 @@ fun LibraryScreenContent(
} }
1 -> { 1 -> {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("New shelf") }, text = { Text(stringResource(R.string.fab_new_shelf)) },
icon = { Icon(Icons.Default.Add, contentDescription = "New shelf") }, icon = { Icon(Icons.Default.Add, contentDescription = "New shelf") },
onClick = onNewShelfClick, onClick = onNewShelfClick,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
@ -757,12 +765,12 @@ fun LibraryScreenContent(
0 -> { 0 -> {
if (recentFiles.isEmpty() && searchQuery.isNotEmpty()) { if (recentFiles.isEmpty() && searchQuery.isNotEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No results found for \"$searchQuery\"") Text(stringResource(R.string.no_results_found, searchQuery))
} }
} else if (recentFiles.isEmpty()) { } else if (recentFiles.isEmpty()) {
EmptyState( EmptyState(
title = "Your Library is Empty", title = stringResource(R.string.your_library_empty),
message = "Select a PDF, EPUB, MOBI, or AZW3 file from your device to get started.", message = stringResource(R.string.library_empty_desc),
onSelectFileClick = onSelectFileClick, onSelectFileClick = onSelectFileClick,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@ -850,12 +858,12 @@ private fun CreateShelfDialog(onConfirm: (String) -> Unit, onDismiss: () -> Unit
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Create New Shelf") }, title = { Text(stringResource(R.string.create_new_shelf)) },
text = { text = {
OutlinedTextField( OutlinedTextField(
value = text, value = text,
onValueChange = { text = it }, onValueChange = { text = it },
placeholder = { Text("Shelf Name") }, placeholder = { Text(stringResource(R.string.shelf_name_hint)) },
singleLine = true, singleLine = true,
modifier = Modifier.focusRequester(focusRequester) modifier = Modifier.focusRequester(focusRequester)
) )
@ -865,12 +873,12 @@ private fun CreateShelfDialog(onConfirm: (String) -> Unit, onDismiss: () -> Unit
onClick = { onConfirm(text) }, onClick = { onConfirm(text) },
enabled = text.isNotBlank() enabled = text.isNotBlank()
) { ) {
Text("Create") Text(stringResource(R.string.action_create))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )
@ -981,14 +989,14 @@ private fun ShelfDetailScreen(
onDismissRequest = { showMoreMenu = false } onDismissRequest = { showMoreMenu = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Rename shelf") }, text = { Text(stringResource(R.string.menu_rename_shelf)) },
onClick = { onClick = {
onRenameShelf() onRenameShelf()
showMoreMenu = false showMoreMenu = false
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete shelf") }, text = { Text(stringResource(R.string.menu_delete_shelf)) },
onClick = { onClick = {
onDeleteShelf() onDeleteShelf()
showMoreMenu = false showMoreMenu = false
@ -1006,7 +1014,7 @@ private fun ShelfDetailScreen(
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = onAddBooksClick, onClick = onAddBooksClick,
icon = { Icon(Icons.Default.Add, contentDescription = null) }, icon = { Icon(Icons.Default.Add, contentDescription = null) },
text = { Text("Add books") } text = { Text(stringResource(R.string.fab_add_books)) }
) )
} }
} }
@ -1016,7 +1024,7 @@ private fun ShelfDetailScreen(
modifier = Modifier.fillMaxSize().padding(paddingValues), modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("This shelf is empty", style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.shelf_empty), style = MaterialTheme.typography.bodyLarge)
} }
} else { } else {
LazyColumn( LazyColumn(
@ -1059,7 +1067,7 @@ private fun AddBooksModeScreen(
modifier = Modifier.statusBarsPadding(), modifier = Modifier.statusBarsPadding(),
topBar = { topBar = {
CustomTopAppBar( CustomTopAppBar(
title = { Text("Add to $shelfName") }, title = { Text(stringResource(R.string.add_to_shelf, shelfName)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
@ -1127,7 +1135,7 @@ private fun AddBooksModeScreen(
floatingActionButton = { floatingActionButton = {
if (selectedBookUris.isNotEmpty()) { if (selectedBookUris.isNotEmpty()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("ADD (${selectedBookUris.size})") }, text = { Text(stringResource(R.string.fab_add_count, selectedBookUris.size)) },
icon = { Icon(Icons.Default.Check, contentDescription = "Add books") }, icon = { Icon(Icons.Default.Check, contentDescription = "Add books") },
onClick = onAddSelectedBooks onClick = onAddSelectedBooks
) )
@ -1140,7 +1148,7 @@ private fun AddBooksModeScreen(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (currentSource == AddBooksSource.UNSHELVED) "No unshelved books to add" else "All books are already in this shelf", text = if (currentSource == AddBooksSource.UNSHELVED) stringResource(R.string.no_unshelved_books) else stringResource(R.string.all_books_in_shelf),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
} }
@ -1431,12 +1439,12 @@ private fun RenameShelfDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Rename Shelf") }, title = { Text(stringResource(R.string.menu_rename_shelf)) },
text = { text = {
OutlinedTextField( OutlinedTextField(
value = textFieldValue, value = textFieldValue,
onValueChange = { textFieldValue = it }, onValueChange = { textFieldValue = it },
placeholder = { Text("Shelf Name") }, placeholder = { Text(stringResource(R.string.shelf_name_hint)) },
singleLine = true, singleLine = true,
modifier = Modifier.focusRequester(focusRequester) modifier = Modifier.focusRequester(focusRequester)
) )
@ -1446,12 +1454,12 @@ private fun RenameShelfDialog(
onClick = { onConfirm(textFieldValue.text) }, onClick = { onConfirm(textFieldValue.text) },
enabled = textFieldValue.text.isNotBlank() && textFieldValue.text != initialName enabled = textFieldValue.text.isNotBlank() && textFieldValue.text != initialName
) { ) {
Text("Rename") Text(stringResource(R.string.action_rename))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )
@ -1470,13 +1478,13 @@ private fun DeleteShelfConfirmationDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Delete Shelf?") }, title = { Text(stringResource(R.string.dialog_delete_shelf)) },
text = { Text("Are you sure you want to delete the '$shelfName' shelf? All books will be moved to Unshelved.") }, text = { Text(stringResource(R.string.dialog_delete_shelf_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { Text("Delete") } TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_delete)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -1488,16 +1496,16 @@ private fun RemoveFromShelfConfirmationDialog(
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val bookStr = if (count == 1) "book" else "books" val bookStr = pluralStringResource(id = R.plurals.book_word, count)
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Remove from Shelf?") }, title = { Text(stringResource(R.string.dialog_remove_from_shelf)) },
text = { Text("Are you sure you want to remove $count $bookStr from the '$shelfName' shelf? The book(s) will remain in your library and appear under Unshelved.") }, text = { Text(stringResource(R.string.dialog_remove_from_shelf_desc, count, bookStr, shelfName)) },
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { Text("Remove") } TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_remove)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -1508,16 +1516,16 @@ private fun DeleteShelvesConfirmationDialog(
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val shelfStr = if (count == 1) "shelf" else "shelves" val shelfStr = pluralStringResource(id = R.plurals.shelf_count, count)
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Delete $shelfStr?") }, title = { Text(stringResource(R.string.dialog_delete_shelves, shelfStr)) },
text = { Text("Are you sure you want to delete the $count selected $shelfStr? All books within will be moved to Unshelved.") }, text = { Text(stringResource(R.string.dialog_delete_shelves_desc, count, shelfStr)) },
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { Text("Delete") } TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_delete)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -1539,7 +1547,7 @@ private fun FolderSyncScreen(
floatingActionButton = { floatingActionButton = {
if (syncedFolders.size < 3) { if (syncedFolders.size < 3) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("Add Folder") }, text = { Text(stringResource(R.string.fab_add_folder)) },
icon = { Icon(Icons.Default.Add, "Add") }, icon = { Icon(Icons.Default.Add, "Add") },
onClick = onAddFolderClick onClick = onAddFolderClick
) )
@ -1570,7 +1578,7 @@ private fun FolderSyncScreen(
Icon(Icons.Default.Search, null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Search, null, modifier = Modifier.size(18.dp))
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(if (isLoading) "Scanning..." else "Scan All") Text(if (isLoading) stringResource(R.string.scanning) else stringResource(R.string.scan_all))
} }
androidx.compose.material3.OutlinedButton( androidx.compose.material3.OutlinedButton(
@ -1581,15 +1589,15 @@ private fun FolderSyncScreen(
) { ) {
Icon(painterResource(id = R.drawable.sync), null, modifier = Modifier.size(18.dp)) Icon(painterResource(id = R.drawable.sync), null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Sync Meta") Text(stringResource(R.string.sync_meta))
} }
} }
} else { } else {
EmptyState( EmptyState(
title = "Sync Local Folders", title = stringResource(R.string.sync_local_folders),
message = "Connect local folders to create a live library. Episteme will monitor files and sync progress.", message = stringResource(R.string.sync_folders_desc),
onSelectFileClick = onAddFolderClick, onSelectFileClick = onAddFolderClick,
primaryButtonText = "Select Folder", primaryButtonText = stringResource(R.string.action_select_folder),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
@ -1632,7 +1640,7 @@ private fun FolderCard(
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
val dateFormat = remember { SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()) } val dateFormat = remember { SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()) }
val lastScanText = if (folder.lastScanTime == 0L) "Never" else dateFormat.format(Date(folder.lastScanTime)) val lastScanText = if (folder.lastScanTime == 0L) stringResource(R.string.never) else dateFormat.format(Date(folder.lastScanTime))
val folderFiles = remember(allRecentFiles, folder.uriString) { val folderFiles = remember(allRecentFiles, folder.uriString) {
allRecentFiles.filter { it.sourceFolderUri == folder.uriString } allRecentFiles.filter { it.sourceFolderUri == folder.uriString }
@ -1676,14 +1684,14 @@ private fun FolderCard(
} }
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Edit Filters") }, text = { Text(stringResource(R.string.menu_edit_filters)) },
onClick = { onClick = {
showMenu = false showMenu = false
onEditFiltersClick(folder) onEditFiltersClick(folder)
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Remove Folder") }, text = { Text(stringResource(R.string.menu_remove_folder)) },
onClick = { onClick = {
showMenu = false showMenu = false
onRemoveClick(folder) onRemoveClick(folder)
@ -1701,7 +1709,7 @@ private fun FolderCard(
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = "LAST SYNC", text = stringResource(R.string.last_sync),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -1711,7 +1719,7 @@ private fun FolderCard(
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) { Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) {
Text( Text(
text = "BOOKS", text = stringResource(R.string.books_count),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -1730,7 +1738,7 @@ private fun FolderCard(
countsByType.forEach { (type, count) -> countsByType.forEach { (type, count) ->
AssistChip( AssistChip(
onClick = { }, onClick = { },
label = { Text("${type.name}: $count") } label = { Text(stringResource(R.string.folder_filter_count, type.name, count)) }
) )
} }
} }
@ -1749,11 +1757,11 @@ private fun EditFolderFiltersDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Filter File Types") }, title = { Text(stringResource(R.string.filter_file_types)) },
text = { text = {
Column { Column {
Text( Text(
text = "Select the file types you want to sync from this folder:", stringResource(R.string.filter_file_types_desc),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -1783,10 +1791,10 @@ private fun EditFolderFiltersDialog(
TextButton( TextButton(
onClick = { onConfirm(selectedTypes) }, onClick = { onConfirm(selectedTypes) },
enabled = selectedTypes.isNotEmpty() enabled = selectedTypes.isNotEmpty()
) { Text("Save") } ) { Text(stringResource(R.string.action_save)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -1812,9 +1820,9 @@ fun LibraryFilterSheet(
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text("Filter Library", style = MaterialTheme.typography.titleLarge) Text(stringResource(R.string.filter_library), style = MaterialTheme.typography.titleLarge)
Text("File Type", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.filter_file_type), style = MaterialTheme.typography.titleMedium)
Row( Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -1832,7 +1840,7 @@ fun LibraryFilterSheet(
} }
if (syncedFolders.isNotEmpty()) { if (syncedFolders.isNotEmpty()) {
Text("Source Folder", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.filter_source_folder), style = MaterialTheme.typography.titleMedium)
Row( Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -1850,7 +1858,7 @@ fun LibraryFilterSheet(
} }
} }
Text("Read Status", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.filter_read_status), style = MaterialTheme.typography.titleMedium)
Row( Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -1866,11 +1874,11 @@ fun LibraryFilterSheet(
Row(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalArrangement = Arrangement.End) { Row(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalArrangement = Arrangement.End) {
TextButton(onClick = { currentFilters = LibraryFilters() }) { TextButton(onClick = { currentFilters = LibraryFilters() }) {
Text("Clear All") Text(stringResource(R.string.clear_all))
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
androidx.compose.material3.Button(onClick = { onApply(currentFilters); onDismiss() }) { androidx.compose.material3.Button(onClick = { onApply(currentFilters); onDismiss() }) {
Text("Apply") Text(stringResource(R.string.action_apply))
} }
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -1930,7 +1938,7 @@ fun OpdsTab(
} }
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text("Add Catalog") }, text = { Text(stringResource(R.string.fab_add_catalog)) },
icon = { Icon(Icons.Default.Add, "Add") }, icon = { Icon(Icons.Default.Add, "Add") },
onClick = { onClick = {
editingCatalog = null editingCatalog = null
@ -1981,7 +1989,7 @@ fun OpdsTab(
OutlinedTextField( OutlinedTextField(
value = query, value = query,
onValueChange = { query = it }, onValueChange = { query = it },
placeholder = { Text("Search catalog...") }, placeholder = { Text(stringResource(R.string.search_catalog_placeholder)) },
modifier = Modifier.weight(1f).padding(vertical = 4.dp) modifier = Modifier.weight(1f).padding(vertical = 4.dp)
.focusRequester(searchFocusRequester), .focusRequester(searchFocusRequester),
singleLine = true, singleLine = true,
@ -2017,7 +2025,7 @@ fun OpdsTab(
) )
} else { } else {
Text( Text(
text = uiState.currentFeed?.title ?: "Loading...", text = uiState.currentFeed?.title ?: stringResource(R.string.status_loading),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
@ -2045,7 +2053,7 @@ fun OpdsTab(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("This feed is empty.") Text(stringResource(R.string.feed_empty))
} }
} else { } else {
val facets = uiState.currentFeed?.facets ?: emptyList() val facets = uiState.currentFeed?.facets ?: emptyList()
@ -2066,7 +2074,7 @@ fun OpdsTab(
FilterChip( FilterChip(
selected = activeFacet?.isActive == true, selected = activeFacet?.isActive == true,
onClick = { expanded = true }, onClick = { expanded = true },
label = { Text("${groupName}: ${activeFacet?.title ?: "Select"}") }, label = { Text(stringResource(R.string.filter_facet, groupName, activeFacet?.title ?: stringResource(R.string.action_select))) },
trailingIcon = { trailingIcon = {
Icon( Icon(
Icons.Default.ArrowDropDown, Icons.Default.ArrowDropDown,
@ -2194,24 +2202,23 @@ fun OpdsTab(
showCatalogDialog = false showCatalogDialog = false
editingCatalog = null editingCatalog = null
}, },
title = { Text(if (isEditMode) "Edit Catalog" else "Add OPDS Catalog") }, title = { Text(if (isEditMode) stringResource(R.string.edit_catalog) else stringResource(R.string.add_opds_catalog)) },
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField( OutlinedTextField(
value = newTitle, value = newTitle,
onValueChange = { newTitle = it }, onValueChange = { newTitle = it },
label = { Text("Catalog Name") }, label = { Text(stringResource(R.string.catalog_name)) },
singleLine = true singleLine = true
) )
OutlinedTextField( OutlinedTextField(
value = newUrl, value = newUrl,
onValueChange = { newUrl = it }, onValueChange = { newUrl = it },
label = { Text("URL") }, label = { Text(stringResource(R.string.url)) },
placeholder = { Text("e.g. http://192.168.1.50:8080/opds") }, placeholder = { Text(stringResource(R.string.url_placeholder)) },
singleLine = true singleLine = true
) )
Text( Text(stringResource(R.string.auth_optional),
"Authentication (Optional)",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp)
@ -2219,13 +2226,13 @@ fun OpdsTab(
OutlinedTextField( OutlinedTextField(
value = newUsername, value = newUsername,
onValueChange = { newUsername = it }, onValueChange = { newUsername = it },
label = { Text("Username") }, label = { Text(stringResource(R.string.username)) },
singleLine = true singleLine = true
) )
OutlinedTextField( OutlinedTextField(
value = newPassword, value = newPassword,
onValueChange = { newPassword = it }, onValueChange = { newPassword = it },
label = { Text("Password") }, label = { Text(stringResource(R.string.password)) },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Password) keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Password)
@ -2244,13 +2251,13 @@ fun OpdsTab(
editingCatalog = null editingCatalog = null
}, },
enabled = newTitle.isNotBlank() && newUrl.isNotBlank() enabled = newTitle.isNotBlank() && newUrl.isNotBlank()
) { Text("Save") } ) { Text(stringResource(R.string.action_save)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { TextButton(onClick = {
showCatalogDialog = false showCatalogDialog = false
editingCatalog = null editingCatalog = null
}) { Text("Cancel") } }) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -2259,14 +2266,14 @@ fun OpdsTab(
val streamedBooksCount = localLibraryFiles.count { it.uriString?.contains("catalogId=${catalogToDelete!!.id}") == true } val streamedBooksCount = localLibraryFiles.count { it.uriString?.contains("catalogId=${catalogToDelete!!.id}") == true }
AlertDialog( AlertDialog(
onDismissRequest = { catalogToDelete = null }, onDismissRequest = { catalogToDelete = null },
title = { Text("Delete Catalog") }, title = { Text(stringResource(R.string.delete_catalog)) },
text = { text = {
Column { Column {
Text("Are you sure you want to delete '${catalogToDelete!!.title}'?") Text(stringResource(R.string.delete_catalog_desc, catalogToDelete!!.title))
if (streamedBooksCount > 0) { if (streamedBooksCount > 0) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
"Deleting this catalog will also permanently remove $streamedBooksCount streaming books associated with it from your library.", stringResource(R.string.delete_catalog_warning, streamedBooksCount),
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
} }
@ -2282,10 +2289,10 @@ fun OpdsTab(
catalogToDelete = null catalogToDelete = null
}, },
colors = androidx.compose.material3.ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) colors = androidx.compose.material3.ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { Text("Delete") } ) { Text(stringResource(R.string.action_delete)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { catalogToDelete = null }) { Text("Cancel") } TextButton(onClick = { catalogToDelete = null }) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -2314,8 +2321,7 @@ fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Un
color = MaterialTheme.colorScheme.secondaryContainer, color = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
) { ) {
Text( Text(stringResource(R.string.preset_label),
text = "Preset",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer, color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
@ -2419,12 +2425,12 @@ fun OpdsBookCard(
) { ) {
Icon(Icons.Default.Check, null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.Check, null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Read") Text(stringResource(R.string.action_read))
} }
} else if (isDownloading) { } else if (isDownloading) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Downloading...", style = MaterialTheme.typography.labelMedium) Text(stringResource(R.string.status_downloading), style = MaterialTheme.typography.labelMedium)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (progress != null) { if (progress != null) {
Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelMedium) Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelMedium)
@ -2446,7 +2452,7 @@ fun OpdsBookCard(
) { ) {
Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(16.dp)) Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Stream") Text(stringResource(R.string.action_stream))
} }
} }
@ -2465,11 +2471,11 @@ fun OpdsBookCard(
if (uniqueAcquisitions.isEmpty()) { if (uniqueAcquisitions.isEmpty()) {
Icon(Icons.Default.Info, null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.Info, null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Unavailable") Text(stringResource(R.string.action_unavailable))
} else { } else {
Icon(Icons.Default.Add, null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.Add, null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("Download") Text(stringResource(R.string.action_download))
} }
} }
} }
@ -2586,14 +2592,14 @@ fun OpdsBookDetailsSheet(
) { ) {
Icon(Icons.Default.Check, contentDescription = "Read") Icon(Icons.Default.Check, contentDescription = "Read")
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Read", fontWeight = FontWeight.Bold) Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold)
} }
} }
if (isDownloading) { if (isDownloading) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Downloading...", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.status_downloading), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (progress != null) { if (progress != null) {
Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.titleMedium) Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.titleMedium)
@ -2618,14 +2624,13 @@ fun OpdsBookDetailsSheet(
) { ) {
Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(18.dp)) Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Stream Now", fontWeight = FontWeight.Bold) Text(stringResource(R.string.action_stream_now), fontWeight = FontWeight.Bold)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
if (uniqueAcquisitions.isNotEmpty()) { if (uniqueAcquisitions.isNotEmpty()) {
Text( Text(stringResource(R.string.download_format),
"Download Format",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -2643,7 +2648,7 @@ fun OpdsBookDetailsSheet(
} }
} }
} else { } else {
Text("No supported formats available.", color = MaterialTheme.colorScheme.error) Text(stringResource(R.string.no_supported_formats), color = MaterialTheme.colorScheme.error)
} }
if (entry.categories.isNotEmpty()) { if (entry.categories.isNotEmpty()) {
@ -2681,20 +2686,20 @@ fun OpdsBookDetailsSheet(
) { ) {
entry.publisher?.takeIf { it.isNotBlank() }?.let { entry.publisher?.takeIf { it.isNotBlank() }?.let {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("PUBLISHER", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.publisher), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(it, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis) Text(it, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis)
} }
} }
entry.published?.takeIf { it.isNotBlank() }?.let { entry.published?.takeIf { it.isNotBlank() }?.let {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("PUBLISHED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.published), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
val cleanDate = it.substringBefore("T") val cleanDate = it.substringBefore("T")
Text(cleanDate, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) Text(cleanDate, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
} }
} }
entry.language?.takeIf { it.isNotBlank() }?.let { entry.language?.takeIf { it.isNotBlank() }?.let {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("LANGUAGE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.language), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(it.uppercase(), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) Text(it.uppercase(), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
} }
} }
@ -2703,7 +2708,7 @@ fun OpdsBookDetailsSheet(
} }
if (!entry.summary.isNullOrBlank()) { if (!entry.summary.isNullOrBlank()) {
Text("Synopsis", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.synopsis), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
val cleanSummary = remember(entry.summary) { val cleanSummary = remember(entry.summary) {
val preProcessed = entry.summary val preProcessed = entry.summary

View file

@ -40,13 +40,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
sealed class BottomBarScreen(val route: String, val label: String, val iconResId: Int) { sealed class BottomBarScreen(val route: String, val stringResId: Int, val iconResId: Int) {
object Home : BottomBarScreen("home", "Home", R.drawable.home) object Home : BottomBarScreen("home", R.string.nav_home, R.drawable.home)
object Library : BottomBarScreen("library", "Library", R.drawable.library_books) object Library : BottomBarScreen("library", R.string.nav_library, R.drawable.library_books)
} }
private val bottomBarItems = listOf( private val bottomBarItems = listOf(
@ -94,14 +95,10 @@ fun MainScreen(
NavigationBar { NavigationBar {
bottomBarItems.forEachIndexed { index, screen -> bottomBarItems.forEachIndexed { index, screen ->
NavigationBarItem( NavigationBarItem(
icon = { Icon(painterResource(id = screen.iconResId), contentDescription = screen.label) }, icon = { Icon(painterResource(id = screen.iconResId), contentDescription = stringResource(screen.stringResId)) },
label = { Text(screen.label) }, label = { Text(stringResource(screen.stringResId)) },
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { onClick = { scope.launch { pagerState.animateScrollToPage(index) } }
scope.launch {
pagerState.animateScrollToPage(index)
}
}
) )
} }
} }

View file

@ -494,7 +494,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
recentFilesRepository.deleteFilePermanently(ids) recentFilesRepository.deleteFilePermanently(ids)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showBanner("Removed ${filesToDelete.size} streaming books.") showBanner(appContext.getString(R.string.banner_removed_streaming_books, filesToDelete.size))
} }
} }
} }
@ -806,7 +806,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
uploadNewFont(font) uploadNewFont(font)
} }
}.onFailure { }.onFailure {
showBanner("Failed to import font: ${it.message}", isError = true) showBanner(appContext.getString(R.string.error_import_font, it.message), isError = true)
} }
_internalState.update { it.copy(isLoading = false) } _internalState.update { it.copy(isLoading = false) }
} }
@ -848,7 +848,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
onDeleted() onDeleted()
showBanner("Text view deleted.") showBanner(appContext.getString(R.string.banner_text_view_deleted))
} }
} }
} }
@ -878,7 +878,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
it.copy(isRequestingDrivePermission = false, isSyncEnabled = false) it.copy(isRequestingDrivePermission = false, isSyncEnabled = false)
} }
prefs.edit { putBoolean(KEY_SYNC_ENABLED, false) } prefs.edit { putBoolean(KEY_SYNC_ENABLED, false) }
showBanner("Sync requires Google Drive permission.", isError = true) showBanner(appContext.getString(R.string.error_sync_drive_permission), isError = true)
} }
private fun verifyPurchaseWithBackend( private fun verifyPurchaseWithBackend(
@ -890,9 +890,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (!isSilentMigrationCheck) { if (!isSilentMigrationCheck) {
_internalState.update { _internalState.update {
it.copy( it.copy(
bannerMessage = BannerMessage( bannerMessage = BannerMessage(appContext.getString(R.string.error_purchase_general), isError = true)
"An error occurred with the purchase.", isError = true
)
) )
} }
} }
@ -905,7 +903,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (result.isSuccess) { if (result.isSuccess) {
Timber.i("Backend verification successful. Firestore will update the app.") Timber.i("Backend verification successful. Firestore will update the app.")
_internalState.update { _internalState.update {
it.copy(bannerMessage = BannerMessage("Upgrade successful! Welcome to Pro.")) it.copy(bannerMessage = BannerMessage(appContext.getString(R.string.banner_upgrade_success)))
} }
verifyDeviceForProUser() verifyDeviceForProUser()
} else { } else {
@ -915,8 +913,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
"Migration check: Purchase token is already claimed by another account. Silently ignoring." "Migration check: Purchase token is already claimed by another account. Silently ignoring."
) )
} else { } else {
val errorMessage = val errorMessage = appContext.getString(R.string.error_purchase_verification)
"Purchase verification failed. Please contact support if you were charged."
Timber.e(exception, "Backend verification failed") Timber.e(exception, "Backend verification failed")
if (!isSilentMigrationCheck) { if (!isSilentMigrationCheck) {
_internalState.update { _internalState.update {
@ -950,7 +947,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.w("Device has been revoked. Signing out.") Timber.w("Device has been revoked. Signing out.")
firestoreRepository.deleteDevice(currentUser.uid, deviceId) // Clean up firestoreRepository.deleteDevice(currentUser.uid, deviceId) // Clean up
signOut() signOut()
showBanner("This device was removed from your account.") showBanner(appContext.getString(R.string.banner_device_removed))
} }
is com.aryan.reader.data.DeviceStatus.NotFound -> { is com.aryan.reader.data.DeviceStatus.NotFound -> {
@ -962,7 +959,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.e(deviceStatus.exception, "Error checking device status.") Timber.e(deviceStatus.exception, "Error checking device status.")
_internalState.update { _internalState.update {
it.copy( it.copy(
errorMessage = "Could not verify this device. Please check your connection." errorMessage = appContext.getString(R.string.error_verify_device)
) )
} }
} }
@ -998,7 +995,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.e("Failed to replace device.") Timber.e("Failed to replace device.")
_internalState.update { _internalState.update {
it.copy( it.copy(
errorMessage = "Failed to update devices. Please try again.", errorMessage = appContext.getString(R.string.error_update_devices),
isReplacingDevice = false isReplacingDevice = false
) )
} }
@ -1044,7 +1041,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
) { ) {
viewModelScope.launch { viewModelScope.launch {
_internalState.update { _internalState.update {
it.copy(isLoading = true, bannerMessage = BannerMessage("Saving PDF...")) it.copy(isLoading = true, bannerMessage = BannerMessage(appContext.getString(R.string.banner_saving_pdf)))
} }
try { try {
val virtualPages = pageLayoutRepository.getLayoutOrNull(bookId) val virtualPages = pageLayoutRepository.getLayoutOrNull(bookId)
@ -1060,13 +1057,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
textBoxes = textBoxes, textBoxes = textBoxes,
highlights = highlights highlights = highlights
) )
showBanner("PDF saved successfully.") showBanner(appContext.getString(R.string.banner_pdf_saved))
} else { } else {
showBanner("Failed to open file for saving.", isError = true) showBanner(appContext.getString(R.string.error_open_file_saving), isError = true)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to save annotated PDF") Timber.e(e, "Failed to save annotated PDF")
showBanner("Error saving PDF: ${e.localizedMessage}", isError = true) showBanner(appContext.getString(R.string.error_saving_pdf, e.localizedMessage), isError = true)
} finally { } finally {
_internalState.update { it.copy(isLoading = false) } _internalState.update { it.copy(isLoading = false) }
} }
@ -1076,7 +1073,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
fun saveOriginalPdf(sourceUri: Uri, destUri: Uri) { fun saveOriginalPdf(sourceUri: Uri, destUri: Uri) {
viewModelScope.launch { viewModelScope.launch {
_internalState.update { _internalState.update {
it.copy(isLoading = true, bannerMessage = BannerMessage("Saving original PDF...")) it.copy(isLoading = true, bannerMessage = BannerMessage(appContext.getString(R.string.banner_saving_original_pdf)))
} }
try { try {
val contentResolver = appContext.contentResolver val contentResolver = appContext.contentResolver
@ -1085,10 +1082,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
input.copyTo(output) input.copyTo(output)
} }
} }
showBanner("Original PDF saved successfully.") showBanner(appContext.getString(R.string.banner_original_pdf_saved))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to save original PDF") Timber.e(e, "Failed to save original PDF")
showBanner("Error saving PDF: ${e.localizedMessage}", isError = true) showBanner(appContext.getString(R.string.error_saving_pdf, e.localizedMessage), isError = true)
} finally { } finally {
_internalState.update { it.copy(isLoading = false) } _internalState.update { it.copy(isLoading = false) }
} }
@ -1190,14 +1187,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
putExtra(Intent.EXTRA_STREAM, contentUri) putExtra(Intent.EXTRA_STREAM, contentUri)
putExtra(Intent.EXTRA_TITLE, filename) putExtra(Intent.EXTRA_TITLE, filename)
putExtra(Intent.EXTRA_SUBJECT, "Sharing: $filename") putExtra(Intent.EXTRA_SUBJECT, appContext.getString(R.string.share_subject, filename))
clipData = ClipData.newRawUri(filename, contentUri) clipData = ClipData.newRawUri(filename, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
val chooser = Intent.createChooser(shareIntent, "Share PDF") val chooser = Intent.createChooser(shareIntent, appContext.getString(R.string.share_chooser_title))
if (activityContext !is android.app.Activity) { if (activityContext !is android.app.Activity) {
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -1206,7 +1203,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
withContext(Dispatchers.Main) { activityContext.startActivity(chooser) } withContext(Dispatchers.Main) { activityContext.startActivity(chooser) }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Share failed") Timber.e(e, "Share failed")
showBanner("Share failed: ${e.localizedMessage}", isError = true) showBanner(appContext.getString(R.string.error_share_failed, e.localizedMessage), isError = true)
} }
} }
} }
@ -1507,12 +1504,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val currentFolders = _internalState.value.syncedFolders val currentFolders = _internalState.value.syncedFolders
if (currentFolders.size >= MAX_FOLDER_LIMIT) { if (currentFolders.size >= MAX_FOLDER_LIMIT) {
showBanner("Limit reached: Maximum $MAX_FOLDER_LIMIT folders allowed.", isError = true) showBanner(appContext.getString(R.string.error_folder_limit_reached, MAX_FOLDER_LIMIT), isError = true)
return return
} }
if (currentFolders.any { it.uriString == folderUri.toString() }) { if (currentFolders.any { it.uriString == folderUri.toString() }) {
showBanner("This folder is already synced.", isError = true) showBanner(appContext.getString(R.string.error_folder_already_synced), isError = true)
return return
} }
@ -1547,11 +1544,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
FolderSyncWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, syncRequest FolderSyncWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, syncRequest
) )
showBanner("Folder added: $name") showBanner(appContext.getString(R.string.banner_folder_added, name))
} catch (e: SecurityException) { } catch (e: SecurityException) {
Timber.e(e, "Failed to take permissions for $folderUri") Timber.e(e, "Failed to take permissions for $folderUri")
showBanner("Failed to access folder permissions.", isError = true) showBanner(appContext.getString(R.string.error_access_folder_permissions), isError = true)
} }
} }
} }
@ -1581,7 +1578,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
WorkManager.getInstance(appContext).cancelUniqueWork(FolderSyncWorker.WORK_NAME) WorkManager.getInstance(appContext).cancelUniqueWork(FolderSyncWorker.WORK_NAME)
} }
showBanner("Folder removed.") showBanner(appContext.getString(R.string.banner_folder_removed))
} }
} }
@ -1616,8 +1613,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
when (workInfo.state) { when (workInfo.state) {
WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> { WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> {
if (showFeedback) { if (showFeedback) {
val msg = val msg = if (metadataOnly) appContext.getString(R.string.banner_folder_sync_updating) else appContext.getString(R.string.banner_folder_sync_scanning)
if (metadataOnly) "Folder Sync: Updating metadata..." else "Scanning folder for new books..."
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
@ -1633,7 +1629,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
it.copy( it.copy(
isLoading = false, isLoading = false,
isRefreshing = false, isRefreshing = false,
bannerMessage = if (showFeedback) BannerMessage("Folder Sync: Scan complete.") else it.bannerMessage, bannerMessage = if (showFeedback) BannerMessage(appContext.getString(R.string.banner_folder_sync_complete)) else it.bannerMessage,
lastFolderScanTime = System.currentTimeMillis(), lastFolderScanTime = System.currentTimeMillis(),
syncedFolders = loadSyncedFoldersFromPrefs() syncedFolders = loadSyncedFoldersFromPrefs()
) )
@ -1645,7 +1641,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
it.copy( it.copy(
isLoading = false, isLoading = false,
isRefreshing = false, isRefreshing = false,
errorMessage = if (showFeedback) "Sync failed." else it.errorMessage, errorMessage = if (showFeedback) appContext.getString(R.string.error_sync_failed) else it.errorMessage,
bannerMessage = null bannerMessage = null
) )
} }
@ -1748,7 +1744,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private fun downloadBook(item: RecentFileItem, openWhenComplete: Boolean = false): Job { private fun downloadBook(item: RecentFileItem, openWhenComplete: Boolean = false): Job {
if (!uiState.value.isSyncEnabled) { if (!uiState.value.isSyncEnabled) {
_internalState.update { it.copy(errorMessage = "Enable sync to download files.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_enable_sync_download)) }
return viewModelScope.launch {} return viewModelScope.launch {}
} }
if (uiState.value.downloadingBookIds.contains(item.bookId)) { if (uiState.value.downloadingBookIds.contains(item.bookId)) {
@ -1800,7 +1796,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to download book ${item.bookId}") Timber.e(e, "Failed to download book ${item.bookId}")
_internalState.update { _internalState.update {
it.copy(errorMessage = "Failed to download ${item.displayName}.") it.copy(errorMessage = appContext.getString(R.string.error_download_failed, item.displayName))
} }
} finally { } finally {
_internalState.update { state -> _internalState.update { state ->
@ -1812,13 +1808,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
fun deleteAllCloudAndLocalData() { fun deleteAllCloudAndLocalData() {
if (!uiState.value.isSyncEnabled) { if (!uiState.value.isSyncEnabled) {
_internalState.update { it.copy(errorMessage = "Enable sync to clear cloud data.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_enable_sync_clear_cloud)) }
return return
} }
if (!googleDriveRepository.isUserSignedInToDrive(appContext)) { if (!googleDriveRepository.isUserSignedInToDrive(appContext)) {
_internalState.update { _internalState.update {
it.copy(errorMessage = "Not signed in, cannot clear cloud data.") it.copy(errorMessage = appContext.getString(R.string.error_not_signed_in_clear_cloud))
} }
return return
} }
@ -1826,7 +1822,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = true, isLoading = true,
bannerMessage = BannerMessage("Clearing all cloud and local data...") bannerMessage = BannerMessage(appContext.getString(R.string.banner_clearing_cloud_local_data))
) )
} }
@ -1855,9 +1851,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (success) { if (success) {
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, bannerMessage = BannerMessage( isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_local_data_cleared))
"All cloud and local data cleared successfully."
)
) )
} }
} else { } else {
@ -1866,7 +1860,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to delete all cloud and local user data.") Timber.e(e, "Failed to delete all cloud and local user data.")
_internalState.update { _internalState.update {
it.copy(isLoading = false, errorMessage = "Error: Failed to clear all data.") it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_clear_all_data))
} }
} }
} }
@ -1913,13 +1907,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, bannerMessage = BannerMessage("All local data cleared.") isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_local_data_cleared))
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to delete all user data.") Timber.e(e, "Failed to delete all user data.")
_internalState.update { _internalState.update {
it.copy(isLoading = false, errorMessage = "Error: Failed to clear all data.") it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_clear_all_data))
} }
} }
} }
@ -1933,9 +1927,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (user == null) { if (user == null) {
_internalState.update { _internalState.update {
it.copy( it.copy(
bannerMessage = BannerMessage( bannerMessage = BannerMessage(appContext.getString(R.string.error_sign_in_failed), isError = true), isLoading = false
"Sign in failed. Please try again.", isError = true
), isLoading = false
) )
} }
} else { } else {
@ -1950,9 +1942,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "An unexpected error occurred during sign-in.") Timber.e(e, "An unexpected error occurred during sign-in.")
val errorMessage = if (e is NoCredentialException) { val errorMessage = if (e is NoCredentialException) {
"Could not find a Google account. This can happen on a fresh install, please try again in a moment." appContext.getString(R.string.error_no_google_account)
} else { } else {
"An error occurred during sign in. Please check your internet connection." appContext.getString(R.string.error_sign_in_internet)
} }
_internalState.update { _internalState.update {
it.copy( it.copy(
@ -2003,7 +1995,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
) )
} }
} ?: run { } ?: run {
showBanner("Please sign in to test device management.", isError = true) showBanner(appContext.getString(R.string.error_sign_in_device_management), isError = true)
} }
} }
} }
@ -2020,7 +2012,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
fun setSyncEnabled(enabled: Boolean) { fun setSyncEnabled(enabled: Boolean) {
if (!uiState.value.isProUser) { if (!uiState.value.isProUser) {
Timber.d("Sync toggle blocked for free user.") Timber.d("Sync toggle blocked for free user.")
_internalState.update { it.copy(errorMessage = "Sync is an Episteme Pro feature.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_sync_pro_feature)) }
return return
} }
@ -2054,14 +2046,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (!hasPermissions || currentUser == null) { if (!hasPermissions || currentUser == null) {
if (showBanner) _internalState.update { if (showBanner) _internalState.update {
it.copy(errorMessage = "Not signed in, cannot sync.") it.copy(errorMessage = appContext.getString(R.string.error_not_signed_in_sync))
} }
return@launch return@launch
} }
if (showBanner) { if (showBanner) {
_internalState.update { _internalState.update {
it.copy(bannerMessage = BannerMessage("Cloud Sync: Checking for updates...")) it.copy(bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_sync_checking)))
} }
} }
@ -2281,7 +2273,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (showBanner) { if (showBanner) {
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, bannerMessage = BannerMessage("Cloud Sync: Complete.") isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_sync_complete))
) )
} }
} }
@ -2289,7 +2281,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.tag("AnnotationSync").e(e, "Error during cloud sync") Timber.tag("AnnotationSync").e(e, "Error during cloud sync")
if (showBanner) { if (showBanner) {
_internalState.update { _internalState.update {
it.copy(isLoading = false, errorMessage = "Failed to sync library.") it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_sync_library_failed))
} }
} }
} }
@ -2654,14 +2646,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) { fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) {
if (isFromRecent) { if (isFromRecent) {
Timber.i("Opening recent file: $uri") Timber.i("Opening recent file: $uri")
// This path is now handled by onRecentFileClicked to preserve the bookId
// We find the book by URI to open it.
viewModelScope.launch { viewModelScope.launch {
val item = recentFilesRepository.getFileByUri(uri.toString()) val item = recentFilesRepository.getFileByUri(uri.toString())
if (item != null) { if (item != null) {
openBook(uri, item.bookId, item.type, item.displayName) openBook(uri, item.bookId, item.type, item.displayName)
} else { } else {
_internalState.update { it.copy(errorMessage = "Could not find recent item.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_recent_item_not_found)) }
} }
} }
} else { } else {
@ -2698,7 +2688,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
} }
_internalState.update { _internalState.update {
it.copy(isLoading = false, errorMessage = "Failed to import file.") it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_import_file_failed))
} }
} }
} }
@ -2736,7 +2726,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val uri = item.getUri() ?: run { val uri = item.getUri() ?: run {
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, errorMessage = "Could not find file location." isLoading = false, errorMessage = appContext.getString(R.string.error_file_location_not_found)
) )
} }
stateUpdateDeferred.complete(false) stateUpdateDeferred.complete(false)
@ -2822,7 +2812,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
errorMessage = "Failed to load generated text view.", errorMessage = appContext.getString(R.string.error_load_generated_text_view),
selectedFileType = null selectedFileType = null
) )
} }
@ -2884,10 +2874,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (newItem != null) { if (newItem != null) {
switchToFileSeamlessly(newItem, autoOpenPage) switchToFileSeamlessly(newItem, autoOpenPage)
} else { } else {
showBanner("Failed to load generated text view.", true) showBanner(appContext.getString(R.string.error_load_generated_text_view), true)
} }
} else { } else {
showBanner("Text view generation failed.", true) showBanner(appContext.getString(R.string.error_text_view_generation_failed), true)
} }
} }
} }
@ -3076,7 +3066,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error parsing FB2 for URI: $uri") Timber.e(e, "Error parsing FB2 for URI: $uri")
_internalState.update { _internalState.update {
it.copy(errorMessage = "Failed to load FB2: ${e.message}", isLoading = false) it.copy(errorMessage = appContext.getString(R.string.error_load_fb2, e.message), isLoading = false)
} }
} }
} }
@ -3131,7 +3121,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error parsing file ($type) for URI: $uri") Timber.e(e, "Error parsing file ($type) for URI: $uri")
_internalState.update { _internalState.update {
it.copy(errorMessage = "Failed to load file: ${e.message}", isLoading = false) it.copy(errorMessage = appContext.getString(R.string.error_load_file, e.message), isLoading = false)
} }
} }
} }
@ -3271,7 +3261,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error parsing MOBI for URI: $uri") Timber.e(e, "Error parsing MOBI for URI: $uri")
_internalState.update { _internalState.update {
it.copy(errorMessage = "Failed to load MOBI: ${e.message}", isLoading = false) it.copy(errorMessage = appContext.getString(R.string.error_load_mobi, e.message), isLoading = false)
} }
} }
} }
@ -3318,7 +3308,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error parsing EPUB for URI: $uri") Timber.e(e, "Error parsing EPUB for URI: $uri")
_internalState.update { _internalState.update {
it.copy(errorMessage = "Failed to load EPUB: ${e.message}", isLoading = false) it.copy(errorMessage = appContext.getString(R.string.error_load_epub, e.message), isLoading = false)
} }
} }
} }
@ -3454,7 +3444,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.tag("FolderSync") Timber.tag("FolderSync")
.i("LazyCleanup: File ${item.displayName} missing. Removing.") .i("LazyCleanup: File ${item.displayName} missing. Removing.")
recentFilesRepository.deleteFilePermanently(listOf(item.bookId)) recentFilesRepository.deleteFilePermanently(listOf(item.bookId))
showBanner("File deleted from folder. Removed from library.") showBanner(appContext.getString(R.string.banner_file_deleted_from_folder))
return@launch return@launch
} }
@ -3463,7 +3453,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
item.getUri()?.let { uri -> item.getUri()?.let { uri ->
openBook(uri, item.bookId, item.type, item.displayName) openBook(uri, item.bookId, item.type, item.displayName)
} ?: run { } ?: run {
_internalState.update { it.copy(errorMessage = "Could not find file location.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_file_location_not_found)) }
} }
} else { } else {
downloadBook(item, openWhenComplete = true) downloadBook(item, openWhenComplete = true)
@ -3477,7 +3467,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
item.getUri()?.let { uri -> item.getUri()?.let { uri ->
openBook(uri, item.bookId, item.type, item.displayName) openBook(uri, item.bookId, item.type, item.displayName)
} ?: run { } ?: run {
_internalState.update { it.copy(errorMessage = "Could not find file location.") } _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_file_location_not_found)) }
return return
} }
} else { } else {
@ -3618,7 +3608,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (newName in currentShelves) { if (newName in currentShelves) {
Timber.w("Cannot rename shelf. A shelf with the name '$newName' already exists.") Timber.w("Cannot rename shelf. A shelf with the name '$newName' already exists.")
_internalState.update { _internalState.update {
it.copy(errorMessage = "A shelf with that name already exists.") it.copy(errorMessage = appContext.getString(R.string.error_shelf_exists))
} }
dismissRenameShelfDialog() dismissRenameShelfDialog()
return return
@ -3938,7 +3928,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = true, isLoading = true,
bannerMessage = BannerMessage("Deleting from all devices...") bannerMessage = BannerMessage(appContext.getString(R.string.banner_deleting_all_devices))
) )
} }
try { try {
@ -3974,7 +3964,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
bannerMessage = BannerMessage("Deletion complete.") bannerMessage = BannerMessage(appContext.getString(R.string.banner_deletion_complete))
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -3986,7 +3976,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
errorMessage = "Cloud sync failed, deleted locally." errorMessage = appContext.getString(R.string.error_cloud_sync_failed_deleted_locally)
) )
} }
} }
@ -4003,7 +3993,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { _internalState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
bannerMessage = BannerMessage("$totalRemoved book(s) removed from library.") bannerMessage = BannerMessage(appContext.getString(R.string.banner_books_removed_library, totalRemoved))
) )
} }
} }
@ -4118,7 +4108,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showBanner("Reflow cache & generated text views cleared.") showBanner(appContext.getString(R.string.banner_reflow_cache_cleared))
} }
} }
} }

View file

@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -175,8 +176,7 @@ fun ProScreen(
shape = CircleShape shape = CircleShape
), ),
text = { text = {
AutoSizeText( AutoSizeText(stringResource(R.string.tab_free),
"Free",
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
@ -209,8 +209,7 @@ fun ProScreen(
tint = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
AutoSizeText( AutoSizeText(stringResource(R.string.drawer_pro_unlocked),
"Episteme Pro",
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
@ -267,19 +266,16 @@ private fun FreeTierCard() {
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(stringResource(R.string.free_plan),
text = "Free Plan",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(stringResource(R.string.price_free),
text = "$0",
style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp), style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(stringResource(R.string.forever_free),
text = "Forever free",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -289,23 +285,20 @@ private fun FreeTierCard() {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
FeatureListItem(iconRes = R.drawable.library_books, text = "Multiple Formats") FeatureListItem(iconRes = R.drawable.library_books, text = stringResource(R.string.feature_multiple_formats))
Text( Text(stringResource(R.string.feature_multiple_formats_desc),
text = "Supports PDF, EPUB, MOBI, AZW3",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.text_to_speech, text = "Android Text-to-Speech") FeatureListItem(iconRes = R.drawable.text_to_speech, text = stringResource(R.string.feature_tts))
Text( Text(stringResource(R.string.feature_tts_desc),
text = "Listen to your books with built-in TTS",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.dictionary, text = "Basic Dictionary") FeatureListItem(iconRes = R.drawable.dictionary, text = stringResource(R.string.feature_dict))
Text( Text(stringResource(R.string.feature_dict_desc),
text = "Look up single words quickly",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
@ -325,7 +318,7 @@ private fun FreeTierCard() {
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) )
) { ) {
Text("Current Plan", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.current_plan), fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
} }
} }
} }
@ -386,8 +379,7 @@ private fun ProTierCard(
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(stringResource(R.string.drawer_pro_unlocked),
text = "Episteme Pro",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -404,8 +396,7 @@ private fun ProTierCard(
withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) { withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
append(originalFormattedPrice) append(originalFormattedPrice)
} }
append(" 50% OFF") append(" " + stringResource(R.string.pro_sale_off)) },
},
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -416,16 +407,14 @@ private fun ProTierCard(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} else { } else {
Text( Text(stringResource(R.string.loading_price),
text = "Loading price...",
style = MaterialTheme.typography.displaySmall.copy(fontSize = 32.sp), style = MaterialTheme.typography.displaySmall.copy(fontSize = 32.sp),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(stringResource(R.string.one_time_payment),
text = "One-time payment",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -435,8 +424,7 @@ private fun ProTierCard(
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) { ) {
Text( Text(stringResource(R.string.lifetime_access),
text = "Lifetime Access",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
) )
@ -461,7 +449,7 @@ private fun ProTierCard(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text("Early Access Sale", style = MaterialTheme.typography.labelLarge) Text(stringResource(R.string.early_access_sale), style = MaterialTheme.typography.labelLarge)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
@ -471,36 +459,31 @@ private fun ProTierCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
Text( Text(stringResource(R.string.pro_includes),
text = "Everything in Free, plus:",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.cloud_sync, text = "Cloud Sync Across Devices") FeatureListItem(iconRes = R.drawable.cloud_sync, text = stringResource(R.string.feature_cloud_sync))
Text( Text(stringResource(R.string.feature_cloud_sync_desc),
text = "Keep your entire library, including book files and reading progress, synced across up to 4 devices.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.summarize, text = "Summarization") FeatureListItem(iconRes = R.drawable.summarize, text = stringResource(R.string.feature_summarize))
Text( Text(stringResource(R.string.feature_summarize_desc),
text = "Get quick summaries of chapters or pages",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.dictionary, text = "Smart Dictionary") FeatureListItem(iconRes = R.drawable.dictionary, text = stringResource(R.string.feature_smart_dict))
Text( Text(stringResource(R.string.feature_smart_dict_desc),
text = "Search phrases and even paragraphs, not just single words",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
) )
FeatureListItem(iconRes = R.drawable.chat_bubble, text = "Priority Feature Requests") FeatureListItem(iconRes = R.drawable.chat_bubble, text = stringResource(R.string.feature_priority))
Text( Text(stringResource(R.string.feature_priority_desc),
text = "Your suggestions get prioritized",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
@ -526,8 +509,7 @@ private fun ProTierCard(
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(stringResource(R.string.pro_unlocked),
text = "Pro Features Unlocked!",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
@ -547,7 +529,7 @@ private fun ProTierCard(
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
AutoSizeText("Sign in Required", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) AutoSizeText(stringResource(R.string.sign_in_required), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold))
} }
} }
proUpgradeState.isVerifying -> { proUpgradeState.isVerifying -> {
@ -565,7 +547,7 @@ private fun ProTierCard(
color = LocalContentColor.current color = LocalContentColor.current
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text("Verifying purchase...") Text(stringResource(R.string.verifying_purchase))
} }
} }
localPurchaseExistsForOtherAccount -> { localPurchaseExistsForOtherAccount -> {
@ -582,7 +564,7 @@ private fun ProTierCard(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
AutoSizeText("Existing Purchase Found") AutoSizeText(stringResource(R.string.existing_purchase_found))
} }
} }
productDetails != null -> { productDetails != null -> {
@ -604,7 +586,7 @@ private fun ProTierCard(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
AutoSizeText("Get Lifetime Access", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) AutoSizeText(stringResource(R.string.get_lifetime_access), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold))
} }
} }
} }
@ -614,8 +596,7 @@ private fun ProTierCard(
} }
} }
else -> { else -> {
Text( Text(stringResource(R.string.upgrade_unavailable),
text = "Upgrade currently unavailable. Please check your internet and try again.",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@ -628,16 +609,14 @@ private fun ProTierCard(
Spacer(modifier = Modifier.height(16.dp)) // Increased spacing Spacer(modifier = Modifier.height(16.dp)) // Increased spacing
when { when {
!isUserSignedIn -> { !isUserSignedIn -> {
Text( Text(stringResource(R.string.sign_in_to_purchase),
text = "Please sign in to your Google account to purchase Episteme Pro.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
proUpgradeState.isVerifying -> { proUpgradeState.isVerifying -> {
Text( Text(stringResource(R.string.verifying_purchase_desc),
text = "This may take a few moments. Your Pro status will be updated automatically.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -691,10 +670,10 @@ fun ExistingPurchaseDialog(onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Existing Purchase Found") }, title = { Text(stringResource(R.string.existing_purchase_found)) },
text = { Text("This device already has a Pro purchase, but it's linked to a different account. Please sign in to the account that was used for the original purchase to restore your Pro features.") }, text = { Text(stringResource(R.string.dialog_existing_purchase_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = onDismiss) { Text("OK") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_ok)) }
} }
) )
} }
@ -704,10 +683,10 @@ fun EarlyAccessInfoDialog(onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Early Access Sale") }, title = { Text(stringResource(R.string.early_access_sale)) },
text = { Text("You're getting Episteme Pro at a special discounted price during our early access period! This is a limited-time offer.") }, text = { Text(stringResource(R.string.dialog_early_access_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = onDismiss) { Text("Got It!") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_got_it)) }
} }
) )
} }
@ -716,14 +695,14 @@ fun EarlyAccessInfoDialog(onDismiss: () -> Unit) {
fun SignInRequiredDialog(onSignInClick: () -> Unit, onDismiss: () -> Unit) { fun SignInRequiredDialog(onSignInClick: () -> Unit, onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) }, // Using crown icon for Pro icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) },
title = { Text("Sign In Required") }, title = { Text(stringResource(R.string.sign_in_required)) },
text = { Text("Please sign in to your Google account to purchase Episteme Pro and unlock all premium features.") }, text = { Text(stringResource(R.string.dialog_sign_in_required_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = onSignInClick) { Text("Sign In") } TextButton(onClick = onSignInClick) { Text(stringResource(R.string.drawer_sign_in)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Not Now") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_not_now)) }
} }
) )
} }

View file

@ -17,6 +17,7 @@
* *
* mail: epistemereader@gmail.com * mail: epistemereader@gmail.com
*/ */
// SharedComposables.kt
package com.aryan.reader package com.aryan.reader
import android.content.Context import android.content.Context
@ -31,7 +32,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -53,22 +53,18 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.ChevronRight
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.FileOpen import androidx.compose.material.icons.outlined.FileOpen
import androidx.compose.material.icons.outlined.Gavel import androidx.compose.material.icons.outlined.Gavel
import androidx.compose.material.icons.outlined.Policy import androidx.compose.material.icons.outlined.Policy
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -76,6 +72,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -88,12 +85,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -102,7 +99,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -146,34 +142,29 @@ fun formatFileSize(bytes: Long): String {
@Composable @Composable
fun LegalText( fun LegalText(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
prefixText: String, // Changed from baseText prefixText: String,
textAlign: TextAlign = TextAlign.Center textAlign: TextAlign = TextAlign.Center
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val fullAgreementText = stringResource(R.string.legal_agreement_full, prefixText, stringResource(R.string.legal_terms_of_service), stringResource(R.string.legal_privacy_policy))
val termsText = stringResource(R.string.legal_terms_of_service)
val privacyText = stringResource(R.string.legal_privacy_policy)
val annotatedString = buildAnnotatedString { val annotatedString = buildAnnotatedString {
append("$prefixText you agree to our ") append(fullAgreementText)
pushStringAnnotation(tag = "terms", annotation = TERMS_URL)
withStyle( val termsStartIndex = fullAgreementText.indexOf(termsText)
style = SpanStyle( if (termsStartIndex >= 0) {
color = MaterialTheme.colorScheme.primary, addStringAnnotation(tag = "terms", annotation = TERMS_URL, start = termsStartIndex, end = termsStartIndex + termsText.length)
textDecoration = TextDecoration.Underline addStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline), start = termsStartIndex, end = termsStartIndex + termsText.length)
)
) {
append("Terms of Service")
} }
pop()
append(" and acknowledge you have read our ") val privacyStartIndex = fullAgreementText.indexOf(privacyText)
pushStringAnnotation(tag = "privacy", annotation = PRIVACY_POLICY_URL) if (privacyStartIndex >= 0) {
withStyle( addStringAnnotation(tag = "privacy", annotation = PRIVACY_POLICY_URL, start = privacyStartIndex, end = privacyStartIndex + privacyText.length)
style = SpanStyle( addStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline), start = privacyStartIndex, end = privacyStartIndex + privacyText.length)
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
)
) {
append("Privacy Policy")
} }
pop()
append(".")
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -225,30 +216,30 @@ fun ContextualTopAppBar(
onDeleteClick: () -> Unit onDeleteClick: () -> Unit
) { ) {
CustomTopAppBar( CustomTopAppBar(
title = { Text("$selectedItemCount selected") }, title = { Text(stringResource(R.string.items_selected_count, selectedItemCount)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavIconClick) { IconButton(onClick = onNavIconClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Clear Selection") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.clear_selection))
} }
}, },
actions = { actions = {
if (onPinClick != null) { if (onPinClick != null) {
IconButton(onClick = onPinClick) { IconButton(onClick = onPinClick) {
Icon(Icons.Filled.PushPin, contentDescription = "Pin/Unpin") Icon(Icons.Filled.PushPin, contentDescription = stringResource(R.string.pin_unpin))
} }
} }
if (selectedItemCount == 1 && onInfoClick != null) { if (selectedItemCount == 1 && onInfoClick != null) {
IconButton(onClick = onInfoClick) { IconButton(onClick = onInfoClick) {
Icon(Icons.Filled.Info, contentDescription = "Info") Icon(Icons.Filled.Info, contentDescription = stringResource(R.string.info))
} }
} }
if (onSelectAllClick != null) { if (onSelectAllClick != null) {
IconButton(onClick = onSelectAllClick) { IconButton(onClick = onSelectAllClick) {
Icon(Icons.Filled.SelectAll, contentDescription = "Select All") Icon(Icons.Filled.SelectAll, contentDescription = stringResource(R.string.select_all))
} }
} }
IconButton(onClick = onDeleteClick) { IconButton(onClick = onDeleteClick) {
Icon(Icons.Filled.Delete, contentDescription = "Delete") Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.action_delete))
} }
} }
) )
@ -302,21 +293,21 @@ fun DeleteConfirmationDialog(
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
isPermanentDelete: Boolean = false, isPermanentDelete: Boolean = false,
containsFolderItems: Boolean = false // New parameter containsFolderItems: Boolean = false
) { ) {
val title = if (isPermanentDelete) "Delete File(s) Permanently" else "Remove from Recents" val title = if (isPermanentDelete) stringResource(R.string.dialog_delete_permanently) else stringResource(R.string.dialog_remove_from_recents)
val text = if (isPermanentDelete) { val text = if (isPermanentDelete) {
if (containsFolderItems) { if (containsFolderItems) {
"Warning: Some selected items are synced from a local folder. Proceeding will delete the actual files from your device storage.\n\nThis action cannot be undone." stringResource(R.string.dialog_warning_folder_sync_delete)
} else { } else {
"Do you want to permanently delete $count selected file(s) from your device? This action cannot be undone." stringResource(R.string.dialog_permanently_delete_desc, count)
} }
} else { } else {
"Do you want to remove $count selected file(s) from the recent files list? It will reappear if you open it again from the library." stringResource(R.string.dialog_remove_recents_desc, count)
} }
val confirmText = if (isPermanentDelete) "Delete" else "Remove" val confirmText = if (isPermanentDelete) stringResource(R.string.action_delete) else stringResource(R.string.action_remove)
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@ -336,7 +327,7 @@ fun DeleteConfirmationDialog(
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -407,7 +398,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
"File Information", stringResource(R.string.file_information),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@ -415,7 +406,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
androidx.compose.material3.OutlinedTextField( androidx.compose.material3.OutlinedTextField(
value = editingName, value = editingName,
onValueChange = { editingName = it }, onValueChange = { editingName = it },
label = { Text("Book Name") }, label = { Text(stringResource(R.string.book_name)) },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 64.dp, max = 130.dp), .heightIn(min = 64.dp, max = 130.dp),
@ -425,14 +416,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
IconButton(onClick = { IconButton(onClick = {
clipboardManager.setText(AnnotatedString(editingName)) clipboardManager.setText(AnnotatedString(editingName))
}) { }) {
Icon(Icons.Default.ContentCopy, contentDescription = "Copy Name", modifier = Modifier.size(20.dp)) Icon(Icons.Default.ContentCopy, contentDescription = stringResource(R.string.copy_name), modifier = Modifier.size(20.dp))
} }
} }
) )
if (hasCustomName) { if (hasCustomName) {
Text( Text(
text = "Original Name: $originalName", text = stringResource(R.string.original_name, originalName),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp), modifier = Modifier.padding(top = 2.dp),
@ -447,11 +438,11 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
modifier = Modifier.align(Alignment.End), modifier = Modifier.align(Alignment.End),
contentPadding = PaddingValues(0.dp) contentPadding = PaddingValues(0.dp)
) { ) {
Text("Revert to Original") Text(stringResource(R.string.revert_to_original))
} }
} else if (originalName != item.displayName) { } else if (originalName != item.displayName) {
Text( Text(
text = "File Name: ${item.displayName}", text = stringResource(R.string.file_name, item.displayName),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2, maxLines = 2,
@ -463,18 +454,27 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
item.author?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }?.let { item.author?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }?.let {
InfoRowDetailed("Author", it) InfoRowDetailed(stringResource(R.string.author), it)
} }
InfoRowDetailed("Format", item.type.name) InfoRowDetailed(stringResource(R.string.format), item.type.name)
InfoRowDetailed("Size", formatFileSize(item.fileSize)) InfoRowDetailed(stringResource(R.string.size), formatFileSize(item.fileSize))
InfoRowDetailed("Added", formattedDate) InfoRowDetailed(stringResource(R.string.added), formattedDate)
val pathTextFinal = if (isOpdsStream) {
stringResource(R.string.source_opds)
} else if (pathText == "In-App Storage") {
stringResource(R.string.source_in_app)
} else {
pathText.replace("Internal storage", stringResource(R.string.internal_storage))
}
InfoRowDetailed( InfoRowDetailed(
label = "Location", label = stringResource(R.string.location),
value = pathText, value = pathTextFinal,
maxLines = 4, maxLines = 4,
isScrollable = true, isScrollable = true,
onCopy = { onCopy = {
clipboardManager.setText(AnnotatedString(pathText)) clipboardManager.setText(AnnotatedString(pathTextFinal))
} }
) )
} }
@ -485,7 +485,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
.padding(top = 8.dp), .padding(top = 8.dp),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
androidx.compose.material3.Button(onClick = { androidx.compose.material3.Button(onClick = {
val finalName = editingName.trim() val finalName = editingName.trim()
@ -497,7 +497,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
} }
} }
onDismiss() onDismiss()
}) { Text("Save") } }) { Text(stringResource(R.string.action_save)) }
} }
} }
} }
@ -509,7 +509,7 @@ private fun InfoRowDetailed(
label: String, label: String,
value: String, value: String,
maxLines: Int = 1, maxLines: Int = 1,
isScrollable: Boolean = false, // ADD THIS isScrollable: Boolean = false,
onCopy: (() -> Unit)? = null onCopy: (() -> Unit)? = null
) { ) {
Row( Row(
@ -603,123 +603,101 @@ fun AboutDialog(onDismiss: () -> Unit) {
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
title = { title = {
Row( Column(
verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.CenterHorizontally,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text( Text(
text = "Episteme", text = stringResource(R.string.about_app_name),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = if (isOss) "Open Source Version" else "Playstore Version", text = if (isOss) stringResource(R.string.about_oss_version) else stringResource(R.string.about_play_version),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
} }
}
}, },
text = { text = {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()),
) { horizontalAlignment = Alignment.CenterHorizontally
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainerLowest,
modifier = Modifier.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(12.dp)
) { ) {
Text( Text(
text = "Version ${BuildConfig.VERSION_NAME}", text = stringResource(R.string.about_version_name, BuildConfig.VERSION_NAME),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
Text( Text(
text = "Build ${BuildConfig.VERSION_CODE}", text = stringResource(R.string.about_build_code, BuildConfig.VERSION_CODE.toString()),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(20.dp))
HorizontalDivider()
if (isOss) { if (isOss) {
Spacer(modifier = Modifier.height(12.dp)) AboutInfoRow(
Row(verticalAlignment = Alignment.CenterVertically) { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Code, painter = painterResource(id = R.drawable.github),
contentDescription = null, contentDescription = stringResource(R.string.about_github),
modifier = Modifier.size(18.dp), modifier = Modifier.size(22.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(8.dp)) },
Text( text = stringResource(R.string.about_github),
text = "Open Source", subtitle = stringResource(R.string.about_github_desc),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(8.dp))
InfoRow(
icon = Icons.Outlined.Code,
text = "GitHub Repository",
subtitle = "Browse source code, star, and fork",
onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme") } onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme") }
) )
InfoRow(
icon = Icons.Outlined.BugReport,
text = "Report an Issue",
subtitle = "File a bug or feature request",
onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme/issues") }
)
} else { } else {
Spacer(modifier = Modifier.height(12.dp)) AboutInfoRow(
Text( icon = {
text = "Legal", Icon(
style = MaterialTheme.typography.titleSmall, imageVector = Icons.Outlined.Policy,
color = MaterialTheme.colorScheme.primary, contentDescription = null,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.size(22.dp),
tint = MaterialTheme.colorScheme.primary
) )
},
InfoRow( text = stringResource(R.string.about_privacy),
icon = Icons.Outlined.Policy, subtitle = stringResource(R.string.about_privacy_desc),
text = "Privacy Policy",
subtitle = "How we handle your data",
onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) } onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }
) )
InfoRow( Spacer(modifier = Modifier.height(10.dp))
icon = Icons.Outlined.Gavel,
text = "Terms of Service", AboutInfoRow(
subtitle = "Usage terms and conditions", icon = {
Icon(
imageVector = Icons.Outlined.Gavel,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colorScheme.primary
)
},
text = stringResource(R.string.about_terms),
subtitle = stringResource(R.string.about_terms_desc),
onClick = { uriHandler.openUri(TERMS_URL) } onClick = { uriHandler.openUri(TERMS_URL) }
) )
InfoRow( Spacer(modifier = Modifier.height(10.dp))
icon = Icons.Outlined.FileOpen,
text = "Licenses", AboutInfoRow(
subtitle = "Libraries", icon = {
Icon(
imageVector = Icons.Outlined.FileOpen,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colorScheme.primary
)
},
text = stringResource(R.string.legal_licenses),
subtitle = stringResource(R.string.about_licenses_desc),
onClick = { uriHandler.openUri(LICENSES_URL) } onClick = { uriHandler.openUri(LICENSES_URL) }
) )
} }
@ -731,50 +709,40 @@ fun AboutDialog(onDismiss: () -> Unit) {
shape = RoundedCornerShape(50), shape = RoundedCornerShape(50),
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp)
) { ) {
Text("Close", fontWeight = FontWeight.Medium) Text(stringResource(R.string.action_close), fontWeight = FontWeight.Medium)
} }
} }
) )
} }
@Composable @Composable
private fun InfoRow( private fun AboutInfoRow(
icon: ImageVector, icon: @Composable () -> Unit,
text: String, text: String,
subtitle: String? = null, subtitle: String? = null,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Card( OutlinedCard(
modifier = Modifier onClick = onClick,
.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
.padding(vertical = 4.dp) shape = RoundedCornerShape(12.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 16.dp), .padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( icon()
imageVector = icon, Spacer(modifier = Modifier.width(14.dp))
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Bold
) )
if (subtitle != null) { if (subtitle != null) {
Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = subtitle, text = subtitle,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@ -782,10 +750,11 @@ private fun InfoRow(
) )
} }
} }
Spacer(modifier = Modifier.width(4.dp))
Icon( Icon(
imageVector = Icons.Outlined.ChevronRight, imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp), modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -798,7 +767,7 @@ fun EmptyState(
message: String, message: String,
onSelectFileClick: () -> Unit, onSelectFileClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
primaryButtonText: String = "Select a File", primaryButtonText: String = stringResource(R.string.empty_select_file),
secondaryButtonText: String? = null, secondaryButtonText: String? = null,
onSecondaryClick: (() -> Unit)? = null onSecondaryClick: (() -> Unit)? = null
) { ) {
@ -858,19 +827,15 @@ fun SelectFileButton(onClick: () -> Unit, text: String) {
fun ClearCloudDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { fun ClearCloudDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Clear All Synced Data?") }, title = { Text(stringResource(R.string.clear_cloud_data_title)) },
text = { Text("Are you sure you want to permanently delete all of your book data from the cloud? This will also wipe your local library to prevent re-syncing. This action cannot be undone.") }, text = { Text(stringResource(R.string.clear_cloud_data_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = onConfirm, onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) { ) { Text(stringResource(R.string.delete_all_data)) }
Text("DELETE ALL DATA")
}
}, },
dismissButton = { dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }
TextButton(onClick = onDismiss) { Text("Cancel") }
}
) )
} }
@ -905,3 +870,23 @@ fun AutoSizeText(
} }
) )
} }
@Composable
fun FileTypeBadge(type: FileType, modifier: Modifier = Modifier, overlay: Boolean = false) {
val containerColor = if (overlay) androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.6f) else MaterialTheme.colorScheme.secondaryContainer
val contentColor = if (overlay) androidx.compose.ui.graphics.Color.White else MaterialTheme.colorScheme.onSecondaryContainer
Surface(
modifier = modifier,
shape = RoundedCornerShape(4.dp),
color = containerColor,
contentColor = contentColor
) {
Text(
text = type.name.uppercase(),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}

View file

@ -36,11 +36,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.aryan.reader.BuildConfig import com.aryan.reader.BuildConfig
import com.aryan.reader.R
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -83,7 +85,7 @@ fun DictionarySettingsDialog(
.padding(24.dp) .padding(24.dp)
) { ) {
Text( Text(
text = "Lookup Settings", text = stringResource(R.string.dict_lookup_settings),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 20.dp) modifier = Modifier.padding(bottom = 20.dp)
@ -98,7 +100,7 @@ fun DictionarySettingsDialog(
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text( Text(
text = "Dictionary Engine", text = stringResource(R.string.dict_dictionary_engine),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -114,29 +116,29 @@ fun DictionarySettingsDialog(
onClick = { onToggleOnlineDictionary(true) }, onClick = { onToggleOnlineDictionary(true) },
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2) shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
) { ) {
Text("Smart (AI)") Text(stringResource(R.string.dict_smart_ai))
} }
SegmentedButton( SegmentedButton(
selected = !useOnlineDictionary, selected = !useOnlineDictionary,
onClick = { onToggleOnlineDictionary(false) }, onClick = { onToggleOnlineDictionary(false) },
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2) shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
) { ) {
Text("External App") Text(stringResource(R.string.dict_external_app))
} }
} }
Text( Text(
text = if (useOnlineDictionary) text = if (useOnlineDictionary)
"Uses AI for definitions. Will fallback to the external app below if offline or if the selected phrase is too long." stringResource(R.string.dict_ai_description)
else else
"Uses the selected app for dictionary lookups.", stringResource(R.string.dict_external_description),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
Text( Text(
text = if (useOnlineDictionary) "Fallback App" else "Dictionary App", text = if (useOnlineDictionary) stringResource(R.string.dict_fallback_app) else stringResource(R.string.dict_dictionary_app),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -146,13 +148,13 @@ fun DictionarySettingsDialog(
apps = dictionaryApps, apps = dictionaryApps,
selectedPackageName = selectedDictionaryPackageName, selectedPackageName = selectedDictionaryPackageName,
onSelect = onSelectDictionaryPackage, onSelect = onSelectDictionaryPackage,
placeholder = "Select an app" placeholder = stringResource(R.string.dict_select_app)
) )
} }
} }
} else { } else {
Text( Text(
text = "Dictionary", text = stringResource(R.string.tooltip_dictionary),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
@ -161,7 +163,7 @@ fun DictionarySettingsDialog(
apps = dictionaryApps, apps = dictionaryApps,
selectedPackageName = selectedDictionaryPackageName, selectedPackageName = selectedDictionaryPackageName,
onSelect = onSelectDictionaryPackage, onSelect = onSelectDictionaryPackage,
placeholder = "Select an app" placeholder = stringResource(R.string.dict_select_app)
) )
} }
@ -169,13 +171,13 @@ fun DictionarySettingsDialog(
// ── Translate ── // ── Translate ──
Text( Text(
text = "Translate", text = stringResource(R.string.dict_translate),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 4.dp) modifier = Modifier.padding(bottom = 4.dp)
) )
Text( Text(
text = "App used for translating selected text.", text = stringResource(R.string.dict_translate_description),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp) modifier = Modifier.padding(bottom = 12.dp)
@ -185,20 +187,20 @@ fun DictionarySettingsDialog(
apps = dictionaryApps, apps = dictionaryApps,
selectedPackageName = selectedTranslatePackageName, selectedPackageName = selectedTranslatePackageName,
onSelect = onSelectTranslatePackage, onSelect = onSelectTranslatePackage,
placeholder = "Select an app" placeholder = stringResource(R.string.dict_select_app)
) )
SectionDivider() SectionDivider()
// ── Search ── // ── Search ──
Text( Text(
text = "Search", text = stringResource(R.string.tooltip_search),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 4.dp) modifier = Modifier.padding(bottom = 4.dp)
) )
Text( Text(
text = "App used for web searches.", text = stringResource(R.string.dict_search_app_description),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp) modifier = Modifier.padding(bottom = 12.dp)
@ -208,7 +210,7 @@ fun DictionarySettingsDialog(
apps = searchApps, apps = searchApps,
selectedPackageName = selectedSearchPackageName, selectedPackageName = selectedSearchPackageName,
onSelect = onSelectSearchPackage, onSelect = onSelectSearchPackage,
placeholder = "Select an app" placeholder = stringResource(R.string.dict_select_app)
) )
} }
} }
@ -272,7 +274,7 @@ private fun AppSelectionDropdown(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text( Text(
"None", stringResource(R.string.dict_none),
color = if (!hasSelection) MaterialTheme.colorScheme.primary color = if (!hasSelection) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface
) )
@ -281,7 +283,7 @@ private fun AppSelectionDropdown(
{ {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
@ -327,7 +329,7 @@ private fun AppSelectionDropdown(
{ {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }

View file

@ -19,13 +19,18 @@
*/ */
package com.aryan.reader.epubreader package com.aryan.reader.epubreader
import android.content.Context
import androidx.compose.foundation.layout.fillMaxWidth
import timber.log.Timber import timber.log.Timber
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.aryan.reader.AiDefinitionPopup import com.aryan.reader.AiDefinitionPopup
import com.aryan.reader.AiDefinitionResult import com.aryan.reader.AiDefinitionResult
import com.aryan.reader.R import com.aryan.reader.R
@ -137,6 +142,7 @@ suspend fun executeRecapLogic(
characterLimit: Int, characterLimit: Int,
summaryCacheManager: SummaryCacheManager, summaryCacheManager: SummaryCacheManager,
paginator: IPaginator?, paginator: IPaginator?,
context: Context,
onProgressUpdate: (String) -> Unit, onProgressUpdate: (String) -> Unit,
onResultUpdate: (String) -> Unit, onResultUpdate: (String) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
@ -216,6 +222,7 @@ suspend fun executeRecapLogic(
fetchRecap( fetchRecap(
pastSummaries = pastSummaries, pastSummaries = pastSummaries,
currentText = finalContextText, currentText = finalContextText,
context = context,
onUpdate = { chunk -> onResultUpdate(chunk) }, onUpdate = { chunk -> onResultUpdate(chunk) },
onError = { error -> onError(error) }, onError = { error -> onError(error) },
onFinish = { onFinish() } onFinish = { onFinish() }
@ -250,7 +257,7 @@ fun EpubReaderAiOverlays(
) { ) {
if (showSummarizationPopup) { if (showSummarizationPopup) {
SummarizationPopup( SummarizationPopup(
title = "Chapter Summary", title = stringResource(R.string.ai_chapter_summary),
result = summarizationResult, result = summarizationResult,
isLoading = isSummarizationLoading, isLoading = isSummarizationLoading,
onDismiss = onDismissSummarization, onDismiss = onDismissSummarization,
@ -260,7 +267,7 @@ fun EpubReaderAiOverlays(
if (showRecapPopup) { if (showRecapPopup) {
SummarizationPopup( SummarizationPopup(
title = "Story Recap (Beta)", title = stringResource(R.string.ai_story_recap_beta),
result = recapResult, result = recapResult,
isLoading = isRecapLoading, isLoading = isRecapLoading,
onDismiss = onDismissRecap, onDismiss = onDismissRecap,
@ -272,16 +279,26 @@ fun EpubReaderAiOverlays(
AlertDialog( AlertDialog(
onDismissRequest = onDismissSummarizationUpsell, onDismissRequest = onDismissSummarizationUpsell,
icon = { Icon(painter = painterResource(id = R.drawable.summarize), contentDescription = null) }, icon = { Icon(painter = painterResource(id = R.drawable.summarize), contentDescription = null) },
title = { Text("Unlock Chapter Summarization") }, title = {
text = { Text("Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.") }, Text(
text = stringResource(R.string.ai_unlock_summarization),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
text = {
Text(
text = stringResource(R.string.ai_unlock_summarization_desc),
)
},
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onDismissSummarizationUpsell() onDismissSummarizationUpsell()
onNavigateToPro() onNavigateToPro()
}) { Text("Learn More") } }) { Text(stringResource(R.string.action_learn_more)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissSummarizationUpsell) { Text("Not Now") } TextButton(onClick = onDismissSummarizationUpsell) { Text(stringResource(R.string.action_not_now)) }
} }
) )
} }
@ -293,7 +310,6 @@ fun EpubReaderAiOverlays(
isLoading = isAiDefinitionLoading, isLoading = isAiDefinitionLoading,
onDismiss = onDismissAiDefinition, onDismiss = onDismissAiDefinition,
isMainTtsActive = isTtsSessionActive, isMainTtsActive = isTtsSessionActive,
// Pass it down
onOpenExternalDictionary = { onOpenExternalDictionary = {
selectedTextForAi?.let { text -> onOpenExternalDictionary(text) } selectedTextForAi?.let { text -> onOpenExternalDictionary(text) }
} }
@ -304,16 +320,16 @@ fun EpubReaderAiOverlays(
AlertDialog( AlertDialog(
onDismissRequest = onDismissDictionaryUpsell, onDismissRequest = onDismissDictionaryUpsell,
icon = { Icon(painter = painterResource(id = R.drawable.ai), contentDescription = null) }, icon = { Icon(painter = painterResource(id = R.drawable.ai), contentDescription = null) },
title = { Text("Unlock Smart Dictionary") }, title = { Text(stringResource(R.string.ai_unlock_smart_dict)) },
text = { Text("Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.") }, text = { Text(stringResource(R.string.ai_unlock_smart_dict_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onDismissDictionaryUpsell() onDismissDictionaryUpsell()
onNavigateToPro() onNavigateToPro()
}) { Text("Learn More") } }) { Text(stringResource(R.string.action_learn_more)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissDictionaryUpsell) { Text("Not Now") } TextButton(onClick = onDismissDictionaryUpsell) { Text(stringResource(R.string.action_not_now)) }
} }
) )
} }

View file

@ -52,6 +52,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit import androidx.core.content.edit
import com.aryan.reader.R import com.aryan.reader.R
@ -365,7 +366,7 @@ fun BookmarkButton(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.bookmark), painter = painterResource(id = R.drawable.bookmark),
contentDescription = "Bookmark", contentDescription = stringResource(R.string.content_desc_bookmark_icon),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -406,14 +407,14 @@ fun PaletteManagerDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Customize Palette", style = MaterialTheme.typography.titleMedium) }, title = { Text(stringResource(R.string.dialog_customize_palette), style = MaterialTheme.typography.titleMedium) },
text = { text = {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text("Tap a slot to edit:", style = MaterialTheme.typography.bodySmall) Text(stringResource(R.string.palette_tap_slot_to_edit), style = MaterialTheme.typography.bodySmall)
Row( Row(
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@ -427,7 +428,7 @@ fun PaletteManagerDialog(
.background(colorEnum.color, CircleShape) .background(colorEnum.color, CircleShape)
.border( .border(
width = if (isSelected) 3.dp else 1.dp, width = if (isSelected) 3.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, // Thin ring if selected color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent,
shape = CircleShape shape = CircleShape
) )
.clip(CircleShape) .clip(CircleShape)
@ -436,7 +437,7 @@ fun PaletteManagerDialog(
if (isSelected) { if (isSelected) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected Slot", contentDescription = stringResource(R.string.content_desc_selected_slot),
tint = if (colorEnum == HighlightColor.WHITE) Color.Black else Color.White, tint = if (colorEnum == HighlightColor.WHITE) Color.Black else Color.White,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
@ -448,7 +449,7 @@ fun PaletteManagerDialog(
HorizontalDivider() HorizontalDivider()
// 2. Bottom Grid: Available Colors // 2. Bottom Grid: Available Colors
Text("Select a color for the slot:", style = MaterialTheme.typography.bodySmall) Text(stringResource(R.string.palette_select_color_for_slot), style = MaterialTheme.typography.bodySmall)
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 40.dp), columns = GridCells.Adaptive(minSize = 40.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),

View file

@ -211,15 +211,15 @@ fun EpubReaderTopBar(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.dictionary), painter = painterResource(id = R.drawable.dictionary),
contentDescription = "Dictionary Settings" contentDescription = stringResource(R.string.content_desc_dictionary_settings)
) )
} }
TooltipIconButton( TooltipIconButton(
text = "Theme", text = stringResource(R.string.tooltip_theme),
description = "Theme Settings", description = stringResource(R.string.tooltip_theme_desc),
onClick = onOpenThemeSettings onClick = onOpenThemeSettings
) { ) {
Icon(painter = painterResource(id = R.drawable.palette), contentDescription = "Theme Settings") Icon(painter = painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc))
} }
Box { Box {
var showMoreMenu by remember { mutableStateOf(false) } var showMoreMenu by remember { mutableStateOf(false) }
@ -228,7 +228,7 @@ fun EpubReaderTopBar(
description = stringResource(R.string.tooltip_more_options_desc), description = stringResource(R.string.tooltip_more_options_desc),
onClick = { showMoreMenu = true } onClick = { showMoreMenu = true }
) { ) {
Icon(Icons.Default.MoreVert, contentDescription = "More Options") Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_more_options))
} }
DropdownMenu( DropdownMenu(
@ -237,7 +237,7 @@ fun EpubReaderTopBar(
) { ) {
if (onToggleReflow != null) { if (onToggleReflow != null) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("View Original PDF") }, text = { Text(stringResource(R.string.menu_view_original_pdf)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onToggleReflow() onToggleReflow()
@ -256,7 +256,7 @@ fun EpubReaderTopBar(
onDeleteReflow?.let { onDeleteReflow?.let {
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete Text View") }, text = { Text(stringResource(R.string.menu_delete_text_view)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
it() it()
@ -275,26 +275,26 @@ fun EpubReaderTopBar(
} }
DropdownMenuItem( DropdownMenuItem(
text = { Text("Reading Mode: Vertical") }, text = { Text(stringResource(R.string.menu_reading_mode_vertical)) },
enabled = !isTtsActive, enabled = !isTtsActive,
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onChangeRenderMode(RenderMode.VERTICAL_SCROLL) onChangeRenderMode(RenderMode.VERTICAL_SCROLL)
}, },
trailingIcon = { if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(Icons.Default.Check, contentDescription = "Selected") } trailingIcon = { if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Reading Mode: Paginated") }, text = { Text(stringResource(R.string.menu_reading_mode_paginated)) },
enabled = !isTtsActive, enabled = !isTtsActive,
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onChangeRenderMode(RenderMode.PAGINATED) onChangeRenderMode(RenderMode.PAGINATED)
}, },
trailingIcon = { if (currentRenderMode == RenderMode.PAGINATED) Icon(Icons.Default.Check, contentDescription = "Selected") } trailingIcon = { if (currentRenderMode == RenderMode.PAGINATED) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text(if (isBookmarked) "Remove bookmark" else "Bookmark this page") }, text = { Text(if (isBookmarked) stringResource(R.string.menu_remove_bookmark) else stringResource(R.string.menu_bookmark_this_page)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onToggleBookmark() onToggleBookmark()
@ -302,20 +302,20 @@ fun EpubReaderTopBar(
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Tap to Turn Pages") }, text = { Text(stringResource(R.string.menu_tap_to_turn_pages)) },
enabled = currentRenderMode == RenderMode.PAGINATED, enabled = currentRenderMode == RenderMode.PAGINATED,
onClick = { onClick = {
onToggleTapToNavigate(!tapToNavigateEnabled) onToggleTapToNavigate(!tapToNavigateEnabled)
showMoreMenu = false showMoreMenu = false
}, },
trailingIcon = { if (tapToNavigateEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } trailingIcon = { if (tapToNavigateEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text( Text(
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) "Volume Button Scrolling" if (currentRenderMode == RenderMode.VERTICAL_SCROLL) stringResource(R.string.menu_volume_button_scrolling)
else "Volume Button Page Turn" else stringResource(R.string.menu_volume_button_page_turn)
) )
}, },
enabled = true, enabled = true,
@ -323,33 +323,33 @@ fun EpubReaderTopBar(
onToggleVolumeScroll(!volumeScrollEnabled) onToggleVolumeScroll(!volumeScrollEnabled)
showMoreMenu = false showMoreMenu = false
}, },
trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Realistic Page Turns") }, text = { Text(stringResource(R.string.menu_realistic_page_turns)) },
enabled = currentRenderMode == RenderMode.PAGINATED, enabled = currentRenderMode == RenderMode.PAGINATED,
onClick = { onClick = {
onTogglePageTurnAnimation(!isPageTurnAnimationEnabled) onTogglePageTurnAnimation(!isPageTurnAnimationEnabled)
showMoreMenu = false showMoreMenu = false
}, },
trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Keep Screen On") }, text = { Text(stringResource(R.string.menu_keep_screen_on)) },
onClick = { onClick = {
onToggleKeepScreenOn(!isKeepScreenOn) onToggleKeepScreenOn(!isKeepScreenOn)
showMoreMenu = false showMoreMenu = false
}, },
trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = "Enabled") } trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) }
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Visual Options") }, text = { Text(stringResource(R.string.menu_visual_options)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onOpenVisualOptions() onOpenVisualOptions()
@ -361,7 +361,7 @@ fun EpubReaderTopBar(
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Auto Scroll") }, text = { Text(stringResource(R.string.menu_auto_scroll)) },
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL, enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
@ -372,7 +372,7 @@ fun EpubReaderTopBar(
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("TTS Voice Settings") }, text = { Text(stringResource(R.string.menu_tts_voice_settings)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onOpenDeviceVoiceSettings() onOpenDeviceVoiceSettings()
@ -384,7 +384,7 @@ fun EpubReaderTopBar(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("TTS Settings (Debug)") }, text = { Text(stringResource(R.string.menu_tts_settings_debug)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onOpenTtsSettings() onOpenTtsSettings()
@ -445,28 +445,28 @@ fun EpubReaderBottomBar(
onClick = onOpenSlider, onClick = onOpenSlider,
enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL
) { ) {
Icon(painter = painterResource(id = R.drawable.slider), contentDescription = "Navigate with slider") Icon(painter = painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
} }
TooltipIconButton( TooltipIconButton(
text = stringResource(R.string.tooltip_toc), text = stringResource(R.string.tooltip_toc),
description = stringResource(R.string.tooltip_toc_desc), description = stringResource(R.string.tooltip_toc_desc),
onClick = onOpenDrawer onClick = onOpenDrawer
) { ) {
Icon(imageVector = Icons.Default.Menu, contentDescription = "Chapters Menu") Icon(imageVector = Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_chapters_menu))
} }
TooltipIconButton( TooltipIconButton(
text = stringResource(R.string.tooltip_format), text = stringResource(R.string.tooltip_format),
description = stringResource(R.string.tooltip_format_desc), description = stringResource(R.string.tooltip_format_desc),
onClick = onToggleFormat onClick = onToggleFormat
) { ) {
Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = "Text Formatting") Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = stringResource(R.string.content_desc_text_formatting))
} }
TooltipIconButton( TooltipIconButton(
text = stringResource(R.string.tooltip_search), text = stringResource(R.string.tooltip_search),
description = stringResource(R.string.tooltip_search_desc), description = stringResource(R.string.tooltip_search_desc),
onClick = onToggleSearch onClick = onToggleSearch
) { ) {
Icon(imageVector = Icons.Default.Search, contentDescription = "Search") Icon(imageVector = Icons.Default.Search, contentDescription = stringResource(R.string.tooltip_search))
} }
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
@ -485,7 +485,7 @@ fun EpubReaderBottomBar(
onDismissRequest = { showAiFeaturesMenu = false } onDismissRequest = { showAiFeaturesMenu = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Chapter Summarization") }, text = { Text(stringResource(R.string.menu_chapter_summarization)) },
onClick = { onClick = {
showAiFeaturesMenu = false showAiFeaturesMenu = false
onSummarize() onSummarize()
@ -494,7 +494,7 @@ fun EpubReaderBottomBar(
if (BuildConfig.DEBUG && isProUser) { if (BuildConfig.DEBUG && isProUser) {
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Recap (Beta)") }, text = { Text(stringResource(R.string.menu_recap_beta)) },
onClick = { onClick = {
showAiFeaturesMenu = false showAiFeaturesMenu = false
onRecap() onRecap()
@ -519,7 +519,7 @@ fun EpubReaderBottomBar(
) { ) {
Icon( Icon(
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech),
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS" contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts)
) )
} }
if (isTtsSessionActive) { if (isTtsSessionActive) {
@ -537,7 +537,7 @@ fun EpubReaderBottomBar(
) { ) {
Icon( Icon(
painter = painterResource(id = if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), painter = painterResource(id = if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
contentDescription = if (ttsState.isPlaying) "Pause TTS" else "Resume TTS" contentDescription = if (ttsState.isPlaying) stringResource(R.string.content_desc_pause_tts) else stringResource(R.string.content_desc_resume_tts)
) )
} }
} }
@ -589,7 +589,7 @@ fun EpubReaderPageSlider(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Exit slider navigation" contentDescription = stringResource(R.string.content_desc_exit_slider)
) )
} }
@ -681,7 +681,7 @@ fun EpubReaderPageSlider(
) { ) {
Image( Image(
bitmap = thumbnail.asImageBitmap(), bitmap = thumbnail.asImageBitmap(),
contentDescription = "Start page thumbnail", contentDescription = stringResource(R.string.content_desc_start_page_thumbnail),
contentScale = ContentScale.FillBounds, contentScale = ContentScale.FillBounds,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@ -918,7 +918,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = Icons.Default.ChevronLeft, imageVector = Icons.Default.ChevronLeft,
contentDescription = "Expand", contentDescription = stringResource(R.string.content_desc_expand),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -934,7 +934,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play", contentDescription = if (isPlaying) stringResource(R.string.content_desc_pause_playback) else stringResource(R.string.content_desc_start_playback),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
@ -967,13 +967,13 @@ fun AutoScrollControls(
.padding(4.dp) .padding(4.dp)
) { ) {
Text( Text(
text = if (isLocalMode) "Local Speed" else "Global Speed", text = if (isLocalMode) stringResource(R.string.auto_scroll_local_speed) else stringResource(R.string.auto_scroll_global_speed),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Icon( Icon(
imageVector = Icons.Default.ArrowDropDown, imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Select Mode", contentDescription = stringResource(R.string.content_desc_select_mode),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -986,8 +986,8 @@ fun AutoScrollControls(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Column { Column {
Text("Global Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.auto_scroll_global_speed), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Applies to all files", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.auto_scroll_applies_all_files), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
}, },
onClick = { onClick = {
@ -1002,8 +1002,8 @@ fun AutoScrollControls(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Column { Column {
Text("Local Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.auto_scroll_local_speed), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Saved for this file only", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.auto_scroll_saved_for_file), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
}, },
onClick = { onClick = {
@ -1024,7 +1024,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.music_note), painter = painterResource(id = R.drawable.music_note),
contentDescription = if (isMusicianMode) "Disable Musician Mode" else "Enable Musician Mode", contentDescription = if (isMusicianMode) stringResource(R.string.content_desc_disable_musician_mode) else stringResource(R.string.content_desc_enable_musician_mode),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = if (isMusicianMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = if (isMusicianMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1035,7 +1035,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = Icons.Default.SwapHoriz, imageVector = Icons.Default.SwapHoriz,
contentDescription = "Swap Controls", contentDescription = stringResource(R.string.content_desc_swap_controls),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1046,7 +1046,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = Icons.Default.ChevronRight, imageVector = Icons.Default.ChevronRight,
contentDescription = "Collapse", contentDescription = stringResource(R.string.content_desc_collapse),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1057,7 +1057,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close", contentDescription = stringResource(R.string.action_close),
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
@ -1085,7 +1085,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play", contentDescription = if (isPlaying) stringResource(R.string.tooltip_tts_pause) else stringResource(R.string.content_desc_start_playback),
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
@ -1110,13 +1110,13 @@ fun AutoScrollControls(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
SpeedDropdown( SpeedDropdown(
label = "Min", label = stringResource(R.string.label_min),
currentValue = minSpeed, currentValue = minSpeed,
options = speedOptions, options = speedOptions,
onValueChange = onMinSpeedChange onValueChange = onMinSpeedChange
) )
SpeedDropdown( SpeedDropdown(
label = "Max", label = stringResource(R.string.label_max),
currentValue = maxSpeed, currentValue = maxSpeed,
options = speedOptions, options = speedOptions,
onValueChange = onMaxSpeedChange onValueChange = onMaxSpeedChange
@ -1193,7 +1193,7 @@ fun AutoScrollControls(
onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(minSpeed)) }, onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(minSpeed)) },
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon(Icons.Default.Remove, "Slower") Icon(Icons.Default.Remove, stringResource(R.string.content_desc_slower))
} }
Text( Text(
text = "%.1fx".format(speed), text = "%.1fx".format(speed),
@ -1204,7 +1204,7 @@ fun AutoScrollControls(
onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(safeMax)) }, onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(safeMax)) },
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
Icon(Icons.Default.Add, "Faster") Icon(Icons.Default.Add, stringResource(R.string.content_desc_faster))
} }
} }
} }

View file

@ -82,11 +82,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastSumBy import androidx.compose.ui.util.fastSumBy
import com.aryan.reader.R
import com.aryan.reader.RenderMode import com.aryan.reader.RenderMode
import com.aryan.reader.epub.EpubChapter import com.aryan.reader.epub.EpubChapter
import com.aryan.reader.epub.EpubTocEntry import com.aryan.reader.epub.EpubTocEntry
@ -224,17 +226,17 @@ fun EpubReaderDrawerSheet(
Tab( Tab(
selected = drawerPagerState.currentPage == 0, selected = drawerPagerState.currentPage == 0,
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(0) } }, onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(0) } },
text = { Text("Chapters") } text = { Text(stringResource(R.string.tab_chapters)) }
) )
Tab( Tab(
selected = drawerPagerState.currentPage == 1, selected = drawerPagerState.currentPage == 1,
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(1) } }, onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(1) } },
text = { Text("Bookmarks") } text = { Text(stringResource(R.string.tab_bookmarks)) }
) )
Tab( Tab(
selected = drawerPagerState.currentPage == 2, selected = drawerPagerState.currentPage == 2,
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } }, onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
text = { Text("Highlights") } text = { Text(stringResource(R.string.tab_highlights)) }
) )
} }
@ -442,7 +444,7 @@ private fun TocTreeItem(
if (hasChildren) { if (hasChildren) {
Icon( Icon(
imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight, imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = if (isExpanded) "Collapse" else "Expand", contentDescription = if (isExpanded) stringResource(R.string.content_desc_collapse) else stringResource(R.string.content_desc_expand),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -477,7 +479,7 @@ private fun BookmarksList(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
"You haven't added any bookmarks yet.", stringResource(R.string.no_bookmarks_yet),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -531,7 +533,7 @@ private fun BookmarksList(
IconButton(onClick = { bookmarkMenuExpandedFor = bookmark }) { IconButton(onClick = { bookmarkMenuExpandedFor = bookmark }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "More options for bookmark" contentDescription = stringResource(R.string.content_desc_more_options_bookmark)
) )
} }
DropdownMenu( DropdownMenu(
@ -539,14 +541,14 @@ private fun BookmarksList(
onDismissRequest = { bookmarkMenuExpandedFor = null } onDismissRequest = { bookmarkMenuExpandedFor = null }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Rename") }, text = { Text(stringResource(R.string.menu_rename)) },
onClick = { onClick = {
showRenameBookmarkDialog = bookmark showRenameBookmarkDialog = bookmark
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text(stringResource(R.string.action_delete)) },
onClick = { onClick = {
showDeleteConfirmDialogFor = bookmark showDeleteConfirmDialogFor = bookmark
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
@ -573,12 +575,12 @@ private fun BookmarksList(
AlertDialog( AlertDialog(
onDismissRequest = { showRenameBookmarkDialog = null }, onDismissRequest = { showRenameBookmarkDialog = null },
title = { Text("Rename Bookmark") }, title = { Text(stringResource(R.string.dialog_rename_bookmark)) },
text = { text = {
androidx.compose.material3.OutlinedTextField( androidx.compose.material3.OutlinedTextField(
value = newTitle, value = newTitle,
onValueChange = { newTitle = it }, onValueChange = { newTitle = it },
label = { Text("New Name") }, label = { Text(stringResource(R.string.label_new_name)) },
placeholder = { placeholder = {
Text( Text(
text = currentName, text = currentName,
@ -601,12 +603,12 @@ private fun BookmarksList(
showRenameBookmarkDialog = null showRenameBookmarkDialog = null
} }
) { ) {
Text("Save") Text(stringResource(R.string.action_save))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showRenameBookmarkDialog = null }) { TextButton(onClick = { showRenameBookmarkDialog = null }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )
@ -615,8 +617,8 @@ private fun BookmarksList(
showDeleteConfirmDialogFor?.let { bookmarkToDelete -> showDeleteConfirmDialogFor?.let { bookmarkToDelete ->
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteConfirmDialogFor = null }, onDismissRequest = { showDeleteConfirmDialogFor = null },
title = { Text("Delete Bookmark?") }, title = { Text(stringResource(R.string.dialog_delete_bookmark)) },
text = { Text("Are you sure you want to permanently delete this bookmark?") }, text = { Text(stringResource(R.string.dialog_delete_bookmark_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -624,12 +626,12 @@ private fun BookmarksList(
showDeleteConfirmDialogFor = null showDeleteConfirmDialogFor = null
} }
) { ) {
Text("Delete") Text(stringResource(R.string.action_delete))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showDeleteConfirmDialogFor = null }) { TextButton(onClick = { showDeleteConfirmDialogFor = null }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )
@ -646,7 +648,7 @@ private fun HighlightsList(
) { ) {
if (userHighlights.isEmpty()) { if (userHighlights.isEmpty()) {
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
Text("No highlights yet.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) Text(stringResource(R.string.no_highlights_yet), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center)
} }
} else { } else {
var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) } var highlightMenuExpandedFor by remember { mutableStateOf<UserHighlight?>(null) }
@ -663,7 +665,7 @@ private fun HighlightsList(
items = userHighlights.sortedBy { it.chapterIndex }, items = userHighlights.sortedBy { it.chapterIndex },
key = { it.id } key = { it.id }
) { highlight -> ) { highlight ->
val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: "Unknown Chapter" val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter)
ListItem( ListItem(
headlineContent = { headlineContent = {
@ -694,7 +696,7 @@ private fun HighlightsList(
IconButton(onClick = { highlightMenuExpandedFor = highlight }) { IconButton(onClick = { highlightMenuExpandedFor = highlight }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "Options" contentDescription = stringResource(R.string.content_desc_options)
) )
} }
DropdownMenu( DropdownMenu(
@ -702,7 +704,7 @@ private fun HighlightsList(
onDismissRequest = { highlightMenuExpandedFor = null } onDismissRequest = { highlightMenuExpandedFor = null }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text(stringResource(R.string.action_delete)) },
onClick = { onClick = {
showHighlightDeleteDialogFor = highlight showHighlightDeleteDialogFor = highlight
highlightMenuExpandedFor = null highlightMenuExpandedFor = null
@ -726,8 +728,8 @@ private fun HighlightsList(
showHighlightDeleteDialogFor?.let { highlightToDelete -> showHighlightDeleteDialogFor?.let { highlightToDelete ->
AlertDialog( AlertDialog(
onDismissRequest = { showHighlightDeleteDialogFor = null }, onDismissRequest = { showHighlightDeleteDialogFor = null },
title = { Text("Delete Highlight?") }, title = { Text(stringResource(R.string.dialog_delete_highlight)) },
text = { Text("Are you sure you want to permanently delete this highlight?") }, text = { Text(stringResource(R.string.dialog_delete_highlight_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -735,12 +737,12 @@ private fun HighlightsList(
showHighlightDeleteDialogFor = null showHighlightDeleteDialogFor = null
} }
) { ) {
Text("Delete") Text(stringResource(R.string.action_delete))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showHighlightDeleteDialogFor = null }) { TextButton(onClick = { showHighlightDeleteDialogFor = null }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )

View file

@ -41,6 +41,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -120,18 +121,22 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@ -144,6 +149,7 @@ import com.aryan.reader.BuiltInThemes
import com.aryan.reader.CustomTopBanner import com.aryan.reader.CustomTopBanner
import com.aryan.reader.DeviceVoiceSettingsSheet import com.aryan.reader.DeviceVoiceSettingsSheet
import com.aryan.reader.MainViewModel import com.aryan.reader.MainViewModel
import com.aryan.reader.R
import com.aryan.reader.ReaderThemePanel import com.aryan.reader.ReaderThemePanel
import com.aryan.reader.RenderMode import com.aryan.reader.RenderMode
import com.aryan.reader.SearchResult import com.aryan.reader.SearchResult
@ -157,6 +163,7 @@ import com.aryan.reader.fetchAiDefinition
import com.aryan.reader.loadCustomThemes import com.aryan.reader.loadCustomThemes
import com.aryan.reader.loadReaderThemeId import com.aryan.reader.loadReaderThemeId
import com.aryan.reader.paginatedreader.BookPaginator import com.aryan.reader.paginatedreader.BookPaginator
import com.aryan.reader.paginatedreader.CfiUtils
import com.aryan.reader.paginatedreader.HeaderBlock import com.aryan.reader.paginatedreader.HeaderBlock
import com.aryan.reader.paginatedreader.IPaginator import com.aryan.reader.paginatedreader.IPaginator
import com.aryan.reader.paginatedreader.ListItemBlock import com.aryan.reader.paginatedreader.ListItemBlock
@ -661,15 +668,13 @@ fun EpubReaderHost(
isAiDefinitionLoading = true isAiDefinitionLoading = true
aiDefinitionResult = null aiDefinitionResult = null
fetchAiDefinition( fetchAiDefinition(
text = word, text = word, onUpdate = { chunk ->
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: "" val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult(definition = currentDefinition + chunk) aiDefinitionResult =
}, AiDefinitionResult(definition = currentDefinition + chunk)
onError = { error -> }, onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error) aiDefinitionResult = AiDefinitionResult(error = error)
}, }, onFinish = { isAiDefinitionLoading = false }, context = context
onFinish = { isAiDefinitionLoading = false }
) )
} }
} else { } else {
@ -911,7 +916,7 @@ fun EpubReaderHost(
} }
} }
val configuration = androidx.compose.ui.platform.LocalConfiguration.current val configuration = LocalConfiguration.current
var lastOrientation by remember { mutableIntStateOf(configuration.orientation) } var lastOrientation by remember { mutableIntStateOf(configuration.orientation) }
LaunchedEffect(configuration.orientation) { LaunchedEffect(configuration.orientation) {
@ -1122,8 +1127,8 @@ fun EpubReaderHost(
var foundIdx = -1 var foundIdx = -1
for (i in chunks.indices) { for (i in chunks.indices) {
val c = chunks[i] val c = chunks[i]
val cPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(c.sourceCfi) val cPath = CfiUtils.getPath(c.sourceCfi)
val bPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(baseCfi) val bPath = CfiUtils.getPath(baseCfi)
if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) { if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) {
foundIdx = i foundIdx = i
break break
@ -1309,9 +1314,10 @@ fun EpubReaderHost(
characterLimit = charLimit, characterLimit = charLimit,
summaryCacheManager = summaryCacheManager, summaryCacheManager = summaryCacheManager,
paginator = paginator, paginator = paginator,
context = context,
onProgressUpdate = { recapProgressMessage = it }, onProgressUpdate = { recapProgressMessage = it },
onResultUpdate = { chunk -> onResultUpdate = { chunk ->
isRecapLoading = false // Start showing content isRecapLoading = false
val current = recapResult?.summary ?: "" val current = recapResult?.summary ?: ""
recapResult = SummarizationResult(summary = current + chunk) recapResult = SummarizationResult(summary = current + chunk)
}, },
@ -1440,7 +1446,7 @@ fun EpubReaderHost(
} }
} }
val pageInfoBottomPadding by androidx.compose.animation.core.animateDpAsState( val pageInfoBottomPadding by animateDpAsState(
targetValue = if (showBars && pageInfoMode == PageInfoMode.SYNC) 45.dp else 0.dp, targetValue = if (showBars && pageInfoMode == PageInfoMode.SYNC) 45.dp else 0.dp,
label = "PageInfoBottomPadding" label = "PageInfoBottomPadding"
) )
@ -2002,8 +2008,9 @@ fun EpubReaderHost(
if (systemUiMode == SystemUiMode.HIDDEN) { if (systemUiMode == SystemUiMode.HIDDEN) {
0.dp 0.dp
} else { } else {
val insets = androidx.core.view.ViewCompat.getRootWindowInsets(view) val insets = ViewCompat.getRootWindowInsets(view)
val ignoringVisibilityTopPx = insets?.getInsetsIgnoringVisibility(androidx.core.view.WindowInsetsCompat.Type.statusBars())?.top ?: 0 val ignoringVisibilityTopPx = insets?.getInsetsIgnoringVisibility(
WindowInsetsCompat.Type.statusBars())?.top ?: 0
val ignoringVisibilityTop = with(density) { ignoringVisibilityTopPx.toDp() } val ignoringVisibilityTop = with(density) { ignoringVisibilityTopPx.toDp() }
if (ignoringVisibilityTop > 0.dp) { if (ignoringVisibilityTop > 0.dp) {
@ -2093,7 +2100,7 @@ fun EpubReaderHost(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("No chapters available for this book.") Text(stringResource(R.string.no_chapters_available))
} }
} else { } else {
AnimatedContent( AnimatedContent(
@ -3496,7 +3503,7 @@ fun EpubReaderHost(
onDeleteReflow = onDeleteReflow onDeleteReflow = onDeleteReflow
) )
val autoScrollPadding by androidx.compose.animation.core.animateDpAsState( val autoScrollPadding by animateDpAsState(
targetValue = if (showBars) (bottomPadding + 45.dp + 16.dp) else 32.dp, targetValue = if (showBars) (bottomPadding + 45.dp + 16.dp) else 32.dp,
label = "AutoScrollPadding" label = "AutoScrollPadding"
) )
@ -3854,7 +3861,7 @@ fun EpubReaderHost(
CircularProgressIndicator() CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Navigating to position...", text = stringResource(R.string.navigating_to_position),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
@ -3865,8 +3872,8 @@ fun EpubReaderHost(
if (showPermissionRationaleDialog) { if (showPermissionRationaleDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showPermissionRationaleDialog = false }, onDismissRequest = { showPermissionRationaleDialog = false },
title = { Text("Permission Required") }, title = { Text(stringResource(R.string.dialog_permission_required)) },
text = { Text("To show playback controls while the app is in the background, please grant the notification permission.") }, text = { Text(stringResource(R.string.dialog_permission_notification_desc)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -3874,7 +3881,7 @@ fun EpubReaderHost(
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} }
) { ) {
Text("Continue") Text(stringResource(R.string.action_continue))
} }
}, },
dismissButton = { dismissButton = {
@ -3884,7 +3891,7 @@ fun EpubReaderHost(
startTts() startTts()
} }
) { ) {
Text("Not now") Text(stringResource(R.string.action_not_now))
} }
} }
) )
@ -3894,11 +3901,11 @@ fun EpubReaderHost(
AlertDialog( AlertDialog(
onDismissRequest = { showJustifyWarningDialog = false }, onDismissRequest = { showJustifyWarningDialog = false },
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Justified Text Limitation") }, title = { Text(stringResource(R.string.dialog_justified_text_limitation)) },
text = { Text("Using Justified alignment in Paginated Mode may cause text selection and highlights to be inaccurate due to layout limitations.") }, text = { Text(stringResource(R.string.dialog_justified_text_limitation_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = { showJustifyWarningDialog = false }) { TextButton(onClick = { showJustifyWarningDialog = false }) {
Text("I Understand") Text(stringResource(R.string.action_i_understand))
} }
} }
) )
@ -3919,7 +3926,7 @@ fun EpubReaderHost(
CircularProgressIndicator() CircularProgressIndicator()
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
"Navigating to chapter...", stringResource(R.string.navigating_to_chapter),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )

View file

@ -97,6 +97,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.ui.res.stringResource
const val SETTINGS_PREFS_NAME = "epub_reader_settings" const val SETTINGS_PREFS_NAME = "epub_reader_settings"
private const val TEXT_ALIGN_KEY = "reader_text_align" private const val TEXT_ALIGN_KEY = "reader_text_align"
@ -389,13 +390,13 @@ fun ReaderTextFormatPanel(
.padding(4.dp) .padding(4.dp)
) { ) {
Text( Text(
text = if (isLocalMode) "Local Format" else "Global Format", text = if (isLocalMode) stringResource(R.string.format_local) else stringResource(R.string.format_global),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Icon( Icon(
imageVector = Icons.Default.ArrowDropDown, imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Select Mode", contentDescription = stringResource(R.string.content_desc_select_mode),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -405,8 +406,8 @@ fun ReaderTextFormatPanel(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Column { Column {
Text("Global Format", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.format_global), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Applies to all files", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.auto_scroll_applies_all_files), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
}, },
onClick = { onLocalModeToggle(false); showModeMenu = false }, onClick = { onLocalModeToggle(false); showModeMenu = false },
@ -416,8 +417,8 @@ fun ReaderTextFormatPanel(
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Column { Column {
Text("Local Format", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.format_local), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
Text("Saved for this file only", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.auto_scroll_saved_for_file), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
}, },
onClick = { onLocalModeToggle(true); showModeMenu = false }, onClick = { onLocalModeToggle(true); showModeMenu = false },
@ -428,10 +429,10 @@ fun ReaderTextFormatPanel(
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) {
TextButton(onClick = onReset, contentPadding = PaddingValues(horizontal = 8.dp)) { TextButton(onClick = onReset, contentPadding = PaddingValues(horizontal = 8.dp)) {
Text("Reset") Text(stringResource(R.string.action_reset))
} }
IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) { IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Close, "Close", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Close, stringResource(R.string.action_close), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
} }
} }
} }
@ -560,15 +561,15 @@ fun FontSelectionSheetContent(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Select Font", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.select_font), style = MaterialTheme.typography.titleMedium)
IconButton(onClick = onDismiss) { IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Close") Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
} }
} }
TabRow(selectedTabIndex = selectedTabIndex) { TabRow(selectedTabIndex = selectedTabIndex) {
Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text("Presets") }) Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text(stringResource(R.string.tab_presets)) })
Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text("Imported") }) Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text(stringResource(R.string.tab_imported)) })
} }
Box(modifier = Modifier.heightIn(min = 200.dp, max = 400.dp)) { Box(modifier = Modifier.heightIn(min = 200.dp, max = 400.dp)) {
@ -582,7 +583,7 @@ fun FontSelectionSheetContent(
Text(font.displayName, fontFamily = getComposeFontFamily(font, null)) Text(font.displayName, fontFamily = getComposeFontFamily(font, null))
}, },
trailingContent = { trailingContent = {
if (isSelected) Icon(Icons.Default.Check, contentDescription = "Selected", tint = MaterialTheme.colorScheme.primary) if (isSelected) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary)
}, },
modifier = Modifier.clickable { onFontSelected(font, null) }, modifier = Modifier.clickable { onFontSelected(font, null) },
colors = if (isSelected) ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)) else ListItemDefaults.colors() colors = if (isSelected) ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)) else ListItemDefaults.colors()
@ -599,14 +600,14 @@ fun FontSelectionSheetContent(
) { ) {
Icon(Icons.Default.Add, contentDescription = null) Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Import from Files") Text(stringResource(R.string.button_import_from_files))
} }
} }
if (customFonts.isEmpty()) { if (customFonts.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text( Text(
"No imported fonts yet.", stringResource(R.string.no_imported_fonts_yet),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 32.dp) modifier = Modifier.padding(bottom = 32.dp)
) )
@ -627,7 +628,7 @@ fun FontSelectionSheetContent(
if (isSelected) { if (isSelected) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
@ -674,16 +675,16 @@ fun VisualOptionsSheet(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Visual Options", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) Text(stringResource(R.string.menu_visual_options), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
IconButton(onClick = onDismiss) { IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Close") Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// System UI // System UI
Text("System UI (Status & Navigation Bars)", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.visual_options_system_ui), style = MaterialTheme.typography.titleMedium)
Text("Control the visibility of the device's system bars.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.visual_options_system_ui_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OptionSegmentedControl( OptionSegmentedControl(
options = SystemUiMode.entries, options = SystemUiMode.entries,
@ -695,8 +696,8 @@ fun VisualOptionsSheet(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Progress Bar // Progress Bar
Text("Progress Bar", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.visual_options_progress_bar), style = MaterialTheme.typography.titleMedium)
Text("The reading progress and chapter indicator at the bottom of the screen.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.visual_options_progress_bar_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OptionSegmentedControl( OptionSegmentedControl(
options = PageInfoMode.entries, options = PageInfoMode.entries,
@ -721,8 +722,8 @@ fun VisualOptionsSheet(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("Seamless Chapter Transition", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.visual_options_seamless_chapter), style = MaterialTheme.typography.titleMedium)
Text("Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.visual_options_seamless_chapter_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Switch(checked = !pullToTurnEnabled, onCheckedChange = { onPullToTurnChange(!it) }) Switch(checked = !pullToTurnEnabled, onCheckedChange = { onPullToTurnChange(!it) })

View file

@ -49,6 +49,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -61,7 +62,7 @@ import timber.log.Timber
private fun launchEmailFeedback(context: android.content.Context) { private fun launchEmailFeedback(context: android.content.Context) {
val intent = Intent(Intent.ACTION_SENDTO).apply { val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:epistemereader@gmail.com".toUri() data = "mailto:epistemereader@gmail.com".toUri()
putExtra(Intent.EXTRA_SUBJECT, "Feedback: Episteme Reader") putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.feedback_email_subject))
} }
try { try {
context.startActivity(intent) context.startActivity(intent)
@ -82,7 +83,7 @@ fun FeedbackScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("Help & Feedback") }, title = { Text(stringResource(R.string.drawer_help_feedback)) },
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
@ -109,16 +110,14 @@ fun FeedbackScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(text = stringResource(R.string.get_in_touch),
text = "Get in Touch",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(text = stringResource(R.string.feedback_desc),
text = "Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or send us an email.",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@ -128,8 +127,8 @@ fun FeedbackScreen(
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
FeedbackOptionCard( FeedbackOptionCard(
title = "GitHub Issues", title = stringResource(R.string.github_issues),
description = "Report bugs, request features, and track development progress.", description = stringResource(R.string.github_issues_desc),
icon = { icon = {
Icon( Icon(
painter = painterResource(id = R.drawable.github), painter = painterResource(id = R.drawable.github),
@ -146,8 +145,8 @@ fun FeedbackScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
FeedbackOptionCard( FeedbackOptionCard(
title = "Email Support", title = stringResource(R.string.email_support),
description = "Contact us directly via email for any other inquiries.", description = stringResource(R.string.email_support_desc),
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Email, imageVector = Icons.Outlined.Email,

View file

@ -145,6 +145,7 @@ import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults import androidx.compose.material3.MenuDefaults
@ -259,6 +260,7 @@ import com.aryan.reader.DeviceVoiceSettingsSheet
import com.aryan.reader.FileType import com.aryan.reader.FileType
import com.aryan.reader.MainViewModel import com.aryan.reader.MainViewModel
import com.aryan.reader.R import com.aryan.reader.R
import com.aryan.reader.ReaderTheme
import com.aryan.reader.ReaderThemePanel import com.aryan.reader.ReaderThemePanel
import com.aryan.reader.SearchResult import com.aryan.reader.SearchResult
import com.aryan.reader.SearchTopBar import com.aryan.reader.SearchTopBar
@ -276,6 +278,7 @@ import com.aryan.reader.paginatedreader.TtsChunk
import com.aryan.reader.pdf.data.AnnotationSettingsRepository import com.aryan.reader.pdf.data.AnnotationSettingsRepository
import com.aryan.reader.pdf.data.PdfAnnotation import com.aryan.reader.pdf.data.PdfAnnotation
import com.aryan.reader.pdf.data.PdfAnnotationRepository import com.aryan.reader.pdf.data.PdfAnnotationRepository
import com.aryan.reader.pdf.data.PdfHighlightRepository
import com.aryan.reader.pdf.data.PdfTextBox import com.aryan.reader.pdf.data.PdfTextBox
import com.aryan.reader.pdf.data.PdfTextBoxRepository import com.aryan.reader.pdf.data.PdfTextBoxRepository
import com.aryan.reader.pdf.data.PdfTextRepository import com.aryan.reader.pdf.data.PdfTextRepository
@ -294,6 +297,7 @@ import io.legere.pdfiumandroid.api.Bookmark
import io.legere.pdfiumandroid.suspend.PdfDocumentKt import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import io.legere.pdfiumandroid.suspend.PdfPageKt import io.legere.pdfiumandroid.suspend.PdfPageKt
import io.legere.pdfiumandroid.suspend.PdfiumCoreKt import io.legere.pdfiumandroid.suspend.PdfiumCoreKt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -314,6 +318,9 @@ import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -370,13 +377,13 @@ private fun loadPdfThemeId(context: Context): String {
} }
val PdfBuiltInThemes = listOf( val PdfBuiltInThemes = listOf(
com.aryan.reader.ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false), ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false),
com.aryan.reader.ReaderTheme("reverse", "Reverse", Color.Black, Color.White, true), ReaderTheme("reverse", "Reverse", Color.Black, Color.White, true),
com.aryan.reader.ReaderTheme("light", "Light", Color(0xFFFFFFFF), Color(0xFF000000), false), ReaderTheme("light", "Light", Color(0xFFFFFFFF), Color(0xFF000000), false),
com.aryan.reader.ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true), ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true),
com.aryan.reader.ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false), ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false),
com.aryan.reader.ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true), ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true),
com.aryan.reader.ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true) ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true)
) )
object PdfiumCoreProvider { object PdfiumCoreProvider {
@ -1453,7 +1460,7 @@ fun PdfViewerScreen(
val pdfTextRepository = remember(context) { PdfTextRepository(context) } val pdfTextRepository = remember(context) { PdfTextRepository(context) }
val annotationRepository = remember(context) { PdfAnnotationRepository(context) } val annotationRepository = remember(context) { PdfAnnotationRepository(context) }
val textBoxRepository = remember(context) { PdfTextBoxRepository(context) } val textBoxRepository = remember(context) { PdfTextBoxRepository(context) }
val highlightRepository = remember(context) { com.aryan.reader.pdf.data.PdfHighlightRepository(context) } val highlightRepository = remember(context) { PdfHighlightRepository(context) }
var allAnnotations by remember { mutableStateOf<Map<Int, List<PdfAnnotation>>>(emptyMap()) } var allAnnotations by remember { mutableStateOf<Map<Int, List<PdfAnnotation>>>(emptyMap()) }
@ -1932,14 +1939,14 @@ fun PdfViewerScreen(
val dx = (currentPoint.x - startPoint.x) * aspectRatio val dx = (currentPoint.x - startPoint.x) * aspectRatio
val dy = (currentPoint.y - startPoint.y) val dy = (currentPoint.y - startPoint.y)
val angleRad = kotlin.math.atan2(dy, dx) val angleRad = atan2(dy, dx)
val angleDeg = (angleRad * 180 / kotlin.math.PI) val angleDeg = (angleRad * 180 / PI)
val absAngle = kotlin.math.abs(angleDeg) val absAngle = abs(angleDeg)
val threshold = 10.0 val threshold = 10.0
val isHorizontal = absAngle < threshold || kotlin.math.abs(absAngle - 180.0) < threshold val isHorizontal = absAngle < threshold || abs(absAngle - 180.0) < threshold
val isVertical = kotlin.math.abs(absAngle - 90.0) < threshold val isVertical = abs(absAngle - 90.0) < threshold
if (isHorizontal) { if (isHorizontal) {
currentPoint.copy(y = startPoint.y) currentPoint.copy(y = startPoint.y)
@ -2215,7 +2222,7 @@ fun PdfViewerScreen(
} }
Timber.tag("PdfPositionDebug").i("UI: Restoration Complete | Now at Page: $currentPage") Timber.tag("PdfPositionDebug").i("UI: Restoration Complete | Now at Page: $currentPage")
} catch (e: Exception) { } catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) { if (e is CancellationException) {
Timber.tag("PdfPositionDebug").w("UI: Restoration cancelled (likely new recomposition)") Timber.tag("PdfPositionDebug").w("UI: Restoration cancelled (likely new recomposition)")
} else { } else {
Timber.tag("PdfPositionDebug").e(e, "UI: Restoration error.") Timber.tag("PdfPositionDebug").e(e, "UI: Restoration error.")
@ -2544,19 +2551,16 @@ fun PdfViewerScreen(
isAiDefinitionLoading = true isAiDefinitionLoading = true
aiDefinitionResult = null aiDefinitionResult = null
fetchAiDefinition( fetchAiDefinition(
text = text, text = text, onUpdate = { chunk ->
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: "" val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult( aiDefinitionResult = AiDefinitionResult(
definition = currentDefinition + chunk definition = currentDefinition + chunk
) )
}, }, onError = { error ->
onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error) aiDefinitionResult = AiDefinitionResult(error = error)
}, }, onFinish = {
onFinish = {
isAiDefinitionLoading = false isAiDefinitionLoading = false
} }, context = context
) )
} }
} else { } else {
@ -3562,7 +3566,7 @@ fun PdfViewerScreen(
pagerState.currentPage pagerState.currentPage
) { ) {
if (paginationDraggingOriginPage != null) { if (paginationDraggingOriginPage != null) {
val distance = kotlin.math.abs(pagerState.currentPage - paginationDraggingOriginPage) val distance = abs(pagerState.currentPage - paginationDraggingOriginPage)
(distance + 1).coerceAtLeast(1) (distance + 1).coerceAtLeast(1)
} else { } else {
1 1
@ -4155,7 +4159,7 @@ fun PdfViewerScreen(
} }
) { pageIndex -> ) { pageIndex ->
val isVisiblePage = remember(pagerState.currentPage, pageIndex) { val isVisiblePage = remember(pagerState.currentPage, pageIndex) {
kotlin.math.abs(pagerState.currentPage - pageIndex) <= 1 abs(pagerState.currentPage - pageIndex) <= 1
} }
val isPageBookmarked by remember(bookmarks, pageIndex) { val isPageBookmarked by remember(bookmarks, pageIndex) {
derivedStateOf { derivedStateOf {
@ -5675,7 +5679,7 @@ fun PdfViewerScreen(
) )
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
androidx.compose.material3.LinearProgressIndicator( LinearProgressIndicator(
progress = { reflowProgressValue }, progress = { reflowProgressValue },
modifier = Modifier.fillMaxWidth().height(6.dp), modifier = Modifier.fillMaxWidth().height(6.dp),
trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="book_count">
<item quantity="one">%1$d book</item>
<item quantity="other">%1$d books</item>
</plurals>
<plurals name="book_word">
<item quantity="one">book</item>
<item quantity="other">books</item>
</plurals>
<plurals name="shelf_count">
<item quantity="one">%1$d shelf</item>
<item quantity="other">%1$d shelves</item>
</plurals>
<plurals name="search_results_count">
<item quantity="one">%1$d result found</item>
<item quantity="other">%1$d results found</item>
</plurals>
</resources>

View file

@ -1,6 +1,249 @@
<resources> <resources>
<string name="app_name">Episteme</string> <string name="app_name">Episteme</string>
<!-- General Actions & Words -->
<string name="action_cancel">Cancel</string>
<string name="action_save">Save</string>
<string name="action_delete">Delete</string>
<string name="action_remove">Remove</string>
<string name="action_ok">OK</string>
<string name="action_close">Close</string>
<string name="action_add">Add</string>
<string name="action_rename">Rename</string>
<string name="action_back">Back</string>
<string name="action_search">Search</string>
<string name="action_clear">Clear</string>
<string name="action_apply">Apply</string>
<string name="tab_free">Free</string>
<string name="action_save_catalog">Save</string>
<!-- Shared Composables & Dialogs -->
<string name="legal_agreement_full">%1$s you agree to our %2$s and acknowledge you have read our %3$s.</string>
<string name="legal_terms_of_service">Terms of Service</string>
<string name="legal_privacy_policy">Privacy Policy</string>
<string name="legal_licenses">Licenses</string>
<string name="items_selected_count">%1$d selected</string>
<string name="clear_selection">Clear Selection</string>
<string name="pin_unpin">Pin/Unpin</string>
<string name="info">Info</string>
<string name="select_all">Select All</string>
<string name="dialog_delete_permanently">Delete File(s) Permanently</string>
<string name="dialog_remove_from_recents">Remove from Recents</string>
<string name="dialog_warning_folder_sync_delete">Warning: Some selected items are synced from a local folder. Proceeding will delete the actual files from your device storage.\n\nThis action cannot be undone.</string>
<string name="dialog_permanently_delete_desc">Do you want to permanently delete %1$d selected file(s) from your device? This action cannot be undone.</string>
<string name="dialog_remove_recents_desc">Do you want to remove %1$d selected file(s) from the recent files list? It will reappear if you open it again from the library.</string>
<string name="file_information">File Information</string>
<string name="book_name">Book Name</string>
<string name="copy_name">Copy Name</string>
<string name="original_name">Original Name: %1$s</string>
<string name="revert_to_original">Revert to Original</string>
<string name="file_name">File Name: %1$s</string>
<string name="author">Author</string>
<string name="format">Format</string>
<string name="size">Size</string>
<string name="added">Added</string>
<string name="location">Location</string>
<string name="source_opds">Source: OPDS Stream</string>
<string name="source_in_app">In-App Storage</string>
<string name="internal_storage">Internal storage</string>
<string name="about_title">About Episteme</string>
<string name="about_version">Version: %1$s (Build: %2$d)</string>
<string name="empty_select_file">Select a File</string>
<string name="clear_cloud_data_title">Clear All Synced Data?</string>
<string name="clear_cloud_data_desc">Are you sure you want to permanently delete all of your book data from the cloud? This will also wipe your local library to prevent re-syncing. This action cannot be undone.</string>
<string name="delete_all_data">DELETE ALL DATA</string>
<!-- Navigation & Home -->
<string name="nav_home">Home</string>
<string name="nav_library">Library</string>
<string name="recent_files">Recent Files</string>
<string name="your_library_empty">Your Library is Empty</string>
<string name="your_library_empty_desc">Select a file to read, or sync a local folder to automatically import books.</string>
<string name="no_recent_files">No Recent Files</string>
<string name="no_recent_files_desc">Open a file from your library to see it here.</string>
<string name="setup_folder_sync">Setup Folder Sync</string>
<string name="sync_folder">Sync Folder</string>
<string name="local_folder">Local Folder</string>
<string name="opds_stream">OPDS Stream</string>
<string name="pinned">Pinned</string>
<string name="progress_complete">%1$d%% complete</string>
<string name="not_available_locally">Not available locally</string>
<!-- Drawer Menu -->
<string name="drawer_sign_in">Sign in with Google</string>
<string name="drawer_by_signing_in">By signing in,</string>
<string name="drawer_pro_unlocked">Episteme Pro</string>
<string name="drawer_upgrade_pro">Upgrade to Episteme Pro</string>
<string name="drawer_sync_library">Sync Library</string>
<string name="drawer_backup_local_folders">Cloud sync for Local Folders</string>
<string name="drawer_backup_desc">Upload books from your synced folders to Google Drive.</string>
<string name="drawer_custom_fonts">Custom Fonts</string>
<string name="drawer_help_feedback">Help &amp; Feedback</string>
<string name="drawer_sign_out">Sign Out</string>
<!-- Top Bar Options -->
<string name="options_recent_limit">Recent Files Limit</string>
<string name="options_no_limit">No limit</string>
<string name="options_files_limit">%1$d files</string>
<string name="options_clear_book_cache">Clear Book Cache</string>
<string name="options_clear_reflow_cache">Clear Reflow Cache</string>
<!-- Library Screen -->
<string name="library_title">Library</string>
<string name="search_placeholder">Search title or author…</string>
<string name="filter_types">Types: %1$s</string>
<string name="filter_folders">Folders: %1$d</string>
<string name="filter_status">Status: %1$s</string>
<string name="tab_all_books">All Books</string>
<string name="tab_shelves">Shelves</string>
<string name="tab_folders">Folders</string>
<string name="tab_catalogs">Catalogs</string>
<string name="no_results_found">No results found for \"%1$s\"</string>
<string name="library_empty_desc">Select a PDF, EPUB, MOBI, or AZW3 file from your device to get started.</string>
<string name="fab_add_file">Add file</string>
<string name="fab_new_shelf">New shelf</string>
<string name="create_new_shelf">Create New Shelf</string>
<string name="shelf_name_hint">Shelf Name</string>
<string name="action_create">Create</string>
<string name="menu_rename_shelf">Rename shelf</string>
<string name="menu_delete_shelf">Delete shelf</string>
<string name="fab_add_books">Add books</string>
<string name="shelf_empty">This shelf is empty</string>
<string name="add_to_shelf">Add to %1$s</string>
<string name="fab_add_count">ADD (%1$d)</string>
<string name="no_unshelved_books">No unshelved books to add</string>
<string name="all_books_in_shelf">All books are already in this shelf</string>
<string name="dialog_rename_shelf">Rename Shelf</string>
<string name="dialog_delete_shelf">Delete Shelf?</string>
<string name="dialog_delete_shelf_desc">Are you sure you want to delete the \'%1$s\' shelf? All books will be moved to Unshelved.</string>
<string name="dialog_remove_from_shelf">Remove from Shelf?</string>
<string name="dialog_remove_from_shelf_desc">Are you sure you want to remove %1$d %2$s from the \'%3$s\' shelf? The book(s) will remain in your library and appear under Unshelved.</string>
<string name="dialog_delete_shelves">Delete %1$s?</string>
<string name="dialog_delete_shelves_desc">Are you sure you want to delete the %1$d selected %2$s? All books within will be moved to Unshelved.</string>
<!-- Folder Sync -->
<string name="sync_local_folders">Sync Local Folders</string>
<string name="sync_folders_desc">Connect local folders to create a live library. Episteme will monitor files and sync progress.</string>
<string name="fab_add_folder">Add Folder</string>
<string name="scan_all">Scan All</string>
<string name="scanning">Scanning…</string>
<string name="sync_meta">Sync Meta</string>
<string name="last_sync">LAST SYNC</string>
<string name="books_count">BOOKS</string>
<string name="menu_edit_filters">Edit Filters</string>
<string name="menu_remove_folder">Remove Folder</string>
<string name="filter_file_types">Filter File Types</string>
<string name="filter_file_types_desc">Select the file types you want to sync from this folder:</string>
<string name="filter_library">Filter Library</string>
<string name="filter_file_type">File Type</string>
<string name="filter_source_folder">Source Folder</string>
<string name="filter_read_status">Read Status</string>
<string name="clear_all">Clear All</string>
<!-- OPDS -->
<string name="fab_add_catalog">Add Catalog</string>
<string name="search_catalog_placeholder">Search catalog…</string>
<string name="feed_empty">This feed is empty.</string>
<string name="status_downloading">Downloading…</string>
<string name="status_loading">Loading…</string>
<string name="action_stream">Stream</string>
<string name="action_unavailable">Unavailable</string>
<string name="action_download">Download</string>
<string name="download_format">Download Format</string>
<string name="action_stream_now">Stream Now</string>
<string name="action_read">Read</string>
<string name="no_supported_formats">No supported formats available.</string>
<string name="publisher">PUBLISHER</string>
<string name="published">PUBLISHED</string>
<string name="language">LANGUAGE</string>
<string name="synopsis">Synopsis</string>
<string name="edit_catalog">Edit Catalog</string>
<string name="add_opds_catalog">Add OPDS Catalog</string>
<string name="catalog_name">Catalog Name</string>
<string name="url">URL</string>
<string name="url_placeholder">http://192.168.1.50:8080/opds</string>
<string name="auth_optional">Authentication (Optional)</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="delete_catalog">Delete Catalog</string>
<string name="delete_catalog_desc">Are you sure you want to delete \'%1$s\'?</string>
<string name="delete_catalog_warning">Deleting this catalog will also permanently remove %1$d streaming books associated with it from your library.</string>
<string name="preset_label">Preset</string>
<!-- Pro Screen -->
<string name="free_plan">Free Plan</string>
<string name="price_free">$0</string>
<string name="forever_free">Forever free</string>
<string name="feature_multiple_formats">Multiple Formats</string>
<string name="feature_multiple_formats_desc">Supports PDF, EPUB, MOBI, AZW3</string>
<string name="feature_tts">Android Text-to-Speech</string>
<string name="feature_tts_desc">Listen to your books with built-in TTS</string>
<string name="feature_dict">Basic Dictionary</string>
<string name="feature_dict_desc">Look up single words quickly</string>
<string name="current_plan">Current Plan</string>
<string name="pro_sale_off">50%% OFF</string>
<string name="loading_price">Loading price…</string>
<string name="one_time_payment">One-time payment</string>
<string name="lifetime_access">Lifetime Access</string>
<string name="early_access_sale">Early Access Sale</string>
<string name="pro_includes">Everything in Free, plus:</string>
<string name="feature_cloud_sync">Cloud Sync Across Devices</string>
<string name="feature_cloud_sync_desc">Keep your entire library, including book files and reading progress, synced across up to 4 devices.</string>
<string name="feature_summarize">Summarization</string>
<string name="feature_summarize_desc">Get quick summaries of chapters or pages</string>
<string name="feature_smart_dict">Smart Dictionary</string>
<string name="feature_smart_dict_desc">Search phrases and even paragraphs, not just single words</string>
<string name="feature_priority">Priority Feature Requests</string>
<string name="feature_priority_desc">Your suggestions get prioritized</string>
<string name="pro_unlocked">Pro Features Unlocked!</string>
<string name="sign_in_required">Sign in Required</string>
<string name="verifying_purchase">Verifying purchase…</string>
<string name="existing_purchase_found">Existing Purchase Found</string>
<string name="get_lifetime_access">Get Lifetime Access</string>
<string name="upgrade_unavailable">Upgrade currently unavailable. Please check your internet and try again.</string>
<string name="sign_in_to_purchase">Please sign in to your Google account to purchase Episteme Pro.</string>
<string name="verifying_purchase_desc">This may take a few moments. Your Pro status will be updated automatically.</string>
<string name="dialog_existing_purchase_desc">This device already has a Pro purchase, but it\'s linked to a different account. Please sign in to the account that was used for the original purchase to restore your Pro features.</string>
<string name="dialog_early_access_desc">You\'re getting Episteme Pro at a special discounted price during our early access period! This is a limited-time offer.</string>
<string name="dialog_sign_in_required_desc">Please sign in to your Google account to purchase Episteme Pro and unlock all premium features.</string>
<string name="action_not_now">Not Now</string>
<string name="action_got_it">Got It!</string>
<!-- Fonts -->
<string name="custom_fonts">Custom Fonts</string>
<string name="import_font">Import Font</string>
<string name="no_custom_fonts">No Custom Fonts</string>
<string name="import_fonts_desc">Import TTF or OTF files to use them in your books.</string>
<string name="font_preview_text">Grumpy wizards make toxic brew for the evil queen! 1234567890 ?.,;:</string>
<string name="font_preview_error">Preview unavailable (Invalid font file)</string>
<string name="dialog_delete_font">Delete Font?</string>
<string name="dialog_delete_font_desc">Are you sure you want to delete \'%1$s\'? This will remove it from all your devices if sync is on.</string>
<!-- Feedback -->
<string name="get_in_touch">Get in Touch</string>
<string name="feedback_desc">Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or send us an email.</string>
<string name="github_issues">GitHub Issues</string>
<string name="github_issues_desc">Report bugs, request features, and track development progress.</string>
<string name="email_support">Email Support</string>
<string name="email_support_desc">Contact us directly via email for any other inquiries.</string>
<!-- Dialogs -->
<string name="dialog_unlock_pro">Unlock Episteme Pro</string>
<string name="dialog_unlock_pro_desc">Sync across devices is a Pro feature. Unlock all pro features with a single, one-time purchase.</string>
<string name="action_upgrade">Upgrade</string>
<string name="dialog_confirm_sign_out">Confirm Sign Out</string>
<string name="dialog_confirm_sign_out_desc">Are you sure you want to sign out?</string>
<string name="device_limit_reached">Device Limit Reached</string>
<string name="device_limit_reached_desc">To use Episteme Pro on this device, please remove one of your existing registered devices.</string>
<string name="last_seen">Last seen: %1$s</string>
<string name="dialog_destructive_action">Confirm Destructive Action</string>
<string name="dialog_destructive_action_desc">This will permanently delete all your books and reading progress from this device AND from your Google Drive account. This action cannot be undone. Are you sure?</string>
<string name="dialog_clear_book_cache">Clear Book Cache</string>
<string name="dialog_clear_book_cache_desc">This will clear all processed pages in pagination mode. This helps fix layout issues but will require books to be re-processed next time you open them.</string>
<string name="action_confirm_clear">Confirm &amp; Clear</string>
<string name="dialog_clear_reflow_cache">Clear Reflow Cache</string>
<string name="dialog_clear_reflow_cache_desc">This will delete all generated \'Text View\' versions of your PDFs and clear their associated images/HTML cache. Your original PDFs will remain untouched.</string>
<!-- Tooltip titles --> <!-- Tooltip titles -->
<string name="tooltip_back">Back</string> <string name="tooltip_back">Back</string>
<string name="tooltip_dictionary">Dictionary</string> <string name="tooltip_dictionary">Dictionary</string>
@ -58,4 +301,328 @@
<string name="tooltip_hide_results_desc">Collapse the search results panel</string> <string name="tooltip_hide_results_desc">Collapse the search results panel</string>
<string name="tooltip_prev_result_desc">Jump to the previous search match in the document</string> <string name="tooltip_prev_result_desc">Jump to the previous search match in the document</string>
<string name="tooltip_next_result_desc">Jump to the next search match in the document</string> <string name="tooltip_next_result_desc">Jump to the next search match in the document</string>
<string name="action_sign_in">Sign In</string>
<string name="action_select_folder">Select Folder</string>
<string name="action_select">Select</string>
<string name="legal_footer_combined">Privacy Policy • Terms of Service • Licenses</string>
<string name="app_name_oss">Episteme OSS</string>
<string name="debug_show_device_management">[Debug] Show Device Management</string>
<string name="debug_clear_cloud_local_data">[Debug] Clear Cloud &amp; Local Data</string>
<string name="debug_fps">FPS: %1$d</string>
<string name="error_folder_selection_unsupported">Your device doesn\'t support folder selection. You can still import files individually.</string>
<string name="error_no_file_manager">No file manager found. Please install a file manager app.</string>
<string name="feedback_email_subject">Feedback: Episteme Reader</string>
<string name="banner_downloaded">Downloaded %1$s</string>
<string name="filter_facet">%1$s: %2$s</string>
<string name="never">Never</string>
<string name="folder_filter_count">%1$s: %2$d</string>
<!-- ViewModel Messages: Banners & Errors -->
<string name="banner_removed_streaming_books">Removed %1$d streaming books.</string>
<string name="error_import_font">Failed to import font: %1$s</string>
<string name="banner_text_view_deleted">Text view deleted.</string>
<string name="error_sync_drive_permission">Sync requires Google Drive permission.</string>
<string name="error_purchase_general">An error occurred with the purchase.</string>
<string name="banner_upgrade_success">Upgrade successful! Welcome to Pro.</string>
<string name="error_purchase_verification">Purchase verification failed. Please contact support if you were charged.</string>
<string name="banner_device_removed">This device was removed from your account.</string>
<string name="error_verify_device">Could not verify this device. Please check your connection.</string>
<string name="error_update_devices">Failed to update devices. Please try again.</string>
<string name="banner_saving_pdf">Saving PDF…</string>
<string name="banner_pdf_saved">PDF saved successfully.</string>
<string name="error_open_file_saving">Failed to open file for saving.</string>
<string name="error_saving_pdf">Error saving PDF: %1$s</string>
<string name="banner_saving_original_pdf">Saving original PDF…</string>
<string name="banner_original_pdf_saved">Original PDF saved successfully.</string>
<string name="share_subject">Sharing: %1$s</string>
<string name="share_chooser_title">Share PDF</string>
<string name="error_share_failed">Share failed: %1$s</string>
<string name="error_folder_limit_reached">Limit reached: Maximum %1$d folders allowed.</string>
<string name="error_folder_already_synced">This folder is already synced.</string>
<string name="banner_folder_added">Folder added: %1$s</string>
<string name="error_access_folder_permissions">Failed to access folder permissions.</string>
<string name="banner_folder_removed">Folder removed.</string>
<string name="banner_folder_sync_updating">Folder Sync: Updating metadata…</string>
<string name="banner_folder_sync_scanning">Scanning folder for new books…</string>
<string name="banner_folder_sync_complete">Folder Sync: Scan complete.</string>
<string name="error_sync_failed">Sync failed.</string>
<string name="error_enable_sync_download">Enable sync to download files.</string>
<string name="error_download_failed">Failed to download %1$s.</string>
<string name="error_enable_sync_clear_cloud">Enable sync to clear cloud data.</string>
<string name="error_not_signed_in_clear_cloud">Not signed in, cannot clear cloud data.</string>
<string name="banner_clearing_cloud_local_data">Clearing all cloud and local data…</string>
<string name="banner_cloud_local_data_cleared">All cloud and local data cleared successfully.</string>
<string name="error_clear_all_data">Error: Failed to clear all data.</string>
<string name="banner_local_data_cleared">All local data cleared.</string>
<string name="error_sign_in_failed">Sign in failed. Please try again.</string>
<string name="error_no_google_account">Could not find a Google account. This can happen on a fresh install, please try again in a moment.</string>
<string name="error_sign_in_internet">An error occurred during sign in. Please check your internet connection.</string>
<string name="error_sign_in_device_management">Please sign in to test device management.</string>
<string name="error_sync_pro_feature">Sync is an Episteme Pro feature.</string>
<string name="error_not_signed_in_sync">Not signed in, cannot sync.</string>
<string name="banner_cloud_sync_checking">Cloud Sync: Checking for updates…</string>
<string name="banner_cloud_sync_complete">Cloud Sync: Complete.</string>
<string name="error_sync_library_failed">Failed to sync library.</string>
<string name="error_recent_item_not_found">Could not find recent item.</string>
<string name="error_import_file_failed">Failed to import file.</string>
<string name="error_file_location_not_found">Could not find file location.</string>
<string name="error_load_generated_text_view">Failed to load generated text view.</string>
<string name="error_text_view_generation_failed">Text view generation failed.</string>
<string name="error_load_fb2">Failed to load FB2: %1$s</string>
<string name="error_load_file">Failed to load file: %1$s</string>
<string name="error_load_mobi">Failed to load MOBI: %1$s</string>
<string name="error_load_epub">Failed to load EPUB: %1$s</string>
<string name="banner_file_deleted_from_folder">File deleted from folder. Removed from library.</string>
<string name="error_shelf_exists">A shelf with that name already exists.</string>
<string name="banner_deleting_all_devices">Deleting from all devices…</string>
<string name="banner_deletion_complete">Deletion complete.</string>
<string name="error_cloud_sync_failed_deleted_locally">Cloud sync failed, deleted locally.</string>
<string name="banner_books_removed_library">%1$d book(s) removed from library.</string>
<string name="banner_reflow_cache_cleared">Reflow cache &amp; generated text views cleared.</string>
<!-- Common.kt: SearchTopBar & SearchNavigationControls -->
<string name="search_in_book">Search in book…</string>
<string name="content_desc_close_search">Close Search</string>
<string name="content_desc_clear_search">Clear Search</string>
<string name="content_desc_hide_results">Hide Results</string>
<string name="content_desc_show_results">Show Results</string>
<string name="content_desc_prev_result">Previous Search Result</string>
<string name="content_desc_next_result">Next Search Result</string>
<string name="search_no_results_simple">No results found.</string>
<!-- Common.kt: SummarizationPopup -->
<string name="generating_summary">Generating summary…</string>
<string name="action_stop">Stop</string>
<string name="action_read_aloud">Read aloud</string>
<string name="action_copy">Copy</string>
<string name="no_summary_generated">No summary could be generated.</string>
<!-- Common.kt: AiDefinitionPopup -->
<string name="ai_thinking">Thinking…</string>
<string name="content_desc_open_dictionary">Open in Dictionary App</string>
<string name="ai_no_definition">AI could not provide a definition.</string>
<string name="ai_asking_about">Asking AI about \'%1$s\'…</string>
<!-- Common.kt: TtsSettingsSheet -->
<string name="tts_settings">Text-to-Speech Settings</string>
<string name="tts_stop_to_change_settings">Please stop playback to change settings.</string>
<string name="tts_synthesis_mode">Synthesis Mode</string>
<string name="tts_mode_on_device">On-Device</string>
<string name="tts_mode_cloud_hq">Cloud (HQ)</string>
<string name="tts_voice_selection">Voice Selection</string>
<string name="tts_play_sample">Play Sample</string>
<!-- Common.kt: DeviceVoiceSettingsSheet -->
<string name="tts_device_voice_settings">On-Device Voice Settings</string>
<string name="content_desc_close_settings">Close Settings</string>
<string name="tts_system_default">System Default</string>
<string name="tts_system_default_desc">Matches your Android system settings</string>
<string name="content_desc_selected">Selected</string>
<string name="tts_loading_voices">Loading voices…</string>
<string name="tts_no_voices">No voices available on this device.</string>
<string name="tts_specific_voices">Specific Voices</string>
<string name="tts_available_voices_count">Available Voices (%1$d)</string>
<string name="tts_no_voices_for_language">No voices found for this language.</string>
<string name="tts_voice_variant">Variant: %1$s</string>
<string name="tts_voice_sample_text">This is a sample of %1$s.</string>
<!-- Common.kt: ReaderThemePanel & ThemeBuilderView -->
<string name="reading_themes">Reading Themes</string>
<string name="theme_presets">Presets</string>
<string name="theme_my_themes">My Themes</string>
<string name="theme_no_custom">No custom themes yet. Tap \'+\' to create one.</string>
<string name="theme_new">New Theme</string>
<string name="theme_edit">Edit Theme</string>
<string name="theme_name">Theme Name</string>
<string name="theme_preview_quote">So many books, so little time.</string>
<string name="theme_preview_author">- Frank Zappa</string>
<string name="theme_low_contrast_warning">⚠️ Low contrast! This might cause eye strain.</string>
<string name="theme_page_color">Page Color</string>
<string name="theme_text_color">Text Color</string>
<string name="theme_color_live_preview">Live Preview</string>
<string name="theme_color_preview_text">Reading is dreaming.</string>
<string name="theme_color_hex">Hex</string>
<!-- Common.kt: RGB Inputs -->
<string name="color_r">R</string>
<string name="color_g">G</string>
<string name="color_b">B</string>
<!-- Common.kt: fetchAiDefinition -->
<string name="error_text_empty">Text is empty.</string>
<string name="error_ai_empty_definition">AI returned an empty definition.</string>
<string name="error_could_not_get_definition">Could not get definition.</string>
<string name="error_unknown_server">An unknown server error occurred.</string>
<string name="error_network_check_connection">Network error. Check connection.</string>
<!-- Common.kt: fetchRecap -->
<string name="error_not_enough_context">Not enough context for a recap.</string>
<string name="error_parse_recap">Failed to parse recap.</string>
<string name="error_network_recap">Network error during recap generation.</string>
<!-- DictionarySettingsDialog.kt -->
<string name="dict_lookup_settings">Lookup Settings</string>
<string name="dict_dictionary_engine">Dictionary Engine</string>
<string name="dict_smart_ai">Smart (AI)</string>
<string name="dict_external_app">External App</string>
<string name="dict_ai_description">Uses AI for definitions. Will fall back to the external app below if offline or if the selected phrase is too long.</string>
<string name="dict_external_description">Uses the selected app for dictionary lookups.</string>
<string name="dict_fallback_app">Fallback App</string>
<string name="dict_dictionary_app">Dictionary App</string>
<string name="dict_select_app">Select an app</string>
<string name="dict_translate">Translate</string>
<string name="dict_translate_description">App used for translating selected text.</string>
<string name="dict_search_app">Search App</string>
<string name="dict_search_app_description">App used for web searches.</string>
<string name="dict_none">None</string>
<!-- EpubReaderAi.kt -->
<string name="ai_error_book_content_empty">The book content is empty.</string>
<string name="ai_error_parse_summary">Failed to parse summary from server response.</string>
<string name="ai_error_fetch_summary">Could not fetch summary.</string>
<string name="ai_error_with_code">Error: %1$d. %2$s</string>
<string name="ai_error_network_server">Network error. Please check connection and server status.</string>
<string name="ai_analyzing_chapter">Analyzing Chapter %1$d…</string>
<string name="ai_reading_position">Reading current position…</string>
<string name="ai_generating_recap">Generating Recap…</string>
<string name="ai_chapter_summary">Chapter Summary</string>
<string name="ai_story_recap_beta">Story Recap (Beta)</string>
<string name="ai_unlock_summarization">Unlock Chapter Summarization</string>
<string name="ai_unlock_summarization_desc">Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.</string>
<string name="action_learn_more">Learn More</string>
<string name="ai_unlock_smart_dict">Unlock Smart Dictionary</string>
<string name="ai_unlock_smart_dict_desc">Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.</string>
<!-- EpubReaderAnnotations.kt -->
<string name="content_desc_bookmark_icon">Bookmark</string>
<string name="content_desc_selected_slot">Selected Slot</string>
<string name="dialog_customize_palette">Customize Palette</string>
<string name="palette_tap_slot_to_edit">Tap a slot to edit:</string>
<string name="palette_select_color_for_slot">Select a color for the slot:</string>
<!-- EpubReaderContent.kt -->
<string name="chapter_empty">This chapter is empty.</string>
<string name="chapter_not_found">Chapter not found</string>
<string name="error_loading_chapter">Error loading chapter</string>
<!-- EpubReaderControls.kt -->
<string name="content_desc_dictionary_settings">Dictionary Settings</string>
<string name="tooltip_theme">Theme</string>
<string name="tooltip_theme_desc">Theme Settings</string>
<string name="content_desc_more_options">More Options</string>
<string name="menu_view_original_pdf">View Original PDF</string>
<string name="menu_delete_text_view">Delete Text View</string>
<string name="menu_reading_mode_vertical">Reading Mode: Vertical</string>
<string name="menu_reading_mode_paginated">Reading Mode: Paginated</string>
<string name="content_desc_enabled">Enabled</string>
<string name="menu_remove_bookmark">Remove bookmark</string>
<string name="menu_bookmark_this_page">Bookmark this page</string>
<string name="menu_tap_to_turn_pages">Tap to Turn Pages</string>
<string name="menu_volume_button_scrolling">Volume Button Scrolling</string>
<string name="menu_volume_button_page_turn">Volume Button Page Turn</string>
<string name="menu_realistic_page_turns">Realistic Page Turns</string>
<string name="menu_keep_screen_on">Keep Screen On</string>
<string name="menu_visual_options">Visual Options</string>
<string name="menu_auto_scroll">Auto Scroll</string>
<string name="menu_tts_voice_settings">TTS Voice Settings</string>
<string name="menu_tts_settings_debug">TTS Settings (Debug)</string>
<string name="content_desc_navigate_slider">Navigate with slider</string>
<string name="content_desc_chapters_menu">Chapters Menu</string>
<string name="content_desc_text_formatting">Text Formatting</string>
<string name="menu_chapter_summarization">Chapter Summarization</string>
<string name="menu_recap_beta">Recap (Beta)</string>
<string name="content_desc_stop_tts">Stop TTS</string>
<string name="content_desc_start_tts">Start TTS</string>
<string name="content_desc_pause_tts">Pause TTS</string>
<string name="content_desc_resume_tts">Resume TTS</string>
<string name="content_desc_exit_slider">Exit slider navigation</string>
<string name="content_desc_start_page_thumbnail">Start page thumbnail</string>
<string name="content_desc_expand">Expand</string>
<string name="content_desc_collapse">Collapse</string>
<string name="content_desc_pause_playback">Pause</string>
<string name="content_desc_start_playback">Play</string>
<string name="auto_scroll_local_speed">Local Speed</string>
<string name="auto_scroll_global_speed">Global Speed</string>
<string name="content_desc_select_mode">Select Mode</string>
<string name="auto_scroll_applies_all_files">Applies to all files</string>
<string name="auto_scroll_saved_for_file">Saved for this file only</string>
<string name="content_desc_disable_musician_mode">Disable Musician Mode</string>
<string name="content_desc_enable_musician_mode">Enable Musician Mode</string>
<string name="content_desc_swap_controls">Swap Controls</string>
<string name="label_min">Min</string>
<string name="label_max">Max</string>
<string name="content_desc_slower">Slower</string>
<string name="content_desc_faster">Faster</string>
<string name="page_of_pages">Page %1$d of %2$d</string>
<!-- EpubReaderDrawer.kt -->
<string name="tab_chapters">Chapters</string>
<string name="tab_bookmarks">Bookmarks</string>
<string name="tab_highlights">Highlights</string>
<string name="no_bookmarks_yet">You haven\'t added any bookmarks yet.</string>
<string name="bookmark_page_of">Page %1$d of %2$d</string>
<string name="content_desc_more_options_bookmark">More options for bookmark</string>
<string name="menu_rename">Rename</string>
<string name="dialog_rename_bookmark">Rename Bookmark</string>
<string name="label_new_name">New Name</string>
<string name="dialog_delete_bookmark">Delete Bookmark?</string>
<string name="dialog_delete_bookmark_desc">Are you sure you want to permanently delete this bookmark?</string>
<string name="no_highlights_yet">No highlights yet.</string>
<string name="unknown_chapter">Unknown Chapter</string>
<string name="content_desc_options">Options</string>
<string name="dialog_delete_highlight">Delete Highlight?</string>
<string name="dialog_delete_highlight_desc">Are you sure you want to permanently delete this highlight?</string>
<!-- EpubReaderScreen.kt -->
<string name="banner_original_pdf_not_found">Original PDF not found.</string>
<string name="error_book_content_not_found">Error: Book content not found. Path: %1$s</string>
<string name="toast_select_dictionary_first">Please select a dictionary app first.</string>
<string name="toast_select_translate_first">Please select a translate app first.</string>
<string name="toast_select_search_first">Please select a search app first.</string>
<string name="no_chapters_available">No chapters available for this book.</string>
<string name="navigating_to_position">Navigating to position…</string>
<string name="dialog_permission_required">Permission Required</string>
<string name="dialog_permission_notification_desc">To show playback controls while the app is in the background, please grant the notification permission.</string>
<string name="action_continue">Continue</string>
<string name="dialog_justified_text_limitation">Justified Text Limitation</string>
<string name="dialog_justified_text_limitation_desc">Using Justified alignment in Paginated Mode may cause text selection and highlights to be inaccurate due to layout limitations.</string>
<string name="action_i_understand">I Understand</string>
<string name="navigating_to_chapter">Navigating to chapter…</string>
<string name="toast_select_offline_dict_first">Select an offline dictionary first.</string>
<string name="banner_book_not_paginated">Book is not paginated yet.</string>
<string name="banner_wait_for_load">Wait for book to load fully.</string>
<string name="release_for_previous_chapter">Release for Previous Chapter</string>
<string name="release_for_next_chapter">Release for Next Chapter</string>
<string name="pull_further_percent">Pull further… (%1$d%%)</string>
<string name="chapter">Chapter</string>
<string name="error_could_not_get_chapter_content">Could not get chapter content.</string>
<string name="error_could_not_determine_chapter">Could not determine current chapter.</string>
<string name="error_webview_not_available">WebView not available.</string>
<string name="page_number_of_total">Page %1$d/%2$d</string>
<!-- EpubReaderSettings.kt -->
<string name="format_local">Local Format</string>
<string name="format_global">Global Format</string>
<string name="action_reset">Reset</string>
<string name="label_size">Size</string>
<string name="label_spacing">Spacing</string>
<string name="select_font">Select Font</string>
<string name="tab_presets">Presets</string>
<string name="tab_imported">Imported</string>
<string name="button_import_from_files">Import from Files</string>
<string name="no_imported_fonts_yet">No imported fonts yet.</string>
<string name="visual_options_title">Visual Options</string>
<string name="visual_options_system_ui">System UI (Status &amp; Navigation Bars)</string>
<string name="visual_options_system_ui_desc">Control the visibility of the device\'s system bars.</string>
<string name="visual_options_progress_bar">Progress Bar</string>
<string name="visual_options_progress_bar_desc">The reading progress and chapter indicator at the bottom of the screen.</string>
<string name="visual_options_seamless_chapter">Seamless Chapter Transition</string>
<string name="visual_options_seamless_chapter_desc">Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation.</string>
<!-- ExternalDictionaryHelper.kt -->
<string name="dict_app_label_search">Search</string>
<string name="error_opening_dictionary">Error opening dictionary</string>
<string name="error_opening_translate">Error opening translate app</string>
<string name="error_opening_search">Error opening search app</string>
</resources> </resources>