New languages (#256)

* Remove custom-onnxruntime-arm64.aar

* Refactor remaining, hardcoded UI strings into localizable resources and added French and Russian to language list

* Fix background auto-advance for Text-to-Speech (TTS)
This commit is contained in:
Aryan 2026-04-29 18:35:23 +05:30 committed by GitHub
parent 224d12d1fb
commit 4582b506f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 940 additions and 488 deletions

View file

@ -33,7 +33,7 @@ android {
versionCode = 46 versionCode = 46
versionName = "1.0.46" versionName = "1.0.46"
resourceConfigurations += setOf("en", "ar", "de", "tr") resourceConfigurations += setOf("en", "ar", "de", "tr", "fr", "ru")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {

View file

@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@ -258,12 +259,12 @@ fun AppNavigation(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text("Error: $errorMessage", color = MaterialTheme.colorScheme.error) Text(stringResource(R.string.error_message_format, errorMessage), color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { Button(onClick = {
viewModel.clearSelectedFile() viewModel.clearSelectedFile()
}) { }) {
Text("Go Back") Text(stringResource(R.string.action_go_back))
} }
} }
} }

View file

@ -1126,10 +1126,13 @@ fun TtsSettingsSheet(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
DeviceVoicesTab(isTtsActive, context, TtsPlaybackManager.TtsMode.BASE) DeviceVoicesTab(isTtsActive, context, TtsPlaybackManager.TtsMode.BASE)
} else { } else {
Text("Active TTS Engine", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.tts_active_engine), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, RoundedCornerShape(24.dp)).padding(4.dp)) { Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, RoundedCornerShape(24.dp)).padding(4.dp)) {
val modes = listOf(TtsPlaybackManager.TtsMode.CLOUD to "Cloud AI", TtsPlaybackManager.TtsMode.BASE to "Device Native") val modes = listOf(
TtsPlaybackManager.TtsMode.CLOUD to stringResource(R.string.tts_mode_cloud_ai),
TtsPlaybackManager.TtsMode.BASE to stringResource(R.string.tts_mode_device_native)
)
modes.forEach { (mode, title) -> modes.forEach { (mode, title) ->
val isSelected = currentMode == mode val isSelected = currentMode == mode
Box( Box(
@ -1150,9 +1153,9 @@ fun TtsSettingsSheet(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
TabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent, divider = {}) { TabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent, divider = {}) {
Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text("Cloud Voices", maxLines = 1, overflow = TextOverflow.Ellipsis) }) Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text(stringResource(R.string.tts_tab_cloud_voices), maxLines = 1, overflow = TextOverflow.Ellipsis) })
Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text("Device Voices", maxLines = 1, overflow = TextOverflow.Ellipsis) }) Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text(stringResource(R.string.tts_tab_device_voices), maxLines = 1, overflow = TextOverflow.Ellipsis) })
Tab(selected = selectedTabIndex == 2, onClick = { selectedTabIndex = 2 }, text = { Text("Cloud Cache", maxLines = 1, overflow = TextOverflow.Ellipsis) }) Tab(selected = selectedTabIndex == 2, onClick = { selectedTabIndex = 2 }, text = { Text(stringResource(R.string.tts_tab_cloud_cache), maxLines = 1, overflow = TextOverflow.Ellipsis) })
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@ -1180,10 +1183,10 @@ fun AiVoicesTab(
val isCloudMode = currentMode == TtsPlaybackManager.TtsMode.CLOUD val isCloudMode = currentMode == TtsPlaybackManager.TtsMode.CLOUD
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("Select High-Quality Cloud Voice", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.tts_select_cloud_voice), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
if (samplePlayer.cachedSpeakers.isNotEmpty()) { if (samplePlayer.cachedSpeakers.isNotEmpty()) {
TextButton(onClick = { samplePlayer.clearSamples() }, modifier = Modifier.heightIn(min = 24.dp)) { TextButton(onClick = { samplePlayer.clearSamples() }, modifier = Modifier.heightIn(min = 24.dp)) {
Text("Clear Samples", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelMedium) Text(stringResource(R.string.tts_clear_samples), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelMedium)
} }
} }
} }
@ -1244,7 +1247,8 @@ fun DeviceVoicesTab(
var allVoices by remember { mutableStateOf<List<Voice>>(emptyList()) } var allVoices by remember { mutableStateOf<List<Voice>>(emptyList()) }
var isTtsLoading by remember { mutableStateOf(true) } var isTtsLoading by remember { mutableStateOf(true) }
var selectedLanguage by remember { mutableStateOf("All") } val allLanguagesLabel = stringResource(R.string.filter_all)
var selectedLanguage by remember { mutableStateOf(allLanguagesLabel) }
var languageMenuExpanded by remember { mutableStateOf(false) } var languageMenuExpanded by remember { mutableStateOf(false) }
DisposableEffect(Unit) { DisposableEffect(Unit) {
@ -1264,13 +1268,13 @@ fun DeviceVoicesTab(
} }
val languages = remember(allVoices) { val languages = remember(allVoices) {
val list = listOf("All") + allVoices.map { it.locale.displayLanguage }.filter { it.isNotBlank() }.distinct().sorted() val list = listOf(allLanguagesLabel) + allVoices.map { it.locale.displayLanguage }.filter { it.isNotBlank() }.distinct().sorted()
Timber.tag("TTS_DIAGNOSE").d("Languages list updated: size=${list.size}, items=$list") Timber.tag("TTS_DIAGNOSE").d("Languages list updated: size=${list.size}, items=$list")
list list
} }
val filteredVoices = remember(allVoices, selectedLanguage) { val filteredVoices = remember(allVoices, selectedLanguage) {
if (selectedLanguage == "All") allVoices if (selectedLanguage == allLanguagesLabel) allVoices
else allVoices.filter { it.locale.displayLanguage == selectedLanguage } else allVoices.filter { it.locale.displayLanguage == selectedLanguage }
} }
@ -1310,8 +1314,8 @@ fun DeviceVoicesTab(
) )
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("System Default Voice", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(stringResource(R.string.tts_system_default_voice), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Uses device settings", style = MaterialTheme.typography.bodySmall) Text(stringResource(R.string.tts_uses_device_settings), style = MaterialTheme.typography.bodySmall)
} }
if (isBaseMode && savedVoiceName == null) Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary) if (isBaseMode && savedVoiceName == null) Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
} }
@ -1326,7 +1330,7 @@ fun DeviceVoicesTab(
value = selectedLanguage, value = selectedLanguage,
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
label = { Text("Language Filter") }, label = { Text(stringResource(R.string.tts_language_filter)) },
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = languageMenuExpanded) }, trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = languageMenuExpanded) },
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(), colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth().menuAnchor(), modifier = Modifier.fillMaxWidth().menuAnchor(),
@ -1359,7 +1363,7 @@ fun DeviceVoicesTab(
ListItem( ListItem(
headlineContent = { Text(voice.locale.displayName, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal) }, headlineContent = { Text(voice.locale.displayName, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal) },
supportingContent = { Text(if (voice.isNetworkConnectionRequired) "Online" else "Offline") }, supportingContent = { Text(if (voice.isNetworkConnectionRequired) stringResource(R.string.tts_online) else stringResource(R.string.tts_offline)) },
leadingContent = { leadingContent = {
if (isSelected) { if (isSelected) {
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
@ -1378,11 +1382,11 @@ fun DeviceVoicesTab(
onClick = { onClick = {
ttsEngine?.apply { ttsEngine?.apply {
this.voice = voice this.voice = voice
speak("This is a voice sample.", TextToSpeech.QUEUE_FLUSH, null, "sample_${voice.name}") speak(context.getString(R.string.tts_voice_sample_generic), TextToSpeech.QUEUE_FLUSH, null, "sample_${voice.name}")
} }
} }
) { ) {
Icon(Icons.Default.PlayArrow, contentDescription = "Play Sample", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.PlayArrow, contentDescription = stringResource(R.string.tts_play_sample), tint = MaterialTheme.colorScheme.primary)
} }
} }
) )
@ -1415,7 +1419,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("Cloud TTS Cache", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) Text(stringResource(R.string.tts_tab_cloud_cache), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
Surface(color = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(8.dp)) { Surface(color = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(8.dp)) {
Text(formatBytes(totalSize), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) Text(formatBytes(totalSize), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
} }
@ -1431,7 +1435,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
value = selectedSpeakerFilter, value = selectedSpeakerFilter,
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
label = { Text("Voice Filter") }, label = { Text(stringResource(R.string.tts_voice_filter)) },
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterMenuExpanded) }, trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterMenuExpanded) },
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(), colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth().menuAnchor() modifier = Modifier.fillMaxWidth().menuAnchor()
@ -1459,7 +1463,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
if (chapters.isEmpty()) { if (chapters.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().height(150.dp), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxWidth().height(150.dp), contentAlignment = Alignment.Center) {
Text("No audio cached for this voice.", style = MaterialTheme.typography.bodyMedium) Text(stringResource(R.string.tts_no_audio_cached_for_voice), style = MaterialTheme.typography.bodyMedium)
} }
} else { } else {
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 240.dp).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp))) { LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 240.dp).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp))) {
@ -1467,7 +1471,16 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
val chapter = chapters[index] val chapter = chapters[index]
ListItem( ListItem(
headlineContent = { headlineContent = {
Text("${chapter.chapterTitle} (${chapter.chunkCount} chunks)", fontWeight = FontWeight.Medium) Text(
"${chapter.chapterTitle} ${
context.resources.getQuantityString(
R.plurals.tts_cache_chunk_count_parenthetical,
chapter.chunkCount,
chapter.chunkCount
)
}",
fontWeight = FontWeight.Medium
)
}, },
supportingContent = { Text(formatBytes(chapter.sizeBytes)) }, supportingContent = { Text(formatBytes(chapter.sizeBytes)) },
trailingContent = { trailingContent = {
@ -1475,7 +1488,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
cacheManager.deleteSpecificFiles(chapter.matchingFiles, chapter.directory) cacheManager.deleteSpecificFiles(chapter.matchingFiles, chapter.directory)
chapters = cacheManager.getChapterCaches(bookTitle, selectedSpeakerFilter) chapters = cacheManager.getChapterCaches(bookTitle, selectedSpeakerFilter)
}) { }) {
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error) Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete), tint = MaterialTheme.colorScheme.error)
} }
} }
) )
@ -1495,7 +1508,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
) { ) {
Icon(Icons.Default.Delete, contentDescription = null) Icon(Icons.Default.Delete, contentDescription = null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Clear Cache for $selectedSpeakerFilter") Text(stringResource(R.string.tts_clear_cache_for_voice, selectedSpeakerFilter))
} }
} }
} }
@ -1958,8 +1971,8 @@ fun ReaderThemePanel(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("Preserve Image Colors", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.theme_preserve_image_colors), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
Text("Keep original image colors when theme changes", style = MaterialTheme.typography.bodySmall, color = Color.Gray) Text(stringResource(R.string.theme_preserve_image_colors_desc), style = MaterialTheme.typography.bodySmall, color = Color.Gray)
} }
androidx.compose.material3.Switch( androidx.compose.material3.Switch(
checked = excludeImages, checked = excludeImages,
@ -1980,7 +1993,7 @@ fun ReaderThemePanel(
) { ) {
Text(stringResource(R.string.theme_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 = stringResource(R.string.theme_new), tint = MaterialTheme.colorScheme.primary)
} }
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@ -2462,7 +2475,7 @@ fun HighlightColorPickerDialog(
.padding(horizontal = 24.dp, vertical = 8.dp) .padding(horizontal = 24.dp, vertical = 8.dp)
) { ) {
Text( Text(
text = "Customize Highlights", text = stringResource(R.string.highlight_customize_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White color = Color.White
@ -2494,7 +2507,7 @@ fun HighlightColorPickerDialog(
if (isSelected) { if (isSelected) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White
) )
} }
@ -2539,7 +2552,7 @@ fun HighlightColorPickerDialog(
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) })
} }
@ -2548,15 +2561,15 @@ fun HighlightColorPickerDialog(
modifier = Modifier.weight(2.4f), modifier = Modifier.weight(2.4f),
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
RgbInputColumn(label = "R", value = currentColor.red, RgbInputColumn(label = stringResource(R.string.color_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(label = "G", value = currentColor.green, RgbInputColumn(label = stringResource(R.string.color_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(label = "B", value = currentColor.blue, RgbInputColumn(label = stringResource(R.string.color_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)
) )
@ -2571,11 +2584,11 @@ fun HighlightColorPickerDialog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextButton(onClick = { updateFromColor(selectedSlot.color) }) { TextButton(onClick = { updateFromColor(selectedSlot.color) }) {
Text("Reset", color = Color(0xFFFF5252)) Text(stringResource(R.string.action_reset), color = Color(0xFFFF5252))
} }
Row { Row {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel", color = Color.Gray) Text(stringResource(R.string.action_cancel), color = Color.Gray)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Button( Button(
@ -2584,7 +2597,7 @@ fun HighlightColorPickerDialog(
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)
} }
} }
} }
@ -2647,7 +2660,7 @@ fun AiHubBottomSheet(
) { ) {
Box(modifier = Modifier.weight(1f)) Box(modifier = Modifier.weight(1f))
Text( Text(
text = "AI Features", text = stringResource(R.string.ai_features_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -2671,9 +2684,12 @@ fun AiHubBottomSheet(
} }
} }
val tabs = mutableListOf("Summary") val summaryTab = stringResource(R.string.ai_tab_summary)
if (onGenerateRecap != null) tabs.add("Recap") val recapTab = stringResource(R.string.ai_tab_recap)
if (summaryCacheManager != null) tabs.add("Cache") val cacheTab = stringResource(R.string.ai_tab_cache)
val tabs = mutableListOf(summaryTab)
if (onGenerateRecap != null) tabs.add(recapTab)
if (summaryCacheManager != null) tabs.add(cacheTab)
TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.padding(bottom = 16.dp)) { TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.padding(bottom = 16.dp)) {
tabs.forEachIndexed { index, title -> tabs.forEachIndexed { index, title ->
@ -2683,11 +2699,11 @@ fun AiHubBottomSheet(
} }
} }
val activeTab = tabs.getOrNull(selectedTabIndex) ?: "Summary" val activeTab = tabs.getOrNull(selectedTabIndex) ?: summaryTab
var cacheRefreshTrigger by remember { mutableIntStateOf(0) } var cacheRefreshTrigger by remember { mutableIntStateOf(0) }
when (activeTab) { when (activeTab) {
"Summary" -> { summaryTab -> {
val cachedSummary = remember(currentChapterIndex, cacheRefreshTrigger) { summaryCacheManager?.getSummary(bookTitle, currentChapterIndex) } val cachedSummary = remember(currentChapterIndex, cacheRefreshTrigger) { summaryCacheManager?.getSummary(bookTitle, currentChapterIndex) }
val effectiveResult = summarizationResult ?: if (cachedSummary != null) SummarizationResult(summary = cachedSummary, isCacheHit = true) else null val effectiveResult = summarizationResult ?: if (cachedSummary != null) SummarizationResult(summary = cachedSummary, isCacheHit = true) else null
@ -2696,7 +2712,7 @@ fun AiHubBottomSheet(
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painterResource(R.drawable.summarize), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary) Icon(painterResource(R.drawable.summarize), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text("No summary for ${chapterTitle.lowercase()} yet.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.ai_no_summary_for_chapter, chapterTitle.lowercase()), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Button( Button(
onClick = { onGenerateSummary(false) }, onClick = { onGenerateSummary(false) },
@ -2704,7 +2720,7 @@ fun AiHubBottomSheet(
) { ) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp)) Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Generate Summary for $chapterTitle") Text(stringResource(R.string.ai_generate_summary_for_chapter, chapterTitle))
} }
} }
} }
@ -2721,14 +2737,14 @@ fun AiHubBottomSheet(
) )
} }
} }
"Recap" -> { recapTab -> {
// Recap Tab // Recap Tab
if (recapResult == null && !isRecapLoading) { if (recapResult == null && !isRecapLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary) Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text("Get a recap of the story up to your current position.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) Text(stringResource(R.string.ai_recap_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Button( Button(
onClick = { onGenerateRecap?.invoke() }, onClick = { onGenerateRecap?.invoke() },
@ -2736,13 +2752,13 @@ fun AiHubBottomSheet(
) { ) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp)) Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Generate Story Recap") Text(stringResource(R.string.ai_generate_story_recap))
} }
} }
} }
} else { } else {
AiResultContentView( AiResultContentView(
title = "Story Recap", title = stringResource(R.string.ai_story_recap),
result = recapResult, result = recapResult,
isLoading = isRecapLoading, isLoading = isRecapLoading,
isMainTtsActive = isMainTtsActive, isMainTtsActive = isMainTtsActive,
@ -2754,7 +2770,7 @@ fun AiHubBottomSheet(
) )
} }
} }
"Cache" -> { cacheTab -> {
if (summaryCacheManager != null) { if (summaryCacheManager != null) {
ManageCacheTab(bookTitle, summaryCacheManager, onCacheChanged = { ManageCacheTab(bookTitle, summaryCacheManager, onCacheChanged = {
cacheRefreshTrigger++ cacheRefreshTrigger++
@ -2804,15 +2820,15 @@ fun AiResultContentView(
) { ) {
Text( Text(
text = if (result.isCacheHit) { text = if (result.isCacheHit) {
"⚡ Cache Hit • Free" stringResource(R.string.ai_cache_hit_free)
} else if (result.cost != null) { } else if (result.cost != null) {
if (result.cost == 0.0 && result.freeRemaining != null) { if (result.cost == 0.0 && result.freeRemaining != null) {
"✨ Generated • Free (${result.freeRemaining}/10 left)" stringResource(R.string.ai_generated_free_remaining, result.freeRemaining ?: 0)
} else { } else {
"✨ Generated • Cost: ${result.cost} credits" stringResource(R.string.ai_generated_cost, result.cost.toString())
} }
} else { } else {
"✨ Generating... • Cost: Calculating" stringResource(R.string.ai_generating_cost_calculating)
}, },
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = if (result.isCacheHit || (result.cost == 0.0 && result.freeRemaining != null)) Color( color = if (result.isCacheHit || (result.cost == 0.0 && result.freeRemaining != null)) Color(
@ -2828,7 +2844,7 @@ fun AiResultContentView(
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(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) {
@ -2856,7 +2872,7 @@ fun AiResultContentView(
IconButton(onClick = onClear) { IconButton(onClick = onClear) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Clear", contentDescription = stringResource(R.string.action_clear),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error
) )
} }
@ -2865,7 +2881,7 @@ fun AiResultContentView(
if (onRegenerate != null) { if (onRegenerate != null) {
TextButton(onClick = onRegenerate) { TextButton(onClick = onRegenerate) {
Text("Regenerate") Text(stringResource(R.string.ai_regenerate))
} }
} }
@ -2881,7 +2897,7 @@ fun AiResultContentView(
ttsController.start( ttsController.start(
chunks = chunks, chunks = chunks,
bookTitle = title, bookTitle = title,
chapterTitle = "AI Output", chapterTitle = context.getString(R.string.ai_output_title),
coverImageUri = null, coverImageUri = null,
ttsMode = loadTtsMode(context), ttsMode = loadTtsMode(context),
playbackSource = "POPUP", playbackSource = "POPUP",
@ -2895,7 +2911,7 @@ fun AiResultContentView(
) { ) {
Icon( Icon(
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow, imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = "Read Aloud" contentDescription = stringResource(R.string.action_read_aloud)
) )
} }
IconButton(onClick = { IconButton(onClick = {
@ -2903,7 +2919,7 @@ fun AiResultContentView(
}) { }) {
Icon( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy" contentDescription = stringResource(R.string.action_copy)
) )
} }
} }
@ -2964,7 +2980,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
if (cachedItems.isEmpty()) { if (cachedItems.isEmpty()) {
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
Text("No cached summaries for this book.", style = MaterialTheme.typography.bodyMedium) Text(stringResource(R.string.ai_no_cached_summaries), style = MaterialTheme.typography.bodyMedium)
} }
} else { } else {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
@ -2990,7 +3006,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Delete", contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error
) )
} }
@ -3016,7 +3032,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
}, },
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) { ) {
Text("Clear All", color = MaterialTheme.colorScheme.error) Text(stringResource(R.string.clear_all), color = MaterialTheme.colorScheme.error)
} }
} }
} }

View file

@ -1,22 +1,3 @@
/*
* Episteme Reader - A native Android document reader.
* Copyright (C) 2026 Episteme
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* mail: epistemereader@gmail.com
*/
// FontsScreen.kt // FontsScreen.kt
@file:Suppress("KotlinConstantConditions") @file:Suppress("KotlinConstantConditions")
@ -93,6 +74,8 @@ fun FontsScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val showGoogleFontsOption = !(BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE)
// Dialog state // Dialog state
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) } var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) }
@ -116,7 +99,7 @@ fun FontsScreen(
title = { Text(stringResource(R.string.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 = stringResource(R.string.action_back))
} }
} }
) )
@ -127,13 +110,15 @@ fun FontsScreen(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
ExtendedFloatingActionButton( if (showGoogleFontsOption) {
onClick = { showGoogleFontsSheet = true }, ExtendedFloatingActionButton(
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) }, onClick = { showGoogleFontsSheet = true },
text = { Text("Google Fonts") }, icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
containerColor = MaterialTheme.colorScheme.secondaryContainer, text = { Text(stringResource(R.string.google_fonts)) },
contentColor = MaterialTheme.colorScheme.onSecondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer,
) contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
}
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { pickFontLauncher.launch(fontMimeTypes) }, onClick = { pickFontLauncher.launch(fontMimeTypes) },
@ -146,13 +131,16 @@ fun FontsScreen(
) { padding -> ) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) { Box(modifier = Modifier.fillMaxSize().padding(padding)) {
if (fonts.isEmpty()) { if (fonts.isEmpty()) {
val secondaryText = if (showGoogleFontsOption) stringResource(R.string.action_browse_google_fonts) else null
val secondaryClick: (() -> Unit)? = if (showGoogleFontsOption) { { showGoogleFontsSheet = true } } else null
EmptyState( EmptyState(
title = stringResource(R.string.no_custom_fonts), title = stringResource(R.string.no_custom_fonts),
message = stringResource(R.string.import_fonts_desc), message = stringResource(R.string.import_fonts_desc),
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) }, onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
secondaryButtonText = "Browse Google Fonts", secondaryButtonText = secondaryText,
onSecondaryClick = { showGoogleFontsSheet = true } onSecondaryClick = secondaryClick
) )
} else { } else {
LazyColumn( LazyColumn(
@ -254,7 +242,7 @@ fun GoogleFontsBottomSheet(
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
Text( Text(
text = "Browse Google Fonts", text = stringResource(R.string.action_browse_google_fonts),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp) modifier = Modifier.padding(bottom = 12.dp)
@ -264,7 +252,7 @@ fun GoogleFontsBottomSheet(
value = searchQuery, value = searchQuery,
onValueChange = { searchQuery = it }, onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search 1900+ fonts...") }, placeholder = { Text(stringResource(R.string.google_fonts_search_placeholder)) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
@ -280,7 +268,7 @@ fun GoogleFontsBottomSheet(
if (searchQuery.isBlank()) { if (searchQuery.isBlank()) {
item { item {
Text( Text(
text = "Popular Choices", text = stringResource(R.string.google_fonts_popular_choices),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
@ -289,7 +277,7 @@ fun GoogleFontsBottomSheet(
} else if (displayList.isEmpty()) { } else if (displayList.isEmpty()) {
item { item {
Text( Text(
text = "No fonts found matching '$searchQuery'", text = stringResource(R.string.google_fonts_no_matches, searchQuery),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
@ -335,7 +323,7 @@ fun GoogleFontsBottomSheet(
isDownloaded -> { isDownloaded -> {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Already Downloaded", contentDescription = stringResource(R.string.content_desc_already_downloaded),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -350,7 +338,7 @@ fun GoogleFontsBottomSheet(
else -> { else -> {
Icon( Icon(
Icons.Default.CloudDownload, Icons.Default.CloudDownload,
contentDescription = "Download", contentDescription = stringResource(R.string.action_download),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -396,7 +384,7 @@ fun FontListItem(
IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) { IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
Icon( Icon(
Icons.Default.Delete, Icons.Default.Delete,
contentDescription = "Delete", contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error
) )
} }

View file

@ -853,7 +853,7 @@ fun RecentFileCard(
) { ) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
.background(MaterialTheme.colorScheme.primary, CircleShape) .background(MaterialTheme.colorScheme.primary, CircleShape)
.padding(8.dp), .padding(8.dp),
@ -1009,13 +1009,13 @@ fun DefaultTopAppBar(
Badge() Badge()
} }
}) { }) {
Icon(Icons.Default.Menu, contentDescription = "Open Drawer") Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_open_drawer))
} }
} }
}, actions = { }, actions = {
Box { Box {
IconButton(onClick = onAppThemeClick) { IconButton(onClick = onAppThemeClick) {
Icon(painterResource(id = R.drawable.palette), contentDescription = "App Theme") Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.content_desc_app_theme))
} }
} }
// Recent Files Limit Menu // Recent Files Limit Menu
@ -1035,7 +1035,7 @@ fun DefaultTopAppBar(
showLimitMenu = false showLimitMenu = false
}, },
trailingIcon = if (uiState.recentFilesLimit == limit) { trailingIcon = if (uiState.recentFilesLimit == limit) {
{ Icon(Icons.Default.Check, contentDescription = "Selected") } { Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
} else null } else null
) )
} }
@ -1045,7 +1045,7 @@ fun DefaultTopAppBar(
// Options Menu (MoreVert) // Options Menu (MoreVert)
Box { Box {
IconButton(onClick = { showOptionsMenu = true }) { IconButton(onClick = { showOptionsMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More Options") Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_more_options))
} }
DropdownMenu( DropdownMenu(
expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) { expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) {
@ -1056,12 +1056,12 @@ fun DefaultTopAppBar(
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(text = { Text("Enable Multi-Tab Reading") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_enable_multi_tab_reading)) }, onClick = {
onTabsToggle(!uiState.isTabsEnabled) onTabsToggle(!uiState.isTabsEnabled)
showOptionsMenu = false showOptionsMenu = false
}, trailingIcon = { }, trailingIcon = {
if (uiState.isTabsEnabled) { if (uiState.isTabsEnabled) {
Icon(Icons.Default.Check, contentDescription = "Enabled") Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
} }
}) })
@ -1070,18 +1070,18 @@ fun DefaultTopAppBar(
showOptionsMenu = false showOptionsMenu = false
}) })
DropdownMenuItem(text = { Text("Use Strict File Filter") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_use_strict_file_filter)) }, onClick = {
onStrictFilterToggleClick() onStrictFilterToggleClick()
showOptionsMenu = false showOptionsMenu = false
}, trailingIcon = { }, trailingIcon = {
if (uiState.useStrictFileFilter) { if (uiState.useStrictFileFilter) {
Icon(Icons.Default.Check, contentDescription = "Enabled") Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
} }
}) })
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(text = { Text("Language") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_language)) }, onClick = {
onLanguageClick() onLanguageClick()
showOptionsMenu = false showOptionsMenu = false
}) })
@ -1098,17 +1098,17 @@ fun DefaultTopAppBar(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(text = { Text("Test Panel ML Detection") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_test_panel_ml_detection)) }, onClick = {
onTestPanelDetectionClick() onTestPanelDetectionClick()
showOptionsMenu = false showOptionsMenu = false
}) })
DropdownMenuItem(text = { Text("Test Speech Bubble ML Detection") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_test_speech_bubble_ml_detection)) }, onClick = {
onTestSpeechBubbleDetectionClick() onTestSpeechBubbleDetectionClick()
showOptionsMenu = false showOptionsMenu = false
}) })
DropdownMenuItem(text = { Text("Export Logs (Last 5000 lines)") }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_export_logs_last_lines, 5000)) }, onClick = {
onExportLogsClick() onExportLogsClick()
showOptionsMenu = false showOptionsMenu = false
}) })
@ -1163,7 +1163,7 @@ private fun AppDrawerContent(
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(photoUrl) model = ImageRequest.Builder(LocalContext.current).data(photoUrl)
.crossfade(true).build(), .crossfade(true).build(),
contentDescription = "Profile picture", contentDescription = stringResource(R.string.content_desc_profile_picture),
modifier = Modifier modifier = Modifier
.size(80.dp) .size(80.dp)
.clip(CircleShape), .clip(CircleShape),
@ -1172,7 +1172,7 @@ private fun AppDrawerContent(
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.AccountCircle, imageVector = Icons.Outlined.AccountCircle,
contentDescription = "Profile", contentDescription = stringResource(R.string.content_desc_profile),
modifier = Modifier.size(80.dp) modifier = Modifier.size(80.dp)
) )
} }
@ -1189,9 +1189,9 @@ private fun AppDrawerContent(
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp)
) { ) {
Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.FormatListNumbered, contentDescription = "Credits", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer) Icon(Icons.Default.FormatListNumbered, contentDescription = stringResource(R.string.credits_tab), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer)
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text("${uiState.credits} Credits", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onTertiaryContainer) Text(stringResource(R.string.credits_count, uiState.credits), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onTertiaryContainer)
} }
} }
} }
@ -1239,7 +1239,7 @@ private fun AppDrawerContent(
if (!uiState.isProUser) { if (!uiState.isProUser) {
Icon( Icon(
imageVector = Icons.Default.VerifiedUser, imageVector = Icons.Default.VerifiedUser,
contentDescription = "Pro Feature", contentDescription = stringResource(R.string.content_desc_pro_feature),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -1294,7 +1294,7 @@ private fun AppDrawerContent(
) { ) {
AsyncImage( AsyncImage(
model = R.mipmap.ic_launcher, model = R.mipmap.ic_launcher,
contentDescription = "App Icon", contentDescription = stringResource(R.string.content_desc_app_icon),
modifier = Modifier.size(64.dp) modifier = Modifier.size(64.dp)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -1467,7 +1467,7 @@ fun DeviceManagementScreen(
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.PhoneAndroid, contentDescription = "Device") Icon(Icons.Default.PhoneAndroid, contentDescription = stringResource(R.string.content_desc_device))
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(device.deviceName, fontWeight = FontWeight.SemiBold) Text(device.deviceName, fontWeight = FontWeight.SemiBold)
@ -1679,9 +1679,9 @@ fun CloseAllTabsDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
fun StrictFilterConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { fun StrictFilterConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Enable Strict File Filter") }, title = { Text(stringResource(R.string.dialog_strict_file_filter_title)) },
text = { Text("If you enable this, some supported file types like AZW3, CB7, and FB2 might not show up depending on your file manager.\n\nAre you sure you want to enable this filter?") }, text = { Text(stringResource(R.string.dialog_strict_file_filter_desc)) },
confirmButton = { TextButton(onClick = onConfirm) { Text("Enable") } }, confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_enable)) } },
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }
) )
} }
@ -1714,13 +1714,13 @@ fun AppThemeBottomSheet(
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
Text( Text(
text = "App Theme", text = stringResource(R.string.app_theme_title),
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("Appearance", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.app_theme_appearance), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) { Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
AppThemeMode.entries.forEach { mode -> AppThemeMode.entries.forEach { mode ->
@ -1731,14 +1731,14 @@ fun AppThemeBottomSheet(
.clickable { onThemeModeChanged(mode) }, .clickable { onThemeModeChanged(mode) },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(mode.displayName, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold) Text(stringResource(mode.labelRes), color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
} }
} }
} }
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text("Contrast", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.app_theme_contrast), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) { Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
AppContrastOption.entries.forEach { option -> AppContrastOption.entries.forEach { option ->
@ -1749,14 +1749,14 @@ fun AppThemeBottomSheet(
.clickable { onContrastOptionChanged(option) }, .clickable { onContrastOptionChanged(option) },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(option.displayName, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold) Text(stringResource(option.labelRes), color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
} }
} }
} }
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text("Text Brightness", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.app_theme_text_brightness), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row( Row(
modifier = Modifier modifier = Modifier
@ -1778,32 +1778,33 @@ fun AppThemeBottomSheet(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text("Color Scheme", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.app_theme_color_scheme), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
val presets = listOf(
R.string.app_theme_preset_ocean to Color(0xFF00668B),
R.string.app_theme_preset_mint to Color(0xFF006C4C),
R.string.app_theme_preset_rose to Color(0xFF9C4146),
R.string.app_theme_preset_sepia to Color(0xFF705D49),
R.string.app_theme_preset_amethyst to Color(0xFF9B59B6),
R.string.app_theme_preset_amber to Color(0xFFFFC107),
R.string.app_theme_preset_sapphire to Color(0xFF0F52BA)
)
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
item { item {
ThemeSwatch( ThemeSwatch(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
isSelected = uiState.appSeedColor == null, isSelected = uiState.appSeedColor == null,
label = "Dynamic", label = stringResource(R.string.app_theme_dynamic),
onClick = { onSeedColorChanged(null) } onClick = { onSeedColorChanged(null) }
) )
} }
val presets = listOf(
"Ocean" to Color(0xFF00668B),
"Mint" to Color(0xFF006C4C),
"Rose" to Color(0xFF9C4146),
"Sepia" to Color(0xFF705D49),
"Amethyst" to Color(0xFF9B59B6),
"Amber" to Color(0xFFFFC107),
"Sapphire" to Color(0xFF0F52BA)
)
items(presets.size) { i -> items(presets.size) { i ->
val (label, color) = presets[i] val (labelRes, color) = presets[i]
ThemeSwatch( ThemeSwatch(
color = color, color = color,
isSelected = uiState.appSeedColor == color, isSelected = uiState.appSeedColor == color,
label = label, label = stringResource(labelRes),
onClick = { onSeedColorChanged(color) } onClick = { onSeedColorChanged(color) }
) )
} }
@ -1812,15 +1813,15 @@ fun AppThemeBottomSheet(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, 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 = { showCreateDialog = true }, modifier = Modifier.size(24.dp)) { IconButton(onClick = { showCreateDialog = true }, modifier = Modifier.size(24.dp)) {
Icon(Icons.Default.Add, contentDescription = "Add Custom Theme", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_add_custom_theme), tint = MaterialTheme.colorScheme.primary)
} }
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
if (uiState.customAppThemes.isEmpty()) { if (uiState.customAppThemes.isEmpty()) {
Text("No custom themes yet.", 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 {
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(uiState.customAppThemes) { theme -> items(uiState.customAppThemes) { theme ->
@ -1873,7 +1874,7 @@ fun ThemeSwatch(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = label, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 64.dp)) Text(text = label, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 64.dp))
if (onDelete != null) { if (onDelete != null) {
Icon(Icons.Default.Close, contentDescription = "Delete", modifier = Modifier.size(16.dp).clickable { onDelete() }, tint = MaterialTheme.colorScheme.error) Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_delete), modifier = Modifier.size(16.dp).clickable { onDelete() }, tint = MaterialTheme.colorScheme.error)
} }
} }
} }
@ -1887,6 +1888,7 @@ fun CreateAppThemeDialog(
onSave: (String, Color) -> Unit onSave: (String, Color) -> Unit
) { ) {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
val context = LocalContext.current
val initialHsv = remember(initialColor) { val initialHsv = remember(initialColor) {
val hsv = FloatArray(3) val hsv = FloatArray(3)
@ -1931,7 +1933,7 @@ fun CreateAppThemeDialog(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = "Create App Theme", text = stringResource(R.string.app_theme_create_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White color = Color.White
@ -1942,7 +1944,7 @@ fun CreateAppThemeDialog(
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)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors( colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
@ -1990,7 +1992,7 @@ fun CreateAppThemeDialog(
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) })
} }
@ -1999,15 +2001,15 @@ fun CreateAppThemeDialog(
modifier = Modifier.weight(2.4f), modifier = Modifier.weight(2.4f),
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
RgbInputColumn(label = "R", value = currentColor.red, RgbInputColumn(label = stringResource(R.string.color_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(label = "G", value = currentColor.green, RgbInputColumn(label = stringResource(R.string.color_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(label = "B", value = currentColor.blue, RgbInputColumn(label = stringResource(R.string.color_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)
) )
@ -2022,14 +2024,14 @@ fun CreateAppThemeDialog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel", color = Color.Gray) Text(stringResource(R.string.action_cancel), color = Color.Gray)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
androidx.compose.material3.Button( androidx.compose.material3.Button(
onClick = { onSave(name.ifBlank { "Custom Theme" }, currentColor) }, onClick = { onSave(name.ifBlank { context.getString(R.string.app_theme_custom_default_name) }, currentColor) },
colors = ButtonDefaults.buttonColors(containerColor = currentColor) colors = ButtonDefaults.buttonColors(containerColor = currentColor)
) { ) {
Text("Save", color = if (currentColor.luminance() > 0.5f) Color.Black else Color.White, fontWeight = FontWeight.Bold) Text(stringResource(R.string.action_save), color = if (currentColor.luminance() > 0.5f) Color.Black else Color.White, fontWeight = FontWeight.Bold)
} }
} }
} }
@ -2043,18 +2045,20 @@ fun LanguageSelectionDialog(onDismiss: () -> Unit) {
val currentTag = if (!currentLocales.isEmpty) currentLocales.get(0)?.language ?: "en" else "en" val currentTag = if (!currentLocales.isEmpty) currentLocales.get(0)?.language ?: "en" else "en"
val languages = listOf( val languages = listOf(
"en" to "English (Default)", "en" to R.string.language_english_default,
"ar" to "العربية (Arabic)", "ar" to R.string.language_arabic,
"de" to "Deutsch (German)", "de" to R.string.language_german,
"tr" to "Türkçe (Turkish)" "tr" to R.string.language_turkish,
"fr" to R.string.language_french,
"ru" to R.string.language_russian
) )
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Language") }, title = { Text(stringResource(R.string.options_language)) },
text = { text = {
Column { Column {
languages.forEach { (tag, name) -> languages.forEach { (tag, nameRes) ->
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -2069,7 +2073,7 @@ fun LanguageSelectionDialog(onDismiss: () -> Unit) {
) { ) {
RadioButton(selected = currentTag == tag, onClick = null) RadioButton(selected = currentTag == tag, onClick = null)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Text(name) Text(stringResource(nameRes))
} }
} }
} }

View file

@ -630,7 +630,7 @@ fun LibraryScreenContent(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { onSearchActiveChange(false) }) { IconButton(onClick = { onSearchActiveChange(false) }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_close_search))
} }
OutlinedTextField( OutlinedTextField(
value = textFieldValue, value = textFieldValue,
@ -654,7 +654,7 @@ fun LibraryScreenContent(
trailingIcon = { trailingIcon = {
if (searchQuery.isNotEmpty()) { if (searchQuery.isNotEmpty()) {
IconButton(onClick = { onSearchQueryChange("") }) { IconButton(onClick = { onSearchQueryChange("") }) {
Icon(Icons.Default.Close, contentDescription = "Clear query") Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_clear_query))
} }
} }
} }
@ -666,17 +666,17 @@ fun LibraryScreenContent(
actions = { actions = {
if (pagerState.currentPage == 0) { if (pagerState.currentPage == 0) {
IconButton(onClick = onFilterClick) { IconButton(onClick = onFilterClick) {
Icon(Icons.Default.FilterList, contentDescription = "Filter") Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.content_desc_filter))
} }
Box { Box {
TextButton(onClick = { showSortMenu = true }) { TextButton(onClick = { showSortMenu = true }) {
Icon( Icon(
painter = painterResource(id = R.drawable.sort), painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort", contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName) Text(stringResource(sortOrder.labelRes))
} }
DropdownMenu( DropdownMenu(
expanded = showSortMenu, expanded = showSortMenu,
@ -684,7 +684,7 @@ fun LibraryScreenContent(
) { ) {
SortOrder.entries.forEach { order -> SortOrder.entries.forEach { order ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(order.displayName) }, text = { Text(stringResource(order.labelRes)) },
onClick = { onClick = {
onSortOrderChange(order) onSortOrderChange(order)
showSortMenu = false showSortMenu = false
@ -693,7 +693,7 @@ fun LibraryScreenContent(
if (order == sortOrder) { if (order == sortOrder) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected" contentDescription = stringResource(R.string.content_desc_selected)
) )
} }
} }
@ -702,7 +702,7 @@ fun LibraryScreenContent(
} }
} }
IconButton(onClick = { onSearchActiveChange(true) }) { IconButton(onClick = { onSearchActiveChange(true) }) {
Icon(Icons.Default.Search, contentDescription = "Search") Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
} }
} }
} }
@ -733,34 +733,34 @@ fun LibraryScreenContent(
AssistChip( AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) }, onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
label = { Text(stringResource(R.string.filter_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 = stringResource(R.string.action_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(stringResource(R.string.filter_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 = stringResource(R.string.action_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(stringResource(R.string.filter_status, libraryFilters.readStatus.displayName)) }, label = { Text(stringResource(R.string.filter_status, stringResource(libraryFilters.readStatus.labelRes))) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
) )
} }
if (libraryFilters.tagIds.isNotEmpty()) { if (libraryFilters.tagIds.isNotEmpty()) {
val selectedTags = allTags.filter { it.id in libraryFilters.tagIds } val selectedTags = allTags.filter { it.id in libraryFilters.tagIds }
val tagLabel = when { val tagLabel = when {
selectedTags.isEmpty() -> "${libraryFilters.tagIds.size} tags" selectedTags.isEmpty() -> pluralStringResource(R.plurals.tag_count, libraryFilters.tagIds.size, libraryFilters.tagIds.size)
selectedTags.size <= 2 -> selectedTags.joinToString { it.name } selectedTags.size <= 2 -> selectedTags.joinToString { it.name }
else -> "${selectedTags.size} tags" else -> pluralStringResource(R.plurals.tag_count, selectedTags.size, selectedTags.size)
} }
AssistChip( AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(tagIds = emptySet())) }, onClick = { onRemoveFilter(libraryFilters.copy(tagIds = emptySet())) },
label = { Text("Tags: $tagLabel") }, label = { Text(stringResource(R.string.filter_tags, tagLabel)) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
) )
} }
} }
@ -775,7 +775,7 @@ fun LibraryScreenContent(
if (recentFiles.isNotEmpty()) { if (recentFiles.isNotEmpty()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.fab_add_file)) }, text = { Text(stringResource(R.string.fab_add_file)) },
icon = { Icon(Icons.Default.Add, contentDescription = "Add file") }, icon = { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.fab_add_file)) },
onClick = onSelectFileClick, onClick = onSelectFileClick,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
@ -784,7 +784,7 @@ fun LibraryScreenContent(
1 -> { 1 -> {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.fab_new_shelf)) }, text = { Text(stringResource(R.string.fab_new_shelf)) },
icon = { Icon(Icons.Default.Add, contentDescription = "New shelf") }, icon = { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.fab_new_shelf)) },
onClick = onNewShelfClick, onClick = onNewShelfClick,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
@ -896,7 +896,7 @@ private fun ShelvesScreen(
item { item {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text( Text(
text = "Browse by tag", text = stringResource(R.string.section_browse_by_tag),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@ -1085,7 +1085,7 @@ private fun ShelfDetailScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { closeShelfSearch() }) { IconButton(onClick = { closeShelfSearch() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_close_search))
} }
OutlinedTextField( OutlinedTextField(
value = searchFieldValue, value = searchFieldValue,
@ -1109,7 +1109,7 @@ private fun ShelfDetailScreen(
trailingIcon = { trailingIcon = {
if (searchQuery.isNotEmpty()) { if (searchQuery.isNotEmpty()) {
IconButton(onClick = { clearShelfSearchQuery() }) { IconButton(onClick = { clearShelfSearchQuery() }) {
Icon(Icons.Default.Close, contentDescription = "Clear query") Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_clear_query))
} }
} }
} }
@ -1128,9 +1128,9 @@ private fun ShelfDetailScreen(
Text( Text(
text = when { text = when {
isFolderShelf && shelf.childShelfCount > 0 && shelf.directBookCount > 0 -> isFolderShelf && shelf.childShelfCount > 0 && shelf.directBookCount > 0 ->
"${shelf.childShelfCount} folders${getBookCountString(shelf.directBookCount)}" "${pluralStringResource(R.plurals.folder_count, shelf.childShelfCount, shelf.childShelfCount)}${getBookCountString(shelf.directBookCount)}"
isFolderShelf && shelf.childShelfCount > 0 -> isFolderShelf && shelf.childShelfCount > 0 ->
"${shelf.childShelfCount} folders" pluralStringResource(R.plurals.folder_count, shelf.childShelfCount, shelf.childShelfCount)
isFolderShelf -> getBookCountString(shelf.directBookCount) isFolderShelf -> getBookCountString(shelf.directBookCount)
else -> getBookCountString(shelf.bookCount) else -> getBookCountString(shelf.bookCount)
}, },
@ -1141,7 +1141,7 @@ private fun ShelfDetailScreen(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
} }
}, },
actions = { actions = {
@ -1149,11 +1149,11 @@ private fun ShelfDetailScreen(
TextButton(onClick = { showSortMenu = true }) { TextButton(onClick = { showSortMenu = true }) {
Icon( Icon(
painter = painterResource(id = R.drawable.sort), painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort", contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName) Text(stringResource(sortOrder.labelRes))
} }
DropdownMenu( DropdownMenu(
expanded = showSortMenu, expanded = showSortMenu,
@ -1161,7 +1161,7 @@ private fun ShelfDetailScreen(
) { ) {
SortOrder.entries.forEach { order -> SortOrder.entries.forEach { order ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(order.displayName) }, text = { Text(stringResource(order.labelRes)) },
onClick = { onClick = {
onSortOrderChange(order) onSortOrderChange(order)
showSortMenu = false showSortMenu = false
@ -1170,7 +1170,7 @@ private fun ShelfDetailScreen(
if (order == sortOrder) { if (order == sortOrder) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected" contentDescription = stringResource(R.string.content_desc_selected)
) )
} }
} }
@ -1182,7 +1182,7 @@ private fun ShelfDetailScreen(
IconButton(onClick = { isSearchActive = true }) { IconButton(onClick = { isSearchActive = true }) {
Icon( Icon(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
contentDescription = "Search shelf" contentDescription = stringResource(R.string.content_desc_search_shelf)
) )
} }
@ -1191,7 +1191,7 @@ private fun ShelfDetailScreen(
IconButton(onClick = { showMoreMenu = true }) { IconButton(onClick = { showMoreMenu = true }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "More options" contentDescription = stringResource(R.string.content_desc_more_options)
) )
} }
DropdownMenu( DropdownMenu(
@ -1252,7 +1252,7 @@ private fun ShelfDetailScreen(
if (isFolderShelf) { if (isFolderShelf) {
item { item {
Text( Text(
text = "Folders", text = stringResource(R.string.section_folders),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -1275,7 +1275,7 @@ private fun ShelfDetailScreen(
} }
item { item {
Text( Text(
text = "Files", text = stringResource(R.string.section_files),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -1321,7 +1321,7 @@ private fun AddBooksModeScreen(
title = { Text(stringResource(R.string.add_to_shelf, 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 = stringResource(R.string.action_back))
} }
}, },
actions = { actions = {
@ -1329,11 +1329,11 @@ private fun AddBooksModeScreen(
TextButton(onClick = { showSortMenu = true }) { TextButton(onClick = { showSortMenu = true }) {
Icon( Icon(
painter = painterResource(id = R.drawable.sort), painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort", contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName) Text(stringResource(sortOrder.labelRes))
} }
DropdownMenu( DropdownMenu(
expanded = showSortMenu, expanded = showSortMenu,
@ -1341,14 +1341,14 @@ private fun AddBooksModeScreen(
) { ) {
SortOrder.entries.forEach { order -> SortOrder.entries.forEach { order ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(order.displayName) }, text = { Text(stringResource(order.labelRes)) },
onClick = { onClick = {
onSortOrderChange(order) onSortOrderChange(order)
showSortMenu = false showSortMenu = false
}, },
trailingIcon = { trailingIcon = {
if (order == sortOrder) { if (order == sortOrder) {
Icon(Icons.Default.Check, contentDescription = "Selected") Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected))
} }
} }
) )
@ -1368,7 +1368,7 @@ private fun AddBooksModeScreen(
FilterChip( FilterChip(
selected = source == currentSource, selected = source == currentSource,
onClick = { onSourceChange(source) }, onClick = { onSourceChange(source) },
label = { Text(source.displayName) } label = { Text(stringResource(source.labelRes)) }
) )
} }
} }
@ -1378,7 +1378,7 @@ private fun AddBooksModeScreen(
if (selectedBookUris.isNotEmpty()) { if (selectedBookUris.isNotEmpty()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.fab_add_count, 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 = stringResource(R.string.fab_add_books)) },
onClick = onAddSelectedBooks onClick = onAddSelectedBooks
) )
} }
@ -1447,7 +1447,7 @@ private fun ShelfCover(shelf: Shelf) {
.fallback(placeholder) .fallback(placeholder)
.crossfade(true) .crossfade(true)
.build(), .build(),
contentDescription = "${shelf.name} shelf cover", contentDescription = stringResource(R.string.content_desc_shelf_cover, shelf.name),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.size(width = coverWidth, height = coverHeight) .size(width = coverWidth, height = coverHeight)
@ -1637,7 +1637,7 @@ private fun LibraryListItem(
modifier = Modifier.matchParentSize().background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)), modifier = Modifier.matchParentSize().background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon(Icons.Default.Check, contentDescription = "Selected", modifier = Modifier.size(36.dp).background(MaterialTheme.colorScheme.primary, CircleShape).padding(6.dp), tint = MaterialTheme.colorScheme.onPrimary) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected), modifier = Modifier.size(36.dp).background(MaterialTheme.colorScheme.primary, CircleShape).padding(6.dp), tint = MaterialTheme.colorScheme.onPrimary)
} }
} }
} }
@ -2239,13 +2239,13 @@ fun LibraryFilterSheet(
FilterChip( FilterChip(
selected = currentFilters.readStatus == status, selected = currentFilters.readStatus == status,
onClick = { currentFilters = currentFilters.copy(readStatus = status) }, onClick = { currentFilters = currentFilters.copy(readStatus = status) },
label = { Text(status.displayName) } label = { Text(stringResource(status.labelRes)) }
) )
} }
} }
if (allTags.isNotEmpty()) { if (allTags.isNotEmpty()) {
Text("Tags", style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleMedium)
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -2740,12 +2740,12 @@ fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Un
} }
if (onEdit != null) { if (onEdit != null) {
IconButton(onClick = onEdit) { IconButton(onClick = onEdit) {
Icon(Icons.Default.Edit, contentDescription = "Edit") Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.label_edit))
} }
} }
if (onDelete != null) { if (onDelete != null) {
IconButton(onClick = onDelete) { IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Remove") Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_remove))
} }
} }
} }
@ -2997,7 +2997,7 @@ fun OpdsBookDetailsSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
Icon(Icons.Default.Check, contentDescription = "Read") Icon(Icons.Default.Check, contentDescription = stringResource(R.string.action_read))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold) Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold)
} }

View file

@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com * mail: epistemereader@gmail.com
*/ */
// MainViewModel.kt // MainViewModel.kt
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION", "ANNOTATION_WILL_BE_APPLIED_ALSO_TO_PROPERTY_OR_FIELD")
package com.aryan.reader package com.aryan.reader
@ -31,8 +31,14 @@ import android.database.Cursor
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import com.aryan.reader.tts.TtsController
import com.aryan.reader.tts.TtsPlaybackManager
import com.aryan.reader.paginatedreader.LocatorConverter
import kotlinx.serialization.protobuf.ProtoBuf
import com.aryan.reader.paginatedreader.semanticBlockModule
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@ -181,20 +187,21 @@ private data class CachedSpeechBubble(
val maskBitmap: Bitmap? val maskBitmap: Bitmap?
) )
enum class AddBooksSource(val displayName: String) { enum class AddBooksSource(@StringRes val labelRes: Int) {
UNSHELVED("Unshelved"), ALL_BOOKS("All Books") UNSHELVED(R.string.add_books_source_unshelved),
ALL_BOOKS(R.string.add_books_source_all_books)
} }
enum class AppThemeMode(val displayName: String) { enum class AppThemeMode(@StringRes val labelRes: Int) {
SYSTEM("System"), SYSTEM(R.string.app_theme_mode_system),
LIGHT("Light"), LIGHT(R.string.app_theme_mode_light),
DARK("Dark") DARK(R.string.app_theme_mode_dark)
} }
enum class AppContrastOption(val displayName: String, val value: Double) { enum class AppContrastOption(@StringRes val labelRes: Int, val value: Double) {
STANDARD("Standard", 0.0), STANDARD(R.string.app_contrast_standard, 0.0),
MEDIUM("Medium", 0.5), MEDIUM(R.string.app_contrast_medium, 0.5),
HIGH("High", 1.0) HIGH(R.string.app_contrast_high, 1.0)
} }
data class CustomAppTheme( data class CustomAppTheme(
@ -240,18 +247,21 @@ data class Shelf(
val childShelfCount: Int get() = childShelfIds.size val childShelfCount: Int get() = childShelfIds.size
} }
enum class SortOrder(val displayName: String) { enum class SortOrder(@StringRes val labelRes: Int) {
RECENT("Recent"), RECENT(R.string.sort_recent),
TITLE_ASC("Title A-Z"), TITLE_ASC(R.string.sort_title_az),
AUTHOR_ASC("Author A-Z"), AUTHOR_ASC(R.string.sort_author_az),
PERCENT_ASC("Percent complete 0-100"), PERCENT_ASC(R.string.sort_percent_asc),
PERCENT_DESC("Percent complete 100-0"), PERCENT_DESC(R.string.sort_percent_desc),
SIZE_ASC("Size (Smallest)"), SIZE_ASC(R.string.sort_size_smallest),
SIZE_DESC("Size (Biggest)") SIZE_DESC(R.string.sort_size_biggest)
} }
enum class ReadStatusFilter(val displayName: String) { enum class ReadStatusFilter(@StringRes val labelRes: Int) {
ALL("All"), UNREAD("Unread"), IN_PROGRESS("In Progress"), COMPLETED("Completed") ALL(R.string.read_status_all),
UNREAD(R.string.read_status_unread),
IN_PROGRESS(R.string.read_status_in_progress),
COMPLETED(R.string.read_status_completed)
} }
data class LibraryFilters( data class LibraryFilters(
@ -652,6 +662,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val proUpgradeState = billingClientWrapper.proUpgradeState val proUpgradeState = billingClientWrapper.proUpgradeState
val ttsController by lazy { TtsController(appContext).apply { connect() } }
private var backgroundTtsBook: EpubBook? = null
private var backgroundTtsBookId: String? = null
private var backgroundTtsCoverPath: String? = null
private val _internalState = MutableStateFlow( private val _internalState = MutableStateFlow(
ReaderScreenState( ReaderScreenState(
renderMode = try { renderMode = try {
@ -1264,7 +1280,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
} }
val result = fontsRepository.importFont(android.net.Uri.fromFile(tempFile)) val result = fontsRepository.importFont(Uri.fromFile(tempFile))
result.onSuccess { font -> result.onSuccess { font ->
if (uiState.value.isSyncEnabled) { if (uiState.value.isSyncEnabled) {
uploadNewFont(font) uploadNewFont(font)
@ -1512,6 +1528,37 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
init { init {
Timber.d("ViewModel instance created.") Timber.d("ViewModel instance created.")
WorkManager.getInstance(application).cancelUniqueWork(FolderSyncWorker.WORK_NAME) WorkManager.getInstance(application).cancelUniqueWork(FolderSyncWorker.WORK_NAME)
// --- ADD THIS BLOCK ---
val locatorConverter = LocatorConverter(
bookCacheDao,
ProtoBuf { serializersModule = semanticBlockModule },
appContext
)
viewModelScope.launch {
var wasSessionFinished = false
ttsController.ttsState.collect { state ->
val isPlaying = state.isPlaying
val sessionFinished = state.sessionFinished
val isReaderSource = state.playbackSource == "READER"
if (isReaderSource) {
if (sessionFinished && !wasSessionFinished) {
if (_internalState.value.selectedEpubBook == null) {
Timber.tag("TTS_BG_ADVANCE").i("Reader is closed. Handling auto-advance in background.")
advanceTtsChapterInBackground(state, locatorConverter)
}
}
if (state.sessionEndedByStop) {
backgroundTtsBook = null
backgroundTtsBookId = null
backgroundTtsCoverPath = null
}
}
wasSessionFinished = sessionFinished
}
}
viewModelScope.launch { viewModelScope.launch {
recentFilesRepository.migrateLegacyShelvesToRoom() recentFilesRepository.migrateLegacyShelvesToRoom()
if (!prefs.getBoolean(KEY_DEFAULT_TAGS_SEEDED, false)) { if (!prefs.getBoolean(KEY_DEFAULT_TAGS_SEEDED, false)) {
@ -2531,6 +2578,20 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val uriString = _internalState.value.selectedPdfUri?.toString() val uriString = _internalState.value.selectedPdfUri?.toString()
?: _internalState.value.selectedEpubUri?.toString() ?: _internalState.value.selectedEpubUri?.toString()
val ttsState = ttsController.ttsState.value
val isTtsActive = ttsState.playbackSource == "READER" &&
(ttsState.isPlaying || ttsState.isLoading || ttsState.sessionFinished || ttsState.currentText != null)
if (isTtsActive && _internalState.value.selectedEpubBook != null) {
backgroundTtsBook = _internalState.value.selectedEpubBook
backgroundTtsBookId = _internalState.value.selectedBookId
backgroundTtsCoverPath = uiState.value.recentFiles.find { it.bookId == backgroundTtsBookId }?.coverImagePath
} else if (!isTtsActive) {
backgroundTtsBook = null
backgroundTtsBookId = null
backgroundTtsCoverPath = null
}
_internalState.update { _internalState.update {
it.copy( it.copy(
selectedPdfUri = null, selectedPdfUri = null,
@ -2600,6 +2661,68 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
} }
private fun advanceTtsChapterInBackground(state: TtsPlaybackManager.TtsState, locatorConverter: LocatorConverter) {
val currentChapterIndex = state.chapterIndex ?: return
val book = backgroundTtsBook ?: return
val bookId = backgroundTtsBookId ?: return
viewModelScope.launch(Dispatchers.IO) {
var nextIdx = currentChapterIndex + 1
val totalChapters = book.chapters.size
var foundContent = false
while (nextIdx < totalChapters) {
Timber.tag("TTS_BG_ADVANCE").d("Trying chapter $nextIdx natively.")
val nativeChunks = locatorConverter.getTtsChunksForChapter(book, nextIdx)
if (!nativeChunks.isNullOrEmpty()) {
val token = getAuthToken()
val mode = try {
TtsPlaybackManager.TtsMode.valueOf(state.ttsMode)
} catch(e: Exception) {
TtsPlaybackManager.TtsMode.CLOUD
}
withContext(Dispatchers.Main) {
ttsController.start(
chunks = nativeChunks,
bookTitle = book.title,
chapterTitle = book.chapters.getOrNull(nextIdx)?.title,
coverImageUri = backgroundTtsCoverPath?.let { Uri.fromFile(File(it)).toString() },
chapterIndex = nextIdx,
ttsMode = mode,
playbackSource = "READER",
authToken = token
)
}
foundContent = true
// Save reading position locally
val cfi = nativeChunks.firstOrNull()?.sourceCfi
if (cfi != null) {
val locator = locatorConverter.getLocatorFromCfi(book, nextIdx, cfi)
if (locator != null) {
recentFilesRepository.getFileByBookId(bookId)?.uriString?.let { uriString ->
recentFilesRepository.updateEpubReadingPosition(uriString, locator, cfi, 0f)
}
}
}
break
} else {
Timber.tag("TTS_BG_ADVANCE").d("Chapter $nextIdx is empty natively. Skipping to next.")
nextIdx++
}
}
if (!foundContent) {
Timber.tag("TTS_BG_ADVANCE").d("Reached end of book or no content found.")
withContext(Dispatchers.Main) {
ttsController.stop()
}
}
}
}
private fun loadSyncedFoldersFromPrefs(): List<SyncedFolder> { private fun loadSyncedFoldersFromPrefs(): List<SyncedFolder> {
val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null) val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null)
val folders = mutableListOf<SyncedFolder>() val folders = mutableListOf<SyncedFolder>()
@ -5408,6 +5531,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
speechBubbleCache.clear() speechBubbleCache.clear()
speechBubbleDetectionJobs.clear() speechBubbleDetectionJobs.clear()
ttsController.release()
Timber.d("ViewModel instance cleared (onCleared).") Timber.d("ViewModel instance cleared (onCleared).")
} }

View file

@ -133,7 +133,7 @@ fun ProScreen(
title = { }, title = { },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@ -179,7 +179,7 @@ fun ProScreen(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
painter = painterResource(id = R.drawable.crown), painter = painterResource(id = R.drawable.crown),
contentDescription = "Pro", contentDescription = stringResource(R.string.drawer_pro_unlocked),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -209,7 +209,7 @@ fun ProScreen(
shape = CircleShape shape = CircleShape
), ),
text = { text = {
AutoSizeText("Credits", AutoSizeText(stringResource(R.string.credits_tab),
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
@ -312,7 +312,7 @@ private fun ProTierCard(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
painter = painterResource(id = R.drawable.crown), painter = painterResource(id = R.drawable.crown),
contentDescription = "Pro Badge", contentDescription = stringResource(R.string.drawer_pro_unlocked),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -422,7 +422,7 @@ private fun ProTierCard(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.crown), painter = painterResource(id = R.drawable.crown),
contentDescription = "Unlocked", contentDescription = stringResource(R.string.pro_unlocked),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@ -477,7 +477,7 @@ private fun ProTierCard(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Info, imageVector = Icons.Default.Info,
contentDescription = "Info", contentDescription = stringResource(R.string.info),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@ -499,7 +499,7 @@ private fun ProTierCard(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
painter = painterResource(id = R.drawable.crown), painter = painterResource(id = R.drawable.crown),
contentDescription = "Pro", contentDescription = stringResource(R.string.drawer_pro_unlocked),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@ -539,7 +539,7 @@ private fun ProTierCard(
) )
} }
else -> { else -> {
LegalText(prefixText = "By purchasing,") LegalText(prefixText = stringResource(R.string.legal_by_purchasing))
} }
} }
} }
@ -631,7 +631,7 @@ private fun CreditTierCard(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("AI & Cloud Credits", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) Text(stringResource(R.string.credits_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "$credits", text = "$credits",
@ -639,7 +639,7 @@ private fun CreditTierCard(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text("Credits Available", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.credits_available), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -698,7 +698,7 @@ private fun CreditTierCard(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
"Estimated Cost Breakdown", stringResource(R.string.credits_estimated_cost_breakdown),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Start) modifier = Modifier.align(Alignment.Start)
@ -707,13 +707,13 @@ private fun CreditTierCard(
CostBreakdownItem( CostBreakdownItem(
iconRes = R.drawable.text_to_speech, iconRes = R.drawable.text_to_speech,
title = "Cloud TTS", title = stringResource(R.string.credits_cloud_tts_title),
description = "Cost: ~3-4 credits per minute of audio generated.\nTo enable: Reader Screen > More > TTS Voice Settings." description = stringResource(R.string.credits_cloud_tts_desc)
) )
CostBreakdownItem( CostBreakdownItem(
iconRes = R.drawable.summarize, iconRes = R.drawable.summarize,
title = "AI Summaries & Recap", title = stringResource(R.string.credits_ai_summaries_title),
description = "Cost: ~1-4 credits per request based on chapter length.\nPro Users get 10 free summaries daily." description = stringResource(R.string.credits_ai_summaries_desc)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
} }

View file

@ -246,7 +246,7 @@ fun ContextualTopAppBar(
actions = { actions = {
if (onTagClick != null) { if (onTagClick != null) {
IconButton(onClick = onTagClick) { IconButton(onClick = onTagClick) {
Icon(painterResource(id = R.drawable.tag), contentDescription = "Tag") Icon(painterResource(id = R.drawable.tag), contentDescription = stringResource(R.string.content_desc_tag))
} }
} }
if (onPinClick != null) { if (onPinClick != null) {
@ -516,14 +516,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("Tags", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
TextButton(onClick = onOpenTags) { Text("+ Add / Edit") } TextButton(onClick = onOpenTags) { Text(stringResource(R.string.action_add_edit)) }
} }
if (item.tags.isNotEmpty()) { if (item.tags.isNotEmpty()) {
BookTagChipsRow(tags = item.tags, compact = false) BookTagChipsRow(tags = item.tags, compact = false)
} else { } else {
Text("No tags assigned.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.msg_no_tags_assigned), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
Row( Row(
@ -599,7 +599,7 @@ private fun InfoRowDetailed(
) { ) {
Icon( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy $label", contentDescription = stringResource(R.string.content_desc_copy_value, label),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
) )
@ -827,7 +827,7 @@ fun EmptyState(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.FileOpen, imageVector = Icons.Outlined.FileOpen,
contentDescription = "No files icon", contentDescription = stringResource(R.string.content_desc_no_files_icon),
modifier = Modifier.size(80.dp), modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1154,13 +1154,13 @@ fun TagSelectionBottomSheet(
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).heightIn(max = 500.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).heightIn(max = 500.dp)) {
Text("Apply Tags", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp)) Text(stringResource(R.string.title_apply_tags), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp))
androidx.compose.material3.OutlinedTextField( androidx.compose.material3.OutlinedTextField(
value = searchQuery, value = searchQuery,
onValueChange = { searchQuery = it }, onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search or create tag...") }, placeholder = { Text(stringResource(R.string.placeholder_search_create_tag)) },
singleLine = true, singleLine = true,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
leadingIcon = { Icon(Icons.Default.Search, null) } leadingIcon = { Icon(Icons.Default.Search, null) }
@ -1180,7 +1180,7 @@ fun TagSelectionBottomSheet(
) { ) {
Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Text("Create \"${searchQuery.trim()}\"", color = MaterialTheme.colorScheme.primary) Text(stringResource(R.string.action_create_tag, searchQuery.trim()), color = MaterialTheme.colorScheme.primary)
} }
} }
} }

View file

@ -463,7 +463,7 @@ fun ChapterWebView(
TextButton(onClick = { TextButton(onClick = {
val clipboard = val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Link", urlToShow) val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_link), urlToShow)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
showExternalLinkDialog = null showExternalLinkDialog = null
}) { Text(stringResource(R.string.action_copy)) } }) { Text(stringResource(R.string.action_copy)) }
@ -1066,7 +1066,7 @@ fun ChapterWebView(
onCopy = { onCopy = {
val clipboard = val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Text", state.selectedText) val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), state.selectedText)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
state.finishActionModeCallback() state.finishActionModeCallback()
localWebViewRef?.clearFocus() localWebViewRef?.clearFocus()

View file

@ -577,7 +577,7 @@ fun AnnotationBottomSheet(
) { ) {
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Delete") Text(stringResource(R.string.action_delete))
} }
Button( Button(
onClick = { onSave(noteText) }, onClick = { onSave(noteText) },
@ -586,7 +586,7 @@ fun AnnotationBottomSheet(
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
Text("Save Note") Text(stringResource(R.string.action_save_note))
} }
} }
} }

View file

@ -225,7 +225,7 @@ fun EpubReaderTopBar(
description = stringResource(R.string.tooltip_back_desc), description = stringResource(R.string.tooltip_back_desc),
onClick = onNavigateBack onClick = onNavigateBack
) { ) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
@ -583,7 +583,7 @@ fun EpubReaderBottomBar(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ai), painter = painterResource(id = R.drawable.ai),
contentDescription = "AI Features" contentDescription = stringResource(R.string.ai_features_title)
) )
} }
} }
@ -1091,7 +1091,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = Icons.Default.ArrowUpward, imageVector = Icons.Default.ArrowUpward,
contentDescription = "Scroll to Top", contentDescription = stringResource(R.string.action_scroll_to_top),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1440,7 +1440,7 @@ fun TtsOverlayControls(
) { ) {
Icon( Icon(
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
"Play/Pause", stringResource(R.string.content_desc_play_pause),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
@ -1514,10 +1514,10 @@ fun TtsOverlayControls(
) )
} }
IconButton(onClick = { onCollapseChange(true) }, modifier = Modifier.size(32.dp)) { IconButton(onClick = { onCollapseChange(true) }, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.ChevronRight, "Collapse", modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) Icon(Icons.Default.ChevronRight, stringResource(R.string.content_desc_collapse), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
} }
IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) { IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Close, "Stop TTS", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Close, stringResource(R.string.content_desc_stop_tts), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
} }
} }
} }
@ -1541,7 +1541,7 @@ fun TtsOverlayControls(
) { ) {
Icon( Icon(
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
"Play/Pause", stringResource(R.string.content_desc_play_pause),
modifier = Modifier.size(28.dp) modifier = Modifier.size(28.dp)
) )
} }
@ -1557,7 +1557,7 @@ fun TtsOverlayControls(
// Unified Sliders Block // Unified Sliders Block
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Spd: %.1fx".format(rate), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp)) Text(stringResource(R.string.tts_speed_short, "%.1f".format(rate)), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
Slider( Slider(
value = rate, value = rate,
onValueChange = { onValueChange = {
@ -1571,11 +1571,11 @@ fun TtsOverlayControls(
modifier = Modifier.weight(1f).height(24.dp) modifier = Modifier.weight(1f).height(24.dp)
) )
IconButton(onClick = { rate = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) { IconButton(onClick = { rate = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Refresh, "Reset Speed", modifier = Modifier.size(16.dp)) Icon(Icons.Default.Refresh, stringResource(R.string.content_desc_reset_speed), modifier = Modifier.size(16.dp))
} }
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Ptch: %.1fx".format(pitch), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp)) Text(stringResource(R.string.tts_pitch_short, "%.1f".format(pitch)), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
Slider( Slider(
value = pitch, value = pitch,
onValueChange = { onValueChange = {
@ -1589,7 +1589,7 @@ fun TtsOverlayControls(
modifier = Modifier.weight(1f).height(24.dp) modifier = Modifier.weight(1f).height(24.dp)
) )
IconButton(onClick = { pitch = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) { IconButton(onClick = { pitch = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Refresh, "Reset Pitch", modifier = Modifier.size(16.dp)) Icon(Icons.Default.Refresh, stringResource(R.string.content_desc_reset_pitch), modifier = Modifier.size(16.dp))
} }
} }
} }

View file

@ -243,7 +243,7 @@ fun EpubReaderDrawerSheet(
Tab( Tab(
selected = drawerPagerState.currentPage == 2, selected = drawerPagerState.currentPage == 2,
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } }, onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
text = { Text("Annotations") } text = { Text(stringResource(R.string.tab_annotations)) }
) )
} }
@ -408,13 +408,13 @@ private fun ChaptersList(
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
TextButton(onClick = { expandedEntryIndices = effectiveToc.indices.toSet() }) { TextButton(onClick = { expandedEntryIndices = effectiveToc.indices.toSet() }) {
Text("Expand All") Text(stringResource(R.string.action_expand_all))
} }
TextButton(onClick = { expandedEntryIndices = emptySet() }) { TextButton(onClick = { expandedEntryIndices = emptySet() }) {
Text("Collapse All") Text(stringResource(R.string.action_collapse_all))
} }
TextButton(onClick = onScrollToCurrent) { TextButton(onClick = onScrollToCurrent) {
Text("Locate") Text(stringResource(R.string.action_locate))
} }
} }

View file

@ -905,7 +905,7 @@ fun EpubReaderHost(
}) })
var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) } var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) }
val ttsController = rememberTtsController() val ttsController = viewModel.ttsController
val ttsState by ttsController.ttsState.collectAsState() val ttsState by ttsController.ttsState.collectAsState()
val totalBookLengthChars = remember(chapters) { val totalBookLengthChars = remember(chapters) {
@ -4979,7 +4979,7 @@ fun EpubReaderHost(
}, },
onCopy = { onCopy = {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Text", targetHighlight.text) val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), targetHighlight.text)
clipboardManager.setPrimaryClip(clip) clipboardManager.setPrimaryClip(clip)
highlightToNoteCfi = null highlightToNoteCfi = null
}, },
@ -5188,13 +5188,13 @@ fun EpubReaderHost(
AlertDialog( AlertDialog(
onDismissRequest = { showInsufficientCreditsDialog = false }, onDismissRequest = { showInsufficientCreditsDialog = false },
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) }, icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
title = { Text("Out of Credits") }, title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
text = { Text("You don't have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.") }, text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showInsufficientCreditsDialog = false showInsufficientCreditsDialog = false
onNavigateToPro() onNavigateToPro()
}) { Text("Get Pro / Add Credits") } }) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showInsufficientCreditsDialog = false }) { TextButton(onClick = { showInsufficientCreditsDialog = false }) {

View file

@ -912,9 +912,9 @@ fun VisualOptionsSheet(
AnimatedVisibility(visible = pullToTurnEnabled) { AnimatedVisibility(visible = pullToTurnEnabled) {
Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) { Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) {
HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)) HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f))
Text("Pull Distance to Change Chapter", style = MaterialTheme.typography.titleSmall) Text(stringResource(R.string.setting_pull_distance_change_chapter), style = MaterialTheme.typography.titleSmall)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Short", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.label_short), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Slider( Slider(
value = pullToTurnMultiplier, value = pullToTurnMultiplier,
onValueChange = onPullToTurnMultiplierChange, onValueChange = onPullToTurnMultiplierChange,
@ -922,7 +922,7 @@ fun VisualOptionsSheet(
steps = 14, steps = 14,
modifier = Modifier.weight(1f).padding(horizontal = 12.dp) modifier = Modifier.weight(1f).padding(horizontal = 12.dp)
) )
Text("Long", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.label_long), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
} }
@ -1079,7 +1079,7 @@ fun FormatSlider(
}, },
modifier = Modifier.size(32.dp) // Slimmer buttons modifier = Modifier.size(32.dp) // Slimmer buttons
) { ) {
Icon(Icons.Default.Remove, contentDescription = "Decrease", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Remove, contentDescription = stringResource(R.string.content_desc_decrease), tint = MaterialTheme.colorScheme.primary)
} }
// Using our new CustomCanvasSlider here! // Using our new CustomCanvasSlider here!
@ -1097,7 +1097,7 @@ fun FormatSlider(
}, },
modifier = Modifier.size(32.dp) // Slimmer buttons modifier = Modifier.size(32.dp) // Slimmer buttons
) { ) {
Icon(Icons.Default.Add, contentDescription = "Increase", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_increase), tint = MaterialTheme.colorScheme.primary)
} }
} }
} }

View file

@ -86,7 +86,7 @@ fun FeedbackScreen(
title = { Text(stringResource(R.string.drawer_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 = stringResource(R.string.action_back))
} }
} }
) )
@ -132,7 +132,7 @@ fun FeedbackScreen(
icon = { icon = {
Icon( Icon(
painter = painterResource(id = R.drawable.github), painter = painterResource(id = R.drawable.github),
contentDescription = "GitHub", contentDescription = stringResource(R.string.github_issues),
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -150,7 +150,7 @@ fun FeedbackScreen(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Email, imageVector = Icons.Outlined.Email,
contentDescription = "Email", contentDescription = stringResource(R.string.email_support),
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -201,7 +201,7 @@ private fun FeedbackOptionCard(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward, imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Open", contentDescription = stringResource(R.string.action_open),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }

View file

@ -109,6 +109,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.imageResource
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.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -2002,10 +2003,10 @@ internal fun PaginatedReaderContent(
val urlToShow = showExternalLinkDialog!! val urlToShow = showExternalLinkDialog!!
AlertDialog( AlertDialog(
onDismissRequest = { showExternalLinkDialog = null }, onDismissRequest = { showExternalLinkDialog = null },
title = { Text("External Link") }, title = { Text(stringResource(R.string.dialog_external_link_title)) },
text = { text = {
Text( Text(
"You clicked on an external link:\n\n$urlToShow\n\nWhat would you like to do?" stringResource(R.string.dialog_external_link_desc, urlToShow)
) )
}, },
confirmButton = { confirmButton = {
@ -2014,10 +2015,10 @@ internal fun PaginatedReaderContent(
onClick = { onClick = {
val clipboard = val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Link", urlToShow) val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_link), urlToShow)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
showExternalLinkDialog = null showExternalLinkDialog = null
}) { Text("Copy") } }) { Text(stringResource(R.string.action_copy)) }
TextButton( TextButton(
onClick = { onClick = {
val intent = Intent(Intent.ACTION_VIEW, urlToShow.toUri()) val intent = Intent(Intent.ACTION_VIEW, urlToShow.toUri())
@ -2028,15 +2029,15 @@ internal fun PaginatedReaderContent(
e, "No activity found to handle intent for URL: $urlToShow" e, "No activity found to handle intent for URL: $urlToShow"
) )
Toast.makeText( Toast.makeText(
context, "No browser found to open the link.", Toast.LENGTH_LONG context, context.getString(R.string.error_no_browser), Toast.LENGTH_LONG
).show() ).show()
} }
showExternalLinkDialog = null showExternalLinkDialog = null
}) { Text("Open") } }) { Text(stringResource(R.string.action_open)) }
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showExternalLinkDialog = null }) { Text("Cancel") } TextButton(onClick = { showExternalLinkDialog = null }) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
@ -2797,7 +2798,7 @@ internal fun PaginatedReaderContent(
AsyncImage( AsyncImage(
model = imageRequest, model = imageRequest,
contentDescription = "List item marker", contentDescription = stringResource(R.string.content_desc_list_item_marker),
modifier = markerAreaModifier.height( modifier = markerAreaModifier.height(
imageSize imageSize
), ),
@ -3555,7 +3556,7 @@ internal fun PaginatedReaderContent(
onCopy = { onCopy = {
val clipboardManager = val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Text", sel.text) val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), sel.text)
clipboardManager.setPrimaryClip(clip) clipboardManager.setPrimaryClip(clip)
activeSelection = null activeSelection = null
}, },
@ -3926,7 +3927,7 @@ internal fun PaginatedReaderContent(
if (showColorPickerDialog != null) { if (showColorPickerDialog != null) {
AlertDialog( AlertDialog(
onDismissRequest = { showColorPickerDialog = null }, onDismissRequest = { showColorPickerDialog = null },
title = { Text("Select Color") }, title = { Text(stringResource(R.string.dialog_select_color)) },
text = { text = {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 48.dp), columns = GridCells.Adaptive(minSize = 48.dp),
@ -3953,7 +3954,7 @@ internal fun PaginatedReaderContent(
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showColorPickerDialog = null showColorPickerDialog = null
}) { Text("Close") } }) { Text(stringResource(R.string.action_close)) }
}) })
} }
@ -3972,7 +3973,7 @@ internal fun PaginatedReaderContent(
} else { } else {
Timber.w("Book has no pages to display.") Timber.w("Book has no pages to display.")
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("This book has no content to display.") Text(stringResource(R.string.msg_book_no_content))
} }
} }
} }
@ -4109,7 +4110,7 @@ private fun RenderFlexChildBlock(
AsyncImage( AsyncImage(
model = imageRequest, model = imageRequest,
contentDescription = "List item marker", contentDescription = stringResource(R.string.content_desc_list_item_marker),
modifier = markerAreaModifier.height(imageSize), modifier = markerAreaModifier.height(imageSize),
alignment = Alignment.CenterEnd, alignment = Alignment.CenterEnd,
contentScale = ContentScale.FillHeight contentScale = ContentScale.FillHeight

View file

@ -395,7 +395,7 @@ fun PenPlayground(onClose: () -> Unit) {
painter = painterResource( painter = painterResource(
id = R.drawable.close id = R.drawable.close
), ),
contentDescription = "Close", tint = Color.Gray contentDescription = stringResource(R.string.action_close), tint = Color.Gray
) )
} }
} }

View file

@ -138,7 +138,7 @@ internal fun OcrLanguageSelectionDialog(
) { ) {
RadioButton(selected = (language == currentLanguage), onClick = null) RadioButton(selected = (language == currentLanguage), onClick = null)
Text( Text(
text = language.displayName, text = stringResource(language.displayNameRes),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp) modifier = Modifier.padding(start = 16.dp)
) )

View file

@ -60,12 +60,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
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 com.aryan.reader.R
import io.legere.pdfiumandroid.api.Bookmark import io.legere.pdfiumandroid.api.Bookmark
import io.legere.pdfiumandroid.suspend.PdfDocumentKt import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -255,7 +257,9 @@ internal fun PdfTocTreeItem(
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 = stringResource(
if (isExpanded) R.string.content_desc_collapse else R.string.content_desc_expand
),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -301,13 +305,13 @@ internal fun PdfNavigationDrawerContent(
) { ) {
Tab(selected = drawerPagerState.currentPage == 0, onClick = { Tab(selected = drawerPagerState.currentPage == 0, onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(0) } 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 = { onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(1) } drawerScope.launch { drawerPagerState.animateScrollToPage(1) }
}, },
text = { Text("Bookmarks") }, text = { Text(stringResource(R.string.tab_bookmarks)) },
modifier = Modifier.testTag("BookmarksTab") modifier = Modifier.testTag("BookmarksTab")
) )
Tab( Tab(
@ -315,7 +319,7 @@ internal fun PdfNavigationDrawerContent(
onClick = { onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(2) } drawerScope.launch { drawerPagerState.animateScrollToPage(2) }
}, },
text = { Text("Highlights") }, text = { Text(stringResource(R.string.tab_highlights)) },
modifier = Modifier.testTag("HighlightsTab") modifier = Modifier.testTag("HighlightsTab")
) )
Tab( Tab(
@ -323,7 +327,7 @@ internal fun PdfNavigationDrawerContent(
onClick = { onClick = {
drawerScope.launch { drawerPagerState.animateScrollToPage(3) } drawerScope.launch { drawerPagerState.animateScrollToPage(3) }
}, },
text = { Text("Pages") }, text = { Text(stringResource(R.string.tab_pages)) },
modifier = Modifier.testTag("PagesTab") modifier = Modifier.testTag("PagesTab")
) )
} }
@ -340,7 +344,7 @@ internal fun PdfNavigationDrawerContent(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
"Chapters are not available for this book.", stringResource(R.string.msg_chapters_not_available),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -434,13 +438,13 @@ internal fun PdfNavigationDrawerContent(
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
TextButton(onClick = { expandedEntryIndices = flatTableOfContents.indices.toSet() }) { TextButton(onClick = { expandedEntryIndices = flatTableOfContents.indices.toSet() }) {
Text("Expand All") Text(stringResource(R.string.action_expand_all))
} }
TextButton(onClick = { expandedEntryIndices = emptySet() }) { TextButton(onClick = { expandedEntryIndices = emptySet() }) {
Text("Collapse All") Text(stringResource(R.string.action_collapse_all))
} }
TextButton(onClick = onScrollToCurrent) { TextButton(onClick = onScrollToCurrent) {
Text("Locate") Text(stringResource(R.string.action_locate))
} }
} }
@ -503,7 +507,7 @@ internal fun PdfNavigationDrawerContent(
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 +535,7 @@ internal fun PdfNavigationDrawerContent(
) )
}, supportingContent = { }, supportingContent = {
Text( Text(
"Page ${bookmark.pageIndex + 1} of ${bookmark.totalPages}", stringResource(R.string.page_of_pages, bookmark.pageIndex + 1, bookmark.totalPages),
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
}, trailingContent = { }, trailingContent = {
@ -542,7 +546,7 @@ internal fun PdfNavigationDrawerContent(
}) { }) {
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(
@ -551,13 +555,13 @@ internal fun PdfNavigationDrawerContent(
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
}) { }) {
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text("Rename") Text(stringResource(R.string.action_rename))
}, onClick = { }, onClick = {
showRenameBookmarkDialog = bookmark showRenameBookmarkDialog = bookmark
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
}) })
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text("Delete") Text(stringResource(R.string.action_delete))
}, onClick = { }, onClick = {
showDeleteConfirmDialogFor = bookmark showDeleteConfirmDialogFor = bookmark
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
@ -581,11 +585,11 @@ internal fun PdfNavigationDrawerContent(
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
showRenameBookmarkDialog = null showRenameBookmarkDialog = null
}, title = { Text("Rename Bookmark") }, text = { }, title = { Text(stringResource(R.string.dialog_rename_bookmark)) }, text = {
OutlinedTextField( OutlinedTextField(
value = newTitle, value = newTitle,
onValueChange = { newTitle = it }, onValueChange = { newTitle = it },
label = { Text("New Title") }, label = { Text(stringResource(R.string.label_new_title)) },
placeholder = { placeholder = {
Text( Text(
text = bookmarkToRename.title, text = bookmarkToRename.title,
@ -605,33 +609,33 @@ internal fun PdfNavigationDrawerContent(
onClick = { onClick = {
onRenameBookmark(bookmarkToRename, newTitle) onRenameBookmark(bookmarkToRename, newTitle)
showRenameBookmarkDialog = null showRenameBookmarkDialog = null
}) { Text("Save") } }) { Text(stringResource(R.string.action_save)) }
}, dismissButton = { }, dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
showRenameBookmarkDialog = null showRenameBookmarkDialog = null
}) { Text("Cancel") } }) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
showDeleteConfirmDialogFor?.let { bookmarkToDelete -> showDeleteConfirmDialogFor?.let { bookmarkToDelete ->
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
showDeleteConfirmDialogFor = null showDeleteConfirmDialogFor = null
}, title = { Text("Delete Bookmark?") }, text = { }, title = { Text(stringResource(R.string.dialog_delete_bookmark)) }, text = {
Text( Text(
"Are you sure you want to permanently delete this bookmark?" stringResource(R.string.dialog_delete_bookmark_desc)
) )
}, confirmButton = { }, confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
onDeleteBookmark(bookmarkToDelete) onDeleteBookmark(bookmarkToDelete)
showDeleteConfirmDialogFor = null showDeleteConfirmDialogFor = null
}) { Text("Delete") } }) { Text(stringResource(R.string.action_delete)) }
}, dismissButton = { }, dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
showDeleteConfirmDialogFor = null showDeleteConfirmDialogFor = null
}) { Text("Cancel") } }) { Text(stringResource(R.string.action_cancel)) }
}) })
} }
} }
@ -645,7 +649,7 @@ internal fun PdfNavigationDrawerContent(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
"You haven't added any highlights yet.", stringResource(R.string.no_highlights_yet),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -662,12 +666,12 @@ internal fun PdfNavigationDrawerContent(
FilterChip( FilterChip(
selected = !filterWithNotesOnly, selected = !filterWithNotesOnly,
onClick = { filterWithNotesOnly = false }, onClick = { filterWithNotesOnly = false },
label = { Text("All") } label = { Text(stringResource(R.string.read_status_all)) }
) )
FilterChip( FilterChip(
selected = filterWithNotesOnly, selected = filterWithNotesOnly,
onClick = { filterWithNotesOnly = true }, onClick = { filterWithNotesOnly = true },
label = { Text("With Notes") } label = { Text(stringResource(R.string.filter_with_notes)) }
) )
} }
@ -689,7 +693,7 @@ internal fun PdfNavigationDrawerContent(
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
text = highlight.text.ifBlank { "Highlighted section" }, text = highlight.text.ifBlank { stringResource(R.string.msg_highlighted_section_default) },
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
@ -707,7 +711,7 @@ internal fun PdfNavigationDrawerContent(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"Page ${highlight.pageIndex + 1}", stringResource(R.string.pdf_page_short, highlight.pageIndex + 1),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -733,14 +737,20 @@ internal fun PdfNavigationDrawerContent(
Box { Box {
var highlightMenuExpanded by remember { mutableStateOf(false) } var highlightMenuExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { highlightMenuExpanded = true }) { IconButton(onClick = { highlightMenuExpanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Options") Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_options))
} }
DropdownMenu( DropdownMenu(
expanded = highlightMenuExpanded, expanded = highlightMenuExpanded,
onDismissRequest = { highlightMenuExpanded = false } onDismissRequest = { highlightMenuExpanded = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") }, text = {
Text(
stringResource(
if (highlight.note.isNullOrBlank()) R.string.menu_add_note else R.string.menu_edit_note
)
)
},
onClick = { onClick = {
onNoteRequested(highlight.id) onNoteRequested(highlight.id)
highlightMenuExpanded = false highlightMenuExpanded = false
@ -748,7 +758,7 @@ internal fun PdfNavigationDrawerContent(
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text(stringResource(R.string.action_delete)) },
onClick = { onClick = {
showDeleteConfirmDialogFor = highlight showDeleteConfirmDialogFor = highlight
highlightMenuExpanded = false highlightMenuExpanded = false
@ -770,20 +780,20 @@ internal fun PdfNavigationDrawerContent(
showDeleteConfirmDialogFor?.let { highlightToDelete -> showDeleteConfirmDialogFor?.let { highlightToDelete ->
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteConfirmDialogFor = null }, onDismissRequest = { showDeleteConfirmDialogFor = 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 = {
onDeleteHighlight(highlightToDelete) onDeleteHighlight(highlightToDelete)
showDeleteConfirmDialogFor = null showDeleteConfirmDialogFor = null
} }
) { Text("Delete") } ) { Text(stringResource(R.string.action_delete)) }
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { showDeleteConfirmDialogFor = null } onClick = { showDeleteConfirmDialogFor = null }
) { Text("Cancel") } ) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -809,7 +819,7 @@ internal fun PdfNavigationDrawerContent(
} }
} }
) { ) {
Text("Locate") Text(stringResource(R.string.action_locate))
} }
} }
@ -880,7 +890,7 @@ internal fun PdfNavigationDrawerContent(
if (thumb != null) { if (thumb != null) {
Image( Image(
bitmap = thumb!!.asImageBitmap(), bitmap = thumb!!.asImageBitmap(),
contentDescription = "Page ${pageIdx + 1}", contentDescription = stringResource(R.string.pdf_page_short, pageIdx + 1),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }

View file

@ -22,6 +22,7 @@ package com.aryan.reader.pdf
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -78,7 +79,9 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
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.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -95,12 +98,12 @@ import com.aryan.reader.pdf.ocr.OcrSymbol
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
enum class OcrLanguage(val displayName: String) { enum class OcrLanguage(@StringRes val displayNameRes: Int) {
LATIN("English, Spanish, French, etc."), LATIN(R.string.ocr_language_latin),
DEVANAGARI("Hindi, Marathi, Sanskrit + English"), DEVANAGARI(R.string.ocr_language_devanagari),
CHINESE("Chinese + English"), CHINESE(R.string.ocr_language_chinese),
JAPANESE("Japanese + English"), JAPANESE(R.string.ocr_language_japanese),
KOREAN("Korean + English") KOREAN(R.string.ocr_language_korean)
} }
internal data class OcrSymbolInfo( internal data class OcrSymbolInfo(
@ -232,6 +235,8 @@ internal fun PdfSelectionMenuPopup(
onTts: (() -> Unit)? = null, onTts: (() -> Unit)? = null,
onNote: (() -> Unit)? = null onNote: (() -> Unit)? = null
) { ) {
val context = LocalContext.current
Popup( Popup(
popupPositionProvider = popupPositionProvider, popupPositionProvider = popupPositionProvider,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@ -266,7 +271,7 @@ internal fun PdfSelectionMenuPopup(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
contentDescription = "Note", contentDescription = stringResource(R.string.label_note),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
@ -321,7 +326,7 @@ internal fun PdfSelectionMenuPopup(
) { ) {
Icon(Icons.Default.CopyAll, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.CopyAll, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Copy Thread") Text(stringResource(R.string.action_copy_thread))
} }
} else { } else {
Row( Row(
@ -360,23 +365,23 @@ internal fun PdfSelectionMenuPopup(
HorizontalDivider() HorizontalDivider()
val actions = mutableListOf<MenuActionItem>() val actions = mutableListOf<MenuActionItem>()
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = { onCopy(menuState.selectedText) })) actions.add(MenuActionItem(iconRes = R.drawable.copy, label = context.getString(R.string.action_copy), onClick = { onCopy(menuState.selectedText) }))
if (onTts != null) { if (onTts != null) {
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts)) actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = context.getString(R.string.label_speak), onClick = onTts))
} }
if (menuState.selectedText.length <= 2000) { if (menuState.selectedText.length <= 2000) {
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = { onAiDefine(menuState.selectedText) })) actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = context.getString(R.string.label_dict), onClick = { onAiDefine(menuState.selectedText) }))
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = { onTranslate(menuState.selectedText) })) actions.add(MenuActionItem(iconRes = R.drawable.translate, label = context.getString(R.string.action_translate), onClick = { onTranslate(menuState.selectedText) }))
actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = "Search", onClick = { onSearch(menuState.selectedText) })) actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = context.getString(R.string.action_search), onClick = { onSearch(menuState.selectedText) }))
} }
if (onNote != null) { if (onNote != null) {
val noteLabel = if (menuState.note.isNullOrBlank()) "Note" else "Edit" val noteLabel = context.getString(if (menuState.note.isNullOrBlank()) R.string.label_note else R.string.label_edit)
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote)) actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
} }
if (!menuState.isExistingHighlight) { if (!menuState.isExistingHighlight) {
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = { onSelectAll() })) actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = context.getString(R.string.select_all), onClick = { onSelectAll() }))
} }
if (menuState.isExistingHighlight) { if (menuState.isExistingHighlight) {
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = { onDelete() }, isError = true)) actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = { onDelete() }, isError = true))
@ -659,7 +664,7 @@ fun PdfHighlightColorRow(
if (selectedColor == colorEnum) { if (selectedColor == colorEnum) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = if (displayColor.luminance() > 0.5f) Color.Black else Color.White, tint = if (displayColor.luminance() > 0.5f) Color.Black else Color.White,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
@ -752,10 +757,10 @@ fun PdfAnnotationBottomSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
PdfBottomSheetToolButton(icon = R.drawable.copy, label = "Copy", effectiveText = effectiveText, onClick = onCopy) PdfBottomSheetToolButton(icon = R.drawable.copy, label = stringResource(R.string.action_copy), effectiveText = effectiveText, onClick = onCopy)
PdfBottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", effectiveText = effectiveText, onClick = onDictionary) PdfBottomSheetToolButton(icon = R.drawable.dictionary, label = stringResource(R.string.label_dict), effectiveText = effectiveText, onClick = onDictionary)
PdfBottomSheetToolButton(icon = R.drawable.translate, label = "Translate", effectiveText = effectiveText, onClick = onTranslate) PdfBottomSheetToolButton(icon = R.drawable.translate, label = stringResource(R.string.action_translate), effectiveText = effectiveText, onClick = onTranslate)
PdfBottomSheetToolButton(icon = R.drawable.search, label = "Search", effectiveText = effectiveText, onClick = onSearch) PdfBottomSheetToolButton(icon = R.drawable.search, label = stringResource(R.string.action_search), effectiveText = effectiveText, onClick = onSearch)
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@ -763,7 +768,7 @@ fun PdfAnnotationBottomSheet(
OutlinedTextField( OutlinedTextField(
value = noteText, value = noteText,
onValueChange = { noteText = it }, onValueChange = { noteText = it },
placeholder = { Text("Add a note...", color = effectiveText.copy(alpha = 0.5f)) }, placeholder = { Text(stringResource(R.string.placeholder_add_note), color = effectiveText.copy(alpha = 0.5f)) },
modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp),
maxLines = 5, maxLines = 5,
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
@ -793,7 +798,7 @@ fun PdfAnnotationBottomSheet(
) { ) {
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Delete") Text(stringResource(R.string.action_delete))
} }
Button( Button(
onClick = { onSave(noteText) }, onClick = { onSave(noteText) },
@ -802,7 +807,7 @@ fun PdfAnnotationBottomSheet(
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
Text("Save Note") Text(stringResource(R.string.action_save_note))
} }
} }
} }

View file

@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
@ -199,7 +200,7 @@ internal fun ThumbnailWithIndicator(
) { ) {
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()
) )
@ -230,7 +231,7 @@ internal fun BookmarkButton(
AnimatedVisibility(visible = isBookmarked, enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = isBookmarked, enter = fadeIn(), exit = fadeOut()) {
Icon( Icon(
painter = painterResource(id = R.drawable.bookmark), painter = painterResource(id = R.drawable.bookmark),
contentDescription = "Bookmark", contentDescription = stringResource(R.string.content_desc_bookmark),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
@ -271,7 +272,7 @@ internal fun ZoomPercentageIndicator(
// Reset Zoom Button // Reset Zoom Button
Icon( Icon(
painter = painterResource(id = R.drawable.zoom_out), painter = painterResource(id = R.drawable.zoom_out),
contentDescription = "Reset Zoom", contentDescription = stringResource(R.string.content_desc_reset_zoom),
tint = Color.White, tint = Color.White,
modifier = Modifier modifier = Modifier
.size(20.dp) .size(20.dp)

View file

@ -32,6 +32,7 @@ import androidx.compose.runtime.remember
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.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -73,7 +74,7 @@ internal fun SearchNavigationPill(
Icon( Icon(
imageVector = if (mode == SearchHighlightMode.ALL) Icons.Default.Visibility imageVector = if (mode == SearchHighlightMode.ALL) Icons.Default.Visibility
else Icons.Default.VisibilityOff, else Icons.Default.VisibilityOff,
contentDescription = "Toggle Highlights", contentDescription = stringResource(R.string.content_desc_toggle_search_highlights),
tint = if (mode == SearchHighlightMode.ALL) MaterialTheme.colorScheme.primary tint = if (mode == SearchHighlightMode.ALL) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -95,7 +96,7 @@ internal fun SearchNavigationPill(
IconButton(onClick = onPrev, enabled = isPrevEnabled) { IconButton(onClick = onPrev, enabled = isPrevEnabled) {
Icon( Icon(
imageVector = Icons.Default.KeyboardArrowUp, imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = "Previous", contentDescription = stringResource(R.string.tooltip_prev_result),
tint = if (isPrevEnabled) MaterialTheme.colorScheme.onSurface tint = if (isPrevEnabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
) )
@ -122,7 +123,7 @@ internal fun SearchNavigationPill(
IconButton(onClick = onNext, enabled = isNextEnabled) { IconButton(onClick = onNext, enabled = isNextEnabled) {
Icon( Icon(
imageVector = Icons.Default.KeyboardArrowDown, imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = "Next", contentDescription = stringResource(R.string.tooltip_next_result),
tint = if (isNextEnabled) MaterialTheme.colorScheme.onSurface tint = if (isNextEnabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
) )
@ -141,12 +142,12 @@ fun PdfSearchResultsPanel(
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
if (lazyResults.itemCount == 0 && lazyResults.loadState.refresh !is LoadState.Loading) { if (lazyResults.itemCount == 0 && lazyResults.loadState.refresh !is LoadState.Loading) {
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 = stringResource(R.string.msg_results_found_pages), text = stringResource(R.string.msg_results_found_pages, totalPageCount),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
) )
@ -195,6 +196,7 @@ fun PdfSearchResultsList(
onResultClick: (SearchResult) -> Unit, onResultClick: (SearchResult) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
if (results.isEmpty()) { if (results.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@ -203,7 +205,11 @@ fun PdfSearchResultsList(
} else { } else {
Column { Column {
Text( Text(
text = "${results.size} matches found", text = context.resources.getQuantityString(
R.plurals.search_matches_count,
results.size,
results.size
),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
) )

View file

@ -61,6 +61,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -450,7 +451,7 @@ private fun DragPill(
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Icon( Icon(
painter = painterResource(id = R.drawable.drag_handle), painter = painterResource(id = R.drawable.drag_handle),
contentDescription = "Drag to move text box", contentDescription = stringResource(R.string.content_desc_drag_text_box),
modifier = Modifier.size((20f / scale).dp) modifier = Modifier.size((20f / scale).dp)
) )
} }

View file

@ -136,14 +136,14 @@ internal fun PdfTopBar(
description = stringResource(R.string.tooltip_back_desc), description = stringResource(R.string.tooltip_back_desc),
onClick = onNavigateBack onClick = onNavigateBack
) { ) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
} }
val titleText = when { val titleText = when {
isLoadingDocument -> stringResource(R.string.loading_pdf) isLoadingDocument -> stringResource(R.string.loading_pdf)
errorMessage != null -> stringResource(R.string.error_loading_pdf) errorMessage != null -> stringResource(R.string.error_loading_pdf)
totalPages > 0 && pagerStatePageCount > 0 -> "Page ${currentPageForDisplay + 1} of $totalPages" totalPages > 0 && pagerStatePageCount > 0 -> stringResource(R.string.page_of_pages, currentPageForDisplay + 1, totalPages)
totalPages > 0 && pagerStatePageCount == 0 -> stringResource(R.string.loading_page) totalPages > 0 && pagerStatePageCount == 0 -> stringResource(R.string.loading_page)
else -> "PDF Viewer" else -> stringResource(R.string.pdf_viewer)
} }
Text( Text(
text = titleText, text = titleText,
@ -179,16 +179,16 @@ internal fun PdfTopBar(
description = stringResource(R.string.tooltip_dictionary_desc), description = stringResource(R.string.tooltip_dictionary_desc),
onClick = onShowDictionarySettings onClick = onShowDictionarySettings
) { ) {
Icon(painterResource(id = R.drawable.dictionary), contentDescription = "Dictionary Settings", tint = MaterialTheme.colorScheme.onSurfaceVariant) Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
TooltipIconButton(text = "Demo Annotations", onClick = onGenerateDemoAnnotations) { TooltipIconButton(text = stringResource(R.string.tooltip_demo_annotations), onClick = onGenerateDemoAnnotations) {
Icon(Icons.Default.BugReport, contentDescription = "Generate Demo Annotations", tint = MaterialTheme.colorScheme.secondary) Icon(Icons.Default.BugReport, contentDescription = stringResource(R.string.content_desc_generate_demo_annotations), tint = MaterialTheme.colorScheme.secondary)
} }
TooltipIconButton(text = stringResource(R.string.pen_playground), onClick = onShowPenPlayground) { TooltipIconButton(text = stringResource(R.string.pen_playground), onClick = onShowPenPlayground) {
Icon(Icons.Default.Star, contentDescription = "Open Pen Playground", tint = MaterialTheme.colorScheme.primary) Icon(Icons.Default.Star, contentDescription = stringResource(R.string.content_desc_open_pen_playground), tint = MaterialTheme.colorScheme.primary)
} }
TooltipIconButton(text = stringResource(R.string.import_svg), onClick = onImportSvg) { TooltipIconButton(text = stringResource(R.string.import_svg), onClick = onImportSvg) {
Icon(Icons.Default.Brush, contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63)) Icon(Icons.Default.Brush, contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63))
@ -392,7 +392,7 @@ internal fun PdfTopBar(
item { item {
IconButton(onClick = onNewTabClick, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp).size(36.dp)) { IconButton(onClick = onNewTabClick, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp).size(36.dp)) {
Icon(Icons.Default.Add, contentDescription = "New Tab", tint = MaterialTheme.colorScheme.onSurfaceVariant) Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_new_tab), tint = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
} }
@ -492,8 +492,8 @@ fun PdfBottomBar(
) { ) {
if (jumpBackPage != null) { if (jumpBackPage != null) {
TooltipIconButton( TooltipIconButton(
text = "Jump Back to Page ${jumpBackPage + 1}", text = stringResource(R.string.action_jump_back_to_page, jumpBackPage + 1),
description = "Return to previous page", description = stringResource(R.string.desc_return_to_previous_page),
onClick = onJumpBack onClick = onJumpBack
) { ) {
Column( Column(
@ -502,7 +502,7 @@ fun PdfBottomBar(
) { ) {
Icon( Icon(
Icons.AutoMirrored.Filled.Undo, Icons.AutoMirrored.Filled.Undo,
contentDescription = "Jump Back", contentDescription = stringResource(R.string.content_desc_jump_back),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
@ -535,7 +535,7 @@ fun PdfBottomBar(
enabled = !isTtsPlayingOrLoading, enabled = !isTtsPlayingOrLoading,
modifier = Modifier.testTag("TocButton") modifier = Modifier.testTag("TocButton")
) { ) {
Icon(Icons.Default.Menu, contentDescription = "Table of Contents") Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
} }
} }
@ -557,7 +557,7 @@ fun PdfBottomBar(
onClick = onToggleHighlights onClick = onToggleHighlights
) { ) {
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp)) if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = "Highlight all text", tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
@ -577,7 +577,7 @@ fun PdfBottomBar(
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc), description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
onClick = onToggleEditMode onClick = onToggleEditMode
) { ) {
Icon(Icons.Default.Edit, contentDescription = "Toggle Editing Mode", tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
@ -587,19 +587,19 @@ fun PdfBottomBar(
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc), description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
onClick = onToggleTts onClick = onToggleTts
) { ) {
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS", tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
if (BuildConfig.FLAVOR != "oss") { if (BuildConfig.FLAVOR != "oss") {
TooltipIconButton( TooltipIconButton(
text = if (isBubbleZoomModeActive) "Exit Smart Zoom" else "Smart Comic Zoom", text = if (isBubbleZoomModeActive) stringResource(R.string.action_exit_smart_zoom) else stringResource(R.string.action_smart_comic_zoom),
description = "Toggle Smart Comic Zoom", description = stringResource(R.string.desc_toggle_smart_comic_zoom),
onClick = onToggleBubbleZoom onClick = onToggleBubbleZoom
) { ) {
Icon( Icon(
painterResource(R.drawable.comic_bubble), painterResource(R.drawable.comic_bubble),
contentDescription = "Smart Comic Zoom", contentDescription = stringResource(R.string.content_desc_smart_comic_zoom),
tint = if (isBubbleZoomModeActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = if (isBubbleZoomModeActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
) )
} }

View file

@ -2089,7 +2089,7 @@ fun PdfViewerScreen(
if (!selectedDictPackage.isNullOrEmpty()) { if (!selectedDictPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text) ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} else { } else {
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_dictionary_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -2102,7 +2102,7 @@ fun PdfViewerScreen(
if (!selectedTranslatePackage.isNullOrEmpty()) { if (!selectedTranslatePackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text) ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text)
} else { } else {
Toast.makeText(context, "Please select a translate app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_translate_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -2113,7 +2113,7 @@ fun PdfViewerScreen(
if (!selectedSearchPackage.isNullOrEmpty()) { if (!selectedSearchPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text) ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text)
} else { } else {
Toast.makeText(context, "Please select a search app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_search_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -3386,7 +3386,7 @@ fun PdfViewerScreen(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = errorMessage ?: "Failed to load PDF.", text = errorMessage ?: stringResource(R.string.error_failed_load_pdf),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
@ -4380,7 +4380,10 @@ fun PdfViewerScreen(
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = "Downloading ${ocrLanguage.displayName.substringBefore("(")} language pack...", text = stringResource(
R.string.msg_downloading_language_pack,
stringResource(ocrLanguage.displayNameRes).substringBefore("(").trim()
),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onTertiaryContainer color = MaterialTheme.colorScheme.onTertiaryContainer
) )
@ -4429,7 +4432,10 @@ fun PdfViewerScreen(
} }
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = "Downloading Bubble Zoom model... ${(progress * 100).toInt()}%", text = stringResource(
R.string.msg_downloading_bubble_zoom_model_progress,
(progress * 100).toInt()
),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onTertiaryContainer color = MaterialTheme.colorScheme.onTertiaryContainer
) )
@ -4473,7 +4479,7 @@ fun PdfViewerScreen(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Exit slider navigation" contentDescription = stringResource(R.string.content_desc_exit_slider_navigation)
) )
} }
@ -4913,15 +4919,23 @@ fun PdfViewerScreen(
) )
else -1 else -1
val text = val text =
if (index >= 0) "Result ${index + 1} / ${searchData.matches.size}" if (index >= 0) context.getString(
else "${searchData.matches.size} Results" R.string.pdf_search_result_position,
index + 1,
searchData.matches.size
)
else context.resources.getQuantityString(
R.plurals.search_results_count,
searchData.matches.size,
searchData.matches.size
)
Triple(text, index > 0, index < searchData.matches.size - 1) Triple(text, index > 0, index < searchData.matches.size - 1)
} }
is SmartSearchResult.Paged -> { is SmartSearchResult.Paged -> {
val page = currentResult?.locationInSource val page = currentResult?.locationInSource
val text = if (page != null) "Page ${page + 1}" val text = if (page != null) context.getString(R.string.pdf_page_short, page + 1)
else "${searchData.totalPageCount}+ Pages" else context.getString(R.string.msg_search_pages_count, searchData.totalPageCount)
Triple(text, true, true) Triple(text, true, true)
} }
@ -5046,9 +5060,9 @@ fun PdfViewerScreen(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "Jump Back", modifier = Modifier.size(18.dp)) Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = stringResource(R.string.content_desc_jump_back), modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Back to Pg ${lastPage + 1}", style = MaterialTheme.typography.labelLarge) Text(stringResource(R.string.pdf_back_to_page_short, lastPage + 1), style = MaterialTheme.typography.labelLarge)
} }
} }
} }
@ -5535,7 +5549,7 @@ fun PdfViewerScreen(
Icon( Icon(
imageVector = if (isTtsPageBelow) Icons.Default.ArrowDownward imageVector = if (isTtsPageBelow) Icons.Default.ArrowDownward
else Icons.Default.ArrowUpward, else Icons.Default.ArrowUpward,
contentDescription = "Scroll to reading page" contentDescription = stringResource(R.string.content_desc_scroll_to_reading_page)
) )
} }
} }
@ -5817,10 +5831,10 @@ fun PdfViewerScreen(
contentDescription = null contentDescription = null
) )
}, },
title = { Text("Unlock Page Summarization") }, title = { Text(stringResource(R.string.dialog_unlock_page_summarization)) },
text = { text = {
Text( Text(
"Get concise summaries of any page with Episteme Pro. Upgrade to start using this feature." stringResource(R.string.dialog_unlock_page_summarization_desc)
) )
}, },
confirmButton = { confirmButton = {
@ -5841,13 +5855,13 @@ fun PdfViewerScreen(
AlertDialog( AlertDialog(
onDismissRequest = { showInsufficientCreditsDialog = false }, onDismissRequest = { showInsufficientCreditsDialog = false },
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) }, icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
title = { Text("Out of Credits") }, title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
text = { Text("You don't have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.") }, text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showInsufficientCreditsDialog = false showInsufficientCreditsDialog = false
onNavigateToPro() onNavigateToPro()
}) { Text("Get Pro / Add Credits") } }) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showInsufficientCreditsDialog = false }) { TextButton(onClick = { showInsufficientCreditsDialog = false }) {
@ -5874,7 +5888,7 @@ fun PdfViewerScreen(
) { ) {
Image( Image(
bitmap = poppedUpPanelBitmap!!.asImageBitmap(), bitmap = poppedUpPanelBitmap!!.asImageBitmap(),
contentDescription = "Annotated Page", contentDescription = stringResource(R.string.content_desc_annotated_page),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
@ -5894,7 +5908,7 @@ fun PdfViewerScreen(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close Image", contentDescription = stringResource(R.string.content_desc_close_image),
tint = Color.White tint = Color.White
) )
} }
@ -5913,16 +5927,16 @@ fun PdfViewerScreen(
AlertDialog( AlertDialog(
onDismissRequest = { showBubbleZoomDownloadDialog = false }, onDismissRequest = { showBubbleZoomDownloadDialog = false },
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Download Bubble Zoom Model") }, title = { Text(stringResource(R.string.dialog_download_bubble_zoom_model)) },
text = { text = {
Text("To use the Bubble Zoom feature, an AI model needs to be downloaded (~134 MB). Do you want to download it now?") Text(stringResource(R.string.dialog_download_bubble_zoom_model_desc))
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showBubbleZoomDownloadDialog = false showBubbleZoomDownloadDialog = false
viewModel.downloadSpeechBubbleModel(context) viewModel.downloadSpeechBubbleModel(context)
}) { }) {
Text("Download") Text(stringResource(R.string.action_download))
} }
}, },
dismissButton = { dismissButton = {

View file

@ -7,6 +7,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.aryan.reader.FileType import com.aryan.reader.FileType
import com.aryan.reader.R
import com.aryan.reader.data.RecentFileItem import com.aryan.reader.data.RecentFileItem
import com.aryan.reader.data.RecentFilesRepository import com.aryan.reader.data.RecentFilesRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -32,7 +33,8 @@ class ReflowWorker(
Timber.tag("PdfToHtmlPerf").e("FAILURE: KEY_PDF_URI is null | bookId=$bookId") Timber.tag("PdfToHtmlPerf").e("FAILURE: KEY_PDF_URI is null | bookId=$bookId")
return@withContext Result.failure() return@withContext Result.failure()
} }
val originalTitle = inputData.getString(KEY_ORIGINAL_TITLE) ?: "Document" val originalTitle = inputData.getString(KEY_ORIGINAL_TITLE)
?: applicationContext.getString(R.string.default_document_title)
val reflowBookId = "${bookId}_reflow" val reflowBookId = "${bookId}_reflow"
Timber.tag("PdfToHtmlPerf").d( Timber.tag("PdfToHtmlPerf").d(
@ -67,11 +69,11 @@ class ReflowWorker(
bookId = reflowBookId, bookId = reflowBookId,
uriString = destFile.toUri().toString(), uriString = destFile.toUri().toString(),
type = FileType.HTML, type = FileType.HTML,
displayName = "$originalTitle (Text View)", displayName = applicationContext.getString(R.string.reflow_display_name_format, originalTitle),
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
coverImagePath = null, coverImagePath = null,
title = "$originalTitle (Reflow)", title = applicationContext.getString(R.string.reflow_title_format, originalTitle),
author = "Generated", author = applicationContext.getString(R.string.generated_author),
isAvailable = true, isAvailable = true,
isRecent = true, isRecent = true,
lastModifiedTimestamp = System.currentTimeMillis(), lastModifiedTimestamp = System.currentTimeMillis(),

View file

@ -409,7 +409,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = activePopup == ActivePopup.FONT_FAMILY, isSelected = activePopup == ActivePopup.FONT_FAMILY,
iconRes = R.drawable.fonts, iconRes = R.drawable.fonts,
contentDescription = "Select Font Family", contentDescription = stringResource(R.string.content_desc_select_font_family),
onClick = { onClick = {
activePopup = activePopup =
if (activePopup == ActivePopup.FONT_FAMILY) ActivePopup.NONE else ActivePopup.FONT_FAMILY if (activePopup == ActivePopup.FONT_FAMILY) ActivePopup.NONE else ActivePopup.FONT_FAMILY
@ -481,7 +481,7 @@ fun TextAnnotationDock(
) )
Icon( Icon(
imageVector = Icons.Default.KeyboardArrowDown, imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = "Select Font Size", contentDescription = stringResource(R.string.content_desc_select_font_size),
tint = Color.Gray, tint = Color.Gray,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
@ -561,7 +561,7 @@ fun TextAnnotationDock(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.font_background), painter = painterResource(id = R.drawable.font_background),
contentDescription = "Font Background", contentDescription = stringResource(R.string.content_desc_font_background),
modifier = Modifier.size(17.dp), modifier = Modifier.size(17.dp),
tint = Color.Black tint = Color.Black
) )
@ -587,7 +587,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = isBold, isSelected = isBold,
iconRes = R.drawable.format_bold, iconRes = R.drawable.format_bold,
contentDescription = "Bold", contentDescription = stringResource(R.string.content_desc_bold),
onClick = { onClick = {
val newW = if (isBold) FontWeight.Normal else FontWeight.Bold val newW = if (isBold) FontWeight.Normal else FontWeight.Bold
onUpdateStyle(currentStyle.copy(fontWeight = newW, fontFamily = currentStyle.fontFamily)) onUpdateStyle(currentStyle.copy(fontWeight = newW, fontFamily = currentStyle.fontFamily))
@ -605,7 +605,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = isItalic, isSelected = isItalic,
iconRes = R.drawable.format_italic, iconRes = R.drawable.format_italic,
contentDescription = "Italic", contentDescription = stringResource(R.string.content_desc_italic),
onClick = { onClick = {
val newStyle = if (isItalic) FontStyle.Normal else FontStyle.Italic val newStyle = if (isItalic) FontStyle.Normal else FontStyle.Italic
onUpdateStyle(currentStyle.copy(fontStyle = newStyle, fontFamily = currentStyle.fontFamily)) onUpdateStyle(currentStyle.copy(fontStyle = newStyle, fontFamily = currentStyle.fontFamily))
@ -625,7 +625,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = isUnderline, isSelected = isUnderline,
iconRes = R.drawable.format_underlined, iconRes = R.drawable.format_underlined,
contentDescription = "Underline", contentDescription = stringResource(R.string.content_desc_underline),
onClick = { onClick = {
val hasStrike = currentDec.contains(TextDecoration.LineThrough) val hasStrike = currentDec.contains(TextDecoration.LineThrough)
val newDec = if (isUnderline) { val newDec = if (isUnderline) {
@ -654,7 +654,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = isStrike, isSelected = isStrike,
iconRes = R.drawable.format_strikethrough, iconRes = R.drawable.format_strikethrough,
contentDescription = "Strikethrough", contentDescription = stringResource(R.string.content_desc_strikethrough),
onClick = { onClick = {
val hasUnd = currentDec.contains(TextDecoration.Underline) val hasUnd = currentDec.contains(TextDecoration.Underline)
val newDec = if (isStrike) { val newDec = if (isStrike) {
@ -682,7 +682,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = false, isSelected = false,
iconRes = R.drawable.text_box, iconRes = R.drawable.text_box,
contentDescription = "Insert Text Box", contentDescription = stringResource(R.string.content_desc_insert_text_box),
onClick = { onClick = {
Timber.tag("PdfTextBoxDebug").d("Dock: Insert Text Box icon clicked") Timber.tag("PdfTextBoxDebug").d("Dock: Insert Text Box icon clicked")
onInsertTextBox() onInsertTextBox()
@ -698,7 +698,7 @@ fun TextAnnotationDock(
FormattingIconButton( FormattingIconButton(
isSelected = false, isSelected = false,
iconVector = Icons.Default.Close, iconVector = Icons.Default.Close,
contentDescription = "Close", contentDescription = stringResource(R.string.action_close),
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
onClearTextBoxSelection() onClearTextBoxSelection()
@ -739,7 +739,7 @@ private fun FontItem(
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,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
@ -814,7 +814,7 @@ private fun ColorPickerBubble(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close", contentDescription = stringResource(R.string.action_close),
tint = Color.White, tint = Color.White,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
@ -1193,12 +1193,12 @@ private fun ColorPickerSpectrumContent(
IconButton(onClick = onBack, modifier = Modifier.size(24.dp)) { IconButton(onClick = onBack, modifier = Modifier.size(24.dp)) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back", contentDescription = stringResource(R.string.action_back),
tint = Color.White tint = Color.White
) )
} }
Text( Text(
text = "Spectrum", text = stringResource(R.string.label_spectrum),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
@ -1264,7 +1264,7 @@ private fun ColorPickerSpectrumContent(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
"Hex", stringResource(R.string.theme_color_hex),
color = Color.Gray, color = Color.Gray,
fontSize = 11.sp, fontSize = 11.sp,
maxLines = 1 maxLines = 1
@ -1282,19 +1282,19 @@ private fun ColorPickerSpectrumContent(
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
RgbInputColumn( RgbInputColumn(
label = "R", label = stringResource(R.string.color_r),
value = currentColor.red, 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 = "G", label = stringResource(R.string.color_g),
value = currentColor.green, 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 = "B", label = stringResource(R.string.color_b),
value = currentColor.blue, 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)
@ -1313,7 +1313,7 @@ private fun ColorPickerSpectrumContent(
modifier = Modifier.fillMaxWidth().height(40.dp), modifier = Modifier.fillMaxWidth().height(40.dp),
contentPadding = PaddingValues(0.dp) contentPadding = PaddingValues(0.dp)
) { ) {
Text("Done", fontSize = 14.sp) Text(stringResource(R.string.action_done), fontSize = 14.sp)
} }
} }
} }

View file

@ -13,6 +13,7 @@ import android.graphics.RectF
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import com.aryan.reader.FileType import com.aryan.reader.FileType
import com.aryan.reader.R
import io.legere.pdfiumandroid.api.Bookmark 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
@ -452,7 +453,7 @@ class OpdsStreamDocumentWrapper(
textSize = 40f textSize = 40f
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
} }
canvas.drawText("Page Unavailable", 400f, 600f, paint) canvas.drawText(context.getString(R.string.msg_page_unavailable), 400f, 600f, paint)
val stream = java.io.ByteArrayOutputStream() val stream = java.io.ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream) bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream)
return stream.toByteArray() return stream.toByteArray()

View file

@ -24,6 +24,12 @@
<item quantity="other">%1$d results found</item> <item quantity="other">%1$d results found</item>
</plurals> </plurals>
<!-- Shown in the PDF search results summary bar. %1$d = total number of matches found. -->
<plurals name="search_matches_count">
<item quantity="one">%1$d match found</item>
<item quantity="other">%1$d matches found</item>
</plurals>
<plurals name="dialog_delete_permanently"> <plurals name="dialog_delete_permanently">
<item quantity="one">Delete File Permanently</item> <item quantity="one">Delete File Permanently</item>
<item quantity="other">Delete Files Permanently</item> <item quantity="other">Delete Files Permanently</item>
@ -52,4 +58,22 @@
<item quantity="one">%1$d book removed from library.</item> <item quantity="one">%1$d book removed from library.</item>
<item quantity="other">%1$d books removed from library.</item> <item quantity="other">%1$d books removed from library.</item>
</plurals> </plurals>
<!-- Word count for folders shown in shelf subtitles. %1$d = number of folders. -->
<plurals name="folder_count">
<item quantity="one">%1$d folder</item>
<item quantity="other">%1$d folders</item>
</plurals>
<!-- Word count for tags shown in filter chips. %1$d = number of tags. -->
<plurals name="tag_count">
<item quantity="one">%1$d tag</item>
<item quantity="other">%1$d tags</item>
</plurals>
<!-- Parenthetical cache chunk count in TTS cache entries. %1$d = number of cached audio chunks. -->
<plurals name="tts_cache_chunk_count_parenthetical">
<item quantity="one">(%1$d chunk)</item>
<item quantity="other">(%1$d chunks)</item>
</plurals>
</resources> </resources>

View file

@ -15,6 +15,10 @@
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_clear">Clear</string> <string name="action_clear">Clear</string>
<string name="action_apply">Apply</string> <string name="action_apply">Apply</string>
<string name="action_enable">Enable</string>
<!-- Generic error label. %1$s = the error message. -->
<string name="error_message_format">Error: %1$s</string>
<string name="action_go_back">Go Back</string>
<!-- Tab label in the multi-tab PDF reader — means the slot is available (unused), NOT "free of cost". --> <!-- Tab label in the multi-tab PDF reader — means the slot is available (unused), NOT "free of cost". -->
<string name="tab_free">Free</string> <string name="tab_free">Free</string>
<string name="active_tabs">Active Tabs</string> <string name="active_tabs">Active Tabs</string>
@ -288,6 +292,13 @@
<!-- Fonts --> <!-- Fonts -->
<string name="custom_fonts">Custom Fonts</string> <string name="custom_fonts">Custom Fonts</string>
<string name="import_font">Import Font</string> <string name="import_font">Import Font</string>
<string name="google_fonts">Google Fonts</string>
<string name="action_browse_google_fonts">Browse Google Fonts</string>
<string name="google_fonts_search_placeholder">Search 1900+ fonts…</string>
<string name="google_fonts_popular_choices">Popular Choices</string>
<!-- Empty state in the Google Fonts browser. %1$s = the user-entered search query. -->
<string name="google_fonts_no_matches">No fonts found matching \'%1$s\'</string>
<string name="content_desc_already_downloaded">Already Downloaded</string>
<string name="no_custom_fonts">No Custom Fonts</string> <string name="no_custom_fonts">No Custom Fonts</string>
<!-- TTF and OTF are font file format names — do not translate. --> <!-- TTF and OTF are font file format names — do not translate. -->
<string name="import_fonts_desc">Import TTF or OTF files to use them in your books.</string> <string name="import_fonts_desc">Import TTF or OTF files to use them in your books.</string>
@ -512,19 +523,20 @@
<string name="search_in_book">Search in book…</string> <string name="search_in_book">Search in book…</string>
<string name="search_no_results_simple">No results found.</string> <string name="search_no_results_simple">No results found.</string>
<!-- Common.kt: — AI feature, not translated. --> <!-- Common.kt: AI feature status. -->
<string name="generating_summary" translatable="false">Generating summary…</string> <string name="generating_summary">Generating summary…</string>
<string name="action_stop">Stop</string> <string name="action_stop">Stop</string>
<string name="action_read_aloud">Read aloud</string> <string name="action_read_aloud">Read aloud</string>
<string name="action_copy">Copy</string> <string name="action_copy">Copy</string>
<string name="no_summary_generated" translatable="false">No summary could be generated.</string> <string name="action_copy_thread">Copy Thread</string>
<string name="no_summary_generated">No summary could be generated.</string>
<!-- Common.kt: AiDefinitionPopup — AI feature, not translated. --> <!-- Common.kt: AiDefinitionPopup — AI feature strings. -->
<string name="ai_thinking" translatable="false">Thinking…</string> <string name="ai_thinking">Thinking…</string>
<string name="content_desc_open_dictionary">Open in Dictionary App</string> <string name="content_desc_open_dictionary">Open in Dictionary App</string>
<string name="ai_no_definition" translatable="false">AI could not provide a definition.</string> <string name="ai_no_definition">AI could not provide a definition.</string>
<!-- AI feature string. %1$s = the word or phrase the user selected. --> <!-- AI feature string. %1$s = the word or phrase the user selected. -->
<string name="ai_asking_about" translatable="false">Asking AI about \'%1$s\'…</string> <string name="ai_asking_about">Asking AI about \'%1$s\'…</string>
<!-- Common.kt: TtsSettingsSheet --> <!-- Common.kt: TtsSettingsSheet -->
<!-- TTS = Text-to-Speech. --> <!-- TTS = Text-to-Speech. -->
@ -534,8 +546,8 @@
<string name="tts_synthesis_mode">Synthesis Mode</string> <string name="tts_synthesis_mode">Synthesis Mode</string>
<!-- On-Device TTS mode — speech is generated locally without internet. --> <!-- On-Device TTS mode — speech is generated locally without internet. -->
<string name="tts_mode_on_device">On-Device</string> <string name="tts_mode_on_device">On-Device</string>
<!-- Cloud TTS mode — speech is generated remotely for higher quality. Not translated (feature name). --> <!-- Cloud TTS mode — speech is generated remotely for higher quality. -->
<string name="tts_mode_cloud_hq" translatable="false">Cloud (HQ)</string> <string name="tts_mode_cloud_hq">Cloud (HQ)</string>
<string name="tts_voice_selection">Voice Selection</string> <string name="tts_voice_selection">Voice Selection</string>
<string name="tts_play_sample">Play Sample</string> <string name="tts_play_sample">Play Sample</string>
@ -586,17 +598,17 @@
<string name="color_g" translatable="false">G</string> <string name="color_g" translatable="false">G</string>
<string name="color_b" translatable="false">B</string> <string name="color_b" translatable="false">B</string>
<!-- Common.kt: fetchAiDefinition — AI feature errors, not translated. --> <!-- Common.kt: fetchAiDefinition — AI feature errors. -->
<string name="error_text_empty" translatable="false">Text is empty.</string> <string name="error_text_empty">Text is empty.</string>
<string name="error_ai_empty_definition" translatable="false">AI returned an empty definition.</string> <string name="error_ai_empty_definition">AI returned an empty definition.</string>
<string name="error_could_not_get_definition" translatable="false">Could not get definition.</string> <string name="error_could_not_get_definition">Could not get definition.</string>
<string name="error_unknown_server" translatable="false">An unknown server error occurred.</string> <string name="error_unknown_server">An unknown server error occurred.</string>
<string name="error_network_check_connection" translatable="false">Network error. Check connection.</string> <string name="error_network_check_connection">Network error. Check connection.</string>
<!-- Common.kt: fetchRecap — Recap feature, not translated. --> <!-- Common.kt: fetchRecap — Recap feature errors. -->
<string name="error_not_enough_context" translatable="false">Not enough context for a recap.</string> <string name="error_not_enough_context">Not enough context for a recap.</string>
<string name="error_parse_recap" translatable="false">Failed to parse recap.</string> <string name="error_parse_recap">Failed to parse recap.</string>
<string name="error_network_recap" translatable="false">Network error during recap generation.</string> <string name="error_network_recap">Network error during recap generation.</string>
<!-- DictionarySettingsDialog.kt --> <!-- DictionarySettingsDialog.kt -->
<string name="dict_lookup_settings">Lookup Settings</string> <string name="dict_lookup_settings">Lookup Settings</string>
@ -616,27 +628,27 @@
<string name="dict_search_app_description">App used for web searches.</string> <string name="dict_search_app_description">App used for web searches.</string>
<string name="dict_none">None</string> <string name="dict_none">None</string>
<!-- EpubReaderAi.kt — AI feature strings, not translated. --> <!-- EpubReaderAi.kt — AI feature strings. -->
<string name="ai_error_book_content_empty" translatable="false">The book content is empty.</string> <string name="ai_error_book_content_empty">The book content is empty.</string>
<string name="ai_error_parse_summary" translatable="false">Failed to parse summary from server response.</string> <string name="ai_error_parse_summary">Failed to parse summary from server response.</string>
<string name="ai_error_fetch_summary" translatable="false">Could not fetch summary.</string> <string name="ai_error_fetch_summary">Could not fetch summary.</string>
<!-- %1$d = HTTP or server error code; %2$s = human-readable error message from the server. --> <!-- %1$d = HTTP or server error code; %2$s = human-readable error message from the server. -->
<string name="ai_error_with_code" translatable="false">Error: %1$d. %2$s</string> <string name="ai_error_with_code">Error: %1$d. %2$s</string>
<string name="ai_error_network_server" translatable="false">Network error. Please check connection and server status.</string> <string name="ai_error_network_server">Network error. Please check connection and server status.</string>
<!-- %1$d = chapter number being analyzed. --> <!-- %1$d = chapter number being analyzed. -->
<string name="ai_analyzing_chapter" translatable="false">Analyzing Chapter %1$d…</string> <string name="ai_analyzing_chapter">Analyzing Chapter %1$d…</string>
<string name="ai_reading_position" translatable="false">Reading current position…</string> <string name="ai_reading_position">Reading current position…</string>
<string name="ai_generating_recap" translatable="false">Generating Recap…</string> <string name="ai_generating_recap">Generating Recap…</string>
<string name="ai_chapter_summary" translatable="false">Chapter Summary</string> <string name="ai_chapter_summary">Chapter Summary</string>
<!-- "(Beta)" indicates this feature is in beta — keep the label consistent. --> <!-- "(Beta)" indicates this feature is in beta — keep the label consistent. -->
<string name="ai_story_recap_beta" translatable="false">Story Recap (Beta)</string> <string name="ai_story_recap_beta">Story Recap (Beta)</string>
<string name="ai_unlock_summarization" translatable="false">Unlock Chapter Summarization</string> <string name="ai_unlock_summarization">Unlock Chapter Summarization</string>
<!-- "Episteme Pro" is the product tier name. --> <!-- "Episteme Pro" is the product tier name. -->
<string name="ai_unlock_summarization_desc" translatable="false">Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.</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="action_learn_more">Learn More</string>
<string name="ai_unlock_smart_dict" translatable="false">Unlock Smart Dictionary</string> <string name="ai_unlock_smart_dict">Unlock Smart Dictionary</string>
<!-- "Pro" refers to the Episteme Pro product tier. --> <!-- "Pro" refers to the Episteme Pro product tier. -->
<string name="ai_unlock_smart_dict_desc" translatable="false">Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.</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 --> <!-- EpubReaderAnnotations.kt -->
<string name="content_desc_bookmark_icon">Bookmark</string> <string name="content_desc_bookmark_icon">Bookmark</string>
@ -684,8 +696,8 @@
<string name="content_desc_text_formatting">Text Formatting</string> <string name="content_desc_text_formatting">Text Formatting</string>
<!-- AI chapter summarization feature menu item. --> <!-- AI chapter summarization feature menu item. -->
<string name="menu_chapter_summarization">Chapter Summarization</string> <string name="menu_chapter_summarization">Chapter Summarization</string>
<!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. Not translated. --> <!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. -->
<string name="menu_recap_beta" translatable="false">Recap (Beta)</string> <string name="menu_recap_beta">Recap (Beta)</string>
<!-- TTS = Text-to-Speech. Accessibility label. --> <!-- TTS = Text-to-Speech. Accessibility label. -->
<string name="content_desc_stop_tts">Stop TTS</string> <string name="content_desc_stop_tts">Stop TTS</string>
<!-- TTS = Text-to-Speech. Accessibility label. --> <!-- TTS = Text-to-Speech. Accessibility label. -->
@ -719,6 +731,8 @@
<string name="label_max">Max</string> <string name="label_max">Max</string>
<string name="content_desc_slower">Slower</string> <string name="content_desc_slower">Slower</string>
<string name="content_desc_faster">Faster</string> <string name="content_desc_faster">Faster</string>
<string name="content_desc_decrease">Decrease</string>
<string name="content_desc_increase">Increase</string>
<!-- Shown in the PDF reader header. %1$d = current page number; %2$d = total page count. --> <!-- Shown in the PDF reader header. %1$d = current page number; %2$d = total page count. -->
<string name="page_of_pages">Page %1$d of %2$d</string> <string name="page_of_pages">Page %1$d of %2$d</string>
@ -726,10 +740,15 @@
<string name="tab_chapters">Chapters</string> <string name="tab_chapters">Chapters</string>
<string name="tab_bookmarks">Bookmarks</string> <string name="tab_bookmarks">Bookmarks</string>
<string name="tab_highlights">Highlights</string> <string name="tab_highlights">Highlights</string>
<string name="tab_pages">Pages</string>
<string name="action_expand_all">Expand All</string>
<string name="action_collapse_all">Collapse All</string>
<string name="action_locate">Locate</string>
<string name="no_bookmarks_yet">You haven\'t added any bookmarks yet.</string> <string name="no_bookmarks_yet">You haven\'t added any bookmarks yet.</string>
<string name="content_desc_more_options_bookmark">More options for bookmark</string> <string name="content_desc_more_options_bookmark">More options for bookmark</string>
<string name="dialog_rename_bookmark">Rename Bookmark</string> <string name="dialog_rename_bookmark">Rename Bookmark</string>
<string name="label_new_name">New Name</string> <string name="label_new_name">New Name</string>
<string name="label_new_title">New Title</string>
<string name="dialog_delete_bookmark">Delete Bookmark?</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="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="no_highlights_yet">No highlights yet.</string>
@ -912,6 +931,14 @@
<string name="content_desc_undo">Undo</string> <string name="content_desc_undo">Undo</string>
<string name="content_desc_redo">Redo</string> <string name="content_desc_redo">Redo</string>
<string name="content_desc_show_dock">Show Dock</string> <string name="content_desc_show_dock">Show Dock</string>
<string name="content_desc_select_font_family">Select Font Family</string>
<string name="content_desc_select_font_size">Select Font Size</string>
<string name="content_desc_font_background">Font Background</string>
<string name="content_desc_bold">Bold</string>
<string name="content_desc_italic">Italic</string>
<string name="content_desc_underline">Underline</string>
<string name="content_desc_strikethrough">Strikethrough</string>
<string name="content_desc_insert_text_box">Insert Text Box</string>
<!-- PdfPageComposable --> <!-- PdfPageComposable -->
<!-- OCR = Optical Character Recognition. %1$s = error reason string. --> <!-- OCR = Optical Character Recognition. %1$s = error reason string. -->
@ -1028,6 +1055,230 @@
<!-- Label for the highlight color picker in text annotations. --> <!-- Label for the highlight color picker in text annotations. -->
<string name="label_highlight_color">Highlight</string> <string name="label_highlight_color">Highlight</string>
<!-- Hardcoded UI String Cleanup -->
<!-- Home overflow menu item: enables the multi-tab reader mode. -->
<string name="options_enable_multi_tab_reading">Enable Multi-Tab Reading</string>
<!-- Home overflow menu item: uses stricter MIME filters when choosing local files. -->
<string name="options_use_strict_file_filter">Use Strict File Filter</string>
<string name="options_language">Language</string>
<!-- Debug-only menu item that runs local panel detection diagnostics. -->
<string name="options_test_panel_ml_detection">Test Panel ML Detection</string>
<!-- Debug-only menu item that runs local speech bubble detection diagnostics. -->
<string name="options_test_speech_bubble_ml_detection">Test Speech Bubble ML Detection</string>
<!-- Debug-only menu item. %1$d = maximum number of log lines included. -->
<string name="options_export_logs_last_lines">Export Logs (Last %1$d lines)</string>
<string name="dialog_strict_file_filter_title">Enable Strict File Filter</string>
<!-- Warning shown before enabling stricter Android file picker MIME filtering. File extensions should remain uppercase. -->
<string name="dialog_strict_file_filter_desc">If you enable this, some supported file types like AZW3, CB7, and FB2 might not show up depending on your file manager.\n\nAre you sure you want to enable this filter?</string>
<!-- App language picker. Language names are shown in their own language followed by an English hint. -->
<string name="language_english_default">English (Default)</string>
<string name="language_arabic">العربية (Arabic)</string>
<string name="language_german">Deutsch (German)</string>
<string name="language_turkish">Türkçe (Turkish)</string>
<string name="language_french">Français (French)</string>
<string name="language_russian">Русский (Russian)</string>
<!-- App-wide theme controls in HomeScreen.kt. -->
<string name="app_theme_title">App Theme</string>
<string name="app_theme_appearance">Appearance</string>
<string name="app_theme_contrast">Contrast</string>
<string name="app_theme_text_brightness">Text Brightness</string>
<string name="app_theme_color_scheme">Color Scheme</string>
<string name="app_theme_dynamic">Dynamic</string>
<string name="app_theme_create_title">Create App Theme</string>
<!-- Fallback name when the user saves a custom app theme without entering a name. -->
<string name="app_theme_custom_default_name">Custom Theme</string>
<string name="app_theme_preset_ocean">Ocean</string>
<string name="app_theme_preset_mint">Mint</string>
<string name="app_theme_preset_rose">Rose</string>
<string name="app_theme_preset_sepia">Sepia</string>
<string name="app_theme_preset_amethyst">Amethyst</string>
<string name="app_theme_preset_amber">Amber</string>
<string name="app_theme_preset_sapphire">Sapphire</string>
<string name="content_desc_add_custom_theme">Add Custom Theme</string>
<string name="content_desc_app_theme">App Theme</string>
<string name="content_desc_app_icon">App Icon</string>
<string name="content_desc_device">Device</string>
<string name="content_desc_open_drawer">Open Drawer</string>
<string name="content_desc_profile_picture">Profile picture</string>
<string name="content_desc_profile">Profile</string>
<string name="content_desc_pro_feature">Pro Feature</string>
<string name="content_desc_filter">Filter</string>
<string name="content_desc_sort">Sort</string>
<string name="content_desc_close_search">Close search</string>
<string name="content_desc_clear_query">Clear query</string>
<string name="content_desc_search_shelf">Search shelf</string>
<!-- Accessibility label. %1$s = shelf name. -->
<string name="content_desc_shelf_cover">%1$s shelf cover</string>
<string name="theme_preserve_image_colors">Preserve Image Colors</string>
<string name="theme_preserve_image_colors_desc">Keep original image colors when theme changes</string>
<!-- Enum labels used by library filters, sorting, and app theme controls. -->
<string name="add_books_source_unshelved">Unshelved</string>
<string name="add_books_source_all_books">All Books</string>
<string name="app_theme_mode_system">System</string>
<string name="app_theme_mode_light">Light</string>
<string name="app_theme_mode_dark">Dark</string>
<string name="app_contrast_standard">Standard</string>
<string name="app_contrast_medium">Medium</string>
<string name="app_contrast_high">High</string>
<string name="sort_recent">Recent</string>
<string name="sort_title_az">Title A-Z</string>
<string name="sort_author_az">Author A-Z</string>
<string name="sort_percent_asc">Percent complete 0-100</string>
<string name="sort_percent_desc">Percent complete 100-0</string>
<string name="sort_size_smallest">Size (Smallest)</string>
<string name="sort_size_biggest">Size (Biggest)</string>
<string name="read_status_all">All</string>
<string name="read_status_unread">Unread</string>
<string name="read_status_in_progress">In Progress</string>
<string name="read_status_completed">Completed</string>
<!-- Active tag filter chip label. %1$s = tag names or a count, already pluralized by code. -->
<string name="filter_tags">Tags: %1$s</string>
<string name="section_browse_by_tag">Browse by tag</string>
<string name="section_tags">Tags</string>
<string name="section_folders">Folders</string>
<string name="section_files">Files</string>
<!-- TTS settings sheet labels. TTS = Text-to-Speech. -->
<string name="tts_active_engine">Active TTS Engine</string>
<string name="tts_mode_cloud_ai">Cloud AI</string>
<string name="tts_mode_device_native">Device Native</string>
<string name="tts_tab_cloud_voices">Cloud Voices</string>
<string name="tts_tab_device_voices">Device Voices</string>
<string name="tts_tab_cloud_cache">Cloud Cache</string>
<string name="tts_select_cloud_voice">Select High-Quality Cloud Voice</string>
<string name="tts_clear_samples">Clear Samples</string>
<string name="tts_system_default_voice">System Default Voice</string>
<string name="tts_uses_device_settings">Uses device settings</string>
<string name="tts_language_filter">Language Filter</string>
<string name="tts_online">Online</string>
<string name="tts_offline">Offline</string>
<string name="tts_voice_filter">Voice Filter</string>
<string name="tts_no_audio_cached_for_voice">No audio cached for this voice.</string>
<!-- Button label. %1$s = selected cloud TTS voice identifier. -->
<string name="tts_clear_cache_for_voice">Clear Cache for %1$s</string>
<string name="tts_voice_sample_generic">This is a voice sample.</string>
<!-- AI hub and Pro credits. AI = Artificial Intelligence. -->
<string name="ai_features_title">AI Features</string>
<string name="ai_tab_summary">Summary</string>
<string name="ai_tab_recap">Recap</string>
<string name="ai_tab_cache">Cache</string>
<!-- %1$s = chapter title, lowercased by code. -->
<string name="ai_no_summary_for_chapter">No summary for %1$s yet.</string>
<!-- %1$s = chapter title. -->
<string name="ai_generate_summary_for_chapter">Generate Summary for %1$s</string>
<string name="ai_recap_desc">Get a recap of the story up to your current position.</string>
<string name="ai_generate_story_recap">Generate Story Recap</string>
<string name="ai_story_recap">Story Recap</string>
<string name="ai_cache_hit_free">Cache Hit • Free</string>
<!-- %1$d = number of free daily summaries remaining. -->
<string name="ai_generated_free_remaining">Generated • Free (%1$d/10 left)</string>
<!-- %1$s = credit cost returned by the server. -->
<string name="ai_generated_cost">Generated • Cost: %1$s credits</string>
<string name="ai_generating_cost_calculating">Generating… • Cost: Calculating</string>
<string name="ai_output_title">AI Output</string>
<string name="ai_regenerate">Regenerate</string>
<string name="ai_no_cached_summaries">No cached summaries for this book.</string>
<string name="credits_tab">Credits</string>
<string name="credits_title">AI &amp; Cloud Credits</string>
<string name="credits_available">Credits Available</string>
<!-- Compact account badge. %1$d = remaining credit count. -->
<string name="credits_count">%1$d Credits</string>
<string name="credits_estimated_cost_breakdown">Estimated Cost Breakdown</string>
<string name="credits_cloud_tts_title">Cloud TTS</string>
<string name="credits_cloud_tts_desc">Cost: ~3-4 credits per minute of audio generated.\nTo enable: Reader Screen &gt; More &gt; TTS Voice Settings.</string>
<string name="credits_ai_summaries_title">AI Summaries &amp; Recap</string>
<string name="credits_ai_summaries_desc">Cost: ~1-4 credits per request based on chapter length.\nPro Users get 10 free summaries daily.</string>
<string name="legal_by_purchasing">By purchasing,</string>
<string name="dialog_out_of_credits_title">Out of Credits</string>
<string name="dialog_out_of_credits_desc">You don\'t have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.</string>
<string name="action_get_pro_or_add_credits">Get Pro / Add Credits</string>
<string name="dialog_unlock_page_summarization">Unlock Page Summarization</string>
<string name="dialog_unlock_page_summarization_desc">Get concise summaries of any page with Episteme Pro. Upgrade to start using this feature.</string>
<string name="dialog_download_bubble_zoom_model">Download Bubble Zoom Model</string>
<string name="dialog_download_bubble_zoom_model_desc">To use the Bubble Zoom feature, an AI model needs to be downloaded (~134 MB). Do you want to download it now?</string>
<!-- PDF and annotation helpers. -->
<string name="action_translate">Translate</string>
<string name="pdf_back_to_page_short">Back to Pg %1$d</string>
<!-- Compact page label used in floating PDF search/navigation controls. %1$d = 1-indexed page number. -->
<string name="pdf_page_short">Page %1$d</string>
<!-- Floating PDF search/navigation control. %1$d = current result number; %2$d = total result count. -->
<string name="pdf_search_result_position">Result %1$d / %2$d</string>
<!-- Error shown when the PDF renderer cannot open a document and did not provide a specific error message. -->
<string name="error_failed_load_pdf">Failed to load PDF.</string>
<!-- Progress banner while downloading the optional Bubble Zoom AI model. %1$d = percentage complete (0-100). -->
<string name="msg_downloading_bubble_zoom_model_progress">Downloading Bubble Zoom model… %1$d%%</string>
<string name="content_desc_exit_slider_navigation">Exit slider navigation</string>
<string name="content_desc_jump_back">Jump Back</string>
<string name="content_desc_scroll_to_reading_page">Scroll to reading page</string>
<string name="content_desc_annotated_page">Annotated Page</string>
<string name="content_desc_close_image">Close Image</string>
<string name="content_desc_toggle_search_highlights">Toggle search highlights</string>
<string name="content_desc_drag_text_box">Drag to move text box</string>
<string name="content_desc_no_files_icon">No files icon</string>
<!-- Accessibility label. %1$s = visible field label being copied. -->
<string name="content_desc_copy_value">Copy %1$s</string>
<string name="content_desc_tag">Tag</string>
<string name="content_desc_list_item_marker">List item marker</string>
<string name="content_desc_reset_zoom">Reset Zoom</string>
<string name="content_desc_generate_demo_annotations">Generate Demo Annotations</string>
<string name="tooltip_demo_annotations">Demo Annotations</string>
<string name="content_desc_open_pen_playground">Open Pen Playground</string>
<string name="content_desc_new_tab">New Tab</string>
<string name="content_desc_highlight_all_text">Highlight all text</string>
<string name="content_desc_toggle_editing_mode">Toggle Editing Mode</string>
<string name="content_desc_smart_comic_zoom">Smart Comic Zoom</string>
<string name="highlight_customize_title">Customize Highlights</string>
<string name="ocr_language_latin">English, Spanish, French, etc.</string>
<string name="ocr_language_devanagari">Hindi, Marathi, Sanskrit + English</string>
<string name="ocr_language_chinese">Chinese + English</string>
<string name="ocr_language_japanese">Japanese + English</string>
<string name="ocr_language_korean">Korean + English</string>
<!-- UniversalDocument --> <!-- UniversalDocument -->
<string name="msg_page_unavailable">Page Unavailable</string> <string name="msg_page_unavailable">Page Unavailable</string>
<!-- ReflowWorker: generated Text View records. "Text View" is the PDF reflow feature name. -->
<string name="default_document_title">Document</string>
<string name="generated_author">Generated</string>
<string name="reflow_display_name_format">%1$s (Text View)</string>
<string name="reflow_title_format">%1$s (Reflow)</string>
<!-- Shared tag management UI. -->
<string name="action_add_edit">Add / Edit</string>
<string name="msg_no_tags_assigned">No tags assigned.</string>
<string name="title_apply_tags">Apply Tags</string>
<string name="placeholder_search_create_tag">Search or create tag…</string>
<!-- Tag creation row. %1$s = tag name typed by the user. -->
<string name="action_create_tag">Create \"%1$s\"</string>
<!-- EPUB/Paginated reader controls. -->
<string name="setting_pull_distance_change_chapter">Pull Distance to Change Chapter</string>
<string name="label_short">Short</string>
<string name="label_long">Long</string>
<!-- Compact TTS speed label. %1$s = formatted multiplier, e.g. 1.2. -->
<string name="tts_speed_short">Spd: %1$sx</string>
<!-- Compact TTS pitch label. %1$s = formatted multiplier, e.g. 1.0. -->
<string name="tts_pitch_short">Ptch: %1$sx</string>
<string name="content_desc_play_pause">Play/Pause</string>
<string name="content_desc_reset_speed">Reset Speed</string>
<string name="content_desc_reset_pitch">Reset Pitch</string>
<string name="dialog_select_color">Select Color</string>
<string name="msg_book_no_content">This book has no content to display.</string>
<string name="clip_label_copied_link">Copied Link</string>
<string name="clip_label_copied_text">Copied Text</string>
<!-- PDF toolbar labels. -->
<string name="content_desc_table_of_contents">Table of Contents</string>
<string name="content_desc_bookmark">Bookmark</string>
<!-- Tooltip/button text. %1$d = 1-indexed page number. -->
<string name="action_jump_back_to_page">Jump Back to Page %1$d</string>
<string name="desc_return_to_previous_page">Return to previous page</string>
<string name="action_exit_smart_zoom">Exit Smart Zoom</string>
<string name="action_smart_comic_zoom">Smart Comic Zoom</string>
<string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string>
</resources> </resources>

View file

@ -3,5 +3,7 @@
<locale android:name="en"/> <locale android:name="en"/>
<locale android:name="ar"/> <locale android:name="ar"/>
<locale android:name="de"/> <locale android:name="de"/>
<locale android:name="fr"/>
<locale android:name="ru"/>
<locale android:name="tr"/> <locale android:name="tr"/>
</locale-config> </locale-config>