diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6813ed..136618b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,7 +33,7 @@ android { versionCode = 46 versionName = "1.0.46" - resourceConfigurations += setOf("en", "ar", "de", "tr") + resourceConfigurations += setOf("en", "ar", "de", "tr", "fr", "ru") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/libs/custom-onnxruntime-arm64.aar b/app/libs/custom-onnxruntime-arm64.aar deleted file mode 100644 index 378f85c..0000000 Binary files a/app/libs/custom-onnxruntime-arm64.aar and /dev/null differ diff --git a/app/src/main/java/com/aryan/reader/AppNavigation.kt b/app/src/main/java/com/aryan/reader/AppNavigation.kt index bc25e61..921919b 100644 --- a/app/src/main/java/com/aryan/reader/AppNavigation.kt +++ b/app/src/main/java/com/aryan/reader/AppNavigation.kt @@ -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)) } } } diff --git a/app/src/main/java/com/aryan/reader/Common.kt b/app/src/main/java/com/aryan/reader/Common.kt index 6cd8416..845270d 100644 --- a/app/src/main/java/com/aryan/reader/Common.kt +++ b/app/src/main/java/com/aryan/reader/Common.kt @@ -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>(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) } } } diff --git a/app/src/main/java/com/aryan/reader/FontsScreen.kt b/app/src/main/java/com/aryan/reader/FontsScreen.kt index 91f1ded..6426473 100644 --- a/app/src/main/java/com/aryan/reader/FontsScreen.kt +++ b/app/src/main/java/com/aryan/reader/FontsScreen.kt @@ -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 . - * - * 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(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)) } } ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/HomeScreen.kt b/app/src/main/java/com/aryan/reader/HomeScreen.kt index b638da2..e7d4e39 100644 --- a/app/src/main/java/com/aryan/reader/HomeScreen.kt +++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt @@ -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)) } } } diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt index d15ea9b..7d10a33 100644 --- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt +++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt @@ -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) } diff --git a/app/src/main/java/com/aryan/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt index a60f26a..1f1bdd5 100644 --- a/app/src/main/java/com/aryan/reader/MainViewModel.kt +++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt @@ -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 { val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null) val folders = mutableListOf() @@ -5408,6 +5531,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio speechBubbleCache.clear() speechBubbleDetectionJobs.clear() + ttsController.release() + Timber.d("ViewModel instance cleared (onCleared).") } diff --git a/app/src/main/java/com/aryan/reader/ProScreen.kt b/app/src/main/java/com/aryan/reader/ProScreen.kt index ac7e217..c52c3ce 100644 --- a/app/src/main/java/com/aryan/reader/ProScreen.kt +++ b/app/src/main/java/com/aryan/reader/ProScreen.kt @@ -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( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/SharedComposables.kt b/app/src/main/java/com/aryan/reader/SharedComposables.kt index 8d1ec22..2498200 100644 --- a/app/src/main/java/com/aryan/reader/SharedComposables.kt +++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt @@ -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) } } } diff --git a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt index 59a99c0..cdf3312 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt @@ -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() diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt index 094dfd7..fd8cc6d 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt @@ -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( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt index 20e1cec..0e82295 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -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)) } } } diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt index 302396b..b9b7031 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt @@ -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( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index b353f66..8651533 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -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 }) { diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt index c724d33..80fcbcf 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt @@ -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) } } } diff --git a/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt b/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt index 4be5216..c1952fa 100644 --- a/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt +++ b/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt @@ -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 ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt index a99aaf8..e673629 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt @@ -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 diff --git a/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt b/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt index 9276160..2d83401 100644 --- a/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt +++ b/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt @@ -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)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfDialogs.kt b/app/src/main/java/com/aryan/reader/pdf/PdfDialogs.kt index 1feb9fd..10f6fd9 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfDialogs.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfDialogs.kt @@ -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)) } }) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfDrawer.kt b/app/src/main/java/com/aryan/reader/pdf/PdfDrawer.kt index a7b813d..ba584a4 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfDrawer.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfDrawer.kt @@ -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( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfHelper.kt b/app/src/main/java/com/aryan/reader/pdf/PdfHelper.kt index d0ead9b..97e1dde 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfHelper.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfHelper.kt @@ -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() - 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) ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfNavigationUI.kt b/app/src/main/java/com/aryan/reader/pdf/PdfNavigationUI.kt index 624a5bb..40fa298 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfNavigationUI.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfNavigationUI.kt @@ -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( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfSearchUI.kt b/app/src/main/java/com/aryan/reader/pdf/PdfSearchUI.kt index 0175d56..7157014 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfSearchUI.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfSearchUI.kt @@ -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( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfTextBox.kt b/app/src/main/java/com/aryan/reader/pdf/PdfTextBox.kt index c9ff4a3..a629661 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfTextBox.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfTextBox.kt @@ -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) ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfToolbars.kt b/app/src/main/java/com/aryan/reader/pdf/PdfToolbars.kt index 2874eed..1f2bfcd 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfToolbars.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfToolbars.kt @@ -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 ) } diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index 4e44028..39f3cd2 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/aryan/reader/pdf/ReflowWorker.kt b/app/src/main/java/com/aryan/reader/pdf/ReflowWorker.kt index b539b4c..08f316b 100644 --- a/app/src/main/java/com/aryan/reader/pdf/ReflowWorker.kt +++ b/app/src/main/java/com/aryan/reader/pdf/ReflowWorker.kt @@ -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" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt b/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt index bee38ce..f66d333 100644 --- a/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt +++ b/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt @@ -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) ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/aryan/reader/pdf/UniversalDocument.kt b/app/src/main/java/com/aryan/reader/pdf/UniversalDocument.kt index 699470b..6d85e33 100644 --- a/app/src/main/java/com/aryan/reader/pdf/UniversalDocument.kt +++ b/app/src/main/java/com/aryan/reader/pdf/UniversalDocument.kt @@ -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() override fun close() {} -} \ No newline at end of file +} diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index af8b3c3..8d007e4 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -24,6 +24,12 @@ %1$d results found + + + %1$d match found + %1$d matches found + + Delete File Permanently Delete Files Permanently @@ -52,4 +58,22 @@ %1$d book removed from library. %1$d books removed from library. - \ No newline at end of file + + + + %1$d folder + %1$d folders + + + + + %1$d tag + %1$d tags + + + + + (%1$d chunk) + (%1$d chunks) + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8bbb1cd..5fa4471 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,10 @@ Search Clear Apply + Enable + + Error: %1$s + Go Back Free Active Tabs @@ -288,6 +292,13 @@ Custom Fonts Import Font + Google Fonts + Browse Google Fonts + Search 1900+ fonts… + Popular Choices + + No fonts found matching \'%1$s\' + Already Downloaded No Custom Fonts Import TTF or OTF files to use them in your books. @@ -512,19 +523,20 @@ Search in book… No results found. - - Generating summary… + + Generating summary… Stop Read aloud Copy - No summary could be generated. + Copy Thread + No summary could be generated. - - Thinking… + + Thinking… Open in Dictionary App - AI could not provide a definition. + AI could not provide a definition. - Asking AI about \'%1$s\'… + Asking AI about \'%1$s\'… @@ -534,8 +546,8 @@ Synthesis Mode On-Device - - Cloud (HQ) + + Cloud (HQ) Voice Selection Play Sample @@ -586,17 +598,17 @@ G B - - Text is empty. - AI returned an empty definition. - Could not get definition. - An unknown server error occurred. - Network error. Check connection. + + Text is empty. + AI returned an empty definition. + Could not get definition. + An unknown server error occurred. + Network error. Check connection. - - Not enough context for a recap. - Failed to parse recap. - Network error during recap generation. + + Not enough context for a recap. + Failed to parse recap. + Network error during recap generation. Lookup Settings @@ -616,27 +628,27 @@ App used for web searches. None - - The book content is empty. - Failed to parse summary from server response. - Could not fetch summary. + + The book content is empty. + Failed to parse summary from server response. + Could not fetch summary. - Error: %1$d. %2$s - Network error. Please check connection and server status. + Error: %1$d. %2$s + Network error. Please check connection and server status. - Analyzing Chapter %1$d… - Reading current position… - Generating Recap… - Chapter Summary + Analyzing Chapter %1$d… + Reading current position… + Generating Recap… + Chapter Summary - Story Recap (Beta) - Unlock Chapter Summarization + Story Recap (Beta) + Unlock Chapter Summarization - Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature. + Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature. Learn More - Unlock Smart Dictionary + Unlock Smart Dictionary - Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text. + Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text. Bookmark @@ -684,8 +696,8 @@ Text Formatting Chapter Summarization - - Recap (Beta) + + Recap (Beta) Stop TTS @@ -719,6 +731,8 @@ Max Slower Faster + Decrease + Increase Page %1$d of %2$d @@ -726,10 +740,15 @@ Chapters Bookmarks Highlights + Pages + Expand All + Collapse All + Locate You haven\'t added any bookmarks yet. More options for bookmark Rename Bookmark New Name + New Title Delete Bookmark? Are you sure you want to permanently delete this bookmark? No highlights yet. @@ -912,6 +931,14 @@ Undo Redo Show Dock + Select Font Family + Select Font Size + Font Background + Bold + Italic + Underline + Strikethrough + Insert Text Box @@ -1028,6 +1055,230 @@ Highlight + + + Enable Multi-Tab Reading + + Use Strict File Filter + Language + + Test Panel ML Detection + + Test Speech Bubble ML Detection + + Export Logs (Last %1$d lines) + Enable Strict File Filter + + 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? + + + English (Default) + العربية (Arabic) + Deutsch (German) + Türkçe (Turkish) + Français (French) + Русский (Russian) + + + App Theme + Appearance + Contrast + Text Brightness + Color Scheme + Dynamic + Create App Theme + + Custom Theme + Ocean + Mint + Rose + Sepia + Amethyst + Amber + Sapphire + Add Custom Theme + App Theme + App Icon + Device + Open Drawer + Profile picture + Profile + Pro Feature + Filter + Sort + Close search + Clear query + Search shelf + + %1$s shelf cover + Preserve Image Colors + Keep original image colors when theme changes + + + Unshelved + All Books + System + Light + Dark + Standard + Medium + High + Recent + Title A-Z + Author A-Z + Percent complete 0-100 + Percent complete 100-0 + Size (Smallest) + Size (Biggest) + All + Unread + In Progress + Completed + + Tags: %1$s + Browse by tag + Tags + Folders + Files + + + Active TTS Engine + Cloud AI + Device Native + Cloud Voices + Device Voices + Cloud Cache + Select High-Quality Cloud Voice + Clear Samples + System Default Voice + Uses device settings + Language Filter + Online + Offline + Voice Filter + No audio cached for this voice. + + Clear Cache for %1$s + This is a voice sample. + + + AI Features + Summary + Recap + Cache + + No summary for %1$s yet. + + Generate Summary for %1$s + Get a recap of the story up to your current position. + Generate Story Recap + Story Recap + Cache Hit • Free + + Generated • Free (%1$d/10 left) + + Generated • Cost: %1$s credits + Generating… • Cost: Calculating + AI Output + Regenerate + No cached summaries for this book. + Credits + AI & Cloud Credits + Credits Available + + %1$d Credits + Estimated Cost Breakdown + Cloud TTS + Cost: ~3-4 credits per minute of audio generated.\nTo enable: Reader Screen > More > TTS Voice Settings. + AI Summaries & Recap + Cost: ~1-4 credits per request based on chapter length.\nPro Users get 10 free summaries daily. + By purchasing, + Out of Credits + 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. + Get Pro / Add Credits + Unlock Page Summarization + Get concise summaries of any page with Episteme Pro. Upgrade to start using this feature. + Download Bubble Zoom Model + To use the Bubble Zoom feature, an AI model needs to be downloaded (~134 MB). Do you want to download it now? + + + Translate + Back to Pg %1$d + + Page %1$d + + Result %1$d / %2$d + + Failed to load PDF. + + Downloading Bubble Zoom model… %1$d%% + Exit slider navigation + Jump Back + Scroll to reading page + Annotated Page + Close Image + Toggle search highlights + Drag to move text box + No files icon + + Copy %1$s + Tag + List item marker + Reset Zoom + Generate Demo Annotations + Demo Annotations + Open Pen Playground + New Tab + Highlight all text + Toggle Editing Mode + Smart Comic Zoom + Customize Highlights + English, Spanish, French, etc. + Hindi, Marathi, Sanskrit + English + Chinese + English + Japanese + English + Korean + English + Page Unavailable + + + Document + Generated + %1$s (Text View) + %1$s (Reflow) + + + Add / Edit + No tags assigned. + Apply Tags + Search or create tag… + + Create \"%1$s\" + + + Pull Distance to Change Chapter + Short + Long + + Spd: %1$sx + + Ptch: %1$sx + Play/Pause + Reset Speed + Reset Pitch + Select Color + This book has no content to display. + Copied Link + Copied Text + + + Table of Contents + Bookmark + + Jump Back to Page %1$d + Return to previous page + Exit Smart Zoom + Smart Comic Zoom + Toggle Smart Comic Zoom diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index f56fafc..b8efb26 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -3,5 +3,7 @@ + + - \ No newline at end of file +