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

@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
@ -258,12 +259,12 @@ fun AppNavigation(
horizontalAlignment = Alignment.CenterHorizontally,
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))
Button(onClick = {
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))
DeviceVoicesTab(isTtsActive, context, TtsPlaybackManager.TtsMode.BASE)
} 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))
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) ->
val isSelected = currentMode == mode
Box(
@ -1150,9 +1153,9 @@ fun TtsSettingsSheet(
Spacer(Modifier.height(16.dp))
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 == 1, onClick = { selectedTabIndex = 1 }, text = { Text("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 == 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(stringResource(R.string.tts_tab_device_voices), 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))
@ -1180,10 +1183,10 @@ fun AiVoicesTab(
val isCloudMode = currentMode == TtsPlaybackManager.TtsMode.CLOUD
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()) {
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 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) }
DisposableEffect(Unit) {
@ -1264,13 +1268,13 @@ fun DeviceVoicesTab(
}
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")
list
}
val filteredVoices = remember(allVoices, selectedLanguage) {
if (selectedLanguage == "All") allVoices
if (selectedLanguage == allLanguagesLabel) allVoices
else allVoices.filter { it.locale.displayLanguage == selectedLanguage }
}
@ -1310,8 +1314,8 @@ fun DeviceVoicesTab(
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text("System Default Voice", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Uses device settings", style = MaterialTheme.typography.bodySmall)
Text(stringResource(R.string.tts_system_default_voice), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
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)
}
@ -1326,7 +1330,7 @@ fun DeviceVoicesTab(
value = selectedLanguage,
onValueChange = {},
readOnly = true,
label = { Text("Language Filter") },
label = { Text(stringResource(R.string.tts_language_filter)) },
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = languageMenuExpanded) },
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth().menuAnchor(),
@ -1359,7 +1363,7 @@ fun DeviceVoicesTab(
ListItem(
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 = {
if (isSelected) {
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
@ -1378,11 +1382,11 @@ fun DeviceVoicesTab(
onClick = {
ttsEngine?.apply {
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()) {
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)) {
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,
onValueChange = {},
readOnly = true,
label = { Text("Voice Filter") },
label = { Text(stringResource(R.string.tts_voice_filter)) },
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterMenuExpanded) },
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth().menuAnchor()
@ -1459,7 +1463,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
if (chapters.isEmpty()) {
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 {
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]
ListItem(
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)) },
trailingContent = {
@ -1475,7 +1488,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
cacheManager.deleteSpecificFiles(chapter.matchingFiles, chapter.directory)
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)
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
) {
Column(modifier = Modifier.weight(1f)) {
Text("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), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
Text(stringResource(R.string.theme_preserve_image_colors_desc), style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
androidx.compose.material3.Switch(
checked = excludeImages,
@ -1980,7 +1993,7 @@ fun ReaderThemePanel(
) {
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)) {
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))
@ -2462,7 +2475,7 @@ fun HighlightColorPickerDialog(
.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Text(
text = "Customize Highlights",
text = stringResource(R.string.highlight_customize_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color.White
@ -2494,7 +2507,7 @@ fun HighlightColorPickerDialog(
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
contentDescription = stringResource(R.string.content_desc_selected),
tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White
)
}
@ -2539,7 +2552,7 @@ fun HighlightColorPickerDialog(
modifier = Modifier.weight(1.6f),
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))
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
}
@ -2548,15 +2561,15 @@ fun HighlightColorPickerDialog(
modifier = Modifier.weight(2.4f),
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)) },
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)) },
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)) },
modifier = Modifier.weight(1f)
)
@ -2571,11 +2584,11 @@ fun HighlightColorPickerDialog(
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = { updateFromColor(selectedSlot.color) }) {
Text("Reset", color = Color(0xFFFF5252))
Text(stringResource(R.string.action_reset), color = Color(0xFFFF5252))
}
Row {
TextButton(onClick = onDismiss) {
Text("Cancel", color = Color.Gray)
Text(stringResource(R.string.action_cancel), color = Color.Gray)
}
Spacer(Modifier.width(8.dp))
Button(
@ -2584,7 +2597,7 @@ fun HighlightColorPickerDialog(
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))
Text(
text = "AI Features",
text = stringResource(R.string.ai_features_title),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
@ -2671,9 +2684,12 @@ fun AiHubBottomSheet(
}
}
val tabs = mutableListOf("Summary")
if (onGenerateRecap != null) tabs.add("Recap")
if (summaryCacheManager != null) tabs.add("Cache")
val summaryTab = stringResource(R.string.ai_tab_summary)
val recapTab = stringResource(R.string.ai_tab_recap)
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)) {
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) }
when (activeTab) {
"Summary" -> {
summaryTab -> {
val cachedSummary = remember(currentChapterIndex, cacheRefreshTrigger) { summaryCacheManager?.getSummary(bookTitle, currentChapterIndex) }
val effectiveResult = summarizationResult ?: if (cachedSummary != null) SummarizationResult(summary = cachedSummary, isCacheHit = true) else null
@ -2696,7 +2712,7 @@ fun AiHubBottomSheet(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painterResource(R.drawable.summarize), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
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))
Button(
onClick = { onGenerateSummary(false) },
@ -2704,7 +2720,7 @@ fun AiHubBottomSheet(
) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.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
if (recapResult == null && !isRecapLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
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))
Button(
onClick = { onGenerateRecap?.invoke() },
@ -2736,13 +2752,13 @@ fun AiHubBottomSheet(
) {
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Generate Story Recap")
Text(stringResource(R.string.ai_generate_story_recap))
}
}
}
} else {
AiResultContentView(
title = "Story Recap",
title = stringResource(R.string.ai_story_recap),
result = recapResult,
isLoading = isRecapLoading,
isMainTtsActive = isMainTtsActive,
@ -2754,7 +2770,7 @@ fun AiHubBottomSheet(
)
}
}
"Cache" -> {
cacheTab -> {
if (summaryCacheManager != null) {
ManageCacheTab(bookTitle, summaryCacheManager, onCacheChanged = {
cacheRefreshTrigger++
@ -2804,15 +2820,15 @@ fun AiResultContentView(
) {
Text(
text = if (result.isCacheHit) {
"⚡ Cache Hit • Free"
stringResource(R.string.ai_cache_hit_free)
} else if (result.cost != 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 {
"✨ Generated • Cost: ${result.cost} credits"
stringResource(R.string.ai_generated_cost, result.cost.toString())
}
} else {
"✨ Generating... • Cost: Calculating"
stringResource(R.string.ai_generating_cost_calculating)
},
style = MaterialTheme.typography.labelSmall,
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) {
Row(verticalAlignment = Alignment.CenterVertically) {
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) {
@ -2856,7 +2872,7 @@ fun AiResultContentView(
IconButton(onClick = onClear) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Clear",
contentDescription = stringResource(R.string.action_clear),
tint = MaterialTheme.colorScheme.error
)
}
@ -2865,7 +2881,7 @@ fun AiResultContentView(
if (onRegenerate != null) {
TextButton(onClick = onRegenerate) {
Text("Regenerate")
Text(stringResource(R.string.ai_regenerate))
}
}
@ -2881,7 +2897,7 @@ fun AiResultContentView(
ttsController.start(
chunks = chunks,
bookTitle = title,
chapterTitle = "AI Output",
chapterTitle = context.getString(R.string.ai_output_title),
coverImageUri = null,
ttsMode = loadTtsMode(context),
playbackSource = "POPUP",
@ -2895,7 +2911,7 @@ fun AiResultContentView(
) {
Icon(
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = "Read Aloud"
contentDescription = stringResource(R.string.action_read_aloud)
)
}
IconButton(onClick = {
@ -2903,7 +2919,7 @@ fun AiResultContentView(
}) {
Icon(
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()) {
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 {
Column(modifier = Modifier.fillMaxSize()) {
@ -2990,7 +3006,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
}) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.error
)
}
@ -3016,7 +3032,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
},
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
@file:Suppress("KotlinConstantConditions")
@ -93,6 +74,8 @@ fun FontsScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val showGoogleFontsOption = !(BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE)
// Dialog state
var showDeleteDialog by remember { mutableStateOf(false) }
var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) }
@ -116,7 +99,7 @@ fun FontsScreen(
title = { Text(stringResource(R.string.custom_fonts)) },
navigationIcon = {
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,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ExtendedFloatingActionButton(
onClick = { showGoogleFontsSheet = true },
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
text = { Text("Google Fonts") },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
if (showGoogleFontsOption) {
ExtendedFloatingActionButton(
onClick = { showGoogleFontsSheet = true },
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
text = { Text(stringResource(R.string.google_fonts)) },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
}
ExtendedFloatingActionButton(
onClick = { pickFontLauncher.launch(fontMimeTypes) },
@ -146,13 +131,16 @@ fun FontsScreen(
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
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(
title = stringResource(R.string.no_custom_fonts),
message = stringResource(R.string.import_fonts_desc),
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
modifier = Modifier.fillMaxSize(),
secondaryButtonText = "Browse Google Fonts",
onSecondaryClick = { showGoogleFontsSheet = true }
secondaryButtonText = secondaryText,
onSecondaryClick = secondaryClick
)
} else {
LazyColumn(
@ -254,7 +242,7 @@ fun GoogleFontsBottomSheet(
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = "Browse Google Fonts",
text = stringResource(R.string.action_browse_google_fonts),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp)
@ -264,7 +252,7 @@ fun GoogleFontsBottomSheet(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search 1900+ fonts...") },
placeholder = { Text(stringResource(R.string.google_fonts_search_placeholder)) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true,
shape = RoundedCornerShape(12.dp)
@ -280,7 +268,7 @@ fun GoogleFontsBottomSheet(
if (searchQuery.isBlank()) {
item {
Text(
text = "Popular Choices",
text = stringResource(R.string.google_fonts_popular_choices),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp)
@ -289,7 +277,7 @@ fun GoogleFontsBottomSheet(
} else if (displayList.isEmpty()) {
item {
Text(
text = "No fonts found matching '$searchQuery'",
text = stringResource(R.string.google_fonts_no_matches, searchQuery),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp)
@ -335,7 +323,7 @@ fun GoogleFontsBottomSheet(
isDownloaded -> {
Icon(
Icons.Default.Check,
contentDescription = "Already Downloaded",
contentDescription = stringResource(R.string.content_desc_already_downloaded),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
@ -350,7 +338,7 @@ fun GoogleFontsBottomSheet(
else -> {
Icon(
Icons.Default.CloudDownload,
contentDescription = "Download",
contentDescription = stringResource(R.string.action_download),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
@ -396,7 +384,7 @@ fun FontListItem(
IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.error
)
}
@ -458,4 +446,4 @@ fun DeleteFontConfirmationDialog(
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
}
)
}
}

View file

@ -853,7 +853,7 @@ fun RecentFileCard(
) {
Icon(
Icons.Default.Check,
contentDescription = "Selected",
contentDescription = stringResource(R.string.content_desc_selected),
modifier = Modifier.size(48.dp)
.background(MaterialTheme.colorScheme.primary, CircleShape)
.padding(8.dp),
@ -1009,13 +1009,13 @@ fun DefaultTopAppBar(
Badge()
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Open Drawer")
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_open_drawer))
}
}
}, actions = {
Box {
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
@ -1035,7 +1035,7 @@ fun DefaultTopAppBar(
showLimitMenu = false
},
trailingIcon = if (uiState.recentFilesLimit == limit) {
{ Icon(Icons.Default.Check, contentDescription = "Selected") }
{ Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
} else null
)
}
@ -1045,7 +1045,7 @@ fun DefaultTopAppBar(
// Options Menu (MoreVert)
Box {
IconButton(onClick = { showOptionsMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_more_options))
}
DropdownMenu(
expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) {
@ -1056,12 +1056,12 @@ fun DefaultTopAppBar(
HorizontalDivider()
DropdownMenuItem(text = { Text("Enable Multi-Tab Reading") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.options_enable_multi_tab_reading)) }, onClick = {
onTabsToggle(!uiState.isTabsEnabled)
showOptionsMenu = false
}, trailingIcon = {
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
})
DropdownMenuItem(text = { Text("Use Strict File Filter") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.options_use_strict_file_filter)) }, onClick = {
onStrictFilterToggleClick()
showOptionsMenu = false
}, trailingIcon = {
if (uiState.useStrictFileFilter) {
Icon(Icons.Default.Check, contentDescription = "Enabled")
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
}
})
HorizontalDivider()
DropdownMenuItem(text = { Text("Language") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.options_language)) }, onClick = {
onLanguageClick()
showOptionsMenu = false
})
@ -1098,17 +1098,17 @@ fun DefaultTopAppBar(
if (BuildConfig.DEBUG) {
HorizontalDivider()
DropdownMenuItem(text = { Text("Test Panel ML Detection") }, onClick = {
DropdownMenuItem(text = { Text(stringResource(R.string.options_test_panel_ml_detection)) }, onClick = {
onTestPanelDetectionClick()
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()
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()
showOptionsMenu = false
})
@ -1163,7 +1163,7 @@ private fun AppDrawerContent(
AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(photoUrl)
.crossfade(true).build(),
contentDescription = "Profile picture",
contentDescription = stringResource(R.string.content_desc_profile_picture),
modifier = Modifier
.size(80.dp)
.clip(CircleShape),
@ -1172,7 +1172,7 @@ private fun AppDrawerContent(
} else {
Icon(
imageVector = Icons.Outlined.AccountCircle,
contentDescription = "Profile",
contentDescription = stringResource(R.string.content_desc_profile),
modifier = Modifier.size(80.dp)
)
}
@ -1189,9 +1189,9 @@ private fun AppDrawerContent(
modifier = Modifier.padding(top = 8.dp)
) {
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))
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) {
Icon(
imageVector = Icons.Default.VerifiedUser,
contentDescription = "Pro Feature",
contentDescription = stringResource(R.string.content_desc_pro_feature),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
@ -1294,7 +1294,7 @@ private fun AppDrawerContent(
) {
AsyncImage(
model = R.mipmap.ic_launcher,
contentDescription = "App Icon",
contentDescription = stringResource(R.string.content_desc_app_icon),
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(8.dp))
@ -1467,7 +1467,7 @@ fun DeviceManagementScreen(
.padding(horizontal = 16.dp, vertical = 12.dp),
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))
Column(modifier = Modifier.weight(1f)) {
Text(device.deviceName, fontWeight = FontWeight.SemiBold)
@ -1679,9 +1679,9 @@ fun CloseAllTabsDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
fun StrictFilterConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Enable Strict File Filter") },
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?") },
confirmButton = { TextButton(onClick = onConfirm) { Text("Enable") } },
title = { Text(stringResource(R.string.dialog_strict_file_filter_title)) },
text = { Text(stringResource(R.string.dialog_strict_file_filter_desc)) },
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_enable)) } },
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }
)
}
@ -1714,13 +1714,13 @@ fun AppThemeBottomSheet(
.padding(bottom = 24.dp)
) {
Text(
text = "App Theme",
text = stringResource(R.string.app_theme_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
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))
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 ->
@ -1731,14 +1731,14 @@ fun AppThemeBottomSheet(
.clickable { onThemeModeChanged(mode) },
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))
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))
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 ->
@ -1749,14 +1749,14 @@ fun AppThemeBottomSheet(
.clickable { onContrastOptionChanged(option) },
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))
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))
Row(
modifier = Modifier
@ -1778,32 +1778,33 @@ fun AppThemeBottomSheet(
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))
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)) {
item {
ThemeSwatch(
color = MaterialTheme.colorScheme.primary,
isSelected = uiState.appSeedColor == null,
label = "Dynamic",
label = stringResource(R.string.app_theme_dynamic),
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 ->
val (label, color) = presets[i]
val (labelRes, color) = presets[i]
ThemeSwatch(
color = color,
isSelected = uiState.appSeedColor == color,
label = label,
label = stringResource(labelRes),
onClick = { onSeedColorChanged(color) }
)
}
@ -1812,15 +1813,15 @@ fun AppThemeBottomSheet(
Spacer(Modifier.height(24.dp))
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)) {
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))
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 {
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(uiState.customAppThemes) { theme ->
@ -1873,7 +1874,7 @@ fun ThemeSwatch(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = label, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 64.dp))
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
) {
var name by remember { mutableStateOf("") }
val context = LocalContext.current
val initialHsv = remember(initialColor) {
val hsv = FloatArray(3)
@ -1931,7 +1933,7 @@ fun CreateAppThemeDialog(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Create App Theme",
text = stringResource(R.string.app_theme_create_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color.White
@ -1942,7 +1944,7 @@ fun CreateAppThemeDialog(
androidx.compose.material3.OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Theme Name") },
label = { Text(stringResource(R.string.theme_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
@ -1990,7 +1992,7 @@ fun CreateAppThemeDialog(
modifier = Modifier.weight(1.6f),
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))
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
}
@ -1999,15 +2001,15 @@ fun CreateAppThemeDialog(
modifier = Modifier.weight(2.4f),
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)) },
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)) },
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)) },
modifier = Modifier.weight(1f)
)
@ -2022,14 +2024,14 @@ fun CreateAppThemeDialog(
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onDismiss) {
Text("Cancel", color = Color.Gray)
Text(stringResource(R.string.action_cancel), color = Color.Gray)
}
Spacer(Modifier.width(8.dp))
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)
) {
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 languages = listOf(
"en" to "English (Default)",
"ar" to "العربية (Arabic)",
"de" to "Deutsch (German)",
"tr" to "Türkçe (Turkish)"
"en" to R.string.language_english_default,
"ar" to R.string.language_arabic,
"de" to R.string.language_german,
"tr" to R.string.language_turkish,
"fr" to R.string.language_french,
"ru" to R.string.language_russian
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Language") },
title = { Text(stringResource(R.string.options_language)) },
text = {
Column {
languages.forEach { (tag, name) ->
languages.forEach { (tag, nameRes) ->
Row(
modifier = Modifier
.fillMaxWidth()
@ -2069,7 +2073,7 @@ fun LanguageSelectionDialog(onDismiss: () -> Unit) {
) {
RadioButton(selected = currentTag == tag, onClick = null)
Spacer(modifier = Modifier.width(16.dp))
Text(name)
Text(stringResource(nameRes))
}
}
}

View file

@ -630,7 +630,7 @@ fun LibraryScreenContent(
verticalAlignment = Alignment.CenterVertically
) {
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(
value = textFieldValue,
@ -654,7 +654,7 @@ fun LibraryScreenContent(
trailingIcon = {
if (searchQuery.isNotEmpty()) {
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 = {
if (pagerState.currentPage == 0) {
IconButton(onClick = onFilterClick) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.content_desc_filter))
}
Box {
TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort",
contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName)
Text(stringResource(sortOrder.labelRes))
}
DropdownMenu(
expanded = showSortMenu,
@ -684,7 +684,7 @@ fun LibraryScreenContent(
) {
SortOrder.entries.forEach { order ->
DropdownMenuItem(
text = { Text(order.displayName) },
text = { Text(stringResource(order.labelRes)) },
onClick = {
onSortOrderChange(order)
showSortMenu = false
@ -693,7 +693,7 @@ fun LibraryScreenContent(
if (order == sortOrder) {
Icon(
Icons.Default.Check,
contentDescription = "Selected"
contentDescription = stringResource(R.string.content_desc_selected)
)
}
}
@ -702,7 +702,7 @@ fun LibraryScreenContent(
}
}
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(
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
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()) {
AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) },
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) {
AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) },
label = { Text(stringResource(R.string.filter_status, libraryFilters.readStatus.displayName)) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
label = { Text(stringResource(R.string.filter_status, stringResource(libraryFilters.readStatus.labelRes))) },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
)
}
if (libraryFilters.tagIds.isNotEmpty()) {
val selectedTags = allTags.filter { it.id in libraryFilters.tagIds }
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 }
else -> "${selectedTags.size} tags"
else -> pluralStringResource(R.plurals.tag_count, selectedTags.size, selectedTags.size)
}
AssistChip(
onClick = { onRemoveFilter(libraryFilters.copy(tagIds = emptySet())) },
label = { Text("Tags: $tagLabel") },
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
label = { Text(stringResource(R.string.filter_tags, tagLabel)) },
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()) {
ExtendedFloatingActionButton(
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,
modifier = Modifier.padding(16.dp)
)
@ -784,7 +784,7 @@ fun LibraryScreenContent(
1 -> {
ExtendedFloatingActionButton(
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,
modifier = Modifier.padding(16.dp)
)
@ -896,7 +896,7 @@ private fun ShelvesScreen(
item {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
text = "Browse by tag",
text = stringResource(R.string.section_browse_by_tag),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
@ -1085,7 +1085,7 @@ private fun ShelfDetailScreen(
verticalAlignment = Alignment.CenterVertically
) {
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(
value = searchFieldValue,
@ -1109,7 +1109,7 @@ private fun ShelfDetailScreen(
trailingIcon = {
if (searchQuery.isNotEmpty()) {
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 = when {
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 ->
"${shelf.childShelfCount} folders"
pluralStringResource(R.plurals.folder_count, shelf.childShelfCount, shelf.childShelfCount)
isFolderShelf -> getBookCountString(shelf.directBookCount)
else -> getBookCountString(shelf.bookCount)
},
@ -1141,7 +1141,7 @@ private fun ShelfDetailScreen(
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
},
actions = {
@ -1149,11 +1149,11 @@ private fun ShelfDetailScreen(
TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort",
contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName)
Text(stringResource(sortOrder.labelRes))
}
DropdownMenu(
expanded = showSortMenu,
@ -1161,7 +1161,7 @@ private fun ShelfDetailScreen(
) {
SortOrder.entries.forEach { order ->
DropdownMenuItem(
text = { Text(order.displayName) },
text = { Text(stringResource(order.labelRes)) },
onClick = {
onSortOrderChange(order)
showSortMenu = false
@ -1170,7 +1170,7 @@ private fun ShelfDetailScreen(
if (order == sortOrder) {
Icon(
Icons.Default.Check,
contentDescription = "Selected"
contentDescription = stringResource(R.string.content_desc_selected)
)
}
}
@ -1182,7 +1182,7 @@ private fun ShelfDetailScreen(
IconButton(onClick = { isSearchActive = true }) {
Icon(
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 }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "More options"
contentDescription = stringResource(R.string.content_desc_more_options)
)
}
DropdownMenu(
@ -1252,7 +1252,7 @@ private fun ShelfDetailScreen(
if (isFolderShelf) {
item {
Text(
text = "Folders",
text = stringResource(R.string.section_folders),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
@ -1275,7 +1275,7 @@ private fun ShelfDetailScreen(
}
item {
Text(
text = "Files",
text = stringResource(R.string.section_files),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
@ -1321,7 +1321,7 @@ private fun AddBooksModeScreen(
title = { Text(stringResource(R.string.add_to_shelf, shelfName)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
},
actions = {
@ -1329,11 +1329,11 @@ private fun AddBooksModeScreen(
TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = "Sort",
contentDescription = stringResource(R.string.content_desc_sort),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(sortOrder.displayName)
Text(stringResource(sortOrder.labelRes))
}
DropdownMenu(
expanded = showSortMenu,
@ -1341,14 +1341,14 @@ private fun AddBooksModeScreen(
) {
SortOrder.entries.forEach { order ->
DropdownMenuItem(
text = { Text(order.displayName) },
text = { Text(stringResource(order.labelRes)) },
onClick = {
onSortOrderChange(order)
showSortMenu = false
},
trailingIcon = {
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(
selected = source == currentSource,
onClick = { onSourceChange(source) },
label = { Text(source.displayName) }
label = { Text(stringResource(source.labelRes)) }
)
}
}
@ -1378,7 +1378,7 @@ private fun AddBooksModeScreen(
if (selectedBookUris.isNotEmpty()) {
ExtendedFloatingActionButton(
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
)
}
@ -1447,7 +1447,7 @@ private fun ShelfCover(shelf: Shelf) {
.fallback(placeholder)
.crossfade(true)
.build(),
contentDescription = "${shelf.name} shelf cover",
contentDescription = stringResource(R.string.content_desc_shelf_cover, shelf.name),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = coverWidth, height = coverHeight)
@ -1637,7 +1637,7 @@ private fun LibraryListItem(
modifier = Modifier.matchParentSize().background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
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(
selected = currentFilters.readStatus == status,
onClick = { currentFilters = currentFilters.copy(readStatus = status) },
label = { Text(status.displayName) }
label = { Text(stringResource(status.labelRes)) }
)
}
}
if (allTags.isNotEmpty()) {
Text("Tags", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleMedium)
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -2740,12 +2740,12 @@ fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Un
}
if (onEdit != null) {
IconButton(onClick = onEdit) {
Icon(Icons.Default.Edit, contentDescription = "Edit")
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.label_edit))
}
}
if (onDelete != null) {
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(),
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))
Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold)
}

View file

@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com
*/
// MainViewModel.kt
@file:Suppress("DEPRECATION")
@file:Suppress("DEPRECATION", "ANNOTATION_WILL_BE_APPLIED_ALSO_TO_PROPERTY_OR_FIELD")
package com.aryan.reader
@ -31,8 +31,14 @@ import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
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.OpenableColumns
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.edit
import androidx.core.graphics.createBitmap
@ -181,20 +187,21 @@ private data class CachedSpeechBubble(
val maskBitmap: Bitmap?
)
enum class AddBooksSource(val displayName: String) {
UNSHELVED("Unshelved"), ALL_BOOKS("All Books")
enum class AddBooksSource(@StringRes val labelRes: Int) {
UNSHELVED(R.string.add_books_source_unshelved),
ALL_BOOKS(R.string.add_books_source_all_books)
}
enum class AppThemeMode(val displayName: String) {
SYSTEM("System"),
LIGHT("Light"),
DARK("Dark")
enum class AppThemeMode(@StringRes val labelRes: Int) {
SYSTEM(R.string.app_theme_mode_system),
LIGHT(R.string.app_theme_mode_light),
DARK(R.string.app_theme_mode_dark)
}
enum class AppContrastOption(val displayName: String, val value: Double) {
STANDARD("Standard", 0.0),
MEDIUM("Medium", 0.5),
HIGH("High", 1.0)
enum class AppContrastOption(@StringRes val labelRes: Int, val value: Double) {
STANDARD(R.string.app_contrast_standard, 0.0),
MEDIUM(R.string.app_contrast_medium, 0.5),
HIGH(R.string.app_contrast_high, 1.0)
}
data class CustomAppTheme(
@ -240,18 +247,21 @@ data class Shelf(
val childShelfCount: Int get() = childShelfIds.size
}
enum class SortOrder(val displayName: String) {
RECENT("Recent"),
TITLE_ASC("Title A-Z"),
AUTHOR_ASC("Author A-Z"),
PERCENT_ASC("Percent complete 0-100"),
PERCENT_DESC("Percent complete 100-0"),
SIZE_ASC("Size (Smallest)"),
SIZE_DESC("Size (Biggest)")
enum class SortOrder(@StringRes val labelRes: Int) {
RECENT(R.string.sort_recent),
TITLE_ASC(R.string.sort_title_az),
AUTHOR_ASC(R.string.sort_author_az),
PERCENT_ASC(R.string.sort_percent_asc),
PERCENT_DESC(R.string.sort_percent_desc),
SIZE_ASC(R.string.sort_size_smallest),
SIZE_DESC(R.string.sort_size_biggest)
}
enum class ReadStatusFilter(val displayName: String) {
ALL("All"), UNREAD("Unread"), IN_PROGRESS("In Progress"), COMPLETED("Completed")
enum class ReadStatusFilter(@StringRes val labelRes: Int) {
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(
@ -652,6 +662,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
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(
ReaderScreenState(
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 ->
if (uiState.value.isSyncEnabled) {
uploadNewFont(font)
@ -1512,6 +1528,37 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
init {
Timber.d("ViewModel instance created.")
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 {
recentFilesRepository.migrateLegacyShelvesToRoom()
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()
?: _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 {
it.copy(
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> {
val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null)
val folders = mutableListOf<SyncedFolder>()
@ -5408,6 +5531,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
speechBubbleCache.clear()
speechBubbleDetectionJobs.clear()
ttsController.release()
Timber.d("ViewModel instance cleared (onCleared).")
}

View file

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

View file

@ -246,7 +246,7 @@ fun ContextualTopAppBar(
actions = {
if (onTagClick != null) {
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) {
@ -516,14 +516,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("Tags", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
TextButton(onClick = onOpenTags) { Text("+ Add / Edit") }
Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
TextButton(onClick = onOpenTags) { Text(stringResource(R.string.action_add_edit)) }
}
if (item.tags.isNotEmpty()) {
BookTagChipsRow(tags = item.tags, compact = false)
} 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(
@ -599,7 +599,7 @@ private fun InfoRowDetailed(
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy $label",
contentDescription = stringResource(R.string.content_desc_copy_value, label),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
)
@ -827,7 +827,7 @@ fun EmptyState(
) {
Icon(
imageVector = Icons.Outlined.FileOpen,
contentDescription = "No files icon",
contentDescription = stringResource(R.string.content_desc_no_files_icon),
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
@ -1154,13 +1154,13 @@ fun TagSelectionBottomSheet(
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
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(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search or create tag...") },
placeholder = { Text(stringResource(R.string.placeholder_search_create_tag)) },
singleLine = true,
shape = RoundedCornerShape(16.dp),
leadingIcon = { Icon(Icons.Default.Search, null) }
@ -1180,7 +1180,7 @@ fun TagSelectionBottomSheet(
) {
Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary)
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 = {
val clipboard =
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)
showExternalLinkDialog = null
}) { Text(stringResource(R.string.action_copy)) }
@ -1066,7 +1066,7 @@ fun ChapterWebView(
onCopy = {
val clipboard =
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)
state.finishActionModeCallback()
localWebViewRef?.clearFocus()

View file

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

View file

@ -225,7 +225,7 @@ fun EpubReaderTopBar(
description = stringResource(R.string.tooltip_back_desc),
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))
Text(
@ -583,7 +583,7 @@ fun EpubReaderBottomBar(
) {
Icon(
painter = painterResource(id = R.drawable.ai),
contentDescription = "AI Features"
contentDescription = stringResource(R.string.ai_features_title)
)
}
}
@ -1091,7 +1091,7 @@ fun AutoScrollControls(
) {
Icon(
imageVector = Icons.Default.ArrowUpward,
contentDescription = "Scroll to Top",
contentDescription = stringResource(R.string.action_scroll_to_top),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
@ -1440,7 +1440,7 @@ fun TtsOverlayControls(
) {
Icon(
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)
)
}
@ -1514,10 +1514,10 @@ fun TtsOverlayControls(
)
}
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)) {
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(
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)
)
}
@ -1557,7 +1557,7 @@ fun TtsOverlayControls(
// Unified Sliders Block
Column(modifier = Modifier.weight(1f)) {
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(
value = rate,
onValueChange = {
@ -1571,11 +1571,11 @@ fun TtsOverlayControls(
modifier = Modifier.weight(1f).height(24.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) {
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(
value = pitch,
onValueChange = {
@ -1589,7 +1589,7 @@ fun TtsOverlayControls(
modifier = Modifier.weight(1f).height(24.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(
selected = drawerPagerState.currentPage == 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
) {
TextButton(onClick = { expandedEntryIndices = effectiveToc.indices.toSet() }) {
Text("Expand All")
Text(stringResource(R.string.action_expand_all))
}
TextButton(onClick = { expandedEntryIndices = emptySet() }) {
Text("Collapse All")
Text(stringResource(R.string.action_collapse_all))
}
TextButton(onClick = onScrollToCurrent) {
Text("Locate")
Text(stringResource(R.string.action_locate))
}
}
@ -884,4 +884,4 @@ private fun HighlightsList(
)
}
}
}
}

View file

@ -905,7 +905,7 @@ fun EpubReaderHost(
})
var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) }
val ttsController = rememberTtsController()
val ttsController = viewModel.ttsController
val ttsState by ttsController.ttsState.collectAsState()
val totalBookLengthChars = remember(chapters) {
@ -4979,7 +4979,7 @@ fun EpubReaderHost(
},
onCopy = {
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)
highlightToNoteCfi = null
},
@ -5188,13 +5188,13 @@ fun EpubReaderHost(
AlertDialog(
onDismissRequest = { showInsufficientCreditsDialog = false },
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
title = { Text("Out of Credits") },
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.") },
title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
confirmButton = {
TextButton(onClick = {
showInsufficientCreditsDialog = false
onNavigateToPro()
}) { Text("Get Pro / Add Credits") }
}) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
},
dismissButton = {
TextButton(onClick = { showInsufficientCreditsDialog = false }) {

View file

@ -912,9 +912,9 @@ fun VisualOptionsSheet(
AnimatedVisibility(visible = pullToTurnEnabled) {
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))
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) {
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(
value = pullToTurnMultiplier,
onValueChange = onPullToTurnMultiplierChange,
@ -922,7 +922,7 @@ fun VisualOptionsSheet(
steps = 14,
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
) {
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!
@ -1097,7 +1097,7 @@ fun FormatSlider(
},
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)) },
navigationIcon = {
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(
painter = painterResource(id = R.drawable.github),
contentDescription = "GitHub",
contentDescription = stringResource(R.string.github_issues),
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary
)
@ -150,7 +150,7 @@ fun FeedbackScreen(
icon = {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = "Email",
contentDescription = stringResource(R.string.email_support),
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.primary
)
@ -201,9 +201,9 @@ private fun FeedbackOptionCard(
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Open",
contentDescription = stringResource(R.string.action_open),
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.res.imageResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle
@ -2002,10 +2003,10 @@ internal fun PaginatedReaderContent(
val urlToShow = showExternalLinkDialog!!
AlertDialog(
onDismissRequest = { showExternalLinkDialog = null },
title = { Text("External Link") },
title = { Text(stringResource(R.string.dialog_external_link_title)) },
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 = {
@ -2014,10 +2015,10 @@ internal fun PaginatedReaderContent(
onClick = {
val clipboard =
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)
showExternalLinkDialog = null
}) { Text("Copy") }
}) { Text(stringResource(R.string.action_copy)) }
TextButton(
onClick = {
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"
)
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()
}
showExternalLinkDialog = null
}) { Text("Open") }
}) { Text(stringResource(R.string.action_open)) }
}
},
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(
model = imageRequest,
contentDescription = "List item marker",
contentDescription = stringResource(R.string.content_desc_list_item_marker),
modifier = markerAreaModifier.height(
imageSize
),
@ -3555,7 +3556,7 @@ internal fun PaginatedReaderContent(
onCopy = {
val 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)
activeSelection = null
},
@ -3926,7 +3927,7 @@ internal fun PaginatedReaderContent(
if (showColorPickerDialog != null) {
AlertDialog(
onDismissRequest = { showColorPickerDialog = null },
title = { Text("Select Color") },
title = { Text(stringResource(R.string.dialog_select_color)) },
text = {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 48.dp),
@ -3953,7 +3954,7 @@ internal fun PaginatedReaderContent(
confirmButton = {
TextButton(onClick = {
showColorPickerDialog = null
}) { Text("Close") }
}) { Text(stringResource(R.string.action_close)) }
})
}
@ -3972,7 +3973,7 @@ internal fun PaginatedReaderContent(
} else {
Timber.w("Book has no pages to display.")
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(
model = imageRequest,
contentDescription = "List item marker",
contentDescription = stringResource(R.string.content_desc_list_item_marker),
modifier = markerAreaModifier.height(imageSize),
alignment = Alignment.CenterEnd,
contentScale = ContentScale.FillHeight

View file

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

View file

@ -138,7 +138,7 @@ internal fun OcrLanguageSelectionDialog(
) {
RadioButton(selected = (language == currentLanguage), onClick = null)
Text(
text = language.displayName,
text = stringResource(language.displayNameRes),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
@ -146,4 +146,4 @@ internal fun OcrLanguageSelectionDialog(
}
}
}, confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } })
}
}

View file

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

View file

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

View file

@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@ -199,7 +200,7 @@ internal fun ThumbnailWithIndicator(
) {
Image(
bitmap = thumbnail.asImageBitmap(),
contentDescription = "Start page thumbnail",
contentDescription = stringResource(R.string.content_desc_start_page_thumbnail),
contentScale = ContentScale.FillBounds,
modifier = Modifier.fillMaxSize()
)
@ -230,7 +231,7 @@ internal fun BookmarkButton(
AnimatedVisibility(visible = isBookmarked, enter = fadeIn(), exit = fadeOut()) {
Icon(
painter = painterResource(id = R.drawable.bookmark),
contentDescription = "Bookmark",
contentDescription = stringResource(R.string.content_desc_bookmark),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
@ -271,7 +272,7 @@ internal fun ZoomPercentageIndicator(
// Reset Zoom Button
Icon(
painter = painterResource(id = R.drawable.zoom_out),
contentDescription = "Reset Zoom",
contentDescription = stringResource(R.string.content_desc_reset_zoom),
tint = Color.White,
modifier = Modifier
.size(20.dp)
@ -280,4 +281,4 @@ internal fun ZoomPercentageIndicator(
)
}
}
}
}

View file

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

View file

@ -61,6 +61,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@ -450,9 +451,9 @@ private fun DragPill(
Box(contentAlignment = Alignment.Center) {
Icon(
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)
)
}
}
}
}

View file

@ -136,14 +136,14 @@ internal fun PdfTopBar(
description = stringResource(R.string.tooltip_back_desc),
onClick = onNavigateBack
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
val titleText = when {
isLoadingDocument -> stringResource(R.string.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)
else -> "PDF Viewer"
else -> stringResource(R.string.pdf_viewer)
}
Text(
text = titleText,
@ -179,16 +179,16 @@ internal fun PdfTopBar(
description = stringResource(R.string.tooltip_dictionary_desc),
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) {
TooltipIconButton(text = "Demo Annotations", onClick = onGenerateDemoAnnotations) {
Icon(Icons.Default.BugReport, contentDescription = "Generate Demo Annotations", tint = MaterialTheme.colorScheme.secondary)
TooltipIconButton(text = stringResource(R.string.tooltip_demo_annotations), onClick = onGenerateDemoAnnotations) {
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) {
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) {
Icon(Icons.Default.Brush, contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63))
@ -392,7 +392,7 @@ internal fun PdfTopBar(
item {
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) {
TooltipIconButton(
text = "Jump Back to Page ${jumpBackPage + 1}",
description = "Return to previous page",
text = stringResource(R.string.action_jump_back_to_page, jumpBackPage + 1),
description = stringResource(R.string.desc_return_to_previous_page),
onClick = onJumpBack
) {
Column(
@ -502,7 +502,7 @@ fun PdfBottomBar(
) {
Icon(
Icons.AutoMirrored.Filled.Undo,
contentDescription = "Jump Back",
contentDescription = stringResource(R.string.content_desc_jump_back),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
@ -535,7 +535,7 @@ fun PdfBottomBar(
enabled = !isTtsPlayingOrLoading,
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
) {
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),
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),
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") {
TooltipIconButton(
text = if (isBubbleZoomModeActive) "Exit Smart Zoom" else "Smart Comic Zoom",
description = "Toggle Smart Comic Zoom",
text = if (isBubbleZoomModeActive) stringResource(R.string.action_exit_smart_zoom) else stringResource(R.string.action_smart_comic_zoom),
description = stringResource(R.string.desc_toggle_smart_comic_zoom),
onClick = onToggleBubbleZoom
) {
Icon(
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
)
}

View file

@ -2089,7 +2089,7 @@ fun PdfViewerScreen(
if (!selectedDictPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} 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
}
}
@ -2102,7 +2102,7 @@ fun PdfViewerScreen(
if (!selectedTranslatePackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text)
} 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
}
}
@ -2113,7 +2113,7 @@ fun PdfViewerScreen(
if (!selectedSearchPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text)
} 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
}
}
@ -3386,7 +3386,7 @@ fun PdfViewerScreen(
contentAlignment = Alignment.Center
) {
Text(
text = errorMessage ?: "Failed to load PDF.",
text = errorMessage ?: stringResource(R.string.error_failed_load_pdf),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp)
)
@ -4380,7 +4380,10 @@ fun PdfViewerScreen(
)
Spacer(modifier = Modifier.width(12.dp))
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,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
@ -4429,7 +4432,10 @@ fun PdfViewerScreen(
}
Spacer(modifier = Modifier.width(12.dp))
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,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
@ -4473,7 +4479,7 @@ fun PdfViewerScreen(
) {
Icon(
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
val text =
if (index >= 0) "Result ${index + 1} / ${searchData.matches.size}"
else "${searchData.matches.size} Results"
if (index >= 0) context.getString(
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)
}
is SmartSearchResult.Paged -> {
val page = currentResult?.locationInSource
val text = if (page != null) "Page ${page + 1}"
else "${searchData.totalPageCount}+ Pages"
val text = if (page != null) context.getString(R.string.pdf_page_short, page + 1)
else context.getString(R.string.msg_search_pages_count, searchData.totalPageCount)
Triple(text, true, true)
}
@ -5046,9 +5060,9 @@ fun PdfViewerScreen(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
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))
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(
imageVector = if (isTtsPageBelow) Icons.Default.ArrowDownward
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
)
},
title = { Text("Unlock Page Summarization") },
title = { Text(stringResource(R.string.dialog_unlock_page_summarization)) },
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 = {
@ -5841,13 +5855,13 @@ fun PdfViewerScreen(
AlertDialog(
onDismissRequest = { showInsufficientCreditsDialog = false },
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
title = { Text("Out of Credits") },
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.") },
title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
confirmButton = {
TextButton(onClick = {
showInsufficientCreditsDialog = false
onNavigateToPro()
}) { Text("Get Pro / Add Credits") }
}) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
},
dismissButton = {
TextButton(onClick = { showInsufficientCreditsDialog = false }) {
@ -5874,7 +5888,7 @@ fun PdfViewerScreen(
) {
Image(
bitmap = poppedUpPanelBitmap!!.asImageBitmap(),
contentDescription = "Annotated Page",
contentDescription = stringResource(R.string.content_desc_annotated_page),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
@ -5894,7 +5908,7 @@ fun PdfViewerScreen(
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close Image",
contentDescription = stringResource(R.string.content_desc_close_image),
tint = Color.White
)
}
@ -5913,16 +5927,16 @@ fun PdfViewerScreen(
AlertDialog(
onDismissRequest = { showBubbleZoomDownloadDialog = false },
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("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 = {
TextButton(onClick = {
showBubbleZoomDownloadDialog = false
viewModel.downloadSpeechBubbleModel(context)
}) {
Text("Download")
Text(stringResource(R.string.action_download))
}
},
dismissButton = {

View file

@ -7,6 +7,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.aryan.reader.FileType
import com.aryan.reader.R
import com.aryan.reader.data.RecentFileItem
import com.aryan.reader.data.RecentFilesRepository
import kotlinx.coroutines.Dispatchers
@ -32,7 +33,8 @@ class ReflowWorker(
Timber.tag("PdfToHtmlPerf").e("FAILURE: KEY_PDF_URI is null | bookId=$bookId")
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"
Timber.tag("PdfToHtmlPerf").d(
@ -67,11 +69,11 @@ class ReflowWorker(
bookId = reflowBookId,
uriString = destFile.toUri().toString(),
type = FileType.HTML,
displayName = "$originalTitle (Text View)",
displayName = applicationContext.getString(R.string.reflow_display_name_format, originalTitle),
timestamp = System.currentTimeMillis(),
coverImagePath = null,
title = "$originalTitle (Reflow)",
author = "Generated",
title = applicationContext.getString(R.string.reflow_title_format, originalTitle),
author = applicationContext.getString(R.string.generated_author),
isAvailable = true,
isRecent = true,
lastModifiedTimestamp = System.currentTimeMillis(),
@ -101,4 +103,4 @@ class ReflowWorker(
const val KEY_ORIGINAL_TITLE = "original_title"
const val KEY_PROGRESS = "progress"
}
}
}

View file

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

View file

@ -13,6 +13,7 @@ import android.graphics.RectF
import android.net.Uri
import android.os.Build
import com.aryan.reader.FileType
import com.aryan.reader.R
import io.legere.pdfiumandroid.api.Bookmark
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import io.legere.pdfiumandroid.suspend.PdfPageKt
@ -452,7 +453,7 @@ class OpdsStreamDocumentWrapper(
textSize = 40f
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()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream)
return stream.toByteArray()
@ -509,4 +510,4 @@ class OpdsStreamDocumentWrapper(
override suspend fun getTableOfContents() = emptyList<Bookmark>()
override fun close() {}
}
}

View file

@ -24,6 +24,12 @@
<item quantity="other">%1$d results found</item>
</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">
<item quantity="one">Delete File 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="other">%1$d books removed from library.</item>
</plurals>
</resources>
<!-- 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>

View file

@ -15,6 +15,10 @@
<string name="action_search">Search</string>
<string name="action_clear">Clear</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". -->
<string name="tab_free">Free</string>
<string name="active_tabs">Active Tabs</string>
@ -288,6 +292,13 @@
<!-- Fonts -->
<string name="custom_fonts">Custom Fonts</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>
<!-- 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>
@ -512,19 +523,20 @@
<string name="search_in_book">Search in book…</string>
<string name="search_no_results_simple">No results found.</string>
<!-- Common.kt: — AI feature, not translated. -->
<string name="generating_summary" translatable="false">Generating summary…</string>
<!-- Common.kt: AI feature status. -->
<string name="generating_summary">Generating summary…</string>
<string name="action_stop">Stop</string>
<string name="action_read_aloud">Read aloud</string>
<string name="action_copy">Copy</string>
<string name="no_summary_generated" 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. -->
<string name="ai_thinking" translatable="false">Thinking…</string>
<!-- Common.kt: AiDefinitionPopup — AI feature strings. -->
<string name="ai_thinking">Thinking…</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. -->
<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 -->
<!-- TTS = Text-to-Speech. -->
@ -534,8 +546,8 @@
<string name="tts_synthesis_mode">Synthesis Mode</string>
<!-- On-Device TTS mode — speech is generated locally without internet. -->
<string name="tts_mode_on_device">On-Device</string>
<!-- Cloud TTS mode — speech is generated remotely for higher quality. Not translated (feature name). -->
<string name="tts_mode_cloud_hq" translatable="false">Cloud (HQ)</string>
<!-- Cloud TTS mode — speech is generated remotely for higher quality. -->
<string name="tts_mode_cloud_hq">Cloud (HQ)</string>
<string name="tts_voice_selection">Voice Selection</string>
<string name="tts_play_sample">Play Sample</string>
@ -586,17 +598,17 @@
<string name="color_g" translatable="false">G</string>
<string name="color_b" translatable="false">B</string>
<!-- Common.kt: fetchAiDefinition — AI feature errors, not translated. -->
<string name="error_text_empty" translatable="false">Text is empty.</string>
<string name="error_ai_empty_definition" translatable="false">AI returned an empty definition.</string>
<string name="error_could_not_get_definition" translatable="false">Could not get definition.</string>
<string name="error_unknown_server" translatable="false">An unknown server error occurred.</string>
<string name="error_network_check_connection" translatable="false">Network error. Check connection.</string>
<!-- Common.kt: fetchAiDefinition — AI feature errors. -->
<string name="error_text_empty">Text is empty.</string>
<string name="error_ai_empty_definition">AI returned an empty definition.</string>
<string name="error_could_not_get_definition">Could not get definition.</string>
<string name="error_unknown_server">An unknown server error occurred.</string>
<string name="error_network_check_connection">Network error. Check connection.</string>
<!-- Common.kt: fetchRecap — Recap feature, not translated. -->
<string name="error_not_enough_context" translatable="false">Not enough context for a recap.</string>
<string name="error_parse_recap" translatable="false">Failed to parse recap.</string>
<string name="error_network_recap" translatable="false">Network error during recap generation.</string>
<!-- Common.kt: fetchRecap — Recap feature errors. -->
<string name="error_not_enough_context">Not enough context for a recap.</string>
<string name="error_parse_recap">Failed to parse recap.</string>
<string name="error_network_recap">Network error during recap generation.</string>
<!-- DictionarySettingsDialog.kt -->
<string name="dict_lookup_settings">Lookup Settings</string>
@ -616,27 +628,27 @@
<string name="dict_search_app_description">App used for web searches.</string>
<string name="dict_none">None</string>
<!-- EpubReaderAi.kt — AI feature strings, not translated. -->
<string name="ai_error_book_content_empty" translatable="false">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_fetch_summary" translatable="false">Could not fetch summary.</string>
<!-- EpubReaderAi.kt — AI feature strings. -->
<string name="ai_error_book_content_empty">The book content is empty.</string>
<string name="ai_error_parse_summary">Failed to parse summary from server response.</string>
<string name="ai_error_fetch_summary">Could not fetch summary.</string>
<!-- %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_network_server" translatable="false">Network error. Please check connection and server status.</string>
<string name="ai_error_with_code">Error: %1$d. %2$s</string>
<string name="ai_error_network_server">Network error. Please check connection and server status.</string>
<!-- %1$d = chapter number being analyzed. -->
<string name="ai_analyzing_chapter" translatable="false">Analyzing Chapter %1$d…</string>
<string name="ai_reading_position" translatable="false">Reading current position…</string>
<string name="ai_generating_recap" translatable="false">Generating Recap…</string>
<string name="ai_chapter_summary" translatable="false">Chapter Summary</string>
<string name="ai_analyzing_chapter">Analyzing Chapter %1$d…</string>
<string name="ai_reading_position">Reading current position…</string>
<string name="ai_generating_recap">Generating Recap…</string>
<string name="ai_chapter_summary">Chapter Summary</string>
<!-- "(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_unlock_summarization" translatable="false">Unlock Chapter Summarization</string>
<string name="ai_story_recap_beta">Story Recap (Beta)</string>
<string name="ai_unlock_summarization">Unlock Chapter Summarization</string>
<!-- "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="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. -->
<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 -->
<string name="content_desc_bookmark_icon">Bookmark</string>
@ -684,8 +696,8 @@
<string name="content_desc_text_formatting">Text Formatting</string>
<!-- AI chapter summarization feature menu item. -->
<string name="menu_chapter_summarization">Chapter Summarization</string>
<!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. Not translated. -->
<string name="menu_recap_beta" translatable="false">Recap (Beta)</string>
<!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. -->
<string name="menu_recap_beta">Recap (Beta)</string>
<!-- TTS = Text-to-Speech. Accessibility label. -->
<string name="content_desc_stop_tts">Stop TTS</string>
<!-- TTS = Text-to-Speech. Accessibility label. -->
@ -719,6 +731,8 @@
<string name="label_max">Max</string>
<string name="content_desc_slower">Slower</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. -->
<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_bookmarks">Bookmarks</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="content_desc_more_options_bookmark">More options for bookmark</string>
<string name="dialog_rename_bookmark">Rename Bookmark</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_desc">Are you sure you want to permanently delete this bookmark?</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_redo">Redo</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 -->
<!-- OCR = Optical Character Recognition. %1$s = error reason string. -->
@ -1028,6 +1055,230 @@
<!-- Label for the highlight color picker in text annotations. -->
<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 -->
<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>

View file

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