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:
parent
da1c5bce1e
commit
381193d774
20 changed files with 1388 additions and 855 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) })
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
19
app/src/main/res/values/plurals.xml
Normal file
19
app/src/main/res/values/plurals.xml
Normal 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>
|
||||||
|
|
@ -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 & 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 & 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 & 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 & 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 & 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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue