diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 48ee2fe..aa48d29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,6 +40,7 @@ android { } } buildConfigField("boolean", "IS_PRO", "false") + buildConfigField("boolean", "IS_OFFLINE", "false") } flavorDimensions += "version" @@ -91,6 +92,12 @@ android { "proguard-rules.pro" ) } + + create("releaseOffline") { + initWith(getByName("release")) + matchingFallbacks += listOf("release") + buildConfigField("boolean", "IS_OFFLINE", "true") + } } applicationVariants.all { diff --git a/app/src/main/java/com/aryan/reader/Common.kt b/app/src/main/java/com/aryan/reader/Common.kt index 9206e28..1252597 100644 --- a/app/src/main/java/com/aryan/reader/Common.kt +++ b/app/src/main/java/com/aryan/reader/Common.kt @@ -390,7 +390,7 @@ fun SearchTopBar( ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.content_desc_close_search) + contentDescription = stringResource(R.string.tooltip_close_search) ) } @@ -426,7 +426,7 @@ fun SearchTopBar( ) { Icon( Icons.Default.Close, - contentDescription = stringResource(R.string.content_desc_clear_search) + contentDescription = stringResource(R.string.tooltip_clear_search) ) } } @@ -448,8 +448,8 @@ fun SearchTopBar( Icon( imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = stringResource( - if (searchState.showSearchResultsPanel) R.string.content_desc_hide_results - else R.string.content_desc_show_results + if (searchState.showSearchResultsPanel) R.string.tooltip_hide_results + else R.string.tooltip_show_results ) ) } @@ -478,7 +478,7 @@ fun SearchNavigationControls( onClick = { onNavigate(searchState.currentSearchResultIndex - 1) }, enabled = searchState.currentSearchResultIndex > 0 ) { - Icon(Icons.Default.ArrowDropUp, contentDescription = stringResource(R.string.content_desc_prev_result)) + Icon(Icons.Default.ArrowDropUp, contentDescription = stringResource(R.string.tooltip_prev_result)) } Text( @@ -493,7 +493,7 @@ fun SearchNavigationControls( onClick = { onNavigate(searchState.currentSearchResultIndex + 1) }, enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1 ) { - Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.content_desc_next_result)) + Icon(Icons.Default.ArrowDropDown, contentDescription = stringResource(R.string.tooltip_next_result)) } } } diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt index eeff620..0171375 100644 --- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt +++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt @@ -18,6 +18,8 @@ * mail: epistemereader@gmail.com */ // LibraryScreen.kt +@file:Suppress("KotlinConstantConditions") + package com.aryan.reader import android.net.Uri @@ -158,9 +160,19 @@ fun LibraryScreen( val sortOrder = uiState.sortOrder val shelves = uiState.shelves val rawLibraryFiles = uiState.rawLibraryFiles + val tabTitles = remember { + buildList { + add(context.getString(R.string.tab_all_books)) + add(context.getString(R.string.tab_shelves)) + add(context.getString(R.string.tab_folders)) + if (!BuildConfig.IS_OFFLINE) { + add(context.getString(R.string.tab_catalogs)) + } + } + } val pagerState = rememberPagerState( initialPage = uiState.libraryScreenStartPage, - pageCount = { 4 } + pageCount = { tabTitles.size } ) val containsFolderItems = remember(selectedItems) { @@ -254,6 +266,7 @@ fun LibraryScreen( Box(modifier = Modifier.fillMaxSize()) { LibraryScreenContent( + tabTitles = tabTitles, recentFiles = uiState.allRecentFiles, rawLibraryFiles = rawLibraryFiles, shelves = shelves, @@ -494,6 +507,7 @@ fun ShelfScreen( @OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryScreenContent( + tabTitles: List, recentFiles: List, rawLibraryFiles: List, shelves: List, @@ -543,12 +557,6 @@ fun LibraryScreenContent( val isBookContextualModeActive = selectedItems.isNotEmpty() val isShelfContextualModeActive = selectedShelves.isNotEmpty() var showSortMenu by remember { mutableStateOf(false) } - 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) { @@ -811,13 +819,15 @@ fun LibraryScreenContent( ) } 3 -> { - OpdsTab( - localLibraryFiles = rawLibraryFiles, - onBookDownloaded = onOpdsBookDownloaded, - onReadBook = onItemClick, - onStreamBook = onStreamOpdsBook, - onDeleteCatalogStreams = onDeleteCatalogStreams - ) + if (!BuildConfig.IS_OFFLINE) { + OpdsTab( + localLibraryFiles = rawLibraryFiles, + onBookDownloaded = onOpdsBookDownloaded, + onReadBook = onItemClick, + onStreamBook = onStreamOpdsBook, + onDeleteCatalogStreams = onDeleteCatalogStreams + ) + } } } } diff --git a/app/src/main/java/com/aryan/reader/SharedComposables.kt b/app/src/main/java/com/aryan/reader/SharedComposables.kt index 83f236b..15279a0 100644 --- a/app/src/main/java/com/aryan/reader/SharedComposables.kt +++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt @@ -664,7 +664,7 @@ fun AboutDialog(onDismiss: () -> Unit) { tint = MaterialTheme.colorScheme.primary ) }, - text = stringResource(R.string.about_privacy), + text = stringResource(R.string.legal_privacy_policy), subtitle = stringResource(R.string.about_privacy_desc), onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) } ) @@ -680,7 +680,7 @@ fun AboutDialog(onDismiss: () -> Unit) { tint = MaterialTheme.colorScheme.primary ) }, - text = stringResource(R.string.about_terms), + text = stringResource(R.string.legal_terms_of_service), subtitle = stringResource(R.string.about_terms_desc), onClick = { uriHandler.openUri(TERMS_URL) } ) diff --git a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt index 23ae79b..10a582f 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt @@ -72,6 +72,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -81,6 +82,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.core.net.toUri +import com.aryan.reader.R import com.aryan.reader.ReaderTexture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -415,8 +417,8 @@ fun ChapterWebView( val urlToShow = showExternalLinkDialog!! AlertDialog( onDismissRequest = { showExternalLinkDialog = null }, - title = { Text("External Link") }, - text = { Text("You clicked on an external link:\n\n$urlToShow\n\nWhat would you like to do?") }, + title = { Text(stringResource(R.string.dialog_external_link_title)) }, + text = { Text(stringResource(R.string.dialog_external_link_desc, urlToShow)) }, confirmButton = { Row(horizontalArrangement = Arrangement.End) { TextButton(onClick = { @@ -425,23 +427,21 @@ fun ChapterWebView( context.startActivity(intent) } catch (e: ActivityNotFoundException) { Timber.e(e, "No activity found to handle intent for URL: $urlToShow") - Toast.makeText( - context, "No browser found to open the link.", Toast.LENGTH_LONG - ).show() + Toast.makeText(context, context.getString(R.string.error_no_browser), Toast.LENGTH_LONG).show() } showExternalLinkDialog = null - }) { Text("Open") } + }) { Text(stringResource(R.string.action_open)) } TextButton(onClick = { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Copied Link", urlToShow) clipboard.setPrimaryClip(clip) showExternalLinkDialog = null - }) { Text("Copy") } + }) { Text(stringResource(R.string.action_copy)) } } }, dismissButton = { - TextButton(onClick = { showExternalLinkDialog = null }) { Text("Cancel") } + TextButton(onClick = { showExternalLinkDialog = null }) { Text(stringResource(R.string.action_cancel)) } }) } 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 3c67a73..0538491 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt @@ -68,7 +68,6 @@ import com.aryan.reader.epub.EpubChapter import org.json.JSONArray import org.json.JSONObject import java.util.UUID -import kotlin.math.min private const val BOOKMARK_PREFS_NAME = "epub_reader_bookmarks" @@ -445,10 +444,10 @@ fun PaletteManagerDialog( } }, confirmButton = { - TextButton(onClick = { onSave(tempPalette) }) { Text("Save") } + TextButton(onClick = { onSave(tempPalette) }) { Text(stringResource(R.string.action_save)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } } ) } @@ -528,10 +527,10 @@ fun AnnotationBottomSheet( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { - BottomSheetToolButton(icon = R.drawable.copy, label = "Copy", onClick = onCopy, effectiveText = effectiveText) - BottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", onClick = onDictionary, effectiveText = effectiveText) - BottomSheetToolButton(icon = R.drawable.translate, label = "Translate", onClick = onTranslate, effectiveText = effectiveText) - BottomSheetToolButton(icon = R.drawable.search, label = "Search", onClick = onSearch, effectiveText = effectiveText) + BottomSheetToolButton(icon = R.drawable.copy, label = stringResource(R.string.action_copy), onClick = onCopy, effectiveText = effectiveText) + BottomSheetToolButton(icon = R.drawable.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary, effectiveText = effectiveText) + BottomSheetToolButton(icon = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate, effectiveText = effectiveText) + BottomSheetToolButton(icon = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch, effectiveText = effectiveText) } Spacer(Modifier.height(16.dp)) @@ -540,7 +539,7 @@ fun AnnotationBottomSheet( OutlinedTextField( value = noteText, onValueChange = { noteText = it }, - placeholder = { Text("Add a note...", color = effectiveText.copy(alpha = 0.5f)) }, + placeholder = { Text(stringResource(R.string.placeholder_add_note), color = effectiveText.copy(alpha = 0.5f)) }, modifier = Modifier .fillMaxWidth() .heightIn(min = 100.dp), @@ -652,23 +651,23 @@ fun PaginatedTextSelectionMenu( } val actions = mutableListOf() - actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy)) + actions.add(MenuActionItem(iconRes = R.drawable.copy, label = stringResource(R.string.action_copy), onClick = onCopy)) if (onTts != null) { - actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts)) + actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = stringResource(R.string.label_speak), onClick = onTts)) } - actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary)) - actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate)) - actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch)) + actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary)) + actions.add(MenuActionItem(iconRes = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate)) + actions.add(MenuActionItem(iconRes = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch)) if (onNote != null) { - actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = "Note", onClick = onNote)) + actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = stringResource(R.string.label_note), onClick = onNote)) } if (onSelectAll != null) { - actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll)) + actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = stringResource(R.string.select_all), onClick = onSelectAll)) } if (onDelete != null) { - actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true)) + actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = stringResource(R.string.action_remove), onClick = onDelete, isError = true)) } Column(modifier = Modifier.padding(bottom = 4.dp)) { @@ -753,7 +752,7 @@ fun HighlightColorRow( if (selectedColor == colorEnum) { Icon( imageVector = Icons.Default.Check, - contentDescription = "Selected", + contentDescription = stringResource(R.string.content_desc_selected), tint = if (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White, modifier = Modifier.size(18.dp) ) @@ -825,13 +824,13 @@ fun PaginatedTextSelectionMenu( Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = Icons.Default.Edit, - contentDescription = "Note", + contentDescription = stringResource(R.string.label_note), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(14.dp) ) Spacer(Modifier.width(6.dp)) Text( - "Note", + stringResource(R.string.label_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold @@ -851,24 +850,24 @@ fun PaginatedTextSelectionMenu( } val actions = mutableListOf() - actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = onCopy)) + actions.add(MenuActionItem(iconRes = R.drawable.copy, label = stringResource(R.string.action_copy), onClick = onCopy)) if (onTts != null) { - actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts)) + actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = stringResource(R.string.label_speak), onClick = onTts)) } - actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = onDictionary)) - actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate)) - actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch)) + actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary)) + actions.add(MenuActionItem(iconRes = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate)) + actions.add(MenuActionItem(iconRes = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch)) if (onNote != null) { - val noteLabel = if (existingNote.isNullOrBlank()) "Note" else "Edit" + val noteLabel = if (existingNote.isNullOrBlank()) stringResource(R.string.label_note) else stringResource(R.string.label_edit) actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote)) } if (onSelectAll != null) { - actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = onSelectAll)) + actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = stringResource(R.string.select_all), onClick = onSelectAll)) } if (onDelete != null) { - actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = onDelete, isError = true)) + actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = stringResource(R.string.action_remove), onClick = onDelete, isError = true)) } Column(modifier = Modifier.padding(bottom = 4.dp)) { diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt index 46df7fe..3ea0081 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt @@ -19,6 +19,8 @@ */ package com.aryan.reader.epubreader +import android.content.Context +import com.aryan.reader.R import timber.log.Timber import com.aryan.reader.epub.EpubBook import com.aryan.reader.paginatedreader.LocatorConverter @@ -40,6 +42,7 @@ data class ChapterLoadingResult( * the initial chunk to display based on navigation state (CFI, overrides, etc.). */ suspend fun loadChapterContent( + context: Context, epubBook: EpubBook, chapterIndex: Int, chunkTargetOverride: Int?, @@ -64,12 +67,12 @@ suspend fun loadChapterContent( chunkOfElements.joinToString(separator = "\n") { it.outerHtml() } } if (chunkedList.isEmpty()) { - head to listOf("

This chapter is empty.

") + head to listOf("

${context.getString(R.string.chapter_empty)}

") } else { head to chunkedList } } else { - "" to listOf("

Chapter not found

") + "" to listOf("

${context.getString(R.string.chapter_not_found)}

") } var targetChunk = 0 @@ -104,7 +107,7 @@ suspend fun loadChapterContent( Timber.e(e, "Failed to parse chapter") ChapterLoadingResult( head = "", - chunks = listOf("

Error loading chapter

${e.message}

"), + chunks = listOf("

${context.getString(R.string.error_loading_chapter)}

${e.message}

"), startChunkIndex = 0, isSuccess = false, errorMessage = e.message 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 7acead9..dc529db 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -277,7 +277,7 @@ fun EpubReaderTopBar( onDismissRequest = { showMoreMenu = false } ) { DropdownMenuItem( - text = { Text("Customize Toolbar") }, + text = { Text(stringResource(R.string.title_customize_toolbar)) }, onClick = { showMoreMenu = false onCustomizeTools() @@ -1078,7 +1078,7 @@ fun AutoScrollControls( ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) stringResource(R.string.content_desc_pause_playback) else stringResource(R.string.content_desc_start_playback), + contentDescription = if (isPlaying) stringResource(R.string.tooltip_tts_pause) else stringResource(R.string.content_desc_start_playback), modifier = Modifier.size(20.dp) ) } @@ -1395,7 +1395,7 @@ fun CustomizeToolsSheet( ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Select the tools you want to keep visible. Unchecking a tool hides it from the UI to give you a distraction-free reading space.", + text = stringResource(R.string.desc_customize_toolbar), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1495,17 +1495,19 @@ fun TtsControlsSheet( ttsController.sliceAndRetainPosition() } + val ttsSample = stringResource(R.string.tts_sample_text) + ModalBottomSheet( onDismissRequest = onDismiss, contentWindowInsets = { WindowInsets.navigationBars } ) { Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) { - Text("Voice Adjustments", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.tts_voice_adjustments), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) Spacer(Modifier.height(16.dp)) // Rate Slider Row(verticalAlignment = Alignment.CenterVertically) { - Text("Speed (${"%.1f".format(rate)}x)", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium) + Text(stringResource(R.string.tts_speed_label, "%.1f".format(rate)), modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium) IconButton(onClick = { rate = 1.0f ttsController.pause() @@ -1534,7 +1536,7 @@ fun TtsControlsSheet( // Pitch Slider Row(verticalAlignment = Alignment.CenterVertically) { - Text("Pitch (${"%.1f".format(pitch)}x)", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium) + Text(stringResource(R.string.tts_pitch_label, "%.1f".format(pitch)), modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium) IconButton(onClick = { pitch = 1.0f ttsController.pause() @@ -1568,7 +1570,7 @@ fun TtsControlsSheet( if (ttsState.isPlaying) ttsController.pause() tts?.setSpeechRate(rate) tts?.setPitch(pitch) - tts?.speak("This is how your current voice settings sound.", TextToSpeech.QUEUE_FLUSH, null, null) + tts?.speak(ttsSample, TextToSpeech.QUEUE_FLUSH, null, null) }, modifier = Modifier.fillMaxWidth(), enabled = isTtsReady, @@ -1611,14 +1613,14 @@ fun TtsControlsSheet( } else { Icon( painter = painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), - contentDescription = if (ttsState.isPlaying) "Pause Book" else "Play Book", + contentDescription = if (ttsState.isPlaying) stringResource(R.string.tts_pause_book) else stringResource(R.string.tts_resume_book), modifier = Modifier.size(32.dp) ) } } Spacer(Modifier.height(8.dp)) Text( - text = if (ttsState.isPlaying) "Pause Book" else "Resume Book", + text = if (ttsState.isPlaying) stringResource(R.string.tts_pause_book) else stringResource(R.string.tts_resume_book), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1635,7 +1637,7 @@ fun TtsControlsSheet( ) { Icon(Icons.Default.Settings, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text("System Voice / Engine Settings") + Text(stringResource(R.string.tts_system_settings)) } } } 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 d89c376..9a644ec 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt @@ -551,7 +551,7 @@ private fun BookmarksList( onDismissRequest = { bookmarkMenuExpandedFor = null } ) { DropdownMenuItem( - text = { Text(stringResource(R.string.menu_rename)) }, + text = { Text(stringResource(R.string.action_rename)) }, onClick = { showRenameBookmarkDialog = bookmark bookmarkMenuExpandedFor = null @@ -680,12 +680,12 @@ private fun HighlightsList( androidx.compose.material3.FilterChip( selected = !filterWithNotesOnly, onClick = { filterWithNotesOnly = false }, - label = { Text("All") } + label = { Text(stringResource(R.string.filter_all)) } ) androidx.compose.material3.FilterChip( selected = filterWithNotesOnly, onClick = { filterWithNotesOnly = true }, - label = { Text("With Notes") } + label = { Text(stringResource(R.string.filter_with_notes)) } ) } @@ -773,7 +773,7 @@ private fun HighlightsList( ) HorizontalDivider() DropdownMenuItem( - text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") }, + text = { Text(if (highlight.note.isNullOrBlank()) stringResource(R.string.menu_add_note) else stringResource(R.string.menu_edit_note)) }, onClick = { onEditNote(highlight) highlightMenuExpandedFor = null 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 de7cbe0..8d35ef4 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -702,7 +702,7 @@ fun EpubReaderHost( if (!selectedDictPackage.isNullOrEmpty()) { ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word) } else { - Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_select_dictionary_first), Toast.LENGTH_SHORT).show() showDictionarySettingsSheet = true } } @@ -712,7 +712,7 @@ fun EpubReaderHost( if (!selectedTranslatePackage.isNullOrEmpty()) { ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text) } else { - Toast.makeText(context, "Please select a translate app first.", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_select_translate_first), Toast.LENGTH_SHORT).show() showDictionarySettingsSheet = true } } @@ -721,7 +721,7 @@ fun EpubReaderHost( if (!selectedSearchPackage.isNullOrEmpty()) { ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text) } else { - Toast.makeText(context, "Please select a search app first.", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_select_search_first), Toast.LENGTH_SHORT).show() showDictionarySettingsSheet = true } } @@ -1371,6 +1371,7 @@ fun EpubReaderHost( activeFragmentId = null val result = loadChapterContent( + context = context, epubBook = epubBook, chapterIndex = currentChapterIndex, chunkTargetOverride = chunkTargetOverride, @@ -2835,7 +2836,7 @@ fun EpubReaderHost( if (pullToTurnEnabled && currentChapterIndex > 0) { ChapterChangeIndicator( - text = "Release for Previous Chapter", + text = stringResource(R.string.release_for_previous_chapter), progress = pullToPrevProgress, isPullingDown = true, modifier = Modifier @@ -2846,7 +2847,7 @@ fun EpubReaderHost( if (pullToTurnEnabled && currentChapterIndex < chapters.size - 1) { ChapterChangeIndicator( - text = "Release for Next Chapter", + text = stringResource(R.string.release_for_next_chapter), progress = pullToNextProgress, isPullingDown = false, modifier = Modifier 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 4cadb06..d3a79df 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt @@ -481,7 +481,7 @@ fun ReaderTextFormatPanel( // FONT & ALIGNMENT SECTION Text( - text = "FONT & ALIGNMENT", + text = stringResource(R.string.section_font_alignment), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, @@ -504,7 +504,7 @@ fun ReaderTextFormatPanel( ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "Aa", + text = stringResource(R.string.label_aa_preview), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSecondaryContainer, @@ -570,7 +570,7 @@ fun ReaderTextFormatPanel( // LAYOUT & SPACING SECTION Text( - text = "LAYOUT & SPACING", + text = stringResource(R.string.section_layout_spacing), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, @@ -581,7 +581,7 @@ fun ReaderTextFormatPanel( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { // Size Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Font Size", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) + Text(stringResource(R.string.label_font_size), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) Slider( value = currentFontSize, onValueChange = onFontSizeChange, @@ -589,11 +589,11 @@ fun ReaderTextFormatPanel( steps = 24, modifier = Modifier.weight(1f) ) - Text(if (currentFontSize in 0.99f..1.01f) "Orig" else "%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) + Text(if (currentFontSize in 0.99f..1.01f) stringResource(R.string.label_original) else "%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) } // Lines Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Line Height", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) + Text(stringResource(R.string.label_line_height), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) Slider( value = currentLineHeight, onValueChange = onLineHeightChange, @@ -601,11 +601,11 @@ fun ReaderTextFormatPanel( steps = 19, modifier = Modifier.weight(1f) ) - Text(if (currentLineHeight <= 1.01f) "Orig" else "%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) + Text(if (currentLineHeight <= 1.01f) stringResource(R.string.label_original) else "%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) } // Paragraph Gap Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Paragraph Gap", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) + Text(stringResource(R.string.label_paragraph_gap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp)) Slider( value = currentParagraphGap, onValueChange = onParagraphGapChange, @@ -613,7 +613,7 @@ fun ReaderTextFormatPanel( steps = 29, modifier = Modifier.weight(1f) ) - Text(if (currentParagraphGap in 0.99f..1.01f) "Orig" else "%.1fx".format(currentParagraphGap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) + Text(if (currentParagraphGap in 0.99f..1.01f) stringResource(R.string.label_original) else "%.1fx".format(currentParagraphGap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) } } Spacer(Modifier.height(8.dp)) diff --git a/app/src/main/java/com/aryan/reader/epubreader/ExternalDictionaryHelper.kt b/app/src/main/java/com/aryan/reader/epubreader/ExternalDictionaryHelper.kt index beda0b2..0619933 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ExternalDictionaryHelper.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ExternalDictionaryHelper.kt @@ -11,6 +11,7 @@ import android.os.Build import android.widget.Toast import timber.log.Timber import androidx.core.net.toUri +import com.aryan.reader.R data class ExternalDictionaryApp( val label: String, @@ -72,7 +73,7 @@ object ExternalDictionaryHelper { sortedApps.add( 0, ExternalDictionaryApp( - label = "Search", + label = context.getString(R.string.dict_app_label_search), packageName = GOOGLE_SEARCH_PKG, icon = null ) @@ -127,7 +128,7 @@ object ExternalDictionaryHelper { } catch (e: Exception) { Timber.e(e, "Failed to launch dictionary app: $packageName") - Toast.makeText(context, "Error opening dictionary", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.error_opening_dictionary), Toast.LENGTH_SHORT).show() } } @@ -172,7 +173,7 @@ object ExternalDictionaryHelper { launchGenericSend(context, packageName, query) } catch (e: Exception) { Timber.e(e, "Failed to launch translate app: $packageName") - Toast.makeText(context, "Error opening translate app", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.error_opening_translate), Toast.LENGTH_SHORT).show() } } @@ -210,7 +211,7 @@ object ExternalDictionaryHelper { launchGenericSend(context, packageName, query) } catch (e: Exception) { Timber.e(e, "Failed to launch search app: $packageName") - Toast.makeText(context, "Error opening search app", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.error_opening_search), Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt b/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt index 2b43eb0..55782f0 100644 --- a/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt +++ b/app/src/main/java/com/aryan/reader/pdf/AnnotationDock.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp @@ -112,7 +113,7 @@ fun AnnotationDock( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Close Edit Mode", + contentDescription = stringResource(R.string.content_desc_close_edit_mode), tint = Color.White, modifier = Modifier.size(iconSize) ) @@ -130,7 +131,7 @@ fun AnnotationDock( ) { Icon( imageVector = visIcon, - contentDescription = "Toggle Visibility", + contentDescription = stringResource(R.string.content_desc_toggle_visibility), tint = visTint, modifier = Modifier.size(iconSize) ) @@ -166,7 +167,7 @@ fun AnnotationDock( ) { Icon( imageVector = iconVector, - contentDescription = "Stylus Only Mode", + contentDescription = stringResource(R.string.content_desc_stylus_only_mode), tint = iconTint, modifier = Modifier.size(iconSize) ) @@ -182,7 +183,7 @@ fun AnnotationDock( iconRes = R.drawable.pen, isActive = isPenActive, tintColor = if(isMinimized) Color.Gray else activePenColor, - description = "Pen", + description = stringResource(R.string.content_desc_pen), size = buttonSize, iconSize = iconSize, onClick = { @@ -202,7 +203,7 @@ fun AnnotationDock( iconRes = R.drawable.marker, isActive = isHighlighterActive, tintColor = if(isMinimized) Color.Gray else activeHighlighterColor.copy(alpha = 1f), - description = "Highlighter", + description = stringResource(R.string.content_desc_highlighter), size = buttonSize, iconSize = iconSize, onClick = { @@ -221,7 +222,7 @@ fun AnnotationDock( iconRes = R.drawable.keyboard, isActive = !isMinimized && selectedTool == InkType.TEXT, tintColor = if(isMinimized) Color.Gray else Color.White, - description = "Text", + description = stringResource(R.string.content_desc_text), size = buttonSize, iconSize = iconSize, onClick = { if(!isMinimized) onToolClick(InkType.TEXT) } @@ -232,7 +233,7 @@ fun AnnotationDock( iconRes = R.drawable.eraser, isActive = !isMinimized && selectedTool == InkType.ERASER, tintColor = if(isMinimized) Color.Gray else Color.White, - description = "Eraser", + description = stringResource(R.string.content_desc_eraser), size = buttonSize, iconSize = iconSize, onClick = { if(!isMinimized) onToolClick(InkType.ERASER) } @@ -249,7 +250,7 @@ fun AnnotationDock( ) { Icon( imageVector = Icons.AutoMirrored.Filled.Undo, - contentDescription = "Undo", + contentDescription = stringResource(R.string.content_desc_undo), tint = if (canUndo && !isMinimized) Color.White else Color.White.copy(alpha = 0.3f), modifier = Modifier.size(iconSize) ) @@ -265,7 +266,7 @@ fun AnnotationDock( ) { Icon( imageVector = Icons.AutoMirrored.Filled.Redo, - contentDescription = "Redo", + contentDescription = stringResource(R.string.content_desc_redo), tint = if (canRedo && !isMinimized) Color.White else Color.White.copy(alpha = 0.3f), modifier = Modifier.size(iconSize) ) @@ -286,7 +287,7 @@ fun AnnotationDock( ) { Icon( imageVector = Icons.Default.VisibilityOff, - contentDescription = "Show Dock", + contentDescription = stringResource(R.string.content_desc_show_dock), tint = Color.White, modifier = Modifier.size(20.dp) ) diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt index 5eea909..e421dc2 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt @@ -93,6 +93,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -1589,6 +1590,10 @@ internal fun PdfPageComposable( } } + val errorSelection = stringResource(R.string.error_selection) + val errorOcrSelection = stringResource(R.string.error_ocr_selection) + val errorProcessingPage = stringResource(R.string.error_processing_page) + BoxWithConstraints( modifier = modifier .onGloballyPositioned { layoutCoordinates = it } @@ -2359,8 +2364,7 @@ internal fun PdfPageComposable( Timber.e( e, "Long press: Error during OCR text selection" ) - pageErrorMessage = - "OCR selection error: ${e.localizedMessage}" + pageErrorMessage = errorOcrSelection } finally { isPerformingOcrForSelection = false ocrRipplePosition = null @@ -2381,7 +2385,7 @@ internal fun PdfPageComposable( e, "Error during long press text selection on page $pageIndex" ) - pageErrorMessage = "Selection error: ${e.localizedMessage}" + pageErrorMessage = errorSelection customMenuState = null selectionCharRange.value = null selectedWordScreenRects = emptyList() @@ -3467,7 +3471,7 @@ internal fun PdfPageComposable( } } catch (e: Exception) { if (e is CancellationException) throw e - pageErrorMessage = "Error processing page: ${e.localizedMessage}" + pageErrorMessage = errorProcessingPage } finally { isLoadingPage = false localBitmap?.recycle() @@ -3869,7 +3873,7 @@ internal fun PdfPageComposable( else -> { Text( - text = "Unable to display page ${pageIndex + 1}.", + text = stringResource(R.string.error_unable_to_display_page), modifier = Modifier .padding(16.dp) .align(Alignment.Center) 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 bd36835..be15ffa 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -1412,7 +1412,7 @@ fun PdfViewerScreen( } catch (e: Exception) { Timber.tag("PdfPrint").e(e, "Failed to initialize print job") coroutineScope.launch { - snackbarHostState.showSnackbar("Could not open print settings") + snackbarHostState.showSnackbar(context.getString(R.string.error_open_print_settings)) } } } @@ -5593,10 +5593,10 @@ fun PdfViewerScreen( verticalReaderState.currentPage } val titleText = when { - isLoadingDocument -> "Loading PDF..." - errorMessage != null -> "Error loading PDF" + isLoadingDocument -> stringResource(R.string.loading_pdf) + errorMessage != null -> stringResource(R.string.error_loading_pdf) totalPages > 0 && pagerState.pageCount > 0 -> "Page ${currentPageForDisplay + 1} of $totalPages" - totalPages > 0 && pagerState.pageCount == 0 -> "Loading page..." + totalPages > 0 && pagerState.pageCount == 0 -> stringResource(R.string.loading_page) else -> "PDF Viewer" } Text( @@ -5610,12 +5610,12 @@ fun PdfViewerScreen( if (!hiddenTools.contains(PdfReaderTool.THEME.name)) { TooltipIconButton( - text = "Theme", - description = "Theme Settings", + text = stringResource(R.string.tooltip_theme), + description = stringResource(R.string.tooltip_theme_desc), onClick = { showThemePanel = true }) { Icon( painter = painterResource(id = R.drawable.palette), - contentDescription = "Theme Settings", + contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -5637,7 +5637,7 @@ fun PdfViewerScreen( }) { Icon( imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, - contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning", + contentDescription = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -5674,7 +5674,7 @@ fun PdfViewerScreen( if (BuildConfig.DEBUG) { TooltipIconButton( - text = "Pen Playground", + text = stringResource(R.string.pen_playground), onClick = { showPenPlayground = true }) { Icon( imageVector = Icons.Default.Star, @@ -5683,7 +5683,7 @@ fun PdfViewerScreen( ) } - TooltipIconButton(text = "Import SVG", onClick = { + TooltipIconButton(text = stringResource(R.string.import_svg), onClick = { val page = if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage @@ -5712,16 +5712,16 @@ fun PdfViewerScreen( } redoStack.clear() - snackbarHostState.showSnackbar("Imported ${svgAnnotations.size} SVG strokes!") + snackbarHostState.showSnackbar(context.getString(R.string.msg_imported_svg_strokes)) } else { - snackbarHostState.showSnackbar("Failed to import SVG or empty.") + snackbarHostState.showSnackbar(context.getString(R.string.error_import_svg_failed)) } } } }) { Icon( imageVector = Icons.Default.Brush, - contentDescription = "Import SVG", + contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63) ) } @@ -5735,7 +5735,7 @@ fun PdfViewerScreen( onClick = { showMoreMenu = true }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = "More Options" + contentDescription = stringResource(R.string.tooltip_more_options) ) } @@ -5744,19 +5744,19 @@ fun PdfViewerScreen( expanded = showMoreMenu, onDismissRequest = { showMoreMenu = false }) { DropdownMenuItem( - text = { Text("Customize Toolbar") }, + text = { Text(stringResource(R.string.title_customize_toolbar)) }, onClick = { showMoreMenu = false showCustomizeToolsSheet = true }, leadingIcon = { - Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(20.dp)) + Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.title_customize_toolbar), modifier = Modifier.size(20.dp)) } ) HorizontalDivider() if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) { DropdownMenuItem( - text = { Text("OCR Language") }, + text = { Text(stringResource(R.string.menu_ocr_language)) }, onClick = { showMoreMenu = false hasSelectedOcrLanguage = true @@ -5767,7 +5767,7 @@ fun PdfViewerScreen( if (!hiddenTools.contains(PdfReaderTool.READING_MODE.name)) { DropdownMenuItem( - text = { Text("Reading Mode: Vertical scroll") }, + text = { Text(stringResource(R.string.menu_reading_mode_vertical)) }, enabled = !isTtsSessionActive, onClick = { displayMode = DisplayMode.VERTICAL_SCROLL @@ -5777,13 +5777,13 @@ fun PdfViewerScreen( if (displayMode == DisplayMode.VERTICAL_SCROLL) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Selected" + contentDescription = stringResource(R.string.content_desc_selected) ) } }) HorizontalDivider() DropdownMenuItem( - text = { Text("Reading Mode: Paginated") }, + text = { Text(stringResource(R.string.menu_reading_mode_paginated)) }, enabled = !isTtsSessionActive, onClick = { displayMode = DisplayMode.PAGINATION @@ -5793,7 +5793,7 @@ fun PdfViewerScreen( if (displayMode == DisplayMode.PAGINATION) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Selected" + contentDescription = stringResource(R.string.content_desc_selected) ) } }) @@ -5801,7 +5801,7 @@ fun PdfViewerScreen( } if (!hiddenTools.contains(PdfReaderTool.KEEP_SCREEN_ON.name)) { DropdownMenuItem( - text = { Text("Keep Screen On") }, + text = { Text(stringResource(R.string.menu_keep_screen_on)) }, onClick = { isKeepScreenOn = !isKeepScreenOn saveKeepScreenOn(context, isKeepScreenOn) @@ -5811,7 +5811,7 @@ fun PdfViewerScreen( if (isKeepScreenOn) { Icon( imageVector = Icons.Filled.Check, - contentDescription = "Selected" + contentDescription = stringResource(R.string.content_desc_selected) ) } }) @@ -5819,7 +5819,7 @@ fun PdfViewerScreen( } if (!hiddenTools.contains(PdfReaderTool.AUTO_SCROLL.name)) { DropdownMenuItem( - text = { Text("Auto Scroll") }, + text = { Text(stringResource(R.string.menu_auto_scroll)) }, enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL, onClick = { showMoreMenu = false @@ -5832,7 +5832,7 @@ fun PdfViewerScreen( } if (!hiddenTools.contains(PdfReaderTool.TTS_SETTINGS.name)) { DropdownMenuItem( - text = { Text("TTS Voice Settings") }, + text = { Text(stringResource(R.string.menu_tts_voice_settings)) }, onClick = { showMoreMenu = false showDeviceVoiceSettingsSheet = true @@ -5848,7 +5848,7 @@ fun PdfViewerScreen( if (BuildConfig.DEBUG) { DropdownMenuItem( - text = { Text("TTS Settings (Debug)") }, + text = { Text(stringResource(R.string.menu_tts_settings_debug)) }, onClick = { showMoreMenu = false showTtsSettingsSheet = true @@ -5867,8 +5867,8 @@ fun PdfViewerScreen( if (!hiddenTools.contains(PdfReaderTool.BOOKMARK.name)) { DropdownMenuItem(text = { Text( - if (isBookmarked) "Remove bookmark" - else "Bookmark this page" + if (isBookmarked) stringResource(R.string.menu_remove_bookmark) + else stringResource(R.string.menu_bookmark_this_page) ) }, onClick = { showMoreMenu = false @@ -5878,7 +5878,7 @@ fun PdfViewerScreen( } if (!hiddenTools.contains(PdfReaderTool.PAGE_MANAGEMENT.name)) { DropdownMenuItem( - text = { Text("Insert Blank Page") }, + text = { Text(stringResource(R.string.menu_insert_blank_page)) }, onClick = { showMoreMenu = false onInsertPage() @@ -5888,7 +5888,7 @@ fun PdfViewerScreen( virtualPages.getOrNull(currentPage) is VirtualPage.BlankPage if (canDelete) { DropdownMenuItem( - text = { Text("Delete Page") }, + text = { Text(stringResource(R.string.menu_delete_page)) }, onClick = { showMoreMenu = false onDeletePage() @@ -5905,9 +5905,9 @@ fun PdfViewerScreen( text = { Text( when { - isReflowingThisBook -> "Generating... ${(reflowProgressValue * 100).toInt()}%" - hasReflowFile -> "Open Text View" - else -> "Generate Text View" + isReflowingThisBook -> stringResource(R.string.generating_reflow_progress) + hasReflowFile -> stringResource(R.string.action_open_text_view) + else -> stringResource(R.string.action_generate_text_view) } ) }, @@ -5964,7 +5964,7 @@ fun PdfViewerScreen( } if (!hiddenTools.contains(PdfReaderTool.SHARE.name)) { DropdownMenuItem( - text = { Text("Share") }, + text = { Text(stringResource(R.string.action_share)) }, onClick = { showMoreMenu = false showShareDialog = true @@ -5979,7 +5979,7 @@ fun PdfViewerScreen( } if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.SAVE_COPY.name)) { DropdownMenuItem( - text = { Text("Save copy to device") }, + text = { Text(stringResource(R.string.action_save_copy_to_device)) }, onClick = { showMoreMenu = false showSaveDialog = true @@ -5993,7 +5993,7 @@ fun PdfViewerScreen( } if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.PRINT.name)) { DropdownMenuItem( - text = { Text("Print") }, + text = { Text(stringResource(R.string.action_print)) }, onClick = { showMoreMenu = false onPrintDocument() @@ -6066,7 +6066,7 @@ fun PdfViewerScreen( }, modifier = Modifier.size(20.dp) ) { - Icon(Icons.Default.Close, contentDescription = "Close Tab", modifier = Modifier.size(16.dp), tint = contentColor) + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.close_tab), modifier = Modifier.size(16.dp), tint = contentColor) } } } @@ -6113,7 +6113,7 @@ fun PdfViewerScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Generating Text View...", + text = stringResource(R.string.generating_text_view), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f) @@ -6162,7 +6162,7 @@ fun PdfViewerScreen( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Indexing pages... ${(backgroundIndexingProgress * 100).toInt()}% done. Search results will update automatically.", + text = stringResource(R.string.msg_indexing_pages_progress), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSecondaryContainer ) @@ -6358,7 +6358,7 @@ fun PdfViewerScreen( ) { Icon( painter = painterResource(id = R.drawable.slider), - contentDescription = "Navigate with slider" + contentDescription = stringResource(R.string.content_desc_navigate_slider) ) } } @@ -6393,7 +6393,7 @@ fun PdfViewerScreen( ) { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search" + contentDescription = stringResource(R.string.action_search) ) } } @@ -6438,7 +6438,7 @@ fun PdfViewerScreen( ) { Icon( painter = painterResource(id = R.drawable.ai), - contentDescription = "AI Features" + contentDescription = stringResource(R.string.tooltip_ai) ) } DropdownMenu( @@ -6446,7 +6446,7 @@ fun PdfViewerScreen( onDismissRequest = { showAiFeaturesMenu = false }) { DropdownMenuItem( text = { - Text("Summarize Page (Page ${currentPage + 1})") + Text(stringResource(R.string.action_summarize_page)) }, onClick = { showAiFeaturesMenu = false if (isProUser) { @@ -6526,7 +6526,7 @@ fun PdfViewerScreen( 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) ) } @@ -6552,21 +6552,21 @@ fun PdfViewerScreen( 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) ) } // Tune button for BASE mode if (currentTtsMode == TtsPlaybackManager.TtsMode.BASE) { TooltipIconButton( - text = "Voice Adjustments", + text = stringResource(R.string.tts_voice_adjustments), description = "Adjust voice speed and pitch", onClick = { showTtsControlsSheet = true } ) { Icon( imageVector = Icons.Default.Tune, - contentDescription = "Voice Adjustments" + contentDescription = stringResource(R.string.tts_voice_adjustments) ) } } @@ -6721,19 +6721,13 @@ fun PdfViewerScreen( .fillMaxSize() .then( when { - isDockDragging -> Modifier // Positioned manually - // via offset during - // drag - dockLocation == DockLocation.TOP -> Modifier // Aligned via Box - // Scope - dockLocation == DockLocation.BOTTOM -> Modifier // Aligned via Box - // Scope - else -> Modifier // Positioned manually - // via offset + isDockDragging -> Modifier + dockLocation == DockLocation.TOP -> Modifier + dockLocation == DockLocation.BOTTOM -> Modifier + else -> Modifier } ) ) { - // Calculate drag offset to apply if floating/dragging val dragModifier = if (isDockDragging || dockLocation == DockLocation.FLOATING) { Modifier.offset { @@ -6742,10 +6736,9 @@ fun PdfViewerScreen( ) } } else { - Modifier // Sticky positions use alignment below + Modifier } - // Calculate Alignment for Sticky states val alignModifier = when { isDockDragging || dockLocation == DockLocation.FLOATING -> Modifier dockLocation == DockLocation.TOP -> Modifier.align(Alignment.TopCenter) @@ -6759,7 +6752,7 @@ fun PdfViewerScreen( } else { Modifier.padding( horizontal = 16.dp - ) // Original padding for floating capsule + ) } val paddingModifier = @@ -7209,10 +7202,10 @@ fun PdfViewerScreen( if (showPermissionRationaleDialog) { AlertDialog( onDismissRequest = { showPermissionRationaleDialog = false }, - title = { Text("Permission Required") }, + title = { Text(stringResource(R.string.dialog_permission_required)) }, text = { Text( - "To show playback controls while the app is in the background, please grant the notification permission." + stringResource(R.string.dialog_permission_notification_desc) ) }, confirmButton = { @@ -7222,14 +7215,14 @@ fun PdfViewerScreen( permissionLauncher.launch( Manifest.permission.POST_NOTIFICATIONS ) - }) { Text("Continue") } + }) { Text(stringResource(R.string.action_continue)) } }, dismissButton = { TextButton( onClick = { showPermissionRationaleDialog = false startTts() - }) { Text("Not now") } + }) { Text(stringResource(R.string.action_not_now)) } }) } if (showSummarizationUpsellDialog) { @@ -7252,11 +7245,11 @@ fun PdfViewerScreen( onClick = { showSummarizationUpsellDialog = false onNavigateToPro() - }) { Text("Learn More") } + }) { Text(stringResource(R.string.action_learn_more)) } }, dismissButton = { TextButton(onClick = { showSummarizationUpsellDialog = false }) { - Text("Not Now") + Text(stringResource(R.string.action_not_now)) } }) } @@ -7287,13 +7280,13 @@ fun PdfViewerScreen( .padding(bottom = 16.dp) ) { Text( - text = "Add PDF to Tab", + text = stringResource(R.string.title_add_pdf_to_tab), style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(16.dp) ) if (pdfFiles.isEmpty()) { Text( - "No other PDFs found in your library.", + stringResource(R.string.msg_no_other_pdfs_found), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -7345,7 +7338,7 @@ fun PdfViewerScreen( if (!selectedDictPackage.isNullOrEmpty()) { ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text) } else { - Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_select_offline_dict_first), Toast.LENGTH_SHORT).show() showDictionarySettingsSheet = true } } @@ -7358,19 +7351,19 @@ fun PdfViewerScreen( painter = painterResource(id = R.drawable.ai), contentDescription = null ) - }, title = { Text("Unlock Smart Dictionary") }, text = { + }, title = { Text(stringResource(R.string.ai_unlock_smart_dict)) }, text = { Text( - "Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text." + stringResource(R.string.ai_unlock_smart_dict_desc) ) }, confirmButton = { TextButton( onClick = { showDictionaryUpsellDialog = false onNavigateToPro() - }) { Text("Learn More") } + }) { Text(stringResource(R.string.action_learn_more)) } }, dismissButton = { TextButton(onClick = { showDictionaryUpsellDialog = false }) { - Text("Not Now") + Text(stringResource(R.string.action_not_now)) } }) } @@ -7380,12 +7373,10 @@ fun PdfViewerScreen( AlertDialog( onDismissRequest = { showReindexDialog = null }, icon = { Icon(Icons.Default.Info, contentDescription = null) }, - title = { Text("Re-index Document?") }, + title = { Text(stringResource(R.string.title_reindex_document)) }, text = { Text( - "You are changing the OCR script to ${newLanguage.displayName}.\n\n" + - "To ensure search accuracy, we need to clear the existing index and re-scan pages that require OCR using this new language.\n\n" + - "This will happen in the background." + stringResource(R.string.desc_reindex_document_warning) ) }, confirmButton = { @@ -7412,11 +7403,11 @@ fun PdfViewerScreen( showOcrLanguageDialog = false } } - ) { Text("Re-index") } + ) { Text(stringResource(R.string.action_reindex)) } }, dismissButton = { TextButton(onClick = { showReindexDialog = null }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } ) @@ -7625,8 +7616,8 @@ fun PdfViewerScreen( val url = clickedLinkUrl!! AlertDialog( onDismissRequest = { clickedLinkUrl = null }, - title = { Text("External Link") }, - text = { Text("You are about to navigate to:\n$url") }, + title = { Text(stringResource(R.string.dialog_external_link_title)) }, + text = { Text(stringResource(R.string.desc_external_link_warning)) }, confirmButton = { TextButton( onClick = { @@ -7636,7 +7627,7 @@ fun PdfViewerScreen( Timber.e(e, "Failed to open URI") } clickedLinkUrl = null - }) { Text("Visit") } + }) { Text(stringResource(R.string.action_visit)) } }, dismissButton = { Row { @@ -7644,9 +7635,9 @@ fun PdfViewerScreen( onClick = { clipboardManager.setText(AnnotatedString(url)) clickedLinkUrl = null - }) { Text("Copy") } + }) { Text(stringResource(R.string.action_copy)) } TextButton(onClick = { clickedLinkUrl = null }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } }) @@ -7655,8 +7646,8 @@ fun PdfViewerScreen( if (showSaveDialog) { AlertDialog( onDismissRequest = { showSaveDialog = false }, - title = { Text("Save to Device") }, - text = { Text("Choose format to save:") }, + title = { Text(stringResource(R.string.title_save_to_device)) }, + text = { Text(stringResource(R.string.desc_choose_format_save)) }, confirmButton = { TextButton( onClick = { @@ -7666,7 +7657,7 @@ fun PdfViewerScreen( originalFileName, isAnnotated = true ) saveLauncher.launch(suggestedName) - }) { Text("With Annotations") } + }) { Text(stringResource(R.string.action_with_annotations)) } }, dismissButton = { Row { @@ -7678,7 +7669,7 @@ fun PdfViewerScreen( originalFileName, isAnnotated = false ) saveLauncher.launch(suggestedName) - }) { Text("Original") } + }) { Text(stringResource(R.string.action_original)) } Spacer(Modifier.width(8.dp)) @@ -7686,7 +7677,7 @@ fun PdfViewerScreen( onClick = { showSaveDialog = false pendingSaveMode = null - }) { Text("Cancel") } + }) { Text(stringResource(R.string.action_cancel)) } } }) } @@ -7694,8 +7685,8 @@ fun PdfViewerScreen( if (showShareDialog) { AlertDialog( onDismissRequest = { showShareDialog = false }, - title = { Text("Share PDF") }, - text = { Text("Choose format to share:") }, + title = { Text(stringResource(R.string.share_chooser_title)) }, + text = { Text(stringResource(R.string.desc_choose_format_share)) }, confirmButton = { TextButton( onClick = { @@ -7721,7 +7712,7 @@ fun PdfViewerScreen( ) isShareLoading = false } - }) { Text("With Annotations") } + }) { Text(stringResource(R.string.action_with_annotations)) } }, dismissButton = { Row { @@ -7742,10 +7733,10 @@ fun PdfViewerScreen( ) isShareLoading = false } - }) { Text("Original") } + }) { Text(stringResource(R.string.action_original)) } Spacer(Modifier.width(8.dp)) TextButton(onClick = { showShareDialog = false }) { - Text("Cancel") + Text(stringResource(R.string.action_cancel)) } } }) @@ -7773,7 +7764,7 @@ fun PdfViewerScreen( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Preparing PDF...", + text = stringResource(R.string.msg_preparing_pdf), style = MaterialTheme.typography.bodyLarge ) } @@ -8087,27 +8078,27 @@ private fun PasswordDialog(isError: Boolean, onDismiss: () -> Unit, onConfirm: ( var password by remember { mutableStateOf("") } var passwordVisible by remember { mutableStateOf(false) } - AlertDialog(onDismissRequest = onDismiss, title = { Text("Password Protected") }, text = { + AlertDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.title_password_protected)) }, text = { Column { - Text("This document is encrypted. Please enter the password to view it.") + Text(stringResource(R.string.desc_password_protected)) Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = password, onValueChange = { password = it }, - label = { Text("Password") }, + label = { Text(stringResource(R.string.password)) }, singleLine = true, visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), keyboardActions = KeyboardActions(onDone = { onConfirm(password) }), isError = isError, supportingText = if (isError) { - { Text("Incorrect password") } + { Text(stringResource(R.string.error_incorrect_password)) } } else null, trailingIcon = { val image = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff - val description = if (passwordVisible) "Hide password" else "Show password" + val description = if (passwordVisible) stringResource(R.string.content_desc_hide_password) else stringResource(R.string.content_desc_show_password) IconButton(onClick = { passwordVisible = !passwordVisible }) { Icon(imageVector = image, description) @@ -8118,9 +8109,9 @@ private fun PasswordDialog(isError: Boolean, onDismiss: () -> Unit, onConfirm: ( } }, confirmButton = { Button(onClick = { onConfirm(password) }, enabled = password.isNotBlank()) { - Text("Open") + Text(stringResource(R.string.action_open)) } - }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }) + }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }) } @Composable @@ -8137,13 +8128,12 @@ fun PenPlayground(onClose: () -> Unit) { Color.Black // Black ) - // Dark Card Background Surface( modifier = Modifier .fillMaxWidth(0.95f) .padding(16.dp), shape = RoundedCornerShape(28.dp), - color = Color(0xFF1E1E1E), // Deep Matte Dark Grey + color = Color(0xFF1E1E1E), shadowElevation = 16.dp, tonalElevation = 0.dp ) { @@ -8151,13 +8141,11 @@ fun PenPlayground(onClose: () -> Unit) { modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // Header with Close Button Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - // Placeholder icon (Star) on left Icon( imageVector = Icons.Default.Star, contentDescription = null, @@ -8169,7 +8157,7 @@ fun PenPlayground(onClose: () -> Unit) { Icon( painter = painterResource( id = R.drawable.close - ), // Ensure you have a close icon or use Icons.Default.Close + ), contentDescription = "Close", tint = Color.Gray ) } @@ -8181,19 +8169,17 @@ fun PenPlayground(onClose: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() - .height(140.dp), // Height for pens + ink stroke space + .height(140.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.Bottom ) { PenType.entries.forEach { type -> val isSelected = selectedPen == type - // Selected pens float up slightly val offsetY by animateDpAsState( targetValue = if (isSelected) (-20).dp else 0.dp, label = "offset" ) - // Selected pens scale up val scale by animateFloatAsState( targetValue = if (isSelected) 1.2f else 1.0f, label = "scale" ) @@ -8205,13 +8191,13 @@ fun PenPlayground(onClose: () -> Unit) { .scale(scale) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = null // Remove ripple for cleaner look + indication = null ) { selectedPen = type }) { // Drawing Area Box( modifier = Modifier .width(40.dp) - .height(120.dp), // Tall enough for stroke + pen + .height(120.dp), contentAlignment = Alignment.BottomCenter ) { PenIcon( @@ -8229,7 +8215,6 @@ fun PenPlayground(onClose: () -> Unit) { Spacer(Modifier.height(24.dp)) - // Subtle Divider HorizontalDivider( modifier = Modifier.padding(horizontal = 12.dp), color = Color.White.copy(alpha = 0.1f), @@ -8292,10 +8277,10 @@ private fun OcrLanguageSelectionDialog( onDismiss: () -> Unit, onLanguageSelected: (OcrLanguage) -> Unit ) { - AlertDialog(onDismissRequest = onDismiss, title = { Text("Select OCR Language") }, text = { + AlertDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.title_select_ocr_language)) }, text = { Column(Modifier.selectableGroup()) { Text( - "Choose the primary language/script of this document for better text recognition results.", + stringResource(R.string.desc_select_ocr_language), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(bottom = 8.dp) ) @@ -8317,7 +8302,7 @@ private fun OcrLanguageSelectionDialog( ) Spacer(Modifier.width(8.dp)) Text( - "You can change this later in More Options > OCR Language.", + stringResource(R.string.desc_ocr_language_change_later), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSecondaryContainer ) @@ -8349,7 +8334,7 @@ private fun OcrLanguageSelectionDialog( } } } - }, confirmButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }) + }, confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }) } @Composable @@ -8454,7 +8439,7 @@ fun PdfSearchResultsPanel( } else { Column { Text( - text = "Results found on ${totalPageCount}+ pages", + text = stringResource(R.string.msg_results_found_pages), style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) ) @@ -8506,7 +8491,7 @@ fun PdfSearchResultsList( Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { if (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 { @@ -8555,14 +8540,14 @@ fun PdfCustomizeToolsSheet( ) { Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) { Text( - text = "Customize Toolbar", + text = stringResource(R.string.title_customize_toolbar), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Select the tools you want to keep visible. Unchecking a tool hides it from the UI to give you a distraction-free reading space.", + text = stringResource(R.string.desc_customize_toolbar), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt b/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt index 6069695..ebd0851 100644 --- a/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt +++ b/app/src/main/java/com/aryan/reader/pdf/TextAnnotationDock.kt @@ -97,6 +97,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -211,7 +212,6 @@ fun TextAnnotationDock( ) { when (activePopup) { ActivePopup.FONT_FAMILY -> { - // Re-using the logic from your EPUB FontSelectionSheetContent but adapted for a Popup Surface( shape = RoundedCornerShape(16.dp), color = Color(0xFF1E1E1E), @@ -229,12 +229,12 @@ fun TextAnnotationDock( Tab( selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, - text = { Text("Presets", fontSize = 12.sp) } + text = { Text(stringResource(R.string.tab_presets), fontSize = 12.sp) } ) Tab( selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, - text = { Text("Imported", fontSize = 12.sp) } + text = { Text(stringResource(R.string.tab_imported), fontSize = 12.sp) } ) } @@ -244,7 +244,7 @@ fun TextAnnotationDock( item { val isSelected = currentFontName == "Default" || currentFontName == null FontItem( - name = "Default System Font", + name = stringResource(R.string.font_default_system), isSelected = isSelected, fontFamily = FontFamily.Default, onClick = { @@ -281,12 +281,12 @@ fun TextAnnotationDock( ) { Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) - Text("Import", fontSize = 12.sp) + Text(stringResource(R.string.action_import), fontSize = 12.sp) } if (customFonts.isEmpty()) { Text( - "No fonts imported", + stringResource(R.string.msg_no_fonts_imported), color = Color.Gray, modifier = Modifier.fillMaxWidth().padding(16.dp), textAlign = TextAlign.Center, @@ -320,7 +320,7 @@ fun TextAnnotationDock( ActivePopup.COLOR -> { if (activeMenuMode == ColorMenuMode.PALETTE) { ColorPickerBubble( - title = "Font color", + title = stringResource(R.string.label_font_color), currentColor = currentStyle.color.takeIf { it != Color.Unspecified } ?: Color.Black, palette = textColorPalette, @@ -359,7 +359,7 @@ fun TextAnnotationDock( ActivePopup.BACKGROUND -> { if (activeMenuMode == ColorMenuMode.PALETTE) { ColorPickerBubble( - title = "Highlight", + title = stringResource(R.string.label_highlight_color), currentColor = when (currentStyle.background) { Color.Unspecified, Color.Transparent -> Color.Transparent else -> currentStyle.background diff --git a/app/src/main/java/com/aryan/reader/pdf/ToolSettingsPopup.kt b/app/src/main/java/com/aryan/reader/pdf/ToolSettingsPopup.kt index 76329d3..b88ee12 100644 --- a/app/src/main/java/com/aryan/reader/pdf/ToolSettingsPopup.kt +++ b/app/src/main/java/com/aryan/reader/pdf/ToolSettingsPopup.kt @@ -70,6 +70,7 @@ import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -80,6 +81,7 @@ import androidx.compose.ui.window.DialogProperties import com.aryan.reader.BrightnessSlider import com.aryan.reader.ColorComparePill import com.aryan.reader.HexInput +import com.aryan.reader.R import com.aryan.reader.RgbInputColumn import com.aryan.reader.SpectrumBox import kotlin.math.roundToInt @@ -262,7 +264,7 @@ fun ToolSettingsPopup( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Straight Line", + text = stringResource(R.string.label_straight_line), color = Color.White, style = MaterialTheme.typography.bodyMedium ) @@ -465,7 +467,7 @@ private fun ColorPickerDialog( .padding(horizontal = 24.dp, vertical = 8.dp) ) { Text( - text = "Spectrum", + text = stringResource(R.string.label_spectrum), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color.White @@ -520,7 +522,7 @@ private fun ColorPickerDialog( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - "Hex", + stringResource(R.string.theme_color_hex), color = Color.Gray, fontSize = 12.sp, maxLines = 1 @@ -537,19 +539,19 @@ private fun ColorPickerDialog( horizontalArrangement = Arrangement.spacedBy(6.dp) ) { RgbInputColumn( - label = "Red", + label = stringResource(R.string.color_red), value = currentColor.red, onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) }, modifier = Modifier.weight(1f) ) RgbInputColumn( - label = "Green", + label = stringResource(R.string.color_green), value = currentColor.green, onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) }, modifier = Modifier.weight(1f) ) RgbInputColumn( - label = "Blue", + label = stringResource(R.string.color_blue), value = currentColor.blue, onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) }, modifier = Modifier.weight(1f) @@ -565,7 +567,7 @@ private fun ColorPickerDialog( verticalAlignment = Alignment.CenterVertically ) { TextButton(onClick = onDismiss) { - Text("Cancel", color = Color.Gray) + Text(stringResource(R.string.action_cancel), color = Color.Gray) } Spacer(Modifier.width(8.dp)) Button( @@ -575,7 +577,7 @@ private fun ColorPickerDialog( contentColor = Color.Black ) ) { - Text("Done") + Text(stringResource(R.string.action_done)) } } } diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 1429c04..8228062 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -1,17 +1,24 @@ + %1$d book %1$d books + + book books + + %1$d shelf %1$d shelves + + %1$d result found %1$d results found diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eac908c..2255322 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ - Episteme + + Episteme Cancel @@ -14,8 +15,8 @@ Search Clear Apply + Free - Save Active Tabs Close Tab Close All Tabs @@ -23,39 +24,50 @@ Are you sure you want to close all active tabs? + %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 @@ -69,18 +81,25 @@ Setup Folder Sync Sync Folder Local Folder - OPDS Stream + + 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 @@ -89,51 +108,70 @@ 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 @@ -148,7 +186,8 @@ Save File? - Do you want to save this external file in the app\'s library? If not, it will be removed.\n\n(You can change this default behavior anytime from the Home Screen > More Options > External File Behavior). + Do you want to save this external file in the app\'s library? If not, it will be removed.\n\n(You can change this default behavior anytime from the Home Screen > More Options > External File Behavior). + Don\'t ask again Keep in Library Remove @@ -157,108 +196,141 @@ Always Remove External File Behavior - + 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 + + 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 + + $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 + + 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 ?.,;: + + 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. + Found a bug, have a feature request, or just want to say hi? Let us know on GitHub or email us. 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. @@ -266,11 +338,15 @@ Dictionary More Options Page Slider + Table of Contents Text Format Search + AI Features + Start Text-to-Speech + Stop Text-to-Speech Pause Resume @@ -298,12 +374,15 @@ Browse chapters and navigate to any section Adjust font, size, line height, alignment, and custom fonts Find any word or phrase in this book + Summarize the current chapter or page using AI Read the book aloud using your device\'s voice engine Stop the current read-aloud session Pause the current read-aloud playback Resume paused read-aloud playback + Invert PDF colors for dark mode + Disable dark mode and restore the original PDF colors Lock horizontal panning on the page Unlock panning to re-enable pinch-to-zoom and drag gestures @@ -322,41 +401,66 @@ 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 + + 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 + + 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. @@ -365,6 +469,7 @@ 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. @@ -375,7 +480,9 @@ 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… @@ -384,105 +491,125 @@ 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… + + Generating summary… Stop Read aloud Copy - No summary could be generated. + No summary could be generated. - - Thinking… + + Thinking… Open in Dictionary App - AI could not provide a definition. - Asking AI about \'%1$s\'… + 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) + + 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 + + - Frank Zappa + ⚠️ Low contrast! This might cause eye strain. Page Color Text Color Live Preview + Reading is dreaming. - Hex + + Hex - - R - G - B + + R + G + B - - Text is empty. - AI returned an empty definition. - Could not get definition. - An unknown server error occurred. - Network error. Check connection. + + Text is empty. + AI returned an empty definition. + Could not get definition. + An unknown server error occurred. + Network error. Check connection. - - Not enough context for a recap. - Failed to parse recap. - Network error during recap generation. + + Not enough context for a recap. + Failed to parse recap. + Network error during recap generation. Lookup Settings 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 @@ -494,25 +621,31 @@ 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. + + 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. + 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: @@ -528,10 +661,15 @@ Theme Theme Settings More Options + View Original PDF + Delete Text View + Reading Mode: Vertical + Reading Mode: Paginated + Enabled Remove bookmark Bookmark this page @@ -542,35 +680,51 @@ Keep Screen On Visual Options Auto Scroll + TTS Voice Settings + TTS Settings (Debug) Navigate with slider Chapters Menu Text Formatting + Chapter Summarization - Recap (Beta) + + 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 @@ -578,9 +732,7 @@ 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? @@ -592,7 +744,9 @@ 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. @@ -602,7 +756,9 @@ 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… @@ -611,15 +767,19 @@ 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 @@ -630,33 +790,246 @@ 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. Remove Edge Padding Removes the horizontal gap on the left and right edges. + Search Error opening dictionary Error opening translate app Error opening search app - Episteme + + Episteme + Open Source Version Playstore Version + Version %1$s + Build %1$s - GitHub + + GitHub + Browse source code, star, fork, and report issues. - Privacy Policy How we handle your data. - Terms of Service Usage terms and conditions. Open source libraries used. + Importing %1$d books… They will appear in your Library shortly. + + + External Link + + You clicked on an external link:\n\n%1$s\n\nWhat would you like to do? + Open + No browser found to open the link. + + + Save Note + Add a note… + + Dict + + Speak + + Note + + Edit + + + + FONT & ALIGNMENT + + LAYOUT & SPACING + Font Size + Line Height + Paragraph Gap + + Orig + + Aa + + + + Voice Adjustments + + Speed (%1$sx) + + Pitch (%1$sx) + This is how your current voice settings sound. + Pause Book + Resume Book + + System Voice / Engine Settings + + + + Global Speed + + Local Speed + Applies to all files + Saved for this file only + Scroll to Top + + + Customize Toolbar + Select the tools you want to keep visible. Unchecking a tool hides it from the UI to give you a distraction-free reading space. + + + Annotations + + All + + With Notes + Add Note + Edit Note + + + + %1$d / %2$d + + Page %1$d of %2$d + + + Close Edit Mode + + Toggle Visibility + + Stylus Only Mode + Pen + Highlighter + Text + Eraser + Undo + Redo + Show Dock + + + + OCR selection error: %1$s + + Selection error: %1$s + + Error processing page: %1$s + + Unable to display page %1$d. + + + Could not open print settings + + Loading PDF… + + Error loading PDF + Loading page… + + PDF Viewer + + Pen Playground + + Import SVG + + Imported %1$d SVG strokes! + + Failed to import SVG or empty. + + OCR Language + Insert Blank Page + Delete Page + + Generating… %1$d%% + + Open Text View + + Generate Text View + Share + Save copy to device + Print + + Generating Text View… + + Indexing pages… %1$d%% done. Search results will update automatically. + + Results found on %1$d+ pages + + Result %1$d / %2$d + + %1$d+ Pages + + Summarize Page (Page %1$d) + + Downloading %1$s language pack… + + Select OCR Language + + Choose the primary language/script of this document for better text recognition results. + + You can change this later in More Options > OCR Language. + + Re-index Document? + + You are changing the OCR script to %1$s.\n\nTo ensure search accuracy, we need to clear the existing index and re-scan pages that require OCR using this new language.\n\nThis will happen in the background. + + Re-index + Password Protected + This document is encrypted. Please enter the password to view it. + Password + Incorrect password + Hide password + Show password + + You are about to navigate to:\n%1$s + Visit + Save to Device + Choose format to save: + With Annotations + Original + Choose format to share: + + Preparing PDF… + + Add PDF to Tab + + No other PDFs found in your library. + + PDF is empty or could not be displayed. + + Page added at %1$d + Page deleted + Extra page removed + Chapters are not available for this book. + + Highlighted section + + + + Straight Line + + Spectrum + Done + Red + Green + Blue + + + + Default System Font + Import + No fonts imported + Font color + + Highlight + + + Page Unavailable diff --git a/app/src/releaseOffline/AndroidManifest.xml b/app/src/releaseOffline/AndroidManifest.xml new file mode 100644 index 0000000..8e52e22 --- /dev/null +++ b/app/src/releaseOffline/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file