diff --git a/app/src/main/java/com/aryan/reader/Common.kt b/app/src/main/java/com/aryan/reader/Common.kt index a121a70..ad14e8a 100644 --- a/app/src/main/java/com/aryan/reader/Common.kt +++ b/app/src/main/java/com/aryan/reader/Common.kt @@ -1,5 +1,5 @@ // Common.kt -@file:kotlin.OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class) package com.aryan.reader @@ -389,14 +389,14 @@ fun SearchTopBar( ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Close Search" + contentDescription = stringResource(R.string.content_desc_close_search) ) } TextField( value = searchState.searchQuery, onValueChange = { searchState.onQueryChange(it) }, - placeholder = { Text("Search in book...") }, + placeholder = { Text(stringResource(R.string.search_in_book)) }, modifier = Modifier .weight(1f) .focusRequester(focusRequester) @@ -425,7 +425,7 @@ fun SearchTopBar( ) { Icon( Icons.Default.Close, - contentDescription = "Clear Search" + contentDescription = stringResource(R.string.content_desc_clear_search) ) } } @@ -446,7 +446,10 @@ fun SearchTopBar( ) { Icon( imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, - contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results" + contentDescription = stringResource( + if (searchState.showSearchResultsPanel) R.string.content_desc_hide_results + else R.string.content_desc_show_results + ) ) } } @@ -474,7 +477,7 @@ fun SearchNavigationControls( onClick = { onNavigate(searchState.currentSearchResultIndex - 1) }, enabled = searchState.currentSearchResultIndex > 0 ) { - Icon(Icons.Default.ArrowDropUp, contentDescription = "Previous Search Result") + Icon(Icons.Default.ArrowDropUp, contentDescription = stringResource(R.string.content_desc_prev_result)) } Text( @@ -489,7 +492,7 @@ fun SearchNavigationControls( onClick = { onNavigate(searchState.currentSearchResultIndex + 1) }, enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1 ) { - Icon(Icons.Default.ArrowDropDown, contentDescription = "Next Search Result") + Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.content_desc_next_result)) } } } @@ -549,7 +552,7 @@ fun SummarizationPopup( verticalAlignment = Alignment.CenterVertically ) { CircularProgressIndicator() - Text("Generating summary...", modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge) + Text(stringResource(R.string.generating_summary), modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge) } } else if (result != null) { val summaryText = result.summary @@ -596,7 +599,7 @@ fun SummarizationPopup( ) { Icon( imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow, - contentDescription = if (isTtsSessionActive) "Stop" else "Read aloud" + contentDescription = stringResource(if (isTtsSessionActive) R.string.action_stop else R.string.action_read_aloud) ) } Spacer(modifier = Modifier.width(8.dp)) @@ -605,7 +608,7 @@ fun SummarizationPopup( }) { Icon( imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy" + contentDescription = stringResource(R.string.action_copy) ) } } @@ -656,7 +659,7 @@ fun SummarizationPopup( onTextLayout = { textLayoutResult = it } ) } else { - Text("No summary could be generated.", style = MaterialTheme.typography.bodyLarge) + Text(stringResource(R.string.no_summary_generated), style = MaterialTheme.typography.bodyLarge) } } } @@ -711,7 +714,7 @@ fun AiDefinitionPopup( verticalAlignment = Alignment.CenterVertically ) { 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) { word?.let { @@ -771,7 +774,7 @@ fun AiDefinitionPopup( ) { Icon( imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow, - contentDescription = if (isTtsSessionActive) "Stop" else "Read aloud" + contentDescription = stringResource(if (isTtsSessionActive) R.string.action_stop else R.string.action_read_aloud) ) } Spacer(modifier = Modifier.width(8.dp)) @@ -780,14 +783,14 @@ fun AiDefinitionPopup( }) { Icon( imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy" + contentDescription = stringResource(R.string.action_copy) ) } Spacer(modifier = Modifier.width(8.dp)) IconButton(onClick = onOpenExternalDictionary) { Icon( painter = painterResource(id = R.drawable.dictionary), - contentDescription = "Open in Dictionary App" + contentDescription = stringResource(R.string.content_desc_open_dictionary) ) } } @@ -839,11 +842,11 @@ fun AiDefinitionPopup( onTextLayout = { textLayoutResult = it } ) } else { - Text("AI could not provide a definition.", style = MaterialTheme.typography.bodyLarge) + Text(stringResource(R.string.ai_no_definition), style = MaterialTheme.typography.bodyLarge) } } else if (word != null) { Text( - text = "Asking AI about '$word'...", + text = stringResource(R.string.ai_asking_about, word), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(vertical = 24.dp), maxLines = 1, @@ -874,13 +877,13 @@ fun SearchResultsPanel( } results.isEmpty() -> { 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 = "${results.size} " + if (results.size == 1) "result found" else "results found", + text = LocalContext.current.resources.getQuantityString(R.plurals.search_results_count, results.size, results.size), style = MaterialTheme.typography.titleSmall, modifier = Modifier .padding(horizontal = 16.dp, vertical = 12.dp) @@ -907,12 +910,13 @@ fun SearchResultsPanel( suspend fun fetchAiDefinition( text: String, + context: Context, onUpdate: (String) -> Unit, onError: (String) -> Unit, onFinish: () -> Unit ) { if (text.isBlank()) { - onError("Text is empty.") + onError(context.getString(R.string.error_text_empty)) onFinish() return } @@ -961,16 +965,16 @@ suspend fun fetchAiDefinition( } Timber.d("Definition: Finished reading stream.") if (!hasReceivedData) { - onError("AI returned an empty definition.") + onError(context.getString(R.string.error_ai_empty_definition)) } } else { val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null } val errorDetail = try { errorBody?.let { JSONObject(it).getString("detail") } } catch (_: Exception) { "Could not get definition." } - onError("Error: $responseCode. ${errorDetail ?: "An unknown server error occurred."}") + onError("${responseCode}. ${errorDetail ?: context.getString(R.string.error_unknown_server)}") } } catch (e: Exception) { Timber.e(e, "Network error fetching AI definition: ${e.message}") - onError("Network error. Check connection.") + onError(context.getString(R.string.error_network_check_connection)) } finally { connection?.disconnect() onFinish() @@ -1091,12 +1095,13 @@ class SummaryCacheManager(context: Context) { suspend fun fetchRecap( pastSummaries: List, currentText: String, + context: Context, onUpdate: (String) -> Unit, onError: (String) -> Unit, onFinish: () -> Unit ) { if (pastSummaries.isEmpty() && currentText.isBlank()) { - onError("Not enough context for a recap.") + onError(context.getString(R.string.error_not_enough_context)) onFinish() return } @@ -1143,14 +1148,14 @@ suspend fun fetchRecap( } } } - if (!hasReceivedData) onError("Failed to parse recap.") + if (!hasReceivedData) onError(context.getString(R.string.error_parse_recap)) } else { val errorBody = try { connection.errorStream?.bufferedReader()?.use { it.readText() } } catch (_: Exception) { null } - onError("Error: $responseCode. ${errorBody ?: ""}") + onError("${responseCode}. ${errorBody ?: ""}") } } catch (e: Exception) { Timber.e(e, "Recap error: ${e.message}") - onError("Network error during recap generation.") + onError(context.getString(R.string.error_network_recap)) } finally { connection?.disconnect() onFinish() @@ -1194,7 +1199,7 @@ fun TtsSettingsSheet( .padding(bottom = 24.dp) ) { Text( - text = "Text-to-Speech Settings", + text = stringResource(R.string.tts_settings), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp) @@ -1213,7 +1218,7 @@ fun TtsSettingsSheet( Icon(Icons.Default.Stop, contentDescription = null, tint = MaterialTheme.colorScheme.onErrorContainer) Spacer(Modifier.width(12.dp)) Text( - "Please stop playback to change settings.", + stringResource(R.string.tts_stop_to_change_settings), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer ) @@ -1222,7 +1227,7 @@ fun TtsSettingsSheet( } Text( - text = "Synthesis Mode", + text = stringResource(R.string.tts_synthesis_mode), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 8.dp) @@ -1239,7 +1244,7 @@ fun TtsSettingsSheet( ) { TtsPlaybackManager.TtsMode.entries.forEach { mode -> val isSelected = currentMode == mode - val label = if (mode == TtsPlaybackManager.TtsMode.BASE) "On-Device" else "Cloud (HQ)" + val label = if (mode == TtsPlaybackManager.TtsMode.BASE) stringResource(R.string.tts_mode_on_device) else stringResource(R.string.tts_mode_cloud_hq) val icon = if (mode == TtsPlaybackManager.TtsMode.BASE) Icons.Default.Smartphone else Icons.Default.Cloud Surface( @@ -1271,7 +1276,7 @@ fun TtsSettingsSheet( if (currentMode == TtsPlaybackManager.TtsMode.CLOUD) { Text( - text = "Voice Selection", + text = stringResource(R.string.tts_voice_selection), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 8.dp) @@ -1306,7 +1311,7 @@ fun TtsSettingsSheet( } else { Icon( imageVector = if (isPlaying) Icons.Default.Stop else Icons.Default.PlayArrow, - contentDescription = "Play Sample", + contentDescription = stringResource(R.string.tts_play_sample), tint = if (isPlaying) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -1503,13 +1508,13 @@ fun DeviceVoiceSettingsSheet( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "On-Device Voice Settings", + text = stringResource(R.string.tts_device_voice_settings), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f) ) IconButton(onClick = onDismiss) { - Icon(Icons.Default.Close, contentDescription = "Close Settings") + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_close_settings)) } } @@ -1534,19 +1539,19 @@ fun DeviceVoiceSettingsSheet( Spacer(Modifier.width(16.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = "System Default", + text = stringResource(R.string.tts_system_default), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Text( - text = "Matches your Android system settings", + text = stringResource(R.string.tts_system_default_desc), style = MaterialTheme.typography.bodySmall ) } if (savedVoiceName == null) { Icon( Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary ) } @@ -1562,7 +1567,7 @@ fun DeviceVoiceSettingsSheet( ) { CircularProgressIndicator() Spacer(modifier = Modifier.height(8.dp)) - Text("Loading voices...", modifier = Modifier.padding(top = 48.dp)) + Text(stringResource(R.string.tts_loading_voices), modifier = Modifier.padding(top = 48.dp)) } } else if (allVoices.isEmpty()) { Box( @@ -1570,7 +1575,7 @@ fun DeviceVoiceSettingsSheet( contentAlignment = Alignment.Center ) { Text( - "No voices available on this device.", + stringResource(R.string.tts_no_voices), color = MaterialTheme.colorScheme.error ) } @@ -1583,7 +1588,7 @@ fun DeviceVoiceSettingsSheet( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Specific Voices", + text = stringResource(R.string.tts_specific_voices), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -1597,7 +1602,7 @@ fun DeviceVoiceSettingsSheet( .clickable { expandedLanguageMenu = true }, shape = RoundedCornerShape(8.dp), color = MaterialTheme.colorScheme.surface, - border = androidx.compose.foundation.BorderStroke( + border = BorderStroke( 1.dp, MaterialTheme.colorScheme.outlineVariant ) ) { @@ -1644,7 +1649,7 @@ fun DeviceVoiceSettingsSheet( if (filteredVoices.isNotEmpty()) { Text( - text = "Available Voices (${filteredVoices.size})", + text = stringResource(R.string.tts_available_voices_count, filteredVoices.size), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 8.dp, start = 4.dp) @@ -1676,13 +1681,13 @@ fun DeviceVoiceSettingsSheet( ) }, supportingContent = if (voice.locale.variant.isNotEmpty()) { - { Text("Variant: ${voice.locale.variant}") } + { Text(stringResource(R.string.tts_voice_variant, voice.locale.variant)) } } else null, leadingContent = { if (isSelected) { Icon( Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary ) } else { @@ -1698,8 +1703,7 @@ fun DeviceVoiceSettingsSheet( Timber.e(e, "Failed to set language for sample") } ttsEngine?.voice = voice - val sampleText = - "This is a sample of ${voice.locale.displayLanguage}." + val sampleText = context.getString(R.string.tts_voice_sample_text, voice.locale.displayLanguage) ttsEngine?.speak( sampleText, TextToSpeech.QUEUE_FLUSH, @@ -1709,7 +1713,7 @@ fun DeviceVoiceSettingsSheet( }) { Icon( imageVector = Icons.Default.PlayArrow, - contentDescription = "Play Sample", + contentDescription = stringResource(R.string.tts_play_sample), tint = MaterialTheme.colorScheme.primary ) } @@ -1736,7 +1740,7 @@ fun DeviceVoiceSettingsSheet( contentAlignment = Alignment.Center ) { Text( - "No voices found for this language.", + stringResource(R.string.tts_no_voices_for_language), color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -2164,14 +2168,13 @@ fun ReaderThemePanel( .padding(16.dp) .padding(bottom = 16.dp) ) { - Text( - "Reading Themes", + Text(stringResource(R.string.reading_themes), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp) ) - Text("Presets", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) + Text(stringResource(R.string.theme_presets), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) Spacer(Modifier.height(8.dp)) ThemeGrid(themes = builtInThemes, currentThemeId = currentThemeId, onThemeSelected = onThemeSelected) @@ -2181,7 +2184,7 @@ fun ReaderThemePanel( 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 = { editingTheme = null; showBuilder = true }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Add, contentDescription = "Create Theme", tint = MaterialTheme.colorScheme.primary) } @@ -2189,7 +2192,7 @@ fun ReaderThemePanel( Spacer(Modifier.height(8.dp)) if (customThemes.isEmpty()) { - Text("No custom themes yet. Tap '+' to create one.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.theme_no_custom), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } else { ThemeGrid( themes = customThemes, @@ -2290,7 +2293,7 @@ fun ThemeBuilderView( .padding(16.dp) ) { Text( - text = if (initialTheme == null) "New Theme" else "Edit Theme", + text = stringResource(if (initialTheme == null) R.string.theme_new else R.string.theme_edit), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface @@ -2302,7 +2305,7 @@ fun ThemeBuilderView( androidx.compose.material3.OutlinedTextField( value = name, onValueChange = { name = it }, - label = { Text("Theme Name") }, + label = { Text(stringResource(R.string.theme_name)) }, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), singleLine = true ) @@ -2325,14 +2328,12 @@ fun ThemeBuilderView( } else this }) { Column(Modifier.padding(16.dp).fillMaxWidth()) { - Text( - text = "So many books, so little time.", + Text(text = stringResource(R.string.theme_preview_quote), color = txtColor, style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(8.dp)) - Text( - text = "- Frank Zappa", + Text(text = stringResource(R.string.theme_preview_author), color = txtColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.fillMaxWidth(), @@ -2344,8 +2345,7 @@ fun ThemeBuilderView( // Animated Contrast Warning AnimatedVisibility(visible = contrast < 4.5f) { - Text( - "⚠️ Low contrast! This might cause eye strain.", + Text(stringResource(R.string.theme_low_contrast_warning), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(bottom = 8.dp) @@ -2357,13 +2357,13 @@ fun ThemeBuilderView( // Sleek Color Swatches Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { ColorSwatchItem( - label = "Page Color", + label = stringResource(R.string.theme_page_color), color = bgColor, onClick = { editingColorType = "bg" }, modifier = Modifier.weight(1f) ) ColorSwatchItem( - label = "Text Color", + label = stringResource(R.string.theme_text_color), color = txtColor, onClick = { editingColorType = "text" }, modifier = Modifier.weight(1f) @@ -2379,13 +2379,13 @@ fun ThemeBuilderView( verticalAlignment = Alignment.CenterVertically ) { TextButton(onClick = onCancel) { - Text("Cancel", color = MaterialTheme.colorScheme.primary) + Text(stringResource(R.string.action_cancel), color = MaterialTheme.colorScheme.primary) } Spacer(Modifier.width(8.dp)) Button(onClick = { onSave(ReaderTheme(id = initialTheme?.id ?: System.currentTimeMillis().toString(), name = name, backgroundColor = bgColor, textColor = txtColor, isDark = isDark, textureId = textureId, isCustom = true)) }) { - Text("Save", color = MaterialTheme.colorScheme.onPrimary) + Text(stringResource(R.string.action_save), color = MaterialTheme.colorScheme.onPrimary) } } } @@ -2393,7 +2393,7 @@ fun ThemeBuilderView( editingColorType?.let { type -> ThemeColorPickerDialog( initialColor = if (type == "bg") bgColor else txtColor, - title = if (type == "bg") "Page Color" else "Text Color", + title = if (type == "bg") stringResource(R.string.theme_page_color) else stringResource(R.string.theme_text_color), bgColor = bgColor, textColor = txtColor, editingColorType = type, @@ -2502,14 +2502,12 @@ fun ThemeColorPickerDialog( border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) ) { Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = "Live Preview", + Text(text = stringResource(R.string.theme_color_live_preview), color = liveTextColor, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold ) - Text( - text = "Reading is dreaming.", + Text(text = stringResource(R.string.theme_color_preview_text), color = liveTextColor, style = MaterialTheme.typography.bodySmall ) @@ -2553,7 +2551,7 @@ fun ThemeColorPickerDialog( 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) }) } @@ -2562,18 +2560,15 @@ fun ThemeColorPickerDialog( 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) ) @@ -2593,7 +2588,7 @@ fun ThemeColorPickerDialog( containerColor = Color.White ) ) { - Text("Save", color = Color.Black, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.action_save), color = Color.Black, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt index ea03e10..c87a614 100644 --- a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt +++ b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt @@ -315,14 +315,19 @@ class FolderSyncWorker( } private fun getFileType(name: String, mimeType: String?): FileType? { + val lowerName = name.lowercase() return when { - mimeType == "application/pdf" || name.endsWith(".pdf", true) -> FileType.PDF - mimeType == "application/epub+zip" || name.endsWith(".epub", true) -> FileType.EPUB - mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || name.endsWith(".docx", true) -> FileType.DOCX - name.endsWith(".mobi", true) || name.endsWith(".azw3", true) -> FileType.MOBI - name.endsWith(".md", true) -> FileType.MD - name.endsWith(".txt", true) -> FileType.TXT - name.endsWith(".html", true) || name.endsWith(".xhtml", true) || name.endsWith(".htm", true) -> FileType.HTML + mimeType == "application/pdf" || lowerName.endsWith(".pdf") -> FileType.PDF + mimeType == "application/epub+zip" || lowerName.endsWith(".epub") -> FileType.EPUB + mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || lowerName.endsWith(".docx") -> FileType.DOCX + lowerName.endsWith(".mobi") || lowerName.endsWith(".azw3") || lowerName.endsWith(".prc") -> FileType.MOBI + lowerName.endsWith(".fb2") || lowerName.endsWith(".fb2.zip") -> FileType.FB2 + lowerName.endsWith(".cbz") -> FileType.CBZ + lowerName.endsWith(".cbr") -> FileType.CBR + lowerName.endsWith(".cb7") -> FileType.CB7 + lowerName.endsWith(".md") || lowerName.endsWith(".markdown") -> FileType.MD + lowerName.endsWith(".txt") -> FileType.TXT + mimeType == "text/html" || lowerName.endsWith(".html") || lowerName.endsWith(".xhtml") || lowerName.endsWith(".htm") -> FileType.HTML else -> null } } diff --git a/app/src/main/java/com/aryan/reader/FontsScreen.kt b/app/src/main/java/com/aryan/reader/FontsScreen.kt index d22789b..fb81a60 100644 --- a/app/src/main/java/com/aryan/reader/FontsScreen.kt +++ b/app/src/main/java/com/aryan/reader/FontsScreen.kt @@ -59,6 +59,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -97,10 +98,10 @@ fun FontsScreen( ) Scaffold( - modifier = Modifier.statusBarsPadding(), // Fixes content flowing under status bar + modifier = Modifier.statusBarsPadding(), topBar = { CustomTopAppBar( - title = { Text("Custom Fonts") }, + title = { Text(stringResource(R.string.custom_fonts)) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") @@ -109,12 +110,11 @@ fun FontsScreen( ) }, floatingActionButton = { - // Hide FAB when empty state is visible (list is empty) if (fonts.isNotEmpty()) { ExtendedFloatingActionButton( onClick = { pickFontLauncher.launch(fontMimeTypes) }, icon = { Icon(Icons.Default.Add, contentDescription = null) }, - text = { Text("Import Font") } + text = { Text(stringResource(R.string.import_font)) } ) } } @@ -122,15 +122,14 @@ fun FontsScreen( Box(modifier = Modifier.fillMaxSize().padding(padding)) { if (fonts.isEmpty()) { EmptyState( - title = "No Custom Fonts", - message = "Import TTF or OTF files to use them in your books.", + title = stringResource(R.string.no_custom_fonts), + message = stringResource(R.string.import_fonts_desc), onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) }, modifier = Modifier.fillMaxSize() ) } else { LazyColumn( modifier = Modifier.fillMaxSize(), - // Padding bottom 88.dp allows scrolling past the FloatingActionButton contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 88.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -223,14 +222,14 @@ fun FontListItem( ) { if (customTypeface != null) { Text( - text = "Grumpy wizards make toxic brew for the evil queen! 1234567890 ?.,;:", + text = stringResource(R.string.font_preview_text), fontFamily = customTypeface, fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface ) } else { Text( - text = "Preview unavailable (Invalid font file)", + text = stringResource(R.string.font_preview_error), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) @@ -255,18 +254,18 @@ fun DeleteFontConfirmationDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete Font?") }, - text = { Text("Are you sure you want to delete '$fontName'? This will remove it from all your devices if sync is on.") }, + title = { Text(stringResource(R.string.dialog_delete_font)) }, + text = { Text(stringResource(R.string.dialog_delete_font_desc, fontName)) }, confirmButton = { TextButton( onClick = onConfirm, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) ) { - Text("Delete") + Text(stringResource(R.string.action_delete)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + 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 dbaa575..ee7c3d9 100644 --- a/app/src/main/java/com/aryan/reader/HomeScreen.kt +++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt @@ -114,6 +114,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -247,7 +248,7 @@ fun HomeScreen( try { fallbackFilePickerLauncher.launch("*/*") } catch (_: android.content.ActivityNotFoundException) { - viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true) + viewModel.showBanner(context.getString(R.string.error_no_file_manager), isError = true) } } } @@ -336,17 +337,17 @@ fun HomeScreen( if (recentFilesForHome.isEmpty()) { if (uiState.recentFiles.isEmpty()) { EmptyState( - title = "Your Library is Empty", - message = "Select a file to read, or sync a local folder to automatically import books.", + title = stringResource(R.string.your_library_empty), + message = stringResource(R.string.your_library_empty_desc), onSelectFileClick = onSelectFileClick, modifier = Modifier.weight(1f), - secondaryButtonText = "Setup Folder Sync", + secondaryButtonText = stringResource(R.string.setup_folder_sync), onSecondaryClick = { viewModel.navigateToFolderSync() } ) } else { EmptyState( - title = "No Recent Files", - message = "Open a file from your library to see it here.", + title = stringResource(R.string.no_recent_files), + message = stringResource(R.string.no_recent_files_desc), onSelectFileClick = onSelectFileClick, modifier = Modifier.weight(1f) ) @@ -409,8 +410,8 @@ fun HomeScreen( if (showClearBookCacheDialog) { DangerousFolderActionDialog( - title = "Clear Book Cache", - message = "This will clear all processed page in pagination mode. This helps fix layout issues but will require books to be re-processed next time you open them.", + title = stringResource(R.string.dialog_clear_book_cache), + message = stringResource(R.string.dialog_clear_book_cache_desc), onConfirm = { viewModel.clearBookCache() showClearBookCacheDialog = false @@ -436,8 +437,8 @@ fun HomeScreen( if (showClearReflowCacheDialog) { DangerousFolderActionDialog( - title = "Clear Reflow Cache", - message = "This will delete all generated 'Text View' versions of your PDFs and clear their associated images/HTML cache. Your original PDFs will remain untouched.", + title = stringResource(R.string.dialog_clear_reflow_cache), + message = stringResource(R.string.dialog_clear_reflow_cache_desc), onConfirm = { viewModel.clearReflowCache() showClearReflowCacheDialog = false @@ -528,10 +529,10 @@ private fun RecentFilesContent( verticalAlignment = Alignment.CenterVertically ) { androidx.compose.material3.Button(onClick = onSelectFileClick) { - Text("Select File") + Text(stringResource(R.string.empty_select_file)) } androidx.compose.material3.Button(onClick = onNavigateToFolderSync) { - Text("Sync Folder") + Text(stringResource(R.string.sync_folder)) } } } @@ -570,7 +571,7 @@ private fun RecentFilesGrid( Column(modifier = modifier) { Text( - text = "Recent Files", + text = stringResource(R.string.recent_files), style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 8.dp, top = 24.dp) ) @@ -650,7 +651,7 @@ fun RecentFileCard( ) { Icon( imageVector = Icons.Default.Folder, - contentDescription = "Local Folder", + contentDescription = stringResource(R.string.local_folder), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer ) @@ -671,7 +672,7 @@ fun RecentFileCard( ) { Icon( imageVector = Icons.Default.Cloud, - contentDescription = "OPDS Stream", + contentDescription = stringResource(R.string.opds_stream), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer ) @@ -691,7 +692,7 @@ fun RecentFileCard( ) { Icon( imageVector = Icons.Default.PushPin, - contentDescription = "Pinned", + contentDescription = stringResource(R.string.pinned), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer ) @@ -710,7 +711,7 @@ fun RecentFileCard( } else { Icon( imageVector = Icons.Filled.Info, - contentDescription = "Not available locally", + contentDescription = stringResource(R.string.not_available_locally), modifier = Modifier.size(48.dp), tint = Color.White ) @@ -745,7 +746,7 @@ fun RecentFileCard( ) { item.progressPercentage?.let { progress -> Text( - text = "${progress.toInt()}% complete", + text = stringResource(R.string.progress_complete, progress.toInt()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary ) @@ -788,7 +789,7 @@ fun DefaultTopAppBar( // Recent Files Limit Menu Box { IconButton(onClick = { showLimitMenu = true }) { - Icon(Icons.Default.FormatListNumbered, contentDescription = "Recent Files Limit") + Icon(Icons.Default.FormatListNumbered, contentDescription = stringResource(R.string.options_recent_limit)) } DropdownMenu( expanded = showLimitMenu, onDismissRequest = { showLimitMenu = false } @@ -796,7 +797,7 @@ fun DefaultTopAppBar( val limitOptions = listOf(0, 10, 20, 50, 100) limitOptions.forEach { limit -> DropdownMenuItem( - text = { Text(if (limit == 0) "No limit" else "$limit files") }, + text = { Text(if (limit == 0) stringResource(R.string.options_no_limit) else stringResource(R.string.options_files_limit, limit)) }, onClick = { onRecentFilesLimitChange(limit) showLimitMenu = false @@ -816,29 +817,28 @@ fun DefaultTopAppBar( } DropdownMenu( expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) { - DropdownMenuItem(text = { Text("About") }, onClick = { + DropdownMenuItem(text = { Text(stringResource(R.string.about_title)) }, onClick = { onAboutClick() showOptionsMenu = false }) HorizontalDivider() - DropdownMenuItem(text = { Text("Clear Book Cache") }, onClick = { + DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = { onClearCache() showOptionsMenu = false }) - DropdownMenuItem(text = { Text("Clear Reflow Cache") }, onClick = { + DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_reflow_cache)) }, onClick = { onClearReflowCache() showOptionsMenu = false }) if (BuildConfig.DEBUG && BuildConfig.FLAVOR != "oss") { HorizontalDivider() - DropdownMenuItem(text = { Text("[Debug] Show Device Management") }, onClick = { + DropdownMenuItem(text = { Text(stringResource(R.string.debug_show_device_management)) }, onClick = { onShowDeviceManagement() showOptionsMenu = false }) - DropdownMenuItem( - text = { Text("[Debug] Clear Cloud & Local Data") }, + DropdownMenuItem(text = { Text(stringResource(R.string.debug_clear_cloud_local_data)) }, onClick = { onClearCloudData() showOptionsMenu = false @@ -905,12 +905,8 @@ private fun AppDrawerContent( // Signed-out: Show Sign In button at the top Spacer(modifier = Modifier.height(8.dp)) NavigationDrawerItem( - icon = { - Icon( - Icons.Outlined.AccountCircle, contentDescription = "Sign In" - ) - }, - label = { Text("Sign in with Google") }, + icon = { Icon(Icons.Outlined.AccountCircle, contentDescription = null) }, + label = { Text(stringResource(R.string.drawer_sign_in)) }, selected = false, onClick = onSignInClick, modifier = Modifier.padding(horizontal = 12.dp) @@ -918,7 +914,7 @@ private fun AppDrawerContent( // LegalText LegalText( - prefixText = "By signing in,", + prefixText = stringResource(R.string.drawer_by_signing_in), modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), textAlign = TextAlign.Start ) @@ -928,14 +924,9 @@ private fun AppDrawerContent( Spacer(modifier = Modifier.height(16.dp)) NavigationDrawerItem( - icon = { - Icon( - Icons.Default.VerifiedUser, contentDescription = "Episteme Pro" - ) - }, + icon = { Icon(Icons.Default.VerifiedUser, contentDescription = null) }, label = { - val text = - if (uiState.isProUser) "Episteme Pro" else "Upgrade to Episteme Pro" + val text = if (uiState.isProUser) stringResource(R.string.drawer_pro_unlocked) else stringResource(R.string.drawer_upgrade_pro) Text(text) }, selected = false, @@ -946,12 +937,9 @@ private fun AppDrawerContent( // Sync Toggle Item if (uiState.currentUser != null) { NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(id = R.drawable.sync), - contentDescription = "Sync Library" - ) - }, label = { Text("Sync Library") }, badge = { + icon = { Icon(painterResource(id = R.drawable.sync), contentDescription = null) }, + label = { Text(stringResource(R.string.drawer_sync_library)) }, + badge = { Row(verticalAlignment = Alignment.CenterVertically) { if (!uiState.isProUser) { Icon( @@ -979,17 +967,12 @@ private fun AppDrawerContent( } if (uiState.currentUser != null && uiState.isSyncEnabled) { NavigationDrawerItem( - icon = { - Icon( - imageVector = Icons.Default.FolderSpecial, - contentDescription = "Backup Local Folders" - ) - }, + icon = { Icon(imageVector = Icons.Default.FolderSpecial, contentDescription = null) }, label = { Column { - Text("Cloud sync for Local Folders") + Text(stringResource(R.string.drawer_backup_local_folders)) Text( - "Upload books from your synced folders to Google Drive).", + stringResource(R.string.drawer_backup_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1020,32 +1003,22 @@ private fun AppDrawerContent( modifier = Modifier.size(64.dp) ) Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Episteme OSS", style = MaterialTheme.typography.titleMedium) + Text(text = stringResource(R.string.app_name_oss), style = MaterialTheme.typography.titleMedium) } HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) } NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(id = R.drawable.fonts), - contentDescription = "Custom Fonts" - ) - }, - label = { Text("Custom Fonts") }, + icon = { Icon(painterResource(id = R.drawable.fonts), contentDescription = null) }, + label = { Text(stringResource(R.string.drawer_custom_fonts)) }, selected = false, onClick = onFontsClick, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) ) NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(id = R.drawable.feedback), - contentDescription = "Feedback" - ) - }, - label = { Text("Help & Feedback") }, + icon = { Icon(painterResource(id = R.drawable.feedback), contentDescription = null) }, + label = { Text(stringResource(R.string.drawer_help_feedback)) }, selected = false, onClick = { navController.navigate("feedback_screen_route") }, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) @@ -1054,13 +1027,8 @@ private fun AppDrawerContent( if (!isOss) { if (uiState.currentUser != null) { NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(id = R.drawable.logout), - contentDescription = "Sign Out" - ) - }, - label = { Text("Sign Out") }, + icon = { Icon(painterResource(id = R.drawable.logout), contentDescription = null) }, + label = { Text(stringResource(R.string.drawer_sign_out)) }, selected = false, onClick = onSignOutClick, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) @@ -1077,7 +1045,7 @@ private fun AppDrawerContent( var scaledTextStyle by remember { mutableStateOf(baseStyle) } Text( - text = "Privacy Policy • Terms of Service • Licenses", + text = stringResource(R.string.legal_footer_combined), style = scaledTextStyle, maxLines = 1, softWrap = false, @@ -1098,29 +1066,21 @@ private fun AppDrawerContent( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Privacy Policy", + text = stringResource(R.string.legal_privacy_policy), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), modifier = Modifier.clickable { uriHandler.openUri(PRIVACY_POLICY_URL) }, softWrap = false ) + Text(" • ", style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)) Text( - " • ", - style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant), - softWrap = false - ) - Text( - text = "Terms of Service", + text = stringResource(R.string.legal_terms_of_service), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), modifier = Modifier.clickable { uriHandler.openUri(TERMS_URL) }, softWrap = false ) + Text(" • ", style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)) Text( - " • ", - style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant), - softWrap = false - ) - Text( - text = "Licenses", + text = stringResource(R.string.legal_licenses), style = scaledTextStyle.copy(color = MaterialTheme.colorScheme.primary), modifier = Modifier.clickable { uriHandler.openUri(LICENSES_URL) }, softWrap = false @@ -1136,13 +1096,13 @@ fun UpgradeDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.VerifiedUser, contentDescription = null) }, - title = { Text("Unlock Episteme Pro") }, - text = { Text("Sync across devices is a Pro feature. Unlock all pro features with a single, one-time purchase.") }, + title = { Text(stringResource(R.string.dialog_unlock_pro)) }, + text = { Text(stringResource(R.string.dialog_unlock_pro_desc)) }, confirmButton = { - TextButton(onClick = onConfirm) { Text("Upgrade") } + TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_upgrade)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }) } @@ -1150,18 +1110,18 @@ fun UpgradeDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { fun SignOutConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Confirm Sign Out") }, - text = { Text("Are you sure you want to sign out?") }, + title = { Text(stringResource(R.string.dialog_confirm_sign_out)) }, + text = { Text(stringResource(R.string.dialog_confirm_sign_out_desc)) }, confirmButton = { TextButton( onClick = onConfirm, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) ) { - Text("Sign Out") + Text(stringResource(R.string.drawer_sign_out)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }) } @@ -1185,13 +1145,13 @@ fun DeviceManagementScreen( verticalArrangement = Arrangement.Center ) { Text( - text = "Device Limit Reached", + text = stringResource(R.string.device_limit_reached), style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "To use Episteme Pro on this device, please remove one of your existing registered devices.", + text = stringResource(R.string.device_limit_reached_desc), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center ) @@ -1218,14 +1178,14 @@ fun DeviceManagementScreen( Text(device.deviceName, fontWeight = FontWeight.SemiBold) device.lastSeen?.let { Text( - "Last seen: ${dateFormatter.format(it)}", + stringResource(R.string.last_seen, dateFormatter.format(it)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } TextButton(onClick = { onRemoveDevice(device.deviceId) }) { - Text("Remove") + Text(stringResource(R.string.action_remove)) } } } @@ -1241,18 +1201,18 @@ fun ClearAllDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.Info, contentDescription = null) }, - title = { Text("Confirm Destructive Action") }, - text = { Text("This will permanently delete all your books and reading progress from this device AND from your Google Drive account. This action cannot be undone. Are you sure?") }, + title = { Text(stringResource(R.string.dialog_destructive_action)) }, + text = { Text(stringResource(R.string.dialog_destructive_action_desc)) }, confirmButton = { TextButton( onClick = onConfirm, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) ) { - Text("Delete Everything") + Text(stringResource(R.string.action_delete)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }) } @@ -1276,7 +1236,7 @@ fun FpsMonitor(modifier: Modifier = Modifier) { } Text( - text = "FPS: $fps", + text = stringResource(R.string.debug_fps, fps), color = Color.Green, style = MaterialTheme.typography.labelLarge, modifier = modifier @@ -1315,12 +1275,12 @@ fun DangerousFolderActionDialog( contentColor = MaterialTheme.colorScheme.error ) ) { - Text("Confirm & Clear") + Text(stringResource(R.string.action_confirm_clear)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt index 29ec226..8d630d6 100644 --- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt +++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt @@ -115,6 +115,8 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -138,14 +140,16 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +@Composable private fun getBookCountString(count: Int): String { - return if (count == 1) "1 book" else "$count books" + return pluralStringResource(id = R.plurals.book_count, count, count) } @Composable fun LibraryScreen( viewModel: MainViewModel, ) { + val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() val selectedItems = uiState.contextualActionItems val isContextualModeActive = selectedItems.isNotEmpty() @@ -187,7 +191,7 @@ fun LibraryScreen( try { pickFolderLauncher.launch(null) } catch (_: android.content.ActivityNotFoundException) { - viewModel.showBanner("Your device doesn't support folder selection. You can still import files individually.", isError = true) + viewModel.showBanner(context.getString(R.string.error_folder_selection_unsupported), isError = true) } } @@ -222,7 +226,7 @@ fun LibraryScreen( try { fallbackFilePickerLauncher.launch("*/*") } catch (_: android.content.ActivityNotFoundException) { - viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true) + viewModel.showBanner(context.getString(R.string.error_no_file_manager), isError = true) } } } @@ -301,7 +305,7 @@ fun LibraryScreen( isLoading = uiState.isLoading, isRefreshing = uiState.isRefreshing, onOpdsBookDownloaded = { uri, title -> - viewModel.showBanner("Downloaded $title") + viewModel.showBanner(context.getString(R.string.banner_downloaded, title)) viewModel.onFileSelected(uri, isFromRecent = false) }, onStreamOpdsBook = { entry, catalog -> @@ -542,7 +546,12 @@ fun LibraryScreenContent( val isBookContextualModeActive = selectedItems.isNotEmpty() val isShelfContextualModeActive = selectedShelves.isNotEmpty() var showSortMenu by remember { mutableStateOf(false) } - val tabTitles = listOf("All Books", "Shelves", "Folders", "Catalogs") + val tabTitles = listOf( + stringResource(R.string.tab_all_books), + stringResource(R.string.tab_shelves), + stringResource(R.string.tab_folders), + stringResource(R.string.tab_catalogs) + ) val searchFocusRequester = remember { FocusRequester() } var textFieldValue by remember(isSearchActive) { @@ -603,7 +612,7 @@ fun LibraryScreenContent( textFieldValue = it onSearchQueryChange(it.text) }, - placeholder = { Text("Search title or author...") }, + placeholder = { Text(stringResource(R.string.search_placeholder)) }, modifier = Modifier .weight(1f) .padding(vertical = 4.dp) @@ -627,8 +636,7 @@ fun LibraryScreenContent( } } } else { - CustomTopAppBar( - title = { Text("Library") }, + CustomTopAppBar(title = { Text(stringResource(R.string.library_title)) }, actions = { if (pagerState.currentPage == 0) { IconButton(onClick = onFilterClick) { @@ -698,21 +706,21 @@ fun LibraryScreenContent( if (libraryFilters.fileTypes.isNotEmpty()) { AssistChip( onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) }, - label = { Text("Types: ${libraryFilters.fileTypes.joinToString { it.name }}") }, + label = { Text(stringResource(R.string.filter_types, libraryFilters.fileTypes.joinToString { it.name })) }, trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } ) } if (libraryFilters.sourceFolders.isNotEmpty()) { AssistChip( onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) }, - label = { Text("Folders: ${libraryFilters.sourceFolders.size}") }, + label = { Text(stringResource(R.string.filter_folders, libraryFilters.sourceFolders.size)) }, trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } ) } if (libraryFilters.readStatus != ReadStatusFilter.ALL) { AssistChip( onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) }, - label = { Text("Status: ${libraryFilters.readStatus.displayName}") }, + label = { Text(stringResource(R.string.filter_status, libraryFilters.readStatus.displayName)) }, trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) } ) } @@ -727,7 +735,7 @@ fun LibraryScreenContent( 0 -> { if (recentFiles.isNotEmpty()) { ExtendedFloatingActionButton( - text = { Text("Add file") }, + text = { Text(stringResource(R.string.fab_add_file)) }, icon = { Icon(Icons.Default.Add, contentDescription = "Add file") }, onClick = onSelectFileClick, modifier = Modifier.padding(16.dp) @@ -736,7 +744,7 @@ fun LibraryScreenContent( } 1 -> { ExtendedFloatingActionButton( - text = { Text("New shelf") }, + text = { Text(stringResource(R.string.fab_new_shelf)) }, icon = { Icon(Icons.Default.Add, contentDescription = "New shelf") }, onClick = onNewShelfClick, modifier = Modifier.padding(16.dp) @@ -757,12 +765,12 @@ fun LibraryScreenContent( 0 -> { if (recentFiles.isEmpty() && searchQuery.isNotEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("No results found for \"$searchQuery\"") + Text(stringResource(R.string.no_results_found, searchQuery)) } } else if (recentFiles.isEmpty()) { EmptyState( - title = "Your Library is Empty", - message = "Select a PDF, EPUB, MOBI, or AZW3 file from your device to get started.", + title = stringResource(R.string.your_library_empty), + message = stringResource(R.string.library_empty_desc), onSelectFileClick = onSelectFileClick, modifier = Modifier.fillMaxSize() ) @@ -850,12 +858,12 @@ private fun CreateShelfDialog(onConfirm: (String) -> Unit, onDismiss: () -> Unit AlertDialog( onDismissRequest = onDismiss, - title = { Text("Create New Shelf") }, + title = { Text(stringResource(R.string.create_new_shelf)) }, text = { OutlinedTextField( value = text, onValueChange = { text = it }, - placeholder = { Text("Shelf Name") }, + placeholder = { Text(stringResource(R.string.shelf_name_hint)) }, singleLine = true, modifier = Modifier.focusRequester(focusRequester) ) @@ -865,12 +873,12 @@ private fun CreateShelfDialog(onConfirm: (String) -> Unit, onDismiss: () -> Unit onClick = { onConfirm(text) }, enabled = text.isNotBlank() ) { - Text("Create") + Text(stringResource(R.string.action_create)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) @@ -981,14 +989,14 @@ private fun ShelfDetailScreen( onDismissRequest = { showMoreMenu = false } ) { DropdownMenuItem( - text = { Text("Rename shelf") }, + text = { Text(stringResource(R.string.menu_rename_shelf)) }, onClick = { onRenameShelf() showMoreMenu = false } ) DropdownMenuItem( - text = { Text("Delete shelf") }, + text = { Text(stringResource(R.string.menu_delete_shelf)) }, onClick = { onDeleteShelf() showMoreMenu = false @@ -1006,7 +1014,7 @@ private fun ShelfDetailScreen( ExtendedFloatingActionButton( onClick = onAddBooksClick, icon = { Icon(Icons.Default.Add, contentDescription = null) }, - text = { Text("Add books") } + text = { Text(stringResource(R.string.fab_add_books)) } ) } } @@ -1016,7 +1024,7 @@ private fun ShelfDetailScreen( modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center ) { - Text("This shelf is empty", style = MaterialTheme.typography.bodyLarge) + Text(stringResource(R.string.shelf_empty), style = MaterialTheme.typography.bodyLarge) } } else { LazyColumn( @@ -1059,7 +1067,7 @@ private fun AddBooksModeScreen( modifier = Modifier.statusBarsPadding(), topBar = { CustomTopAppBar( - title = { Text("Add to $shelfName") }, + title = { Text(stringResource(R.string.add_to_shelf, shelfName)) }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") @@ -1127,7 +1135,7 @@ private fun AddBooksModeScreen( floatingActionButton = { if (selectedBookUris.isNotEmpty()) { ExtendedFloatingActionButton( - text = { Text("ADD (${selectedBookUris.size})") }, + text = { Text(stringResource(R.string.fab_add_count, selectedBookUris.size)) }, icon = { Icon(Icons.Default.Check, contentDescription = "Add books") }, onClick = onAddSelectedBooks ) @@ -1140,7 +1148,7 @@ private fun AddBooksModeScreen( contentAlignment = Alignment.Center ) { Text( - text = if (currentSource == AddBooksSource.UNSHELVED) "No unshelved books to add" else "All books are already in this shelf", + text = if (currentSource == AddBooksSource.UNSHELVED) stringResource(R.string.no_unshelved_books) else stringResource(R.string.all_books_in_shelf), style = MaterialTheme.typography.bodyLarge ) } @@ -1431,12 +1439,12 @@ private fun RenameShelfDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text("Rename Shelf") }, + title = { Text(stringResource(R.string.menu_rename_shelf)) }, text = { OutlinedTextField( value = textFieldValue, onValueChange = { textFieldValue = it }, - placeholder = { Text("Shelf Name") }, + placeholder = { Text(stringResource(R.string.shelf_name_hint)) }, singleLine = true, modifier = Modifier.focusRequester(focusRequester) ) @@ -1446,12 +1454,12 @@ private fun RenameShelfDialog( onClick = { onConfirm(textFieldValue.text) }, enabled = textFieldValue.text.isNotBlank() && textFieldValue.text != initialName ) { - Text("Rename") + Text(stringResource(R.string.action_rename)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) @@ -1470,13 +1478,13 @@ private fun DeleteShelfConfirmationDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete Shelf?") }, - text = { Text("Are you sure you want to delete the '$shelfName' shelf? All books will be moved to Unshelved.") }, + title = { Text(stringResource(R.string.dialog_delete_shelf)) }, + text = { Text(stringResource(R.string.dialog_delete_shelf_desc)) }, confirmButton = { - TextButton(onClick = onConfirm) { Text("Delete") } + TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_delete)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -1488,16 +1496,16 @@ private fun RemoveFromShelfConfirmationDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { - val bookStr = if (count == 1) "book" else "books" + val bookStr = pluralStringResource(id = R.plurals.book_word, count) AlertDialog( onDismissRequest = onDismiss, - title = { Text("Remove from Shelf?") }, - text = { Text("Are you sure you want to remove $count $bookStr from the '$shelfName' shelf? The book(s) will remain in your library and appear under Unshelved.") }, + title = { Text(stringResource(R.string.dialog_remove_from_shelf)) }, + text = { Text(stringResource(R.string.dialog_remove_from_shelf_desc, count, bookStr, shelfName)) }, confirmButton = { - TextButton(onClick = onConfirm) { Text("Remove") } + TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_remove)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -1508,16 +1516,16 @@ private fun DeleteShelvesConfirmationDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { - val shelfStr = if (count == 1) "shelf" else "shelves" + val shelfStr = pluralStringResource(id = R.plurals.shelf_count, count) AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete $shelfStr?") }, - text = { Text("Are you sure you want to delete the $count selected $shelfStr? All books within will be moved to Unshelved.") }, + title = { Text(stringResource(R.string.dialog_delete_shelves, shelfStr)) }, + text = { Text(stringResource(R.string.dialog_delete_shelves_desc, count, shelfStr)) }, confirmButton = { - TextButton(onClick = onConfirm) { Text("Delete") } + TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_delete)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -1539,7 +1547,7 @@ private fun FolderSyncScreen( floatingActionButton = { if (syncedFolders.size < 3) { ExtendedFloatingActionButton( - text = { Text("Add Folder") }, + text = { Text(stringResource(R.string.fab_add_folder)) }, icon = { Icon(Icons.Default.Add, "Add") }, onClick = onAddFolderClick ) @@ -1570,7 +1578,7 @@ private fun FolderSyncScreen( Icon(Icons.Default.Search, null, modifier = Modifier.size(18.dp)) } Spacer(modifier = Modifier.width(8.dp)) - Text(if (isLoading) "Scanning..." else "Scan All") + Text(if (isLoading) stringResource(R.string.scanning) else stringResource(R.string.scan_all)) } androidx.compose.material3.OutlinedButton( @@ -1581,15 +1589,15 @@ private fun FolderSyncScreen( ) { Icon(painterResource(id = R.drawable.sync), null, modifier = Modifier.size(18.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text("Sync Meta") + Text(stringResource(R.string.sync_meta)) } } } else { EmptyState( - title = "Sync Local Folders", - message = "Connect local folders to create a live library. Episteme will monitor files and sync progress.", + title = stringResource(R.string.sync_local_folders), + message = stringResource(R.string.sync_folders_desc), onSelectFileClick = onAddFolderClick, - primaryButtonText = "Select Folder", + primaryButtonText = stringResource(R.string.action_select_folder), modifier = Modifier.fillMaxSize() ) } @@ -1632,7 +1640,7 @@ private fun FolderCard( ) { var showMenu by remember { mutableStateOf(false) } val dateFormat = remember { SimpleDateFormat("MMM d, h:mm a", Locale.getDefault()) } - val lastScanText = if (folder.lastScanTime == 0L) "Never" else dateFormat.format(Date(folder.lastScanTime)) + val lastScanText = if (folder.lastScanTime == 0L) stringResource(R.string.never) else dateFormat.format(Date(folder.lastScanTime)) val folderFiles = remember(allRecentFiles, folder.uriString) { allRecentFiles.filter { it.sourceFolderUri == folder.uriString } @@ -1676,14 +1684,14 @@ private fun FolderCard( } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { DropdownMenuItem( - text = { Text("Edit Filters") }, + text = { Text(stringResource(R.string.menu_edit_filters)) }, onClick = { showMenu = false onEditFiltersClick(folder) } ) DropdownMenuItem( - text = { Text("Remove Folder") }, + text = { Text(stringResource(R.string.menu_remove_folder)) }, onClick = { showMenu = false onRemoveClick(folder) @@ -1701,7 +1709,7 @@ private fun FolderCard( Row(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.weight(1f)) { Text( - text = "LAST SYNC", + text = stringResource(R.string.last_sync), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold @@ -1711,7 +1719,7 @@ private fun FolderCard( Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) { Text( - text = "BOOKS", + text = stringResource(R.string.books_count), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold @@ -1730,7 +1738,7 @@ private fun FolderCard( countsByType.forEach { (type, count) -> AssistChip( onClick = { }, - label = { Text("${type.name}: $count") } + label = { Text(stringResource(R.string.folder_filter_count, type.name, count)) } ) } } @@ -1749,11 +1757,11 @@ private fun EditFolderFiltersDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text("Filter File Types") }, + title = { Text(stringResource(R.string.filter_file_types)) }, text = { Column { Text( - text = "Select the file types you want to sync from this folder:", + stringResource(R.string.filter_file_types_desc), style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(8.dp)) @@ -1783,10 +1791,10 @@ private fun EditFolderFiltersDialog( TextButton( onClick = { onConfirm(selectedTypes) }, enabled = selectedTypes.isNotEmpty() - ) { Text("Save") } + ) { Text(stringResource(R.string.action_save)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -1812,9 +1820,9 @@ fun LibraryFilterSheet( .padding(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text("Filter Library", style = MaterialTheme.typography.titleLarge) + Text(stringResource(R.string.filter_library), style = MaterialTheme.typography.titleLarge) - Text("File Type", style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.filter_file_type), style = MaterialTheme.typography.titleMedium) Row( modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -1832,7 +1840,7 @@ fun LibraryFilterSheet( } if (syncedFolders.isNotEmpty()) { - Text("Source Folder", style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.filter_source_folder), style = MaterialTheme.typography.titleMedium) Row( modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -1850,7 +1858,7 @@ fun LibraryFilterSheet( } } - Text("Read Status", style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.filter_read_status), style = MaterialTheme.typography.titleMedium) Row( modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -1866,11 +1874,11 @@ fun LibraryFilterSheet( Row(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalArrangement = Arrangement.End) { TextButton(onClick = { currentFilters = LibraryFilters() }) { - Text("Clear All") + Text(stringResource(R.string.clear_all)) } Spacer(modifier = Modifier.width(8.dp)) androidx.compose.material3.Button(onClick = { onApply(currentFilters); onDismiss() }) { - Text("Apply") + Text(stringResource(R.string.action_apply)) } } Spacer(modifier = Modifier.height(32.dp)) @@ -1930,7 +1938,7 @@ fun OpdsTab( } ExtendedFloatingActionButton( - text = { Text("Add Catalog") }, + text = { Text(stringResource(R.string.fab_add_catalog)) }, icon = { Icon(Icons.Default.Add, "Add") }, onClick = { editingCatalog = null @@ -1981,7 +1989,7 @@ fun OpdsTab( OutlinedTextField( value = query, onValueChange = { query = it }, - placeholder = { Text("Search catalog...") }, + placeholder = { Text(stringResource(R.string.search_catalog_placeholder)) }, modifier = Modifier.weight(1f).padding(vertical = 4.dp) .focusRequester(searchFocusRequester), singleLine = true, @@ -2017,7 +2025,7 @@ fun OpdsTab( ) } else { Text( - text = uiState.currentFeed?.title ?: "Loading...", + text = uiState.currentFeed?.title ?: stringResource(R.string.status_loading), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, maxLines = 1, @@ -2045,7 +2053,7 @@ fun OpdsTab( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text("This feed is empty.") + Text(stringResource(R.string.feed_empty)) } } else { val facets = uiState.currentFeed?.facets ?: emptyList() @@ -2066,7 +2074,7 @@ fun OpdsTab( FilterChip( selected = activeFacet?.isActive == true, onClick = { expanded = true }, - label = { Text("${groupName}: ${activeFacet?.title ?: "Select"}") }, + label = { Text(stringResource(R.string.filter_facet, groupName, activeFacet?.title ?: stringResource(R.string.action_select))) }, trailingIcon = { Icon( Icons.Default.ArrowDropDown, @@ -2194,24 +2202,23 @@ fun OpdsTab( showCatalogDialog = false editingCatalog = null }, - title = { Text(if (isEditMode) "Edit Catalog" else "Add OPDS Catalog") }, + title = { Text(if (isEditMode) stringResource(R.string.edit_catalog) else stringResource(R.string.add_opds_catalog)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( value = newTitle, onValueChange = { newTitle = it }, - label = { Text("Catalog Name") }, + label = { Text(stringResource(R.string.catalog_name)) }, singleLine = true ) OutlinedTextField( value = newUrl, onValueChange = { newUrl = it }, - label = { Text("URL") }, - placeholder = { Text("e.g. http://192.168.1.50:8080/opds") }, + label = { Text(stringResource(R.string.url)) }, + placeholder = { Text(stringResource(R.string.url_placeholder)) }, singleLine = true ) - Text( - "Authentication (Optional)", + Text(stringResource(R.string.auth_optional), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(top = 8.dp) @@ -2219,13 +2226,13 @@ fun OpdsTab( OutlinedTextField( value = newUsername, onValueChange = { newUsername = it }, - label = { Text("Username") }, + label = { Text(stringResource(R.string.username)) }, singleLine = true ) OutlinedTextField( value = newPassword, onValueChange = { newPassword = it }, - label = { Text("Password") }, + label = { Text(stringResource(R.string.password)) }, singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Password) @@ -2244,13 +2251,13 @@ fun OpdsTab( editingCatalog = null }, enabled = newTitle.isNotBlank() && newUrl.isNotBlank() - ) { Text("Save") } + ) { Text(stringResource(R.string.action_save)) } }, dismissButton = { TextButton(onClick = { showCatalogDialog = false editingCatalog = null - }) { Text("Cancel") } + }) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -2259,14 +2266,14 @@ fun OpdsTab( val streamedBooksCount = localLibraryFiles.count { it.uriString?.contains("catalogId=${catalogToDelete!!.id}") == true } AlertDialog( onDismissRequest = { catalogToDelete = null }, - title = { Text("Delete Catalog") }, + title = { Text(stringResource(R.string.delete_catalog)) }, text = { Column { - Text("Are you sure you want to delete '${catalogToDelete!!.title}'?") + Text(stringResource(R.string.delete_catalog_desc, catalogToDelete!!.title)) if (streamedBooksCount > 0) { Spacer(modifier = Modifier.height(8.dp)) Text( - "Deleting this catalog will also permanently remove $streamedBooksCount streaming books associated with it from your library.", + stringResource(R.string.delete_catalog_warning, streamedBooksCount), color = MaterialTheme.colorScheme.error ) } @@ -2282,10 +2289,10 @@ fun OpdsTab( catalogToDelete = null }, colors = androidx.compose.material3.ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) - ) { Text("Delete") } + ) { Text(stringResource(R.string.action_delete)) } }, dismissButton = { - TextButton(onClick = { catalogToDelete = null }) { Text("Cancel") } + TextButton(onClick = { catalogToDelete = null }) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -2314,8 +2321,7 @@ fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Un color = MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.small ) { - Text( - text = "Preset", + Text(stringResource(R.string.preset_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) @@ -2419,12 +2425,12 @@ fun OpdsBookCard( ) { Icon(Icons.Default.Check, null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("Read") + Text(stringResource(R.string.action_read)) } } else if (isDownloading) { Column(modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically) { - Text("Downloading...", style = MaterialTheme.typography.labelMedium) + Text(stringResource(R.string.status_downloading), style = MaterialTheme.typography.labelMedium) Spacer(modifier = Modifier.weight(1f)) if (progress != null) { Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelMedium) @@ -2446,7 +2452,7 @@ fun OpdsBookCard( ) { Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("Stream") + Text(stringResource(R.string.action_stream)) } } @@ -2465,11 +2471,11 @@ fun OpdsBookCard( if (uniqueAcquisitions.isEmpty()) { Icon(Icons.Default.Info, null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("Unavailable") + Text(stringResource(R.string.action_unavailable)) } else { Icon(Icons.Default.Add, null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("Download") + Text(stringResource(R.string.action_download)) } } } @@ -2586,14 +2592,14 @@ fun OpdsBookDetailsSheet( ) { Icon(Icons.Default.Check, contentDescription = "Read") Spacer(modifier = Modifier.width(8.dp)) - Text("Read", fontWeight = FontWeight.Bold) + Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold) } } if (isDownloading) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Text("Downloading...", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.status_downloading), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.weight(1f)) if (progress != null) { Text("${(progress * 100).toInt()}%", style = MaterialTheme.typography.titleMedium) @@ -2618,14 +2624,13 @@ fun OpdsBookDetailsSheet( ) { Icon(painterResource(id = R.drawable.play), null, modifier = Modifier.size(18.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text("Stream Now", fontWeight = FontWeight.Bold) + Text(stringResource(R.string.action_stream_now), fontWeight = FontWeight.Bold) } Spacer(modifier = Modifier.height(16.dp)) } if (uniqueAcquisitions.isNotEmpty()) { - Text( - "Download Format", + Text(stringResource(R.string.download_format), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -2643,7 +2648,7 @@ fun OpdsBookDetailsSheet( } } } else { - Text("No supported formats available.", color = MaterialTheme.colorScheme.error) + Text(stringResource(R.string.no_supported_formats), color = MaterialTheme.colorScheme.error) } if (entry.categories.isNotEmpty()) { @@ -2681,20 +2686,20 @@ fun OpdsBookDetailsSheet( ) { entry.publisher?.takeIf { it.isNotBlank() }?.let { Column(modifier = Modifier.weight(1f)) { - Text("PUBLISHER", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.publisher), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(it, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis) } } entry.published?.takeIf { it.isNotBlank() }?.let { Column(modifier = Modifier.weight(1f)) { - Text("PUBLISHED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.published), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) val cleanDate = it.substringBefore("T") Text(cleanDate, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) } } entry.language?.takeIf { it.isNotBlank() }?.let { Column(modifier = Modifier.weight(1f)) { - Text("LANGUAGE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.language), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(it.uppercase(), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) } } @@ -2703,7 +2708,7 @@ fun OpdsBookDetailsSheet( } if (!entry.summary.isNullOrBlank()) { - Text("Synopsis", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.synopsis), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) val cleanSummary = remember(entry.summary) { val preProcessed = entry.summary diff --git a/app/src/main/java/com/aryan/reader/MainScreen.kt b/app/src/main/java/com/aryan/reader/MainScreen.kt index 127af1c..cc624c8 100644 --- a/app/src/main/java/com/aryan/reader/MainScreen.kt +++ b/app/src/main/java/com/aryan/reader/MainScreen.kt @@ -40,13 +40,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import kotlinx.coroutines.launch -sealed class BottomBarScreen(val route: String, val label: String, val iconResId: Int) { - object Home : BottomBarScreen("home", "Home", R.drawable.home) - object Library : BottomBarScreen("library", "Library", R.drawable.library_books) +sealed class BottomBarScreen(val route: String, val stringResId: Int, val iconResId: Int) { + object Home : BottomBarScreen("home", R.string.nav_home, R.drawable.home) + object Library : BottomBarScreen("library", R.string.nav_library, R.drawable.library_books) } private val bottomBarItems = listOf( @@ -94,14 +95,10 @@ fun MainScreen( NavigationBar { bottomBarItems.forEachIndexed { index, screen -> NavigationBarItem( - icon = { Icon(painterResource(id = screen.iconResId), contentDescription = screen.label) }, - label = { Text(screen.label) }, + icon = { Icon(painterResource(id = screen.iconResId), contentDescription = stringResource(screen.stringResId)) }, + label = { Text(stringResource(screen.stringResId)) }, selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - } + onClick = { scope.launch { pagerState.animateScrollToPage(index) } } ) } } diff --git a/app/src/main/java/com/aryan/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt index 0f063bd..7eeea57 100644 --- a/app/src/main/java/com/aryan/reader/MainViewModel.kt +++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt @@ -494,7 +494,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } recentFilesRepository.deleteFilePermanently(ids) withContext(Dispatchers.Main) { - showBanner("Removed ${filesToDelete.size} streaming books.") + showBanner(appContext.getString(R.string.banner_removed_streaming_books, filesToDelete.size)) } } } @@ -806,7 +806,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio uploadNewFont(font) } }.onFailure { - showBanner("Failed to import font: ${it.message}", isError = true) + showBanner(appContext.getString(R.string.error_import_font, it.message), isError = true) } _internalState.update { it.copy(isLoading = false) } } @@ -848,7 +848,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio withContext(Dispatchers.Main) { onDeleted() - showBanner("Text view deleted.") + showBanner(appContext.getString(R.string.banner_text_view_deleted)) } } } @@ -878,7 +878,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio it.copy(isRequestingDrivePermission = false, isSyncEnabled = false) } prefs.edit { putBoolean(KEY_SYNC_ENABLED, false) } - showBanner("Sync requires Google Drive permission.", isError = true) + showBanner(appContext.getString(R.string.error_sync_drive_permission), isError = true) } private fun verifyPurchaseWithBackend( @@ -890,9 +890,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (!isSilentMigrationCheck) { _internalState.update { it.copy( - bannerMessage = BannerMessage( - "An error occurred with the purchase.", isError = true - ) + bannerMessage = BannerMessage(appContext.getString(R.string.error_purchase_general), isError = true) ) } } @@ -905,7 +903,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (result.isSuccess) { Timber.i("Backend verification successful. Firestore will update the app.") _internalState.update { - it.copy(bannerMessage = BannerMessage("Upgrade successful! Welcome to Pro.")) + it.copy(bannerMessage = BannerMessage(appContext.getString(R.string.banner_upgrade_success))) } verifyDeviceForProUser() } else { @@ -915,8 +913,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio "Migration check: Purchase token is already claimed by another account. Silently ignoring." ) } else { - val errorMessage = - "Purchase verification failed. Please contact support if you were charged." + val errorMessage = appContext.getString(R.string.error_purchase_verification) Timber.e(exception, "Backend verification failed") if (!isSilentMigrationCheck) { _internalState.update { @@ -950,7 +947,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio Timber.w("Device has been revoked. Signing out.") firestoreRepository.deleteDevice(currentUser.uid, deviceId) // Clean up signOut() - showBanner("This device was removed from your account.") + showBanner(appContext.getString(R.string.banner_device_removed)) } is com.aryan.reader.data.DeviceStatus.NotFound -> { @@ -962,7 +959,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio Timber.e(deviceStatus.exception, "Error checking device status.") _internalState.update { it.copy( - errorMessage = "Could not verify this device. Please check your connection." + errorMessage = appContext.getString(R.string.error_verify_device) ) } } @@ -998,7 +995,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio Timber.e("Failed to replace device.") _internalState.update { it.copy( - errorMessage = "Failed to update devices. Please try again.", + errorMessage = appContext.getString(R.string.error_update_devices), isReplacingDevice = false ) } @@ -1044,7 +1041,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio ) { viewModelScope.launch { _internalState.update { - it.copy(isLoading = true, bannerMessage = BannerMessage("Saving PDF...")) + it.copy(isLoading = true, bannerMessage = BannerMessage(appContext.getString(R.string.banner_saving_pdf))) } try { val virtualPages = pageLayoutRepository.getLayoutOrNull(bookId) @@ -1060,13 +1057,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio textBoxes = textBoxes, highlights = highlights ) - showBanner("PDF saved successfully.") + showBanner(appContext.getString(R.string.banner_pdf_saved)) } else { - showBanner("Failed to open file for saving.", isError = true) + showBanner(appContext.getString(R.string.error_open_file_saving), isError = true) } } catch (e: Exception) { Timber.e(e, "Failed to save annotated PDF") - showBanner("Error saving PDF: ${e.localizedMessage}", isError = true) + showBanner(appContext.getString(R.string.error_saving_pdf, e.localizedMessage), isError = true) } finally { _internalState.update { it.copy(isLoading = false) } } @@ -1076,7 +1073,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio fun saveOriginalPdf(sourceUri: Uri, destUri: Uri) { viewModelScope.launch { _internalState.update { - it.copy(isLoading = true, bannerMessage = BannerMessage("Saving original PDF...")) + it.copy(isLoading = true, bannerMessage = BannerMessage(appContext.getString(R.string.banner_saving_original_pdf))) } try { val contentResolver = appContext.contentResolver @@ -1085,10 +1082,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio input.copyTo(output) } } - showBanner("Original PDF saved successfully.") + showBanner(appContext.getString(R.string.banner_original_pdf_saved)) } catch (e: Exception) { Timber.e(e, "Failed to save original PDF") - showBanner("Error saving PDF: ${e.localizedMessage}", isError = true) + showBanner(appContext.getString(R.string.error_saving_pdf, e.localizedMessage), isError = true) } finally { _internalState.update { it.copy(isLoading = false) } } @@ -1190,14 +1187,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio putExtra(Intent.EXTRA_STREAM, contentUri) putExtra(Intent.EXTRA_TITLE, filename) - putExtra(Intent.EXTRA_SUBJECT, "Sharing: $filename") + putExtra(Intent.EXTRA_SUBJECT, appContext.getString(R.string.share_subject, filename)) clipData = ClipData.newRawUri(filename, contentUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val chooser = Intent.createChooser(shareIntent, "Share PDF") + val chooser = Intent.createChooser(shareIntent, appContext.getString(R.string.share_chooser_title)) if (activityContext !is android.app.Activity) { chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -1206,7 +1203,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio withContext(Dispatchers.Main) { activityContext.startActivity(chooser) } } catch (e: Exception) { Timber.e(e, "Share failed") - showBanner("Share failed: ${e.localizedMessage}", isError = true) + showBanner(appContext.getString(R.string.error_share_failed, e.localizedMessage), isError = true) } } } @@ -1507,12 +1504,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio val currentFolders = _internalState.value.syncedFolders if (currentFolders.size >= MAX_FOLDER_LIMIT) { - showBanner("Limit reached: Maximum $MAX_FOLDER_LIMIT folders allowed.", isError = true) + showBanner(appContext.getString(R.string.error_folder_limit_reached, MAX_FOLDER_LIMIT), isError = true) return } if (currentFolders.any { it.uriString == folderUri.toString() }) { - showBanner("This folder is already synced.", isError = true) + showBanner(appContext.getString(R.string.error_folder_already_synced), isError = true) return } @@ -1547,11 +1544,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio FolderSyncWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, syncRequest ) - showBanner("Folder added: $name") + showBanner(appContext.getString(R.string.banner_folder_added, name)) } catch (e: SecurityException) { Timber.e(e, "Failed to take permissions for $folderUri") - showBanner("Failed to access folder permissions.", isError = true) + showBanner(appContext.getString(R.string.error_access_folder_permissions), isError = true) } } } @@ -1581,7 +1578,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio WorkManager.getInstance(appContext).cancelUniqueWork(FolderSyncWorker.WORK_NAME) } - showBanner("Folder removed.") + showBanner(appContext.getString(R.string.banner_folder_removed)) } } @@ -1616,8 +1613,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio when (workInfo.state) { WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> { if (showFeedback) { - val msg = - if (metadataOnly) "Folder Sync: Updating metadata..." else "Scanning folder for new books..." + val msg = if (metadataOnly) appContext.getString(R.string.banner_folder_sync_updating) else appContext.getString(R.string.banner_folder_sync_scanning) _internalState.update { it.copy( isLoading = false, @@ -1633,7 +1629,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio it.copy( isLoading = false, isRefreshing = false, - bannerMessage = if (showFeedback) BannerMessage("Folder Sync: Scan complete.") else it.bannerMessage, + bannerMessage = if (showFeedback) BannerMessage(appContext.getString(R.string.banner_folder_sync_complete)) else it.bannerMessage, lastFolderScanTime = System.currentTimeMillis(), syncedFolders = loadSyncedFoldersFromPrefs() ) @@ -1645,7 +1641,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio it.copy( isLoading = false, isRefreshing = false, - errorMessage = if (showFeedback) "Sync failed." else it.errorMessage, + errorMessage = if (showFeedback) appContext.getString(R.string.error_sync_failed) else it.errorMessage, bannerMessage = null ) } @@ -1748,7 +1744,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio private fun downloadBook(item: RecentFileItem, openWhenComplete: Boolean = false): Job { if (!uiState.value.isSyncEnabled) { - _internalState.update { it.copy(errorMessage = "Enable sync to download files.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_enable_sync_download)) } return viewModelScope.launch {} } if (uiState.value.downloadingBookIds.contains(item.bookId)) { @@ -1800,7 +1796,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Failed to download book ${item.bookId}") _internalState.update { - it.copy(errorMessage = "Failed to download ${item.displayName}.") + it.copy(errorMessage = appContext.getString(R.string.error_download_failed, item.displayName)) } } finally { _internalState.update { state -> @@ -1812,13 +1808,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio fun deleteAllCloudAndLocalData() { if (!uiState.value.isSyncEnabled) { - _internalState.update { it.copy(errorMessage = "Enable sync to clear cloud data.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_enable_sync_clear_cloud)) } return } if (!googleDriveRepository.isUserSignedInToDrive(appContext)) { _internalState.update { - it.copy(errorMessage = "Not signed in, cannot clear cloud data.") + it.copy(errorMessage = appContext.getString(R.string.error_not_signed_in_clear_cloud)) } return } @@ -1826,7 +1822,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = true, - bannerMessage = BannerMessage("Clearing all cloud and local data...") + bannerMessage = BannerMessage(appContext.getString(R.string.banner_clearing_cloud_local_data)) ) } @@ -1855,9 +1851,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (success) { _internalState.update { it.copy( - isLoading = false, bannerMessage = BannerMessage( - "All cloud and local data cleared successfully." - ) + isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_local_data_cleared)) ) } } else { @@ -1866,7 +1860,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Failed to delete all cloud and local user data.") _internalState.update { - it.copy(isLoading = false, errorMessage = "Error: Failed to clear all data.") + it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_clear_all_data)) } } } @@ -1913,13 +1907,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( - isLoading = false, bannerMessage = BannerMessage("All local data cleared.") + isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_local_data_cleared)) ) } } catch (e: Exception) { Timber.e(e, "Failed to delete all user data.") _internalState.update { - it.copy(isLoading = false, errorMessage = "Error: Failed to clear all data.") + it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_clear_all_data)) } } } @@ -1933,9 +1927,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (user == null) { _internalState.update { it.copy( - bannerMessage = BannerMessage( - "Sign in failed. Please try again.", isError = true - ), isLoading = false + bannerMessage = BannerMessage(appContext.getString(R.string.error_sign_in_failed), isError = true), isLoading = false ) } } else { @@ -1950,9 +1942,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "An unexpected error occurred during sign-in.") val errorMessage = if (e is NoCredentialException) { - "Could not find a Google account. This can happen on a fresh install, please try again in a moment." + appContext.getString(R.string.error_no_google_account) } else { - "An error occurred during sign in. Please check your internet connection." + appContext.getString(R.string.error_sign_in_internet) } _internalState.update { it.copy( @@ -2003,7 +1995,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio ) } } ?: run { - showBanner("Please sign in to test device management.", isError = true) + showBanner(appContext.getString(R.string.error_sign_in_device_management), isError = true) } } } @@ -2020,7 +2012,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio fun setSyncEnabled(enabled: Boolean) { if (!uiState.value.isProUser) { Timber.d("Sync toggle blocked for free user.") - _internalState.update { it.copy(errorMessage = "Sync is an Episteme Pro feature.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_sync_pro_feature)) } return } @@ -2054,14 +2046,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (!hasPermissions || currentUser == null) { if (showBanner) _internalState.update { - it.copy(errorMessage = "Not signed in, cannot sync.") + it.copy(errorMessage = appContext.getString(R.string.error_not_signed_in_sync)) } return@launch } if (showBanner) { _internalState.update { - it.copy(bannerMessage = BannerMessage("Cloud Sync: Checking for updates...")) + it.copy(bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_sync_checking))) } } @@ -2281,7 +2273,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (showBanner) { _internalState.update { it.copy( - isLoading = false, bannerMessage = BannerMessage("Cloud Sync: Complete.") + isLoading = false, bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_sync_complete)) ) } } @@ -2289,7 +2281,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio Timber.tag("AnnotationSync").e(e, "Error during cloud sync") if (showBanner) { _internalState.update { - it.copy(isLoading = false, errorMessage = "Failed to sync library.") + it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_sync_library_failed)) } } } @@ -2654,14 +2646,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) { if (isFromRecent) { Timber.i("Opening recent file: $uri") - // This path is now handled by onRecentFileClicked to preserve the bookId - // We find the book by URI to open it. viewModelScope.launch { val item = recentFilesRepository.getFileByUri(uri.toString()) if (item != null) { openBook(uri, item.bookId, item.type, item.displayName) } else { - _internalState.update { it.copy(errorMessage = "Could not find recent item.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_recent_item_not_found)) } } } } else { @@ -2698,7 +2688,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } } _internalState.update { - it.copy(isLoading = false, errorMessage = "Failed to import file.") + it.copy(isLoading = false, errorMessage = appContext.getString(R.string.error_import_file_failed)) } } } @@ -2736,7 +2726,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio val uri = item.getUri() ?: run { _internalState.update { it.copy( - isLoading = false, errorMessage = "Could not find file location." + isLoading = false, errorMessage = appContext.getString(R.string.error_file_location_not_found) ) } stateUpdateDeferred.complete(false) @@ -2822,7 +2812,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = false, - errorMessage = "Failed to load generated text view.", + errorMessage = appContext.getString(R.string.error_load_generated_text_view), selectedFileType = null ) } @@ -2884,10 +2874,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (newItem != null) { switchToFileSeamlessly(newItem, autoOpenPage) } else { - showBanner("Failed to load generated text view.", true) + showBanner(appContext.getString(R.string.error_load_generated_text_view), true) } } else { - showBanner("Text view generation failed.", true) + showBanner(appContext.getString(R.string.error_text_view_generation_failed), true) } } } @@ -3076,7 +3066,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Error parsing FB2 for URI: $uri") _internalState.update { - it.copy(errorMessage = "Failed to load FB2: ${e.message}", isLoading = false) + it.copy(errorMessage = appContext.getString(R.string.error_load_fb2, e.message), isLoading = false) } } } @@ -3131,7 +3121,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Error parsing file ($type) for URI: $uri") _internalState.update { - it.copy(errorMessage = "Failed to load file: ${e.message}", isLoading = false) + it.copy(errorMessage = appContext.getString(R.string.error_load_file, e.message), isLoading = false) } } } @@ -3271,7 +3261,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Error parsing MOBI for URI: $uri") _internalState.update { - it.copy(errorMessage = "Failed to load MOBI: ${e.message}", isLoading = false) + it.copy(errorMessage = appContext.getString(R.string.error_load_mobi, e.message), isLoading = false) } } } @@ -3318,7 +3308,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } catch (e: Exception) { Timber.e(e, "Error parsing EPUB for URI: $uri") _internalState.update { - it.copy(errorMessage = "Failed to load EPUB: ${e.message}", isLoading = false) + it.copy(errorMessage = appContext.getString(R.string.error_load_epub, e.message), isLoading = false) } } } @@ -3454,7 +3444,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio Timber.tag("FolderSync") .i("LazyCleanup: File ${item.displayName} missing. Removing.") recentFilesRepository.deleteFilePermanently(listOf(item.bookId)) - showBanner("File deleted from folder. Removed from library.") + showBanner(appContext.getString(R.string.banner_file_deleted_from_folder)) return@launch } @@ -3463,7 +3453,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio item.getUri()?.let { uri -> openBook(uri, item.bookId, item.type, item.displayName) } ?: run { - _internalState.update { it.copy(errorMessage = "Could not find file location.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_file_location_not_found)) } } } else { downloadBook(item, openWhenComplete = true) @@ -3477,7 +3467,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio item.getUri()?.let { uri -> openBook(uri, item.bookId, item.type, item.displayName) } ?: run { - _internalState.update { it.copy(errorMessage = "Could not find file location.") } + _internalState.update { it.copy(errorMessage = appContext.getString(R.string.error_file_location_not_found)) } return } } else { @@ -3618,7 +3608,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (newName in currentShelves) { Timber.w("Cannot rename shelf. A shelf with the name '$newName' already exists.") _internalState.update { - it.copy(errorMessage = "A shelf with that name already exists.") + it.copy(errorMessage = appContext.getString(R.string.error_shelf_exists)) } dismissRenameShelfDialog() return @@ -3938,7 +3928,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = true, - bannerMessage = BannerMessage("Deleting from all devices...") + bannerMessage = BannerMessage(appContext.getString(R.string.banner_deleting_all_devices)) ) } try { @@ -3974,7 +3964,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = false, - bannerMessage = BannerMessage("Deletion complete.") + bannerMessage = BannerMessage(appContext.getString(R.string.banner_deletion_complete)) ) } } catch (e: Exception) { @@ -3986,7 +3976,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = false, - errorMessage = "Cloud sync failed, deleted locally." + errorMessage = appContext.getString(R.string.error_cloud_sync_failed_deleted_locally) ) } } @@ -4003,7 +3993,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio _internalState.update { it.copy( isLoading = false, - bannerMessage = BannerMessage("$totalRemoved book(s) removed from library.") + bannerMessage = BannerMessage(appContext.getString(R.string.banner_books_removed_library, totalRemoved)) ) } } @@ -4118,7 +4108,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } withContext(Dispatchers.Main) { - showBanner("Reflow cache & generated text views cleared.") + showBanner(appContext.getString(R.string.banner_reflow_cache_cleared)) } } } diff --git a/app/src/main/java/com/aryan/reader/ProScreen.kt b/app/src/main/java/com/aryan/reader/ProScreen.kt index d6177ef..dbe18f4 100644 --- a/app/src/main/java/com/aryan/reader/ProScreen.kt +++ b/app/src/main/java/com/aryan/reader/ProScreen.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Color 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.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -175,8 +176,7 @@ fun ProScreen( shape = CircleShape ), text = { - AutoSizeText( - "Free", + AutoSizeText(stringResource(R.string.tab_free), style = LocalTextStyle.current.copy( color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold @@ -209,8 +209,7 @@ fun ProScreen( tint = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(4.dp)) - AutoSizeText( - "Episteme Pro", + AutoSizeText(stringResource(R.string.drawer_pro_unlocked), style = LocalTextStyle.current.copy( color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.SemiBold @@ -267,19 +266,16 @@ private fun FreeTierCard() { .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "Free Plan", + Text(stringResource(R.string.free_plan), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "$0", + Text(stringResource(R.string.price_free), style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp), fontWeight = FontWeight.Bold ) - Text( - text = "Forever free", + Text(stringResource(R.string.forever_free), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -289,23 +285,20 @@ private fun FreeTierCard() { modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start ) { - FeatureListItem(iconRes = R.drawable.library_books, text = "Multiple Formats") - Text( - text = "Supports PDF, EPUB, MOBI, AZW3", + FeatureListItem(iconRes = R.drawable.library_books, text = stringResource(R.string.feature_multiple_formats)) + Text(stringResource(R.string.feature_multiple_formats_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.text_to_speech, text = "Android Text-to-Speech") - Text( - text = "Listen to your books with built-in TTS", + FeatureListItem(iconRes = R.drawable.text_to_speech, text = stringResource(R.string.feature_tts)) + Text(stringResource(R.string.feature_tts_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.dictionary, text = "Basic Dictionary") - Text( - text = "Look up single words quickly", + FeatureListItem(iconRes = R.drawable.dictionary, text = stringResource(R.string.feature_dict)) + Text(stringResource(R.string.feature_dict_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) @@ -325,7 +318,7 @@ private fun FreeTierCard() { contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) ) { - Text("Current Plan", fontSize = 16.sp, fontWeight = FontWeight.SemiBold) + Text(stringResource(R.string.current_plan), fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } } } @@ -386,8 +379,7 @@ private fun ProTierCard( tint = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Episteme Pro", + Text(stringResource(R.string.drawer_pro_unlocked), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold @@ -404,8 +396,7 @@ private fun ProTierCard( withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) { append(originalFormattedPrice) } - append(" 50% OFF") - }, + append(" " + stringResource(R.string.pro_sale_off)) }, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -416,16 +407,14 @@ private fun ProTierCard( fontWeight = FontWeight.Bold ) } else { - Text( - text = "Loading price...", + Text(stringResource(R.string.loading_price), style = MaterialTheme.typography.displaySmall.copy(fontSize = 32.sp), fontWeight = FontWeight.Bold ) } Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "One-time payment", + Text(stringResource(R.string.one_time_payment), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -435,8 +424,7 @@ private fun ProTierCard( color = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) { - Text( - text = "Lifetime Access", + Text(stringResource(R.string.lifetime_access), style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) @@ -461,7 +449,7 @@ private fun ProTierCard( modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text("Early Access Sale", style = MaterialTheme.typography.labelLarge) + Text(stringResource(R.string.early_access_sale), style = MaterialTheme.typography.labelLarge) } Spacer(modifier = Modifier.height(16.dp)) } @@ -471,36 +459,31 @@ private fun ProTierCard( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start ) { - Text( - text = "Everything in Free, plus:", + Text(stringResource(R.string.pro_includes), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.cloud_sync, text = "Cloud Sync Across Devices") - Text( - text = "Keep your entire library, including book files and reading progress, synced across up to 4 devices.", + FeatureListItem(iconRes = R.drawable.cloud_sync, text = stringResource(R.string.feature_cloud_sync)) + Text(stringResource(R.string.feature_cloud_sync_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.summarize, text = "Summarization") - Text( - text = "Get quick summaries of chapters or pages", + FeatureListItem(iconRes = R.drawable.summarize, text = stringResource(R.string.feature_summarize)) + Text(stringResource(R.string.feature_summarize_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.dictionary, text = "Smart Dictionary") - Text( - text = "Search phrases and even paragraphs, not just single words", + FeatureListItem(iconRes = R.drawable.dictionary, text = stringResource(R.string.feature_smart_dict)) + Text(stringResource(R.string.feature_smart_dict_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) ) - FeatureListItem(iconRes = R.drawable.chat_bubble, text = "Priority Feature Requests") - Text( - text = "Your suggestions get prioritized", + FeatureListItem(iconRes = R.drawable.chat_bubble, text = stringResource(R.string.feature_priority)) + Text(stringResource(R.string.feature_priority_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 36.dp, bottom = 8.dp) @@ -526,8 +509,7 @@ private fun ProTierCard( tint = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Pro Features Unlocked!", + Text(stringResource(R.string.pro_unlocked), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary @@ -547,7 +529,7 @@ private fun ProTierCard( contentColor = MaterialTheme.colorScheme.onPrimary ) ) { - AutoSizeText("Sign in Required", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) + AutoSizeText(stringResource(R.string.sign_in_required), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) } } proUpgradeState.isVerifying -> { @@ -565,7 +547,7 @@ private fun ProTierCard( color = LocalContentColor.current ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text("Verifying purchase...") + Text(stringResource(R.string.verifying_purchase)) } } localPurchaseExistsForOtherAccount -> { @@ -582,7 +564,7 @@ private fun ProTierCard( modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - AutoSizeText("Existing Purchase Found") + AutoSizeText(stringResource(R.string.existing_purchase_found)) } } productDetails != null -> { @@ -604,7 +586,7 @@ private fun ProTierCard( modifier = Modifier.size(20.dp) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - AutoSizeText("Get Lifetime Access", style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) + AutoSizeText(stringResource(R.string.get_lifetime_access), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) } } } @@ -614,8 +596,7 @@ private fun ProTierCard( } } else -> { - Text( - text = "Upgrade currently unavailable. Please check your internet and try again.", + Text(stringResource(R.string.upgrade_unavailable), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -628,16 +609,14 @@ private fun ProTierCard( Spacer(modifier = Modifier.height(16.dp)) // Increased spacing when { !isUserSignedIn -> { - Text( - text = "Please sign in to your Google account to purchase Episteme Pro.", + Text(stringResource(R.string.sign_in_to_purchase), style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant ) } proUpgradeState.isVerifying -> { - Text( - text = "This may take a few moments. Your Pro status will be updated automatically.", + Text(stringResource(R.string.verifying_purchase_desc), style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center ) @@ -691,10 +670,10 @@ fun ExistingPurchaseDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.Info, contentDescription = null) }, - title = { Text("Existing Purchase Found") }, - text = { Text("This device already has a Pro purchase, but it's linked to a different account. Please sign in to the account that was used for the original purchase to restore your Pro features.") }, + title = { Text(stringResource(R.string.existing_purchase_found)) }, + text = { Text(stringResource(R.string.dialog_existing_purchase_desc)) }, confirmButton = { - TextButton(onClick = onDismiss) { Text("OK") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_ok)) } } ) } @@ -704,10 +683,10 @@ fun EarlyAccessInfoDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, icon = { Icon(Icons.Default.Info, contentDescription = null) }, - title = { Text("Early Access Sale") }, - text = { Text("You're getting Episteme Pro at a special discounted price during our early access period! This is a limited-time offer.") }, + title = { Text(stringResource(R.string.early_access_sale)) }, + text = { Text(stringResource(R.string.dialog_early_access_desc)) }, confirmButton = { - TextButton(onClick = onDismiss) { Text("Got It!") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_got_it)) } } ) } @@ -716,14 +695,14 @@ fun EarlyAccessInfoDialog(onDismiss: () -> Unit) { fun SignInRequiredDialog(onSignInClick: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) }, // Using crown icon for Pro - title = { Text("Sign In Required") }, - text = { Text("Please sign in to your Google account to purchase Episteme Pro and unlock all premium features.") }, + icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) }, + title = { Text(stringResource(R.string.sign_in_required)) }, + text = { Text(stringResource(R.string.dialog_sign_in_required_desc)) }, confirmButton = { - TextButton(onClick = onSignInClick) { Text("Sign In") } + TextButton(onClick = onSignInClick) { Text(stringResource(R.string.drawer_sign_in)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Not Now") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_not_now)) } } ) } \ 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 3502836..83f236b 100644 --- a/app/src/main/java/com/aryan/reader/SharedComposables.kt +++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt @@ -17,6 +17,7 @@ * * mail: epistemereader@gmail.com */ +// SharedComposables.kt package com.aryan.reader import android.content.Context @@ -31,7 +32,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -53,22 +53,18 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.SelectAll -import androidx.compose.material.icons.outlined.BugReport -import androidx.compose.material.icons.outlined.ChevronRight -import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.FileOpen import androidx.compose.material.icons.outlined.Gavel import androidx.compose.material.icons.outlined.Policy import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -76,6 +72,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -88,12 +85,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -102,7 +99,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog @@ -146,34 +142,29 @@ fun formatFileSize(bytes: Long): String { @Composable fun LegalText( modifier: Modifier = Modifier, - prefixText: String, // Changed from baseText + prefixText: String, textAlign: TextAlign = TextAlign.Center ) { val uriHandler = LocalUriHandler.current + + val fullAgreementText = stringResource(R.string.legal_agreement_full, prefixText, stringResource(R.string.legal_terms_of_service), stringResource(R.string.legal_privacy_policy)) + val termsText = stringResource(R.string.legal_terms_of_service) + val privacyText = stringResource(R.string.legal_privacy_policy) + val annotatedString = buildAnnotatedString { - append("$prefixText you agree to our ") - pushStringAnnotation(tag = "terms", annotation = TERMS_URL) - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ) - ) { - append("Terms of Service") + append(fullAgreementText) + + val termsStartIndex = fullAgreementText.indexOf(termsText) + if (termsStartIndex >= 0) { + addStringAnnotation(tag = "terms", annotation = TERMS_URL, start = termsStartIndex, end = termsStartIndex + termsText.length) + addStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline), start = termsStartIndex, end = termsStartIndex + termsText.length) } - pop() - append(" and acknowledge you have read our ") - pushStringAnnotation(tag = "privacy", annotation = PRIVACY_POLICY_URL) - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ) - ) { - append("Privacy Policy") + + val privacyStartIndex = fullAgreementText.indexOf(privacyText) + if (privacyStartIndex >= 0) { + addStringAnnotation(tag = "privacy", annotation = PRIVACY_POLICY_URL, start = privacyStartIndex, end = privacyStartIndex + privacyText.length) + addStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline), start = privacyStartIndex, end = privacyStartIndex + privacyText.length) } - pop() - append(".") } @Suppress("DEPRECATION") @@ -225,30 +216,30 @@ fun ContextualTopAppBar( onDeleteClick: () -> Unit ) { CustomTopAppBar( - title = { Text("$selectedItemCount selected") }, + title = { Text(stringResource(R.string.items_selected_count, selectedItemCount)) }, navigationIcon = { IconButton(onClick = onNavIconClick) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Clear Selection") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.clear_selection)) } }, actions = { if (onPinClick != null) { IconButton(onClick = onPinClick) { - Icon(Icons.Filled.PushPin, contentDescription = "Pin/Unpin") + Icon(Icons.Filled.PushPin, contentDescription = stringResource(R.string.pin_unpin)) } } if (selectedItemCount == 1 && onInfoClick != null) { IconButton(onClick = onInfoClick) { - Icon(Icons.Filled.Info, contentDescription = "Info") + Icon(Icons.Filled.Info, contentDescription = stringResource(R.string.info)) } } if (onSelectAllClick != null) { IconButton(onClick = onSelectAllClick) { - Icon(Icons.Filled.SelectAll, contentDescription = "Select All") + Icon(Icons.Filled.SelectAll, contentDescription = stringResource(R.string.select_all)) } } IconButton(onClick = onDeleteClick) { - Icon(Icons.Filled.Delete, contentDescription = "Delete") + Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.action_delete)) } } ) @@ -302,21 +293,21 @@ fun DeleteConfirmationDialog( onConfirm: () -> Unit, onDismiss: () -> Unit, isPermanentDelete: Boolean = false, - containsFolderItems: Boolean = false // New parameter + containsFolderItems: Boolean = false ) { - val title = if (isPermanentDelete) "Delete File(s) Permanently" else "Remove from Recents" + val title = if (isPermanentDelete) stringResource(R.string.dialog_delete_permanently) else stringResource(R.string.dialog_remove_from_recents) val text = if (isPermanentDelete) { if (containsFolderItems) { - "Warning: Some selected items are synced from a local folder. Proceeding will delete the actual files from your device storage.\n\nThis action cannot be undone." + stringResource(R.string.dialog_warning_folder_sync_delete) } else { - "Do you want to permanently delete $count selected file(s) from your device? This action cannot be undone." + stringResource(R.string.dialog_permanently_delete_desc, count) } } else { - "Do you want to remove $count selected file(s) from the recent files list? It will reappear if you open it again from the library." + stringResource(R.string.dialog_remove_recents_desc, count) } - val confirmText = if (isPermanentDelete) "Delete" else "Remove" + val confirmText = if (isPermanentDelete) stringResource(R.string.action_delete) else stringResource(R.string.action_remove) AlertDialog( onDismissRequest = onDismiss, @@ -336,7 +327,7 @@ fun DeleteConfirmationDialog( } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -407,7 +398,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - "File Information", + stringResource(R.string.file_information), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold ) @@ -415,7 +406,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S androidx.compose.material3.OutlinedTextField( value = editingName, onValueChange = { editingName = it }, - label = { Text("Book Name") }, + label = { Text(stringResource(R.string.book_name)) }, modifier = Modifier .fillMaxWidth() .heightIn(min = 64.dp, max = 130.dp), @@ -425,14 +416,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S IconButton(onClick = { clipboardManager.setText(AnnotatedString(editingName)) }) { - Icon(Icons.Default.ContentCopy, contentDescription = "Copy Name", modifier = Modifier.size(20.dp)) + Icon(Icons.Default.ContentCopy, contentDescription = stringResource(R.string.copy_name), modifier = Modifier.size(20.dp)) } } ) if (hasCustomName) { Text( - text = "Original Name: $originalName", + text = stringResource(R.string.original_name, originalName), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp), @@ -447,11 +438,11 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S modifier = Modifier.align(Alignment.End), contentPadding = PaddingValues(0.dp) ) { - Text("Revert to Original") + Text(stringResource(R.string.revert_to_original)) } } else if (originalName != item.displayName) { Text( - text = "File Name: ${item.displayName}", + text = stringResource(R.string.file_name, item.displayName), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, @@ -463,18 +454,27 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { item.author?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }?.let { - InfoRowDetailed("Author", it) + InfoRowDetailed(stringResource(R.string.author), it) } - InfoRowDetailed("Format", item.type.name) - InfoRowDetailed("Size", formatFileSize(item.fileSize)) - InfoRowDetailed("Added", formattedDate) + InfoRowDetailed(stringResource(R.string.format), item.type.name) + InfoRowDetailed(stringResource(R.string.size), formatFileSize(item.fileSize)) + InfoRowDetailed(stringResource(R.string.added), formattedDate) + + val pathTextFinal = if (isOpdsStream) { + stringResource(R.string.source_opds) + } else if (pathText == "In-App Storage") { + stringResource(R.string.source_in_app) + } else { + pathText.replace("Internal storage", stringResource(R.string.internal_storage)) + } + InfoRowDetailed( - label = "Location", - value = pathText, + label = stringResource(R.string.location), + value = pathTextFinal, maxLines = 4, isScrollable = true, onCopy = { - clipboardManager.setText(AnnotatedString(pathText)) + clipboardManager.setText(AnnotatedString(pathTextFinal)) } ) } @@ -485,7 +485,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S .padding(top = 8.dp), horizontalArrangement = Arrangement.End ) { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } Spacer(modifier = Modifier.width(8.dp)) androidx.compose.material3.Button(onClick = { val finalName = editingName.trim() @@ -497,7 +497,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S } } onDismiss() - }) { Text("Save") } + }) { Text(stringResource(R.string.action_save)) } } } } @@ -509,7 +509,7 @@ private fun InfoRowDetailed( label: String, value: String, maxLines: Int = 1, - isScrollable: Boolean = false, // ADD THIS + isScrollable: Boolean = false, onCopy: (() -> Unit)? = null ) { Row( @@ -603,123 +603,101 @@ fun AboutDialog(onDismiss: () -> Unit) { shape = RoundedCornerShape(24.dp), containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - Icon( - painter = painterResource(id = R.drawable.ic_launcher_foreground), - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary + Text( + text = stringResource(R.string.about_app_name), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = if (isOss) stringResource(R.string.about_oss_version) else stringResource(R.string.about_play_version), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = "Episteme", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - Text( - text = if (isOss) "Open Source Version" else "Playstore Version", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - } } }, text = { Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally ) { - Surface( - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest, - modifier = Modifier.fillMaxWidth() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(12.dp) - ) { - Text( - text = "Version ${BuildConfig.VERSION_NAME}", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = "Build ${BuildConfig.VERSION_CODE}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + Text( + text = stringResource(R.string.about_version_name, BuildConfig.VERSION_NAME), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(R.string.about_build_code, BuildConfig.VERSION_CODE.toString()), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) - Spacer(modifier = Modifier.height(16.dp)) - - HorizontalDivider() + Spacer(modifier = Modifier.height(20.dp)) if (isOss) { - Spacer(modifier = Modifier.height(12.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Outlined.Code, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Open Source", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium - ) - } - Spacer(modifier = Modifier.height(8.dp)) - - InfoRow( - icon = Icons.Outlined.Code, - text = "GitHub Repository", - subtitle = "Browse source code, star, and fork", + AboutInfoRow( + icon = { + Icon( + painter = painterResource(id = R.drawable.github), + contentDescription = stringResource(R.string.about_github), + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + text = stringResource(R.string.about_github), + subtitle = stringResource(R.string.about_github_desc), onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme") } ) - - InfoRow( - icon = Icons.Outlined.BugReport, - text = "Report an Issue", - subtitle = "File a bug or feature request", - onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme/issues") } - ) } else { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = "Legal", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(bottom = 8.dp) - ) - - InfoRow( - icon = Icons.Outlined.Policy, - text = "Privacy Policy", - subtitle = "How we handle your data", + AboutInfoRow( + icon = { + Icon( + imageVector = Icons.Outlined.Policy, + contentDescription = null, + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + text = stringResource(R.string.about_privacy), + subtitle = stringResource(R.string.about_privacy_desc), onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) } ) - InfoRow( - icon = Icons.Outlined.Gavel, - text = "Terms of Service", - subtitle = "Usage terms and conditions", + Spacer(modifier = Modifier.height(10.dp)) + + AboutInfoRow( + icon = { + Icon( + imageVector = Icons.Outlined.Gavel, + contentDescription = null, + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + text = stringResource(R.string.about_terms), + subtitle = stringResource(R.string.about_terms_desc), onClick = { uriHandler.openUri(TERMS_URL) } ) - InfoRow( - icon = Icons.Outlined.FileOpen, - text = "Licenses", - subtitle = "Libraries", + Spacer(modifier = Modifier.height(10.dp)) + + AboutInfoRow( + icon = { + Icon( + imageVector = Icons.Outlined.FileOpen, + contentDescription = null, + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + text = stringResource(R.string.legal_licenses), + subtitle = stringResource(R.string.about_licenses_desc), onClick = { uriHandler.openUri(LICENSES_URL) } ) } @@ -731,50 +709,40 @@ fun AboutDialog(onDismiss: () -> Unit) { shape = RoundedCornerShape(50), modifier = Modifier.padding(horizontal = 8.dp) ) { - Text("Close", fontWeight = FontWeight.Medium) + Text(stringResource(R.string.action_close), fontWeight = FontWeight.Medium) } } ) } @Composable -private fun InfoRow( - icon: ImageVector, +private fun AboutInfoRow( + icon: @Composable () -> Unit, text: String, subtitle: String? = null, onClick: () -> Unit ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clickable(onClick = onClick), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest - ), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + OutlinedCard( + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), + .padding(horizontal = 14.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) + icon() + Spacer(modifier = Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = text, style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Bold ) if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) Text( text = subtitle, style = MaterialTheme.typography.bodySmall, @@ -782,10 +750,11 @@ private fun InfoRow( ) } } + Spacer(modifier = Modifier.width(4.dp)) Icon( - imageVector = Icons.Outlined.ChevronRight, + imageVector = Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -798,7 +767,7 @@ fun EmptyState( message: String, onSelectFileClick: () -> Unit, modifier: Modifier = Modifier, - primaryButtonText: String = "Select a File", + primaryButtonText: String = stringResource(R.string.empty_select_file), secondaryButtonText: String? = null, onSecondaryClick: (() -> Unit)? = null ) { @@ -858,19 +827,15 @@ fun SelectFileButton(onClick: () -> Unit, text: String) { fun ClearCloudDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Clear All Synced Data?") }, - text = { Text("Are you sure you want to permanently delete all of your book data from the cloud? This will also wipe your local library to prevent re-syncing. This action cannot be undone.") }, + title = { Text(stringResource(R.string.clear_cloud_data_title)) }, + text = { Text(stringResource(R.string.clear_cloud_data_desc)) }, confirmButton = { TextButton( onClick = onConfirm, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) - ) { - Text("DELETE ALL DATA") - } + ) { Text(stringResource(R.string.delete_all_data)) } }, - dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } - } + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -904,4 +869,24 @@ fun AutoSizeText( } } ) +} + +@Composable +fun FileTypeBadge(type: FileType, modifier: Modifier = Modifier, overlay: Boolean = false) { + val containerColor = if (overlay) androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.6f) else MaterialTheme.colorScheme.secondaryContainer + val contentColor = if (overlay) androidx.compose.ui.graphics.Color.White else MaterialTheme.colorScheme.onSecondaryContainer + + Surface( + modifier = modifier, + shape = RoundedCornerShape(4.dp), + color = containerColor, + contentColor = contentColor + ) { + Text( + text = type.name.uppercase(), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt b/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt index 53e5ac1..dde0201 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt @@ -36,11 +36,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.core.graphics.drawable.toBitmap import com.aryan.reader.BuildConfig +import com.aryan.reader.R @Suppress("KotlinConstantConditions") @OptIn(ExperimentalMaterial3Api::class) @@ -83,7 +85,7 @@ fun DictionarySettingsDialog( .padding(24.dp) ) { Text( - text = "Lookup Settings", + text = stringResource(R.string.dict_lookup_settings), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 20.dp) @@ -98,7 +100,7 @@ fun DictionarySettingsDialog( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Dictionary Engine", + text = stringResource(R.string.dict_dictionary_engine), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 8.dp) @@ -114,29 +116,29 @@ fun DictionarySettingsDialog( onClick = { onToggleOnlineDictionary(true) }, shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2) ) { - Text("Smart (AI)") + Text(stringResource(R.string.dict_smart_ai)) } SegmentedButton( selected = !useOnlineDictionary, onClick = { onToggleOnlineDictionary(false) }, shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2) ) { - Text("External App") + Text(stringResource(R.string.dict_external_app)) } } Text( text = if (useOnlineDictionary) - "Uses AI for definitions. Will fallback to the external app below if offline or if the selected phrase is too long." + stringResource(R.string.dict_ai_description) else - "Uses the selected app for dictionary lookups.", + stringResource(R.string.dict_external_description), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 16.dp) ) Text( - text = if (useOnlineDictionary) "Fallback App" else "Dictionary App", + text = if (useOnlineDictionary) stringResource(R.string.dict_fallback_app) else stringResource(R.string.dict_dictionary_app), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 8.dp) @@ -146,13 +148,13 @@ fun DictionarySettingsDialog( apps = dictionaryApps, selectedPackageName = selectedDictionaryPackageName, onSelect = onSelectDictionaryPackage, - placeholder = "Select an app" + placeholder = stringResource(R.string.dict_select_app) ) } } } else { Text( - text = "Dictionary", + text = stringResource(R.string.tooltip_dictionary), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 8.dp) @@ -161,7 +163,7 @@ fun DictionarySettingsDialog( apps = dictionaryApps, selectedPackageName = selectedDictionaryPackageName, onSelect = onSelectDictionaryPackage, - placeholder = "Select an app" + placeholder = stringResource(R.string.dict_select_app) ) } @@ -169,13 +171,13 @@ fun DictionarySettingsDialog( // ── Translate ── Text( - text = "Translate", + text = stringResource(R.string.dict_translate), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 4.dp) ) Text( - text = "App used for translating selected text.", + text = stringResource(R.string.dict_translate_description), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 12.dp) @@ -185,20 +187,20 @@ fun DictionarySettingsDialog( apps = dictionaryApps, selectedPackageName = selectedTranslatePackageName, onSelect = onSelectTranslatePackage, - placeholder = "Select an app" + placeholder = stringResource(R.string.dict_select_app) ) SectionDivider() // ── Search ── Text( - text = "Search", + text = stringResource(R.string.tooltip_search), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(bottom = 4.dp) ) Text( - text = "App used for web searches.", + text = stringResource(R.string.dict_search_app_description), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 12.dp) @@ -208,7 +210,7 @@ fun DictionarySettingsDialog( apps = searchApps, selectedPackageName = selectedSearchPackageName, onSelect = onSelectSearchPackage, - placeholder = "Select an app" + placeholder = stringResource(R.string.dict_select_app) ) } } @@ -272,7 +274,7 @@ private fun AppSelectionDropdown( DropdownMenuItem( text = { Text( - "None", + stringResource(R.string.dict_none), color = if (!hasSelection) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface ) @@ -281,7 +283,7 @@ private fun AppSelectionDropdown( { Icon( imageVector = Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary ) } @@ -327,7 +329,7 @@ private fun AppSelectionDropdown( { Icon( imageVector = Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary ) } diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt index 4ad992f..c0f3781 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt @@ -19,13 +19,18 @@ */ package com.aryan.reader.epubreader +import android.content.Context +import androidx.compose.foundation.layout.fillMaxWidth import timber.log.Timber import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import com.aryan.reader.AiDefinitionPopup import com.aryan.reader.AiDefinitionResult import com.aryan.reader.R @@ -137,6 +142,7 @@ suspend fun executeRecapLogic( characterLimit: Int, summaryCacheManager: SummaryCacheManager, paginator: IPaginator?, + context: Context, onProgressUpdate: (String) -> Unit, onResultUpdate: (String) -> Unit, onError: (String) -> Unit, @@ -216,6 +222,7 @@ suspend fun executeRecapLogic( fetchRecap( pastSummaries = pastSummaries, currentText = finalContextText, + context = context, onUpdate = { chunk -> onResultUpdate(chunk) }, onError = { error -> onError(error) }, onFinish = { onFinish() } @@ -250,7 +257,7 @@ fun EpubReaderAiOverlays( ) { if (showSummarizationPopup) { SummarizationPopup( - title = "Chapter Summary", + title = stringResource(R.string.ai_chapter_summary), result = summarizationResult, isLoading = isSummarizationLoading, onDismiss = onDismissSummarization, @@ -260,7 +267,7 @@ fun EpubReaderAiOverlays( if (showRecapPopup) { SummarizationPopup( - title = "Story Recap (Beta)", + title = stringResource(R.string.ai_story_recap_beta), result = recapResult, isLoading = isRecapLoading, onDismiss = onDismissRecap, @@ -272,16 +279,26 @@ fun EpubReaderAiOverlays( AlertDialog( onDismissRequest = onDismissSummarizationUpsell, icon = { Icon(painter = painterResource(id = R.drawable.summarize), contentDescription = null) }, - title = { Text("Unlock Chapter Summarization") }, - text = { Text("Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.") }, + title = { + Text( + text = stringResource(R.string.ai_unlock_summarization), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + }, + text = { + Text( + text = stringResource(R.string.ai_unlock_summarization_desc), + ) + }, confirmButton = { TextButton(onClick = { onDismissSummarizationUpsell() onNavigateToPro() - }) { Text("Learn More") } + }) { Text(stringResource(R.string.action_learn_more)) } }, dismissButton = { - TextButton(onClick = onDismissSummarizationUpsell) { Text("Not Now") } + TextButton(onClick = onDismissSummarizationUpsell) { Text(stringResource(R.string.action_not_now)) } } ) } @@ -293,7 +310,6 @@ fun EpubReaderAiOverlays( isLoading = isAiDefinitionLoading, onDismiss = onDismissAiDefinition, isMainTtsActive = isTtsSessionActive, - // Pass it down onOpenExternalDictionary = { selectedTextForAi?.let { text -> onOpenExternalDictionary(text) } } @@ -304,16 +320,16 @@ fun EpubReaderAiOverlays( AlertDialog( onDismissRequest = onDismissDictionaryUpsell, icon = { Icon(painter = painterResource(id = R.drawable.ai), contentDescription = null) }, - title = { Text("Unlock Smart Dictionary") }, - text = { Text("Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.") }, + title = { Text(stringResource(R.string.ai_unlock_smart_dict)) }, + text = { Text(stringResource(R.string.ai_unlock_smart_dict_desc)) }, confirmButton = { TextButton(onClick = { onDismissDictionaryUpsell() onNavigateToPro() - }) { Text("Learn More") } + }) { Text(stringResource(R.string.action_learn_more)) } }, dismissButton = { - TextButton(onClick = onDismissDictionaryUpsell) { Text("Not Now") } + TextButton(onClick = onDismissDictionaryUpsell) { Text(stringResource(R.string.action_not_now)) } } ) } 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 a4da7f5..194a4e4 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.edit import com.aryan.reader.R @@ -365,7 +366,7 @@ fun BookmarkButton( ) { Icon( painter = painterResource(id = R.drawable.bookmark), - contentDescription = "Bookmark", + contentDescription = stringResource(R.string.content_desc_bookmark_icon), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary ) @@ -406,14 +407,14 @@ fun PaletteManagerDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text("Customize Palette", style = MaterialTheme.typography.titleMedium) }, + title = { Text(stringResource(R.string.dialog_customize_palette), style = MaterialTheme.typography.titleMedium) }, text = { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Tap a slot to edit:", style = MaterialTheme.typography.bodySmall) + Text(stringResource(R.string.palette_tap_slot_to_edit), style = MaterialTheme.typography.bodySmall) Row( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth() @@ -427,7 +428,7 @@ fun PaletteManagerDialog( .background(colorEnum.color, CircleShape) .border( width = if (isSelected) 3.dp else 1.dp, - color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, // Thin ring if selected + color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, shape = CircleShape ) .clip(CircleShape) @@ -436,7 +437,7 @@ fun PaletteManagerDialog( if (isSelected) { Icon( imageVector = Icons.Default.Check, - contentDescription = "Selected Slot", + contentDescription = stringResource(R.string.content_desc_selected_slot), tint = if (colorEnum == HighlightColor.WHITE) Color.Black else Color.White, modifier = Modifier.size(24.dp) ) @@ -448,7 +449,7 @@ fun PaletteManagerDialog( HorizontalDivider() // 2. Bottom Grid: Available Colors - Text("Select a color for the slot:", style = MaterialTheme.typography.bodySmall) + Text(stringResource(R.string.palette_select_color_for_slot), style = MaterialTheme.typography.bodySmall) LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 40.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), 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 7de0935..6e25c63 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -211,15 +211,15 @@ fun EpubReaderTopBar( ) { Icon( painter = painterResource(id = R.drawable.dictionary), - contentDescription = "Dictionary Settings" + contentDescription = stringResource(R.string.content_desc_dictionary_settings) ) } TooltipIconButton( - text = "Theme", - description = "Theme Settings", + text = stringResource(R.string.tooltip_theme), + description = stringResource(R.string.tooltip_theme_desc), onClick = onOpenThemeSettings ) { - Icon(painter = painterResource(id = R.drawable.palette), contentDescription = "Theme Settings") + Icon(painter = painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc)) } Box { var showMoreMenu by remember { mutableStateOf(false) } @@ -228,7 +228,7 @@ fun EpubReaderTopBar( description = stringResource(R.string.tooltip_more_options_desc), onClick = { showMoreMenu = true } ) { - Icon(Icons.Default.MoreVert, contentDescription = "More Options") + Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_more_options)) } DropdownMenu( @@ -237,7 +237,7 @@ fun EpubReaderTopBar( ) { if (onToggleReflow != null) { DropdownMenuItem( - text = { Text("View Original PDF") }, + text = { Text(stringResource(R.string.menu_view_original_pdf)) }, onClick = { showMoreMenu = false onToggleReflow() @@ -256,7 +256,7 @@ fun EpubReaderTopBar( onDeleteReflow?.let { HorizontalDivider() DropdownMenuItem( - text = { Text("Delete Text View") }, + text = { Text(stringResource(R.string.menu_delete_text_view)) }, onClick = { showMoreMenu = false it() @@ -275,26 +275,26 @@ fun EpubReaderTopBar( } DropdownMenuItem( - text = { Text("Reading Mode: Vertical") }, + text = { Text(stringResource(R.string.menu_reading_mode_vertical)) }, enabled = !isTtsActive, onClick = { showMoreMenu = false onChangeRenderMode(RenderMode.VERTICAL_SCROLL) }, - trailingIcon = { if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(Icons.Default.Check, contentDescription = "Selected") } + trailingIcon = { if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) } ) DropdownMenuItem( - text = { Text("Reading Mode: Paginated") }, + text = { Text(stringResource(R.string.menu_reading_mode_paginated)) }, enabled = !isTtsActive, onClick = { showMoreMenu = false onChangeRenderMode(RenderMode.PAGINATED) }, - trailingIcon = { if (currentRenderMode == RenderMode.PAGINATED) Icon(Icons.Default.Check, contentDescription = "Selected") } + trailingIcon = { if (currentRenderMode == RenderMode.PAGINATED) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) } ) HorizontalDivider() DropdownMenuItem( - text = { Text(if (isBookmarked) "Remove bookmark" else "Bookmark this page") }, + text = { Text(if (isBookmarked) stringResource(R.string.menu_remove_bookmark) else stringResource(R.string.menu_bookmark_this_page)) }, onClick = { showMoreMenu = false onToggleBookmark() @@ -302,20 +302,20 @@ fun EpubReaderTopBar( ) HorizontalDivider() DropdownMenuItem( - text = { Text("Tap to Turn Pages") }, + text = { Text(stringResource(R.string.menu_tap_to_turn_pages)) }, enabled = currentRenderMode == RenderMode.PAGINATED, onClick = { onToggleTapToNavigate(!tapToNavigateEnabled) showMoreMenu = false }, - trailingIcon = { if (tapToNavigateEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } + trailingIcon = { if (tapToNavigateEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) } ) HorizontalDivider() DropdownMenuItem( text = { Text( - if (currentRenderMode == RenderMode.VERTICAL_SCROLL) "Volume Button Scrolling" - else "Volume Button Page Turn" + if (currentRenderMode == RenderMode.VERTICAL_SCROLL) stringResource(R.string.menu_volume_button_scrolling) + else stringResource(R.string.menu_volume_button_page_turn) ) }, enabled = true, @@ -323,33 +323,33 @@ fun EpubReaderTopBar( onToggleVolumeScroll(!volumeScrollEnabled) showMoreMenu = false }, - trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } + trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) } ) HorizontalDivider() DropdownMenuItem( - text = { Text("Realistic Page Turns") }, + text = { Text(stringResource(R.string.menu_realistic_page_turns)) }, enabled = currentRenderMode == RenderMode.PAGINATED, onClick = { onTogglePageTurnAnimation(!isPageTurnAnimationEnabled) showMoreMenu = false }, - trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") } + trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) } ) HorizontalDivider() DropdownMenuItem( - text = { Text("Keep Screen On") }, + text = { Text(stringResource(R.string.menu_keep_screen_on)) }, onClick = { onToggleKeepScreenOn(!isKeepScreenOn) showMoreMenu = false }, - trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = "Enabled") } + trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled)) } ) HorizontalDivider() DropdownMenuItem( - text = { Text("Visual Options") }, + text = { Text(stringResource(R.string.menu_visual_options)) }, onClick = { showMoreMenu = false onOpenVisualOptions() @@ -361,7 +361,7 @@ fun EpubReaderTopBar( HorizontalDivider() DropdownMenuItem( - text = { Text("Auto Scroll") }, + text = { Text(stringResource(R.string.menu_auto_scroll)) }, enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL, onClick = { showMoreMenu = false @@ -372,7 +372,7 @@ fun EpubReaderTopBar( HorizontalDivider() DropdownMenuItem( - text = { Text("TTS Voice Settings") }, + text = { Text(stringResource(R.string.menu_tts_voice_settings)) }, onClick = { showMoreMenu = false onOpenDeviceVoiceSettings() @@ -384,7 +384,7 @@ fun EpubReaderTopBar( if (BuildConfig.DEBUG) { DropdownMenuItem( - text = { Text("TTS Settings (Debug)") }, + text = { Text(stringResource(R.string.menu_tts_settings_debug)) }, onClick = { showMoreMenu = false onOpenTtsSettings() @@ -445,28 +445,28 @@ fun EpubReaderBottomBar( onClick = onOpenSlider, enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL ) { - Icon(painter = painterResource(id = R.drawable.slider), contentDescription = "Navigate with slider") + Icon(painter = painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider)) } TooltipIconButton( text = stringResource(R.string.tooltip_toc), description = stringResource(R.string.tooltip_toc_desc), onClick = onOpenDrawer ) { - Icon(imageVector = Icons.Default.Menu, contentDescription = "Chapters Menu") + Icon(imageVector = Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_chapters_menu)) } TooltipIconButton( text = stringResource(R.string.tooltip_format), description = stringResource(R.string.tooltip_format_desc), onClick = onToggleFormat ) { - Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = "Text Formatting") + Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = stringResource(R.string.content_desc_text_formatting)) } TooltipIconButton( text = stringResource(R.string.tooltip_search), description = stringResource(R.string.tooltip_search_desc), onClick = onToggleSearch ) { - Icon(imageVector = Icons.Default.Search, contentDescription = "Search") + Icon(imageVector = Icons.Default.Search, contentDescription = stringResource(R.string.tooltip_search)) } @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") @@ -485,7 +485,7 @@ fun EpubReaderBottomBar( onDismissRequest = { showAiFeaturesMenu = false } ) { DropdownMenuItem( - text = { Text("Chapter Summarization") }, + text = { Text(stringResource(R.string.menu_chapter_summarization)) }, onClick = { showAiFeaturesMenu = false onSummarize() @@ -494,7 +494,7 @@ fun EpubReaderBottomBar( if (BuildConfig.DEBUG && isProUser) { HorizontalDivider() DropdownMenuItem( - text = { Text("Recap (Beta)") }, + text = { Text(stringResource(R.string.menu_recap_beta)) }, onClick = { showAiFeaturesMenu = false onRecap() @@ -519,7 +519,7 @@ fun EpubReaderBottomBar( ) { Icon( painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), - contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS" + contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts) ) } if (isTtsSessionActive) { @@ -537,7 +537,7 @@ fun EpubReaderBottomBar( ) { Icon( painter = painterResource(id = if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), - contentDescription = if (ttsState.isPlaying) "Pause TTS" else "Resume TTS" + contentDescription = if (ttsState.isPlaying) stringResource(R.string.content_desc_pause_tts) else stringResource(R.string.content_desc_resume_tts) ) } } @@ -589,7 +589,7 @@ fun EpubReaderPageSlider( ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Exit slider navigation" + contentDescription = stringResource(R.string.content_desc_exit_slider) ) } @@ -681,7 +681,7 @@ fun EpubReaderPageSlider( ) { Image( bitmap = thumbnail.asImageBitmap(), - contentDescription = "Start page thumbnail", + contentDescription = stringResource(R.string.content_desc_start_page_thumbnail), contentScale = ContentScale.FillBounds, modifier = Modifier.fillMaxSize() ) @@ -918,7 +918,7 @@ fun AutoScrollControls( ) { Icon( imageVector = Icons.Default.ChevronLeft, - contentDescription = "Expand", + contentDescription = stringResource(R.string.content_desc_expand), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -934,7 +934,7 @@ fun AutoScrollControls( ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) "Pause" else "Play", + contentDescription = if (isPlaying) stringResource(R.string.content_desc_pause_playback) else stringResource(R.string.content_desc_start_playback), modifier = Modifier.size(20.dp) ) } @@ -967,13 +967,13 @@ fun AutoScrollControls( .padding(4.dp) ) { Text( - text = if (isLocalMode) "Local Speed" else "Global Speed", + text = if (isLocalMode) stringResource(R.string.auto_scroll_local_speed) else stringResource(R.string.auto_scroll_global_speed), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary ) Icon( imageVector = Icons.Default.ArrowDropDown, - contentDescription = "Select Mode", + contentDescription = stringResource(R.string.content_desc_select_mode), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) @@ -986,8 +986,8 @@ fun AutoScrollControls( DropdownMenuItem( text = { Column { - Text("Global Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) - Text("Applies to all files", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.auto_scroll_global_speed), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.auto_scroll_applies_all_files), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } }, onClick = { @@ -1002,8 +1002,8 @@ fun AutoScrollControls( DropdownMenuItem( text = { Column { - Text("Local Speed", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) - Text("Saved for this file only", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.auto_scroll_local_speed), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.auto_scroll_saved_for_file), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } }, onClick = { @@ -1024,7 +1024,7 @@ fun AutoScrollControls( ) { Icon( painter = painterResource(id = R.drawable.music_note), - contentDescription = if (isMusicianMode) "Disable Musician Mode" else "Enable Musician Mode", + contentDescription = if (isMusicianMode) stringResource(R.string.content_desc_disable_musician_mode) else stringResource(R.string.content_desc_enable_musician_mode), modifier = Modifier.size(18.dp), tint = if (isMusicianMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1035,7 +1035,7 @@ fun AutoScrollControls( ) { Icon( imageVector = Icons.Default.SwapHoriz, - contentDescription = "Swap Controls", + contentDescription = stringResource(R.string.content_desc_swap_controls), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1046,7 +1046,7 @@ fun AutoScrollControls( ) { Icon( imageVector = Icons.Default.ChevronRight, - contentDescription = "Collapse", + contentDescription = stringResource(R.string.content_desc_collapse), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1057,7 +1057,7 @@ fun AutoScrollControls( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Close", + contentDescription = stringResource(R.string.action_close), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp) ) @@ -1085,7 +1085,7 @@ fun AutoScrollControls( ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) "Pause" else "Play", + contentDescription = if (isPlaying) stringResource(R.string.tooltip_tts_pause) else stringResource(R.string.content_desc_start_playback), modifier = Modifier.size(24.dp) ) } @@ -1110,13 +1110,13 @@ fun AutoScrollControls( horizontalArrangement = Arrangement.SpaceBetween ) { SpeedDropdown( - label = "Min", + label = stringResource(R.string.label_min), currentValue = minSpeed, options = speedOptions, onValueChange = onMinSpeedChange ) SpeedDropdown( - label = "Max", + label = stringResource(R.string.label_max), currentValue = maxSpeed, options = speedOptions, onValueChange = onMaxSpeedChange @@ -1193,7 +1193,7 @@ fun AutoScrollControls( onClick = { onSpeedChange((speed - 0.1f).coerceAtLeast(minSpeed)) }, modifier = Modifier.size(48.dp) ) { - Icon(Icons.Default.Remove, "Slower") + Icon(Icons.Default.Remove, stringResource(R.string.content_desc_slower)) } Text( text = "%.1fx".format(speed), @@ -1204,7 +1204,7 @@ fun AutoScrollControls( onClick = { onSpeedChange((speed + 0.1f).coerceAtMost(safeMax)) }, modifier = Modifier.size(48.dp) ) { - Icon(Icons.Default.Add, "Faster") + Icon(Icons.Default.Add, stringResource(R.string.content_desc_faster)) } } } 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 7204edb..f533ef8 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt @@ -82,11 +82,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.util.fastSumBy +import com.aryan.reader.R import com.aryan.reader.RenderMode import com.aryan.reader.epub.EpubChapter import com.aryan.reader.epub.EpubTocEntry @@ -224,17 +226,17 @@ fun EpubReaderDrawerSheet( 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)) } ) Tab( selected = drawerPagerState.currentPage == 2, onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } }, - text = { Text("Highlights") } + text = { Text(stringResource(R.string.tab_highlights)) } ) } @@ -442,7 +444,7 @@ private fun TocTreeItem( if (hasChildren) { Icon( imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = if (isExpanded) "Collapse" else "Expand", + contentDescription = if (isExpanded) stringResource(R.string.content_desc_collapse) else stringResource(R.string.content_desc_expand), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -477,7 +479,7 @@ private fun BookmarksList( 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 +533,7 @@ private fun BookmarksList( IconButton(onClick = { bookmarkMenuExpandedFor = bookmark }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = "More options for bookmark" + contentDescription = stringResource(R.string.content_desc_more_options_bookmark) ) } DropdownMenu( @@ -539,14 +541,14 @@ private fun BookmarksList( onDismissRequest = { bookmarkMenuExpandedFor = null } ) { DropdownMenuItem( - text = { Text("Rename") }, + text = { Text(stringResource(R.string.menu_rename)) }, onClick = { showRenameBookmarkDialog = bookmark bookmarkMenuExpandedFor = null } ) DropdownMenuItem( - text = { Text("Delete") }, + text = { Text(stringResource(R.string.action_delete)) }, onClick = { showDeleteConfirmDialogFor = bookmark bookmarkMenuExpandedFor = null @@ -573,12 +575,12 @@ private fun BookmarksList( AlertDialog( onDismissRequest = { showRenameBookmarkDialog = null }, - title = { Text("Rename Bookmark") }, + title = { Text(stringResource(R.string.dialog_rename_bookmark)) }, text = { androidx.compose.material3.OutlinedTextField( value = newTitle, onValueChange = { newTitle = it }, - label = { Text("New Name") }, + label = { Text(stringResource(R.string.label_new_name)) }, placeholder = { Text( text = currentName, @@ -601,12 +603,12 @@ private fun BookmarksList( showRenameBookmarkDialog = null } ) { - Text("Save") + Text(stringResource(R.string.action_save)) } }, dismissButton = { TextButton(onClick = { showRenameBookmarkDialog = null }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) @@ -615,8 +617,8 @@ private fun BookmarksList( showDeleteConfirmDialogFor?.let { bookmarkToDelete -> AlertDialog( onDismissRequest = { showDeleteConfirmDialogFor = null }, - title = { Text("Delete Bookmark?") }, - text = { Text("Are you sure you want to permanently delete this bookmark?") }, + title = { Text(stringResource(R.string.dialog_delete_bookmark)) }, + text = { Text(stringResource(R.string.dialog_delete_bookmark_desc)) }, confirmButton = { TextButton( onClick = { @@ -624,12 +626,12 @@ private fun BookmarksList( showDeleteConfirmDialogFor = null } ) { - Text("Delete") + Text(stringResource(R.string.action_delete)) } }, dismissButton = { TextButton(onClick = { showDeleteConfirmDialogFor = null }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) @@ -646,7 +648,7 @@ private fun HighlightsList( ) { if (userHighlights.isEmpty()) { Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { - Text("No highlights yet.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) + Text(stringResource(R.string.no_highlights_yet), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) } } else { var highlightMenuExpandedFor by remember { mutableStateOf(null) } @@ -663,7 +665,7 @@ private fun HighlightsList( items = userHighlights.sortedBy { it.chapterIndex }, key = { it.id } ) { highlight -> - val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: "Unknown Chapter" + val chapterTitle = chapters.getOrNull(highlight.chapterIndex)?.title ?: stringResource(R.string.unknown_chapter) ListItem( headlineContent = { @@ -694,7 +696,7 @@ private fun HighlightsList( IconButton(onClick = { highlightMenuExpandedFor = highlight }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = "Options" + contentDescription = stringResource(R.string.content_desc_options) ) } DropdownMenu( @@ -702,7 +704,7 @@ private fun HighlightsList( onDismissRequest = { highlightMenuExpandedFor = null } ) { DropdownMenuItem( - text = { Text("Delete") }, + text = { Text(stringResource(R.string.action_delete)) }, onClick = { showHighlightDeleteDialogFor = highlight highlightMenuExpandedFor = null @@ -726,8 +728,8 @@ private fun HighlightsList( showHighlightDeleteDialogFor?.let { highlightToDelete -> AlertDialog( onDismissRequest = { showHighlightDeleteDialogFor = 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 = { @@ -735,12 +737,12 @@ private fun HighlightsList( showHighlightDeleteDialogFor = null } ) { - Text("Delete") + Text(stringResource(R.string.action_delete)) } }, dismissButton = { TextButton(onClick = { showHighlightDeleteDialogFor = null }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) 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 adc0a42..e9312d9 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -41,6 +41,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -120,18 +121,22 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.edit +import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -144,6 +149,7 @@ import com.aryan.reader.BuiltInThemes import com.aryan.reader.CustomTopBanner import com.aryan.reader.DeviceVoiceSettingsSheet import com.aryan.reader.MainViewModel +import com.aryan.reader.R import com.aryan.reader.ReaderThemePanel import com.aryan.reader.RenderMode import com.aryan.reader.SearchResult @@ -157,6 +163,7 @@ import com.aryan.reader.fetchAiDefinition import com.aryan.reader.loadCustomThemes import com.aryan.reader.loadReaderThemeId import com.aryan.reader.paginatedreader.BookPaginator +import com.aryan.reader.paginatedreader.CfiUtils import com.aryan.reader.paginatedreader.HeaderBlock import com.aryan.reader.paginatedreader.IPaginator import com.aryan.reader.paginatedreader.ListItemBlock @@ -661,15 +668,13 @@ fun EpubReaderHost( isAiDefinitionLoading = true aiDefinitionResult = null fetchAiDefinition( - text = word, - onUpdate = { chunk -> - val currentDefinition = aiDefinitionResult?.definition ?: "" - aiDefinitionResult = AiDefinitionResult(definition = currentDefinition + chunk) - }, - onError = { error -> - aiDefinitionResult = AiDefinitionResult(error = error) - }, - onFinish = { isAiDefinitionLoading = false } + text = word, onUpdate = { chunk -> + val currentDefinition = aiDefinitionResult?.definition ?: "" + aiDefinitionResult = + AiDefinitionResult(definition = currentDefinition + chunk) + }, onError = { error -> + aiDefinitionResult = AiDefinitionResult(error = error) + }, onFinish = { isAiDefinitionLoading = false }, context = context ) } } else { @@ -911,7 +916,7 @@ fun EpubReaderHost( } } - val configuration = androidx.compose.ui.platform.LocalConfiguration.current + val configuration = LocalConfiguration.current var lastOrientation by remember { mutableIntStateOf(configuration.orientation) } LaunchedEffect(configuration.orientation) { @@ -1122,8 +1127,8 @@ fun EpubReaderHost( var foundIdx = -1 for (i in chunks.indices) { val c = chunks[i] - val cPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(c.sourceCfi) - val bPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(baseCfi) + val cPath = CfiUtils.getPath(c.sourceCfi) + val bPath = CfiUtils.getPath(baseCfi) if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) { foundIdx = i break @@ -1309,9 +1314,10 @@ fun EpubReaderHost( characterLimit = charLimit, summaryCacheManager = summaryCacheManager, paginator = paginator, + context = context, onProgressUpdate = { recapProgressMessage = it }, onResultUpdate = { chunk -> - isRecapLoading = false // Start showing content + isRecapLoading = false val current = recapResult?.summary ?: "" recapResult = SummarizationResult(summary = current + chunk) }, @@ -1440,7 +1446,7 @@ fun EpubReaderHost( } } - val pageInfoBottomPadding by androidx.compose.animation.core.animateDpAsState( + val pageInfoBottomPadding by animateDpAsState( targetValue = if (showBars && pageInfoMode == PageInfoMode.SYNC) 45.dp else 0.dp, label = "PageInfoBottomPadding" ) @@ -2002,8 +2008,9 @@ fun EpubReaderHost( if (systemUiMode == SystemUiMode.HIDDEN) { 0.dp } else { - val insets = androidx.core.view.ViewCompat.getRootWindowInsets(view) - val ignoringVisibilityTopPx = insets?.getInsetsIgnoringVisibility(androidx.core.view.WindowInsetsCompat.Type.statusBars())?.top ?: 0 + val insets = ViewCompat.getRootWindowInsets(view) + val ignoringVisibilityTopPx = insets?.getInsetsIgnoringVisibility( + WindowInsetsCompat.Type.statusBars())?.top ?: 0 val ignoringVisibilityTop = with(density) { ignoringVisibilityTopPx.toDp() } if (ignoringVisibilityTop > 0.dp) { @@ -2093,7 +2100,7 @@ fun EpubReaderHost( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text("No chapters available for this book.") + Text(stringResource(R.string.no_chapters_available)) } } else { AnimatedContent( @@ -3496,7 +3503,7 @@ fun EpubReaderHost( onDeleteReflow = onDeleteReflow ) - val autoScrollPadding by androidx.compose.animation.core.animateDpAsState( + val autoScrollPadding by animateDpAsState( targetValue = if (showBars) (bottomPadding + 45.dp + 16.dp) else 32.dp, label = "AutoScrollPadding" ) @@ -3854,7 +3861,7 @@ fun EpubReaderHost( CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Navigating to position...", + text = stringResource(R.string.navigating_to_position), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground ) @@ -3865,8 +3872,8 @@ fun EpubReaderHost( if (showPermissionRationaleDialog) { AlertDialog( onDismissRequest = { showPermissionRationaleDialog = false }, - title = { Text("Permission Required") }, - text = { Text("To show playback controls while the app is in the background, please grant the notification permission.") }, + title = { Text(stringResource(R.string.dialog_permission_required)) }, + text = { Text(stringResource(R.string.dialog_permission_notification_desc)) }, confirmButton = { TextButton( onClick = { @@ -3874,7 +3881,7 @@ fun EpubReaderHost( permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } ) { - Text("Continue") + Text(stringResource(R.string.action_continue)) } }, dismissButton = { @@ -3884,7 +3891,7 @@ fun EpubReaderHost( startTts() } ) { - Text("Not now") + Text(stringResource(R.string.action_not_now)) } } ) @@ -3894,11 +3901,11 @@ fun EpubReaderHost( AlertDialog( onDismissRequest = { showJustifyWarningDialog = false }, icon = { Icon(Icons.Default.Info, contentDescription = null) }, - title = { Text("Justified Text Limitation") }, - text = { Text("Using Justified alignment in Paginated Mode may cause text selection and highlights to be inaccurate due to layout limitations.") }, + title = { Text(stringResource(R.string.dialog_justified_text_limitation)) }, + text = { Text(stringResource(R.string.dialog_justified_text_limitation_desc)) }, confirmButton = { TextButton(onClick = { showJustifyWarningDialog = false }) { - Text("I Understand") + Text(stringResource(R.string.action_i_understand)) } } ) @@ -3919,7 +3926,7 @@ fun EpubReaderHost( CircularProgressIndicator() Spacer(Modifier.height(16.dp)) Text( - "Navigating to chapter...", + stringResource(R.string.navigating_to_chapter), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground ) 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 7be0148..87dc785 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt @@ -97,6 +97,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars +import androidx.compose.ui.res.stringResource const val SETTINGS_PREFS_NAME = "epub_reader_settings" private const val TEXT_ALIGN_KEY = "reader_text_align" @@ -389,13 +390,13 @@ fun ReaderTextFormatPanel( .padding(4.dp) ) { Text( - text = if (isLocalMode) "Local Format" else "Global Format", + text = if (isLocalMode) stringResource(R.string.format_local) else stringResource(R.string.format_global), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary ) Icon( imageVector = Icons.Default.ArrowDropDown, - contentDescription = "Select Mode", + contentDescription = stringResource(R.string.content_desc_select_mode), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp) ) @@ -405,8 +406,8 @@ fun ReaderTextFormatPanel( DropdownMenuItem( text = { Column { - Text("Global Format", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) - Text("Applies to all files", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.format_global), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.auto_scroll_applies_all_files), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } }, onClick = { onLocalModeToggle(false); showModeMenu = false }, @@ -416,8 +417,8 @@ fun ReaderTextFormatPanel( DropdownMenuItem( text = { Column { - Text("Local Format", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) - Text("Saved for this file only", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.format_local), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.auto_scroll_saved_for_file), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } }, onClick = { onLocalModeToggle(true); showModeMenu = false }, @@ -428,10 +429,10 @@ fun ReaderTextFormatPanel( Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { TextButton(onClick = onReset, contentPadding = PaddingValues(horizontal = 8.dp)) { - Text("Reset") + Text(stringResource(R.string.action_reset)) } IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) { - Icon(Icons.Default.Close, "Close", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp)) + Icon(Icons.Default.Close, stringResource(R.string.action_close), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp)) } } } @@ -560,15 +561,15 @@ fun FontSelectionSheetContent( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text("Select Font", style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.select_font), style = MaterialTheme.typography.titleMedium) IconButton(onClick = onDismiss) { - Icon(Icons.Default.Close, contentDescription = "Close") + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close)) } } TabRow(selectedTabIndex = selectedTabIndex) { - Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text("Presets") }) - Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text("Imported") }) + Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text(stringResource(R.string.tab_presets)) }) + Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text(stringResource(R.string.tab_imported)) }) } Box(modifier = Modifier.heightIn(min = 200.dp, max = 400.dp)) { @@ -582,7 +583,7 @@ fun FontSelectionSheetContent( Text(font.displayName, fontFamily = getComposeFontFamily(font, null)) }, trailingContent = { - if (isSelected) Icon(Icons.Default.Check, contentDescription = "Selected", tint = MaterialTheme.colorScheme.primary) + if (isSelected) Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary) }, modifier = Modifier.clickable { onFontSelected(font, null) }, colors = if (isSelected) ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)) else ListItemDefaults.colors() @@ -599,14 +600,14 @@ fun FontSelectionSheetContent( ) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text("Import from Files") + Text(stringResource(R.string.button_import_from_files)) } } if (customFonts.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( - "No imported fonts yet.", + stringResource(R.string.no_imported_fonts_yet), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 32.dp) ) @@ -627,7 +628,7 @@ fun FontSelectionSheetContent( if (isSelected) { Icon( imageVector = Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = MaterialTheme.colorScheme.primary ) } @@ -674,16 +675,16 @@ fun VisualOptionsSheet( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text("Visual Options", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.menu_visual_options), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) IconButton(onClick = onDismiss) { - Icon(Icons.Default.Close, contentDescription = "Close") + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close)) } } Spacer(modifier = Modifier.height(16.dp)) // System UI - Text("System UI (Status & Navigation Bars)", style = MaterialTheme.typography.titleMedium) - Text("Control the visibility of the device's system bars.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.visual_options_system_ui), style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.visual_options_system_ui_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(modifier = Modifier.height(12.dp)) OptionSegmentedControl( options = SystemUiMode.entries, @@ -695,8 +696,8 @@ fun VisualOptionsSheet( Spacer(modifier = Modifier.height(24.dp)) // Progress Bar - Text("Progress Bar", style = MaterialTheme.typography.titleMedium) - Text("The reading progress and chapter indicator at the bottom of the screen.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.visual_options_progress_bar), style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.visual_options_progress_bar_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(modifier = Modifier.height(12.dp)) OptionSegmentedControl( options = PageInfoMode.entries, @@ -721,8 +722,8 @@ fun VisualOptionsSheet( horizontalArrangement = Arrangement.SpaceBetween ) { Column(modifier = Modifier.weight(1f)) { - Text("Seamless Chapter Transition", style = MaterialTheme.typography.titleMedium) - Text("Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.visual_options_seamless_chapter), style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.visual_options_seamless_chapter_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } Spacer(modifier = Modifier.width(16.dp)) Switch(checked = !pullToTurnEnabled, onCheckedChange = { onPullToTurnChange(!it) }) 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 bb8a848..4be5216 100644 --- a/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt +++ b/app/src/main/java/com/aryan/reader/feedback/FeedbackScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -61,7 +62,7 @@ import timber.log.Timber private fun launchEmailFeedback(context: android.content.Context) { val intent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:epistemereader@gmail.com".toUri() - putExtra(Intent.EXTRA_SUBJECT, "Feedback: Episteme Reader") + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.feedback_email_subject)) } try { context.startActivity(intent) @@ -82,7 +83,7 @@ fun FeedbackScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Help & Feedback") }, + title = { Text(stringResource(R.string.drawer_help_feedback)) }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") @@ -109,16 +110,14 @@ fun FeedbackScreen( Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Get in Touch", + Text(text = stringResource(R.string.get_in_touch), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or send us an email.", + Text(text = stringResource(R.string.feedback_desc), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -128,8 +127,8 @@ fun FeedbackScreen( Spacer(modifier = Modifier.height(48.dp)) FeedbackOptionCard( - title = "GitHub Issues", - description = "Report bugs, request features, and track development progress.", + title = stringResource(R.string.github_issues), + description = stringResource(R.string.github_issues_desc), icon = { Icon( painter = painterResource(id = R.drawable.github), @@ -146,8 +145,8 @@ fun FeedbackScreen( Spacer(modifier = Modifier.height(16.dp)) FeedbackOptionCard( - title = "Email Support", - description = "Contact us directly via email for any other inquiries.", + title = stringResource(R.string.email_support), + description = stringResource(R.string.email_support_desc), icon = { Icon( imageVector = Icons.Outlined.Email, 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 1fb9fdf..28b3928 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -145,6 +145,7 @@ import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults @@ -259,6 +260,7 @@ import com.aryan.reader.DeviceVoiceSettingsSheet import com.aryan.reader.FileType import com.aryan.reader.MainViewModel import com.aryan.reader.R +import com.aryan.reader.ReaderTheme import com.aryan.reader.ReaderThemePanel import com.aryan.reader.SearchResult import com.aryan.reader.SearchTopBar @@ -276,6 +278,7 @@ import com.aryan.reader.paginatedreader.TtsChunk import com.aryan.reader.pdf.data.AnnotationSettingsRepository import com.aryan.reader.pdf.data.PdfAnnotation import com.aryan.reader.pdf.data.PdfAnnotationRepository +import com.aryan.reader.pdf.data.PdfHighlightRepository import com.aryan.reader.pdf.data.PdfTextBox import com.aryan.reader.pdf.data.PdfTextBoxRepository import com.aryan.reader.pdf.data.PdfTextRepository @@ -294,6 +297,7 @@ import io.legere.pdfiumandroid.api.Bookmark import io.legere.pdfiumandroid.suspend.PdfDocumentKt import io.legere.pdfiumandroid.suspend.PdfPageKt import io.legere.pdfiumandroid.suspend.PdfiumCoreKt +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -314,6 +318,9 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.net.HttpURLConnection import java.net.URL +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.atan2 import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -370,13 +377,13 @@ private fun loadPdfThemeId(context: Context): String { } val PdfBuiltInThemes = listOf( - com.aryan.reader.ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false), - com.aryan.reader.ReaderTheme("reverse", "Reverse", Color.Black, Color.White, true), - com.aryan.reader.ReaderTheme("light", "Light", Color(0xFFFFFFFF), Color(0xFF000000), false), - com.aryan.reader.ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true), - com.aryan.reader.ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false), - com.aryan.reader.ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true), - com.aryan.reader.ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true) + ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false), + ReaderTheme("reverse", "Reverse", Color.Black, Color.White, true), + ReaderTheme("light", "Light", Color(0xFFFFFFFF), Color(0xFF000000), false), + ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true), + ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false), + ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true), + ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true) ) object PdfiumCoreProvider { @@ -1453,7 +1460,7 @@ fun PdfViewerScreen( val pdfTextRepository = remember(context) { PdfTextRepository(context) } val annotationRepository = remember(context) { PdfAnnotationRepository(context) } val textBoxRepository = remember(context) { PdfTextBoxRepository(context) } - val highlightRepository = remember(context) { com.aryan.reader.pdf.data.PdfHighlightRepository(context) } + val highlightRepository = remember(context) { PdfHighlightRepository(context) } var allAnnotations by remember { mutableStateOf>>(emptyMap()) } @@ -1932,14 +1939,14 @@ fun PdfViewerScreen( val dx = (currentPoint.x - startPoint.x) * aspectRatio val dy = (currentPoint.y - startPoint.y) - val angleRad = kotlin.math.atan2(dy, dx) - val angleDeg = (angleRad * 180 / kotlin.math.PI) - val absAngle = kotlin.math.abs(angleDeg) + val angleRad = atan2(dy, dx) + val angleDeg = (angleRad * 180 / PI) + val absAngle = abs(angleDeg) val threshold = 10.0 - val isHorizontal = absAngle < threshold || kotlin.math.abs(absAngle - 180.0) < threshold - val isVertical = kotlin.math.abs(absAngle - 90.0) < threshold + val isHorizontal = absAngle < threshold || abs(absAngle - 180.0) < threshold + val isVertical = abs(absAngle - 90.0) < threshold if (isHorizontal) { currentPoint.copy(y = startPoint.y) @@ -2215,7 +2222,7 @@ fun PdfViewerScreen( } Timber.tag("PdfPositionDebug").i("UI: Restoration Complete | Now at Page: $currentPage") } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) { + if (e is CancellationException) { Timber.tag("PdfPositionDebug").w("UI: Restoration cancelled (likely new recomposition)") } else { Timber.tag("PdfPositionDebug").e(e, "UI: Restoration error.") @@ -2544,19 +2551,16 @@ fun PdfViewerScreen( isAiDefinitionLoading = true aiDefinitionResult = null fetchAiDefinition( - text = text, - onUpdate = { chunk -> - val currentDefinition = aiDefinitionResult?.definition ?: "" - aiDefinitionResult = AiDefinitionResult( - definition = currentDefinition + chunk - ) - }, - onError = { error -> - aiDefinitionResult = AiDefinitionResult(error = error) - }, - onFinish = { - isAiDefinitionLoading = false - } + text = text, onUpdate = { chunk -> + val currentDefinition = aiDefinitionResult?.definition ?: "" + aiDefinitionResult = AiDefinitionResult( + definition = currentDefinition + chunk + ) + }, onError = { error -> + aiDefinitionResult = AiDefinitionResult(error = error) + }, onFinish = { + isAiDefinitionLoading = false + }, context = context ) } } else { @@ -3562,7 +3566,7 @@ fun PdfViewerScreen( pagerState.currentPage ) { if (paginationDraggingOriginPage != null) { - val distance = kotlin.math.abs(pagerState.currentPage - paginationDraggingOriginPage) + val distance = abs(pagerState.currentPage - paginationDraggingOriginPage) (distance + 1).coerceAtLeast(1) } else { 1 @@ -4155,7 +4159,7 @@ fun PdfViewerScreen( } ) { pageIndex -> val isVisiblePage = remember(pagerState.currentPage, pageIndex) { - kotlin.math.abs(pagerState.currentPage - pageIndex) <= 1 + abs(pagerState.currentPage - pageIndex) <= 1 } val isPageBookmarked by remember(bookmarks, pageIndex) { derivedStateOf { @@ -5675,7 +5679,7 @@ fun PdfViewerScreen( ) } Spacer(Modifier.height(8.dp)) - androidx.compose.material3.LinearProgressIndicator( + LinearProgressIndicator( progress = { reflowProgressValue }, modifier = Modifier.fillMaxWidth().height(6.dp), trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000..1429c04 --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,19 @@ + + + + %1$d book + %1$d books + + + book + books + + + %1$d shelf + %1$d shelves + + + %1$d result found + %1$d results found + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4825a13..24bf6a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,249 @@ Episteme + + Cancel + Save + Delete + Remove + OK + Close + Add + Rename + Back + Search + Clear + Apply + Free + Save + + + %1$s you agree to our %2$s and acknowledge you have read our %3$s. + Terms of Service + Privacy Policy + Licenses + %1$d selected + Clear Selection + Pin/Unpin + Info + Select All + Delete File(s) Permanently + Remove from Recents + Warning: Some selected items are synced from a local folder. Proceeding will delete the actual files from your device storage.\n\nThis action cannot be undone. + Do you want to permanently delete %1$d selected file(s) from your device? This action cannot be undone. + Do you want to remove %1$d selected file(s) from the recent files list? It will reappear if you open it again from the library. + File Information + Book Name + Copy Name + Original Name: %1$s + Revert to Original + File Name: %1$s + Author + Format + Size + Added + Location + Source: OPDS Stream + In-App Storage + Internal storage + About Episteme + Version: %1$s (Build: %2$d) + Select a File + Clear All Synced Data? + Are you sure you want to permanently delete all of your book data from the cloud? This will also wipe your local library to prevent re-syncing. This action cannot be undone. + DELETE ALL DATA + + + Home + Library + Recent Files + Your Library is Empty + Select a file to read, or sync a local folder to automatically import books. + No Recent Files + Open a file from your library to see it here. + Setup Folder Sync + Sync Folder + Local Folder + OPDS Stream + Pinned + %1$d%% complete + Not available locally + + + Sign in with Google + By signing in, + Episteme Pro + Upgrade to Episteme Pro + Sync Library + Cloud sync for Local Folders + Upload books from your synced folders to Google Drive. + Custom Fonts + Help & Feedback + Sign Out + + + Recent Files Limit + No limit + %1$d files + Clear Book Cache + Clear Reflow Cache + + + Library + Search title or author… + Types: %1$s + Folders: %1$d + Status: %1$s + All Books + Shelves + Folders + Catalogs + No results found for \"%1$s\" + Select a PDF, EPUB, MOBI, or AZW3 file from your device to get started. + Add file + New shelf + Create New Shelf + Shelf Name + Create + Rename shelf + Delete shelf + Add books + This shelf is empty + Add to %1$s + ADD (%1$d) + No unshelved books to add + All books are already in this shelf + Rename Shelf + Delete Shelf? + Are you sure you want to delete the \'%1$s\' shelf? All books will be moved to Unshelved. + Remove from Shelf? + Are you sure you want to remove %1$d %2$s from the \'%3$s\' shelf? The book(s) will remain in your library and appear under Unshelved. + Delete %1$s? + Are you sure you want to delete the %1$d selected %2$s? All books within will be moved to Unshelved. + + + Sync Local Folders + Connect local folders to create a live library. Episteme will monitor files and sync progress. + Add Folder + Scan All + Scanning… + Sync Meta + LAST SYNC + BOOKS + Edit Filters + Remove Folder + Filter File Types + Select the file types you want to sync from this folder: + Filter Library + File Type + Source Folder + Read Status + Clear All + + + Add Catalog + Search catalog… + This feed is empty. + Downloading… + Loading… + Stream + Unavailable + Download + Download Format + Stream Now + Read + No supported formats available. + PUBLISHER + PUBLISHED + LANGUAGE + Synopsis + Edit Catalog + Add OPDS Catalog + Catalog Name + URL + http://192.168.1.50:8080/opds + Authentication (Optional) + Username + Password + Delete Catalog + Are you sure you want to delete \'%1$s\'? + Deleting this catalog will also permanently remove %1$d streaming books associated with it from your library. + Preset + + + Free Plan + $0 + Forever free + Multiple Formats + Supports PDF, EPUB, MOBI, AZW3 + Android Text-to-Speech + Listen to your books with built-in TTS + Basic Dictionary + Look up single words quickly + Current Plan + 50%% OFF + Loading price… + One-time payment + Lifetime Access + Early Access Sale + Everything in Free, plus: + Cloud Sync Across Devices + Keep your entire library, including book files and reading progress, synced across up to 4 devices. + Summarization + Get quick summaries of chapters or pages + Smart Dictionary + Search phrases and even paragraphs, not just single words + Priority Feature Requests + Your suggestions get prioritized + Pro Features Unlocked! + Sign in Required + Verifying purchase… + Existing Purchase Found + Get Lifetime Access + Upgrade currently unavailable. Please check your internet and try again. + Please sign in to your Google account to purchase Episteme Pro. + This may take a few moments. Your Pro status will be updated automatically. + This device already has a Pro purchase, but it\'s linked to a different account. Please sign in to the account that was used for the original purchase to restore your Pro features. + You\'re getting Episteme Pro at a special discounted price during our early access period! This is a limited-time offer. + Please sign in to your Google account to purchase Episteme Pro and unlock all premium features. + Not Now + Got It! + + + Custom Fonts + Import Font + No Custom Fonts + Import TTF or OTF files to use them in your books. + Grumpy wizards make toxic brew for the evil queen! 1234567890 ?.,;: + Preview unavailable (Invalid font file) + Delete Font? + Are you sure you want to delete \'%1$s\'? This will remove it from all your devices if sync is on. + + + Get in Touch + Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or send us an email. + GitHub Issues + Report bugs, request features, and track development progress. + Email Support + Contact us directly via email for any other inquiries. + + + Unlock Episteme Pro + Sync across devices is a Pro feature. Unlock all pro features with a single, one-time purchase. + Upgrade + Confirm Sign Out + Are you sure you want to sign out? + Device Limit Reached + To use Episteme Pro on this device, please remove one of your existing registered devices. + Last seen: %1$s + Confirm Destructive Action + This will permanently delete all your books and reading progress from this device AND from your Google Drive account. This action cannot be undone. Are you sure? + Clear Book Cache + This will clear all processed pages in pagination mode. This helps fix layout issues but will require books to be re-processed next time you open them. + Confirm & Clear + Clear Reflow Cache + This will delete all generated \'Text View\' versions of your PDFs and clear their associated images/HTML cache. Your original PDFs will remain untouched. + Back Dictionary @@ -58,4 +301,328 @@ Collapse the search results panel Jump to the previous search match in the document Jump to the next search match in the document + + Sign In + Select Folder + Select + Privacy Policy • Terms of Service • Licenses + Episteme OSS + [Debug] Show Device Management + [Debug] Clear Cloud & Local Data + FPS: %1$d + Your device doesn\'t support folder selection. You can still import files individually. + No file manager found. Please install a file manager app. + Feedback: Episteme Reader + Downloaded %1$s + %1$s: %2$s + Never + %1$s: %2$d + + + Removed %1$d streaming books. + Failed to import font: %1$s + Text view deleted. + Sync requires Google Drive permission. + An error occurred with the purchase. + Upgrade successful! Welcome to Pro. + Purchase verification failed. Please contact support if you were charged. + This device was removed from your account. + Could not verify this device. Please check your connection. + Failed to update devices. Please try again. + Saving PDF… + PDF saved successfully. + Failed to open file for saving. + Error saving PDF: %1$s + Saving original PDF… + Original PDF saved successfully. + Sharing: %1$s + Share PDF + Share failed: %1$s + Limit reached: Maximum %1$d folders allowed. + This folder is already synced. + Folder added: %1$s + Failed to access folder permissions. + Folder removed. + Folder Sync: Updating metadata… + Scanning folder for new books… + Folder Sync: Scan complete. + Sync failed. + Enable sync to download files. + Failed to download %1$s. + Enable sync to clear cloud data. + Not signed in, cannot clear cloud data. + Clearing all cloud and local data… + All cloud and local data cleared successfully. + Error: Failed to clear all data. + All local data cleared. + Sign in failed. Please try again. + Could not find a Google account. This can happen on a fresh install, please try again in a moment. + An error occurred during sign in. Please check your internet connection. + Please sign in to test device management. + Sync is an Episteme Pro feature. + Not signed in, cannot sync. + Cloud Sync: Checking for updates… + Cloud Sync: Complete. + Failed to sync library. + Could not find recent item. + Failed to import file. + Could not find file location. + Failed to load generated text view. + Text view generation failed. + Failed to load FB2: %1$s + Failed to load file: %1$s + Failed to load MOBI: %1$s + Failed to load EPUB: %1$s + File deleted from folder. Removed from library. + A shelf with that name already exists. + Deleting from all devices… + Deletion complete. + Cloud sync failed, deleted locally. + %1$d book(s) removed from library. + Reflow cache & generated text views cleared. + + + Search in book… + Close Search + Clear Search + Hide Results + Show Results + Previous Search Result + Next Search Result + No results found. + + + Generating summary… + Stop + Read aloud + Copy + No summary could be generated. + + + Thinking… + Open in Dictionary App + AI could not provide a definition. + Asking AI about \'%1$s\'… + + + Text-to-Speech Settings + Please stop playback to change settings. + Synthesis Mode + On-Device + Cloud (HQ) + Voice Selection + Play Sample + + + On-Device Voice Settings + Close Settings + System Default + Matches your Android system settings + Selected + Loading voices… + No voices available on this device. + Specific Voices + Available Voices (%1$d) + No voices found for this language. + Variant: %1$s + This is a sample of %1$s. + + + Reading Themes + Presets + My Themes + No custom themes yet. Tap \'+\' to create one. + New Theme + Edit Theme + Theme Name + So many books, so little time. + - Frank Zappa + ⚠️ Low contrast! This might cause eye strain. + Page Color + Text Color + Live Preview + Reading is dreaming. + Hex + + + R + G + B + + + 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. + + + Lookup Settings + Dictionary Engine + Smart (AI) + External App + Uses AI for definitions. Will fall back to the external app below if offline or if the selected phrase is too long. + Uses the selected app for dictionary lookups. + Fallback App + Dictionary App + Select an app + Translate + App used for translating selected text. + Search App + App used for web searches. + None + + + 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. + Analyzing Chapter %1$d… + Reading current position… + Generating Recap… + Chapter Summary + Story Recap (Beta) + Unlock Chapter Summarization + Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature. + Learn More + 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. + + + Bookmark + Selected Slot + Customize Palette + Tap a slot to edit: + Select a color for the slot: + + + This chapter is empty. + Chapter not found + Error loading chapter + + + Dictionary Settings + Theme + Theme Settings + More Options + View Original PDF + Delete Text View + Reading Mode: Vertical + Reading Mode: Paginated + Enabled + Remove bookmark + Bookmark this page + Tap to Turn Pages + Volume Button Scrolling + Volume Button Page Turn + Realistic Page Turns + Keep Screen On + Visual Options + Auto Scroll + TTS Voice Settings + TTS Settings (Debug) + Navigate with slider + Chapters Menu + Text Formatting + Chapter Summarization + Recap (Beta) + Stop TTS + Start TTS + Pause TTS + Resume TTS + Exit slider navigation + Start page thumbnail + Expand + Collapse + Pause + Play + Local Speed + Global Speed + Select Mode + Applies to all files + Saved for this file only + Disable Musician Mode + Enable Musician Mode + Swap Controls + Min + Max + Slower + Faster + Page %1$d of %2$d + + + Chapters + Bookmarks + Highlights + You haven\'t added any bookmarks yet. + Page %1$d of %2$d + More options for bookmark + Rename + Rename Bookmark + New Name + Delete Bookmark? + Are you sure you want to permanently delete this bookmark? + No highlights yet. + Unknown Chapter + Options + Delete Highlight? + Are you sure you want to permanently delete this highlight? + + + Original PDF not found. + Error: Book content not found. Path: %1$s + Please select a dictionary app first. + Please select a translate app first. + Please select a search app first. + No chapters available for this book. + Navigating to position… + Permission Required + To show playback controls while the app is in the background, please grant the notification permission. + Continue + Justified Text Limitation + Using Justified alignment in Paginated Mode may cause text selection and highlights to be inaccurate due to layout limitations. + I Understand + Navigating to chapter… + Select an offline dictionary first. + Book is not paginated yet. + Wait for book to load fully. + Release for Previous Chapter + Release for Next Chapter + Pull further… (%1$d%%) + Chapter + Could not get chapter content. + Could not determine current chapter. + WebView not available. + Page %1$d/%2$d + + + Local Format + Global Format + Reset + Size + Spacing + Select Font + Presets + Imported + Import from Files + No imported fonts yet. + Visual Options + System UI (Status & Navigation Bars) + Control the visibility of the device\'s system bars. + Progress Bar + The reading progress and chapter indicator at the bottom of the screen. + Seamless Chapter Transition + Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation. + + + Search + Error opening dictionary + Error opening translate app + Error opening search app