String extraction (#174)

* Refactored the EPUB reader to use localized string resources instead of hardcoded strings across multiple components.

* Refactored the PDF viewer and annotation components to use centralized string resources instead of hardcoded text.

* Updated `strings.xml` with extensive translation hints and localization metadata.
This commit is contained in:
Aryan 2026-04-12 16:34:47 +05:30 committed by GitHub
parent 17a0097d4a
commit b027331bd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 716 additions and 313 deletions

View file

@ -40,6 +40,7 @@ android {
} }
} }
buildConfigField("boolean", "IS_PRO", "false") buildConfigField("boolean", "IS_PRO", "false")
buildConfigField("boolean", "IS_OFFLINE", "false")
} }
flavorDimensions += "version" flavorDimensions += "version"
@ -91,6 +92,12 @@ android {
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
create("releaseOffline") {
initWith(getByName("release"))
matchingFallbacks += listOf("release")
buildConfigField("boolean", "IS_OFFLINE", "true")
}
} }
applicationVariants.all { applicationVariants.all {

View file

@ -390,7 +390,7 @@ fun SearchTopBar(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, 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( Icon(
Icons.Default.Close, 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( Icon(
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
contentDescription = stringResource( contentDescription = stringResource(
if (searchState.showSearchResultsPanel) R.string.content_desc_hide_results if (searchState.showSearchResultsPanel) R.string.tooltip_hide_results
else R.string.content_desc_show_results else R.string.tooltip_show_results
) )
) )
} }
@ -478,7 +478,7 @@ fun SearchNavigationControls(
onClick = { onNavigate(searchState.currentSearchResultIndex - 1) }, onClick = { onNavigate(searchState.currentSearchResultIndex - 1) },
enabled = searchState.currentSearchResultIndex > 0 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( Text(
@ -493,7 +493,7 @@ fun SearchNavigationControls(
onClick = { onNavigate(searchState.currentSearchResultIndex + 1) }, onClick = { onNavigate(searchState.currentSearchResultIndex + 1) },
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 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))
} }
} }
} }

View file

@ -18,6 +18,8 @@
* mail: epistemereader@gmail.com * mail: epistemereader@gmail.com
*/ */
// LibraryScreen.kt // LibraryScreen.kt
@file:Suppress("KotlinConstantConditions")
package com.aryan.reader package com.aryan.reader
import android.net.Uri import android.net.Uri
@ -158,9 +160,19 @@ fun LibraryScreen(
val sortOrder = uiState.sortOrder val sortOrder = uiState.sortOrder
val shelves = uiState.shelves val shelves = uiState.shelves
val rawLibraryFiles = uiState.rawLibraryFiles 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( val pagerState = rememberPagerState(
initialPage = uiState.libraryScreenStartPage, initialPage = uiState.libraryScreenStartPage,
pageCount = { 4 } pageCount = { tabTitles.size }
) )
val containsFolderItems = remember(selectedItems) { val containsFolderItems = remember(selectedItems) {
@ -254,6 +266,7 @@ fun LibraryScreen(
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
LibraryScreenContent( LibraryScreenContent(
tabTitles = tabTitles,
recentFiles = uiState.allRecentFiles, recentFiles = uiState.allRecentFiles,
rawLibraryFiles = rawLibraryFiles, rawLibraryFiles = rawLibraryFiles,
shelves = shelves, shelves = shelves,
@ -494,6 +507,7 @@ fun ShelfScreen(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LibraryScreenContent( fun LibraryScreenContent(
tabTitles: List<String>,
recentFiles: List<RecentFileItem>, recentFiles: List<RecentFileItem>,
rawLibraryFiles: List<RecentFileItem>, rawLibraryFiles: List<RecentFileItem>,
shelves: List<Shelf>, shelves: List<Shelf>,
@ -543,12 +557,6 @@ fun LibraryScreenContent(
val isBookContextualModeActive = selectedItems.isNotEmpty() val isBookContextualModeActive = selectedItems.isNotEmpty()
val isShelfContextualModeActive = selectedShelves.isNotEmpty() val isShelfContextualModeActive = selectedShelves.isNotEmpty()
var showSortMenu by remember { mutableStateOf(false) } 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() } val searchFocusRequester = remember { FocusRequester() }
var textFieldValue by remember(isSearchActive) { var textFieldValue by remember(isSearchActive) {
@ -811,13 +819,15 @@ fun LibraryScreenContent(
) )
} }
3 -> { 3 -> {
OpdsTab( if (!BuildConfig.IS_OFFLINE) {
localLibraryFiles = rawLibraryFiles, OpdsTab(
onBookDownloaded = onOpdsBookDownloaded, localLibraryFiles = rawLibraryFiles,
onReadBook = onItemClick, onBookDownloaded = onOpdsBookDownloaded,
onStreamBook = onStreamOpdsBook, onReadBook = onItemClick,
onDeleteCatalogStreams = onDeleteCatalogStreams onStreamBook = onStreamOpdsBook,
) onDeleteCatalogStreams = onDeleteCatalogStreams
)
}
} }
} }
} }

View file

@ -664,7 +664,7 @@ fun AboutDialog(onDismiss: () -> Unit) {
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
}, },
text = stringResource(R.string.about_privacy), text = stringResource(R.string.legal_privacy_policy),
subtitle = stringResource(R.string.about_privacy_desc), subtitle = stringResource(R.string.about_privacy_desc),
onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) } onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }
) )
@ -680,7 +680,7 @@ fun AboutDialog(onDismiss: () -> Unit) {
tint = MaterialTheme.colorScheme.primary 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), subtitle = stringResource(R.string.about_terms_desc),
onClick = { uriHandler.openUri(TERMS_URL) } onClick = { uriHandler.openUri(TERMS_URL) }
) )

View file

@ -72,6 +72,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize 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.Popup
import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupPositionProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import com.aryan.reader.R
import com.aryan.reader.ReaderTexture import com.aryan.reader.ReaderTexture
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -415,8 +417,8 @@ fun ChapterWebView(
val urlToShow = showExternalLinkDialog!! val urlToShow = showExternalLinkDialog!!
AlertDialog( AlertDialog(
onDismissRequest = { showExternalLinkDialog = null }, onDismissRequest = { showExternalLinkDialog = null },
title = { Text("External Link") }, title = { Text(stringResource(R.string.dialog_external_link_title)) },
text = { Text("You clicked on an external link:\n\n$urlToShow\n\nWhat would you like to do?") }, text = { Text(stringResource(R.string.dialog_external_link_desc, urlToShow)) },
confirmButton = { confirmButton = {
Row(horizontalArrangement = Arrangement.End) { Row(horizontalArrangement = Arrangement.End) {
TextButton(onClick = { TextButton(onClick = {
@ -425,23 +427,21 @@ fun ChapterWebView(
context.startActivity(intent) context.startActivity(intent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Timber.e(e, "No activity found to handle intent for URL: $urlToShow") Timber.e(e, "No activity found to handle intent for URL: $urlToShow")
Toast.makeText( Toast.makeText(context, context.getString(R.string.error_no_browser), Toast.LENGTH_LONG).show()
context, "No browser found to open the link.", Toast.LENGTH_LONG
).show()
} }
showExternalLinkDialog = null showExternalLinkDialog = null
}) { Text("Open") } }) { Text(stringResource(R.string.action_open)) }
TextButton(onClick = { TextButton(onClick = {
val clipboard = val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Link", urlToShow) val clip = ClipData.newPlainText("Copied Link", urlToShow)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
showExternalLinkDialog = null showExternalLinkDialog = null
}) { Text("Copy") } }) { Text(stringResource(R.string.action_copy)) }
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showExternalLinkDialog = null }) { Text("Cancel") } TextButton(onClick = { showExternalLinkDialog = null }) { Text(stringResource(R.string.action_cancel)) }
}) })
} }

View file

@ -68,7 +68,6 @@ import com.aryan.reader.epub.EpubChapter
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.UUID import java.util.UUID
import kotlin.math.min
private const val BOOKMARK_PREFS_NAME = "epub_reader_bookmarks" private const val BOOKMARK_PREFS_NAME = "epub_reader_bookmarks"
@ -445,10 +444,10 @@ fun PaletteManagerDialog(
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { onSave(tempPalette) }) { Text("Save") } TextButton(onClick = { onSave(tempPalette) }) { Text(stringResource(R.string.action_save)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") } TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
} }
) )
} }
@ -528,10 +527,10 @@ fun AnnotationBottomSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
BottomSheetToolButton(icon = R.drawable.copy, label = "Copy", onClick = onCopy, effectiveText = effectiveText) BottomSheetToolButton(icon = R.drawable.copy, label = stringResource(R.string.action_copy), onClick = onCopy, effectiveText = effectiveText)
BottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", onClick = onDictionary, effectiveText = effectiveText) BottomSheetToolButton(icon = R.drawable.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary, effectiveText = effectiveText)
BottomSheetToolButton(icon = R.drawable.translate, label = "Translate", onClick = onTranslate, effectiveText = effectiveText) BottomSheetToolButton(icon = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate, effectiveText = effectiveText)
BottomSheetToolButton(icon = R.drawable.search, label = "Search", onClick = onSearch, effectiveText = effectiveText) BottomSheetToolButton(icon = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch, effectiveText = effectiveText)
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@ -540,7 +539,7 @@ fun AnnotationBottomSheet(
OutlinedTextField( OutlinedTextField(
value = noteText, value = noteText,
onValueChange = { noteText = it }, onValueChange = { noteText = it },
placeholder = { Text("Add a note...", color = effectiveText.copy(alpha = 0.5f)) }, placeholder = { Text(stringResource(R.string.placeholder_add_note), color = effectiveText.copy(alpha = 0.5f)) },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 100.dp), .heightIn(min = 100.dp),
@ -652,23 +651,23 @@ fun PaginatedTextSelectionMenu(
} }
val actions = mutableListOf<MenuActionItem>() val actions = mutableListOf<MenuActionItem>()
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) { 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.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary))
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate)) actions.add(MenuActionItem(iconRes = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate))
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch)) actions.add(MenuActionItem(iconRes = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch))
if (onNote != null) { 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) { 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) { 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)) { Column(modifier = Modifier.padding(bottom = 4.dp)) {
@ -753,7 +752,7 @@ fun HighlightColorRow(
if (selectedColor == colorEnum) { if (selectedColor == colorEnum) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Selected", contentDescription = stringResource(R.string.content_desc_selected),
tint = if (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White, tint = if (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
@ -825,13 +824,13 @@ fun PaginatedTextSelectionMenu(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
contentDescription = "Note", contentDescription = stringResource(R.string.label_note),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text( Text(
"Note", stringResource(R.string.label_note),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -851,24 +850,24 @@ fun PaginatedTextSelectionMenu(
} }
val actions = mutableListOf<MenuActionItem>() val actions = mutableListOf<MenuActionItem>()
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) { 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.dictionary, label = stringResource(R.string.label_dict), onClick = onDictionary))
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = onTranslate)) actions.add(MenuActionItem(iconRes = R.drawable.translate, label = stringResource(R.string.dict_translate), onClick = onTranslate))
actions.add(MenuActionItem(iconRes = R.drawable.search, label = "Search", onClick = onSearch)) actions.add(MenuActionItem(iconRes = R.drawable.search, label = stringResource(R.string.action_search), onClick = onSearch))
if (onNote != null) { 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)) actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
} }
if (onSelectAll != null) { 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) { 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)) { Column(modifier = Modifier.padding(bottom = 4.dp)) {

View file

@ -19,6 +19,8 @@
*/ */
package com.aryan.reader.epubreader package com.aryan.reader.epubreader
import android.content.Context
import com.aryan.reader.R
import timber.log.Timber import timber.log.Timber
import com.aryan.reader.epub.EpubBook import com.aryan.reader.epub.EpubBook
import com.aryan.reader.paginatedreader.LocatorConverter 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.). * the initial chunk to display based on navigation state (CFI, overrides, etc.).
*/ */
suspend fun loadChapterContent( suspend fun loadChapterContent(
context: Context,
epubBook: EpubBook, epubBook: EpubBook,
chapterIndex: Int, chapterIndex: Int,
chunkTargetOverride: Int?, chunkTargetOverride: Int?,
@ -64,12 +67,12 @@ suspend fun loadChapterContent(
chunkOfElements.joinToString(separator = "\n") { it.outerHtml() } chunkOfElements.joinToString(separator = "\n") { it.outerHtml() }
} }
if (chunkedList.isEmpty()) { if (chunkedList.isEmpty()) {
head to listOf("<body><p>This chapter is empty.</p></body>") head to listOf("<body><p>${context.getString(R.string.chapter_empty)}</p></body>")
} else { } else {
head to chunkedList head to chunkedList
} }
} else { } else {
"" to listOf("<h1>Chapter not found</h1>") "" to listOf("<h1>${context.getString(R.string.chapter_not_found)}</h1>")
} }
var targetChunk = 0 var targetChunk = 0
@ -104,7 +107,7 @@ suspend fun loadChapterContent(
Timber.e(e, "Failed to parse chapter") Timber.e(e, "Failed to parse chapter")
ChapterLoadingResult( ChapterLoadingResult(
head = "", head = "",
chunks = listOf("<h1>Error loading chapter</h1><p>${e.message}</p>"), chunks = listOf("<h1>${context.getString(R.string.error_loading_chapter)}</h1><p>${e.message}</p>"),
startChunkIndex = 0, startChunkIndex = 0,
isSuccess = false, isSuccess = false,
errorMessage = e.message errorMessage = e.message

View file

@ -277,7 +277,7 @@ fun EpubReaderTopBar(
onDismissRequest = { showMoreMenu = false } onDismissRequest = { showMoreMenu = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Customize Toolbar") }, text = { Text(stringResource(R.string.title_customize_toolbar)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onCustomizeTools() onCustomizeTools()
@ -1078,7 +1078,7 @@ fun AutoScrollControls(
) { ) {
Icon( Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, 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) modifier = Modifier.size(20.dp)
) )
} }
@ -1395,7 +1395,7 @@ fun CustomizeToolsSheet(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( 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, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1495,17 +1495,19 @@ fun TtsControlsSheet(
ttsController.sliceAndRetainPosition() ttsController.sliceAndRetainPosition()
} }
val ttsSample = stringResource(R.string.tts_sample_text)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
contentWindowInsets = { WindowInsets.navigationBars } contentWindowInsets = { WindowInsets.navigationBars }
) { ) {
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) { 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)) Spacer(Modifier.height(16.dp))
// Rate Slider // Rate Slider
Row(verticalAlignment = Alignment.CenterVertically) { 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 = { IconButton(onClick = {
rate = 1.0f rate = 1.0f
ttsController.pause() ttsController.pause()
@ -1534,7 +1536,7 @@ fun TtsControlsSheet(
// Pitch Slider // Pitch Slider
Row(verticalAlignment = Alignment.CenterVertically) { 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 = { IconButton(onClick = {
pitch = 1.0f pitch = 1.0f
ttsController.pause() ttsController.pause()
@ -1568,7 +1570,7 @@ fun TtsControlsSheet(
if (ttsState.isPlaying) ttsController.pause() if (ttsState.isPlaying) ttsController.pause()
tts?.setSpeechRate(rate) tts?.setSpeechRate(rate)
tts?.setPitch(pitch) 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(), modifier = Modifier.fillMaxWidth(),
enabled = isTtsReady, enabled = isTtsReady,
@ -1611,14 +1613,14 @@ fun TtsControlsSheet(
} else { } else {
Icon( Icon(
painter = painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play), 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) modifier = Modifier.size(32.dp)
) )
} }
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( 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, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1635,7 +1637,7 @@ fun TtsControlsSheet(
) { ) {
Icon(Icons.Default.Settings, contentDescription = null) Icon(Icons.Default.Settings, contentDescription = null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("System Voice / Engine Settings") Text(stringResource(R.string.tts_system_settings))
} }
} }
} }

View file

@ -551,7 +551,7 @@ private fun BookmarksList(
onDismissRequest = { bookmarkMenuExpandedFor = null } onDismissRequest = { bookmarkMenuExpandedFor = null }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.menu_rename)) }, text = { Text(stringResource(R.string.action_rename)) },
onClick = { onClick = {
showRenameBookmarkDialog = bookmark showRenameBookmarkDialog = bookmark
bookmarkMenuExpandedFor = null bookmarkMenuExpandedFor = null
@ -680,12 +680,12 @@ private fun HighlightsList(
androidx.compose.material3.FilterChip( androidx.compose.material3.FilterChip(
selected = !filterWithNotesOnly, selected = !filterWithNotesOnly,
onClick = { filterWithNotesOnly = false }, onClick = { filterWithNotesOnly = false },
label = { Text("All") } label = { Text(stringResource(R.string.filter_all)) }
) )
androidx.compose.material3.FilterChip( androidx.compose.material3.FilterChip(
selected = filterWithNotesOnly, selected = filterWithNotesOnly,
onClick = { filterWithNotesOnly = true }, onClick = { filterWithNotesOnly = true },
label = { Text("With Notes") } label = { Text(stringResource(R.string.filter_with_notes)) }
) )
} }
@ -773,7 +773,7 @@ private fun HighlightsList(
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( 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 = { onClick = {
onEditNote(highlight) onEditNote(highlight)
highlightMenuExpandedFor = null highlightMenuExpandedFor = null

View file

@ -702,7 +702,7 @@ fun EpubReaderHost(
if (!selectedDictPackage.isNullOrEmpty()) { if (!selectedDictPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word) ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word)
} else { } else {
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_dictionary_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -712,7 +712,7 @@ fun EpubReaderHost(
if (!selectedTranslatePackage.isNullOrEmpty()) { if (!selectedTranslatePackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text) ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text)
} else { } else {
Toast.makeText(context, "Please select a translate app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_translate_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -721,7 +721,7 @@ fun EpubReaderHost(
if (!selectedSearchPackage.isNullOrEmpty()) { if (!selectedSearchPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text) ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text)
} else { } else {
Toast.makeText(context, "Please select a search app first.", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.toast_select_search_first), Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true showDictionarySettingsSheet = true
} }
} }
@ -1371,6 +1371,7 @@ fun EpubReaderHost(
activeFragmentId = null activeFragmentId = null
val result = loadChapterContent( val result = loadChapterContent(
context = context,
epubBook = epubBook, epubBook = epubBook,
chapterIndex = currentChapterIndex, chapterIndex = currentChapterIndex,
chunkTargetOverride = chunkTargetOverride, chunkTargetOverride = chunkTargetOverride,
@ -2835,7 +2836,7 @@ fun EpubReaderHost(
if (pullToTurnEnabled && currentChapterIndex > 0) { if (pullToTurnEnabled && currentChapterIndex > 0) {
ChapterChangeIndicator( ChapterChangeIndicator(
text = "Release for Previous Chapter", text = stringResource(R.string.release_for_previous_chapter),
progress = pullToPrevProgress, progress = pullToPrevProgress,
isPullingDown = true, isPullingDown = true,
modifier = Modifier modifier = Modifier
@ -2846,7 +2847,7 @@ fun EpubReaderHost(
if (pullToTurnEnabled && currentChapterIndex < chapters.size - 1) { if (pullToTurnEnabled && currentChapterIndex < chapters.size - 1) {
ChapterChangeIndicator( ChapterChangeIndicator(
text = "Release for Next Chapter", text = stringResource(R.string.release_for_next_chapter),
progress = pullToNextProgress, progress = pullToNextProgress,
isPullingDown = false, isPullingDown = false,
modifier = Modifier modifier = Modifier

View file

@ -481,7 +481,7 @@ fun ReaderTextFormatPanel(
// FONT & ALIGNMENT SECTION // FONT & ALIGNMENT SECTION
Text( Text(
text = "FONT & ALIGNMENT", text = stringResource(R.string.section_font_alignment),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@ -504,7 +504,7 @@ fun ReaderTextFormatPanel(
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "Aa", text = stringResource(R.string.label_aa_preview),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer, color = MaterialTheme.colorScheme.onSecondaryContainer,
@ -570,7 +570,7 @@ fun ReaderTextFormatPanel(
// LAYOUT & SPACING SECTION // LAYOUT & SPACING SECTION
Text( Text(
text = "LAYOUT & SPACING", text = stringResource(R.string.section_layout_spacing),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@ -581,7 +581,7 @@ fun ReaderTextFormatPanel(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Size // Size
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { 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( Slider(
value = currentFontSize, value = currentFontSize,
onValueChange = onFontSizeChange, onValueChange = onFontSizeChange,
@ -589,11 +589,11 @@ fun ReaderTextFormatPanel(
steps = 24, steps = 24,
modifier = Modifier.weight(1f) 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 // Lines
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { 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( Slider(
value = currentLineHeight, value = currentLineHeight,
onValueChange = onLineHeightChange, onValueChange = onLineHeightChange,
@ -601,11 +601,11 @@ fun ReaderTextFormatPanel(
steps = 19, steps = 19,
modifier = Modifier.weight(1f) 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 // Paragraph Gap
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { 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( Slider(
value = currentParagraphGap, value = currentParagraphGap,
onValueChange = onParagraphGapChange, onValueChange = onParagraphGapChange,
@ -613,7 +613,7 @@ fun ReaderTextFormatPanel(
steps = 29, steps = 29,
modifier = Modifier.weight(1f) 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)) Spacer(Modifier.height(8.dp))

View file

@ -11,6 +11,7 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import timber.log.Timber import timber.log.Timber
import androidx.core.net.toUri import androidx.core.net.toUri
import com.aryan.reader.R
data class ExternalDictionaryApp( data class ExternalDictionaryApp(
val label: String, val label: String,
@ -72,7 +73,7 @@ object ExternalDictionaryHelper {
sortedApps.add( sortedApps.add(
0, 0,
ExternalDictionaryApp( ExternalDictionaryApp(
label = "Search", label = context.getString(R.string.dict_app_label_search),
packageName = GOOGLE_SEARCH_PKG, packageName = GOOGLE_SEARCH_PKG,
icon = null icon = null
) )
@ -127,7 +128,7 @@ object ExternalDictionaryHelper {
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to launch dictionary app: $packageName") 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) launchGenericSend(context, packageName, query)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to launch translate app: $packageName") 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) launchGenericSend(context, packageName, query)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to launch search app: $packageName") 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()
} }
} }

View file

@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -112,7 +113,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Close Edit Mode", contentDescription = stringResource(R.string.content_desc_close_edit_mode),
tint = Color.White, tint = Color.White,
modifier = Modifier.size(iconSize) modifier = Modifier.size(iconSize)
) )
@ -130,7 +131,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = visIcon, imageVector = visIcon,
contentDescription = "Toggle Visibility", contentDescription = stringResource(R.string.content_desc_toggle_visibility),
tint = visTint, tint = visTint,
modifier = Modifier.size(iconSize) modifier = Modifier.size(iconSize)
) )
@ -166,7 +167,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = iconVector, imageVector = iconVector,
contentDescription = "Stylus Only Mode", contentDescription = stringResource(R.string.content_desc_stylus_only_mode),
tint = iconTint, tint = iconTint,
modifier = Modifier.size(iconSize) modifier = Modifier.size(iconSize)
) )
@ -182,7 +183,7 @@ fun AnnotationDock(
iconRes = R.drawable.pen, iconRes = R.drawable.pen,
isActive = isPenActive, isActive = isPenActive,
tintColor = if(isMinimized) Color.Gray else activePenColor, tintColor = if(isMinimized) Color.Gray else activePenColor,
description = "Pen", description = stringResource(R.string.content_desc_pen),
size = buttonSize, size = buttonSize,
iconSize = iconSize, iconSize = iconSize,
onClick = { onClick = {
@ -202,7 +203,7 @@ fun AnnotationDock(
iconRes = R.drawable.marker, iconRes = R.drawable.marker,
isActive = isHighlighterActive, isActive = isHighlighterActive,
tintColor = if(isMinimized) Color.Gray else activeHighlighterColor.copy(alpha = 1f), tintColor = if(isMinimized) Color.Gray else activeHighlighterColor.copy(alpha = 1f),
description = "Highlighter", description = stringResource(R.string.content_desc_highlighter),
size = buttonSize, size = buttonSize,
iconSize = iconSize, iconSize = iconSize,
onClick = { onClick = {
@ -221,7 +222,7 @@ fun AnnotationDock(
iconRes = R.drawable.keyboard, iconRes = R.drawable.keyboard,
isActive = !isMinimized && selectedTool == InkType.TEXT, isActive = !isMinimized && selectedTool == InkType.TEXT,
tintColor = if(isMinimized) Color.Gray else Color.White, tintColor = if(isMinimized) Color.Gray else Color.White,
description = "Text", description = stringResource(R.string.content_desc_text),
size = buttonSize, size = buttonSize,
iconSize = iconSize, iconSize = iconSize,
onClick = { if(!isMinimized) onToolClick(InkType.TEXT) } onClick = { if(!isMinimized) onToolClick(InkType.TEXT) }
@ -232,7 +233,7 @@ fun AnnotationDock(
iconRes = R.drawable.eraser, iconRes = R.drawable.eraser,
isActive = !isMinimized && selectedTool == InkType.ERASER, isActive = !isMinimized && selectedTool == InkType.ERASER,
tintColor = if(isMinimized) Color.Gray else Color.White, tintColor = if(isMinimized) Color.Gray else Color.White,
description = "Eraser", description = stringResource(R.string.content_desc_eraser),
size = buttonSize, size = buttonSize,
iconSize = iconSize, iconSize = iconSize,
onClick = { if(!isMinimized) onToolClick(InkType.ERASER) } onClick = { if(!isMinimized) onToolClick(InkType.ERASER) }
@ -249,7 +250,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.Undo, 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), tint = if (canUndo && !isMinimized) Color.White else Color.White.copy(alpha = 0.3f),
modifier = Modifier.size(iconSize) modifier = Modifier.size(iconSize)
) )
@ -265,7 +266,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.Redo, 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), tint = if (canRedo && !isMinimized) Color.White else Color.White.copy(alpha = 0.3f),
modifier = Modifier.size(iconSize) modifier = Modifier.size(iconSize)
) )
@ -286,7 +287,7 @@ fun AnnotationDock(
) { ) {
Icon( Icon(
imageVector = Icons.Default.VisibilityOff, imageVector = Icons.Default.VisibilityOff,
contentDescription = "Show Dock", contentDescription = stringResource(R.string.content_desc_show_dock),
tint = Color.White, tint = Color.White,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )

View file

@ -93,6 +93,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight 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( BoxWithConstraints(
modifier = modifier modifier = modifier
.onGloballyPositioned { layoutCoordinates = it } .onGloballyPositioned { layoutCoordinates = it }
@ -2359,8 +2364,7 @@ internal fun PdfPageComposable(
Timber.e( Timber.e(
e, "Long press: Error during OCR text selection" e, "Long press: Error during OCR text selection"
) )
pageErrorMessage = pageErrorMessage = errorOcrSelection
"OCR selection error: ${e.localizedMessage}"
} finally { } finally {
isPerformingOcrForSelection = false isPerformingOcrForSelection = false
ocrRipplePosition = null ocrRipplePosition = null
@ -2381,7 +2385,7 @@ internal fun PdfPageComposable(
e, e,
"Error during long press text selection on page $pageIndex" "Error during long press text selection on page $pageIndex"
) )
pageErrorMessage = "Selection error: ${e.localizedMessage}" pageErrorMessage = errorSelection
customMenuState = null customMenuState = null
selectionCharRange.value = null selectionCharRange.value = null
selectedWordScreenRects = emptyList() selectedWordScreenRects = emptyList()
@ -3467,7 +3471,7 @@ internal fun PdfPageComposable(
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
pageErrorMessage = "Error processing page: ${e.localizedMessage}" pageErrorMessage = errorProcessingPage
} finally { } finally {
isLoadingPage = false isLoadingPage = false
localBitmap?.recycle() localBitmap?.recycle()
@ -3869,7 +3873,7 @@ internal fun PdfPageComposable(
else -> { else -> {
Text( Text(
text = "Unable to display page ${pageIndex + 1}.", text = stringResource(R.string.error_unable_to_display_page),
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)
.align(Alignment.Center) .align(Alignment.Center)

View file

@ -1412,7 +1412,7 @@ fun PdfViewerScreen(
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag("PdfPrint").e(e, "Failed to initialize print job") Timber.tag("PdfPrint").e(e, "Failed to initialize print job")
coroutineScope.launch { 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 verticalReaderState.currentPage
} }
val titleText = when { val titleText = when {
isLoadingDocument -> "Loading PDF..." isLoadingDocument -> stringResource(R.string.loading_pdf)
errorMessage != null -> "Error 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 -> "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" else -> "PDF Viewer"
} }
Text( Text(
@ -5610,12 +5610,12 @@ fun PdfViewerScreen(
if (!hiddenTools.contains(PdfReaderTool.THEME.name)) { if (!hiddenTools.contains(PdfReaderTool.THEME.name)) {
TooltipIconButton( TooltipIconButton(
text = "Theme", text = stringResource(R.string.tooltip_theme),
description = "Theme Settings", description = stringResource(R.string.tooltip_theme_desc),
onClick = { showThemePanel = true }) { onClick = { showThemePanel = true }) {
Icon( Icon(
painter = painterResource(id = R.drawable.palette), painter = painterResource(id = R.drawable.palette),
contentDescription = "Theme Settings", contentDescription = stringResource(R.string.tooltip_theme_desc),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -5637,7 +5637,7 @@ fun PdfViewerScreen(
}) { }) {
Icon( Icon(
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, 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 tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@ -5674,7 +5674,7 @@ fun PdfViewerScreen(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
TooltipIconButton( TooltipIconButton(
text = "Pen Playground", text = stringResource(R.string.pen_playground),
onClick = { showPenPlayground = true }) { onClick = { showPenPlayground = true }) {
Icon( Icon(
imageVector = Icons.Default.Star, 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 = val page =
if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
@ -5712,16 +5712,16 @@ fun PdfViewerScreen(
} }
redoStack.clear() redoStack.clear()
snackbarHostState.showSnackbar("Imported ${svgAnnotations.size} SVG strokes!") snackbarHostState.showSnackbar(context.getString(R.string.msg_imported_svg_strokes))
} else { } else {
snackbarHostState.showSnackbar("Failed to import SVG or empty.") snackbarHostState.showSnackbar(context.getString(R.string.error_import_svg_failed))
} }
} }
} }
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Brush, imageVector = Icons.Default.Brush,
contentDescription = "Import SVG", contentDescription = stringResource(R.string.import_svg),
tint = Color(0xFFE91E63) tint = Color(0xFFE91E63)
) )
} }
@ -5735,7 +5735,7 @@ fun PdfViewerScreen(
onClick = { showMoreMenu = true }) { onClick = { showMoreMenu = true }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "More Options" contentDescription = stringResource(R.string.tooltip_more_options)
) )
} }
@ -5744,19 +5744,19 @@ fun PdfViewerScreen(
expanded = showMoreMenu, expanded = showMoreMenu,
onDismissRequest = { showMoreMenu = false }) { onDismissRequest = { showMoreMenu = false }) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Customize Toolbar") }, text = { Text(stringResource(R.string.title_customize_toolbar)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
showCustomizeToolsSheet = true showCustomizeToolsSheet = true
}, },
leadingIcon = { 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() HorizontalDivider()
if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) { if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("OCR Language") }, text = { Text(stringResource(R.string.menu_ocr_language)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
hasSelectedOcrLanguage = true hasSelectedOcrLanguage = true
@ -5767,7 +5767,7 @@ fun PdfViewerScreen(
if (!hiddenTools.contains(PdfReaderTool.READING_MODE.name)) { if (!hiddenTools.contains(PdfReaderTool.READING_MODE.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Reading Mode: Vertical scroll") }, text = { Text(stringResource(R.string.menu_reading_mode_vertical)) },
enabled = !isTtsSessionActive, enabled = !isTtsSessionActive,
onClick = { onClick = {
displayMode = DisplayMode.VERTICAL_SCROLL displayMode = DisplayMode.VERTICAL_SCROLL
@ -5777,13 +5777,13 @@ fun PdfViewerScreen(
if (displayMode == DisplayMode.VERTICAL_SCROLL) { if (displayMode == DisplayMode.VERTICAL_SCROLL) {
Icon( Icon(
imageVector = Icons.Filled.Check, imageVector = Icons.Filled.Check,
contentDescription = "Selected" contentDescription = stringResource(R.string.content_desc_selected)
) )
} }
}) })
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Reading Mode: Paginated") }, text = { Text(stringResource(R.string.menu_reading_mode_paginated)) },
enabled = !isTtsSessionActive, enabled = !isTtsSessionActive,
onClick = { onClick = {
displayMode = DisplayMode.PAGINATION displayMode = DisplayMode.PAGINATION
@ -5793,7 +5793,7 @@ fun PdfViewerScreen(
if (displayMode == DisplayMode.PAGINATION) { if (displayMode == DisplayMode.PAGINATION) {
Icon( Icon(
imageVector = Icons.Filled.Check, 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)) { if (!hiddenTools.contains(PdfReaderTool.KEEP_SCREEN_ON.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Keep Screen On") }, text = { Text(stringResource(R.string.menu_keep_screen_on)) },
onClick = { onClick = {
isKeepScreenOn = !isKeepScreenOn isKeepScreenOn = !isKeepScreenOn
saveKeepScreenOn(context, isKeepScreenOn) saveKeepScreenOn(context, isKeepScreenOn)
@ -5811,7 +5811,7 @@ fun PdfViewerScreen(
if (isKeepScreenOn) { if (isKeepScreenOn) {
Icon( Icon(
imageVector = Icons.Filled.Check, 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)) { if (!hiddenTools.contains(PdfReaderTool.AUTO_SCROLL.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Auto Scroll") }, text = { Text(stringResource(R.string.menu_auto_scroll)) },
enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL, enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL,
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
@ -5832,7 +5832,7 @@ fun PdfViewerScreen(
} }
if (!hiddenTools.contains(PdfReaderTool.TTS_SETTINGS.name)) { if (!hiddenTools.contains(PdfReaderTool.TTS_SETTINGS.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("TTS Voice Settings") }, text = { Text(stringResource(R.string.menu_tts_voice_settings)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
showDeviceVoiceSettingsSheet = true showDeviceVoiceSettingsSheet = true
@ -5848,7 +5848,7 @@ fun PdfViewerScreen(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("TTS Settings (Debug)") }, text = { Text(stringResource(R.string.menu_tts_settings_debug)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
showTtsSettingsSheet = true showTtsSettingsSheet = true
@ -5867,8 +5867,8 @@ fun PdfViewerScreen(
if (!hiddenTools.contains(PdfReaderTool.BOOKMARK.name)) { if (!hiddenTools.contains(PdfReaderTool.BOOKMARK.name)) {
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text( Text(
if (isBookmarked) "Remove bookmark" if (isBookmarked) stringResource(R.string.menu_remove_bookmark)
else "Bookmark this page" else stringResource(R.string.menu_bookmark_this_page)
) )
}, onClick = { }, onClick = {
showMoreMenu = false showMoreMenu = false
@ -5878,7 +5878,7 @@ fun PdfViewerScreen(
} }
if (!hiddenTools.contains(PdfReaderTool.PAGE_MANAGEMENT.name)) { if (!hiddenTools.contains(PdfReaderTool.PAGE_MANAGEMENT.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Insert Blank Page") }, text = { Text(stringResource(R.string.menu_insert_blank_page)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onInsertPage() onInsertPage()
@ -5888,7 +5888,7 @@ fun PdfViewerScreen(
virtualPages.getOrNull(currentPage) is VirtualPage.BlankPage virtualPages.getOrNull(currentPage) is VirtualPage.BlankPage
if (canDelete) { if (canDelete) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete Page") }, text = { Text(stringResource(R.string.menu_delete_page)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onDeletePage() onDeletePage()
@ -5905,9 +5905,9 @@ fun PdfViewerScreen(
text = { text = {
Text( Text(
when { when {
isReflowingThisBook -> "Generating... ${(reflowProgressValue * 100).toInt()}%" isReflowingThisBook -> stringResource(R.string.generating_reflow_progress)
hasReflowFile -> "Open Text View" hasReflowFile -> stringResource(R.string.action_open_text_view)
else -> "Generate Text View" else -> stringResource(R.string.action_generate_text_view)
} }
) )
}, },
@ -5964,7 +5964,7 @@ fun PdfViewerScreen(
} }
if (!hiddenTools.contains(PdfReaderTool.SHARE.name)) { if (!hiddenTools.contains(PdfReaderTool.SHARE.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Share") }, text = { Text(stringResource(R.string.action_share)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
showShareDialog = true showShareDialog = true
@ -5979,7 +5979,7 @@ fun PdfViewerScreen(
} }
if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.SAVE_COPY.name)) { if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.SAVE_COPY.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Save copy to device") }, text = { Text(stringResource(R.string.action_save_copy_to_device)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
showSaveDialog = true showSaveDialog = true
@ -5993,7 +5993,7 @@ fun PdfViewerScreen(
} }
if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.PRINT.name)) { if (uiState.selectedFileType == FileType.PDF && !hiddenTools.contains(PdfReaderTool.PRINT.name)) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Print") }, text = { Text(stringResource(R.string.action_print)) },
onClick = { onClick = {
showMoreMenu = false showMoreMenu = false
onPrintDocument() onPrintDocument()
@ -6066,7 +6066,7 @@ fun PdfViewerScreen(
}, },
modifier = Modifier.size(20.dp) 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() modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = "Generating Text View...", text = stringResource(R.string.generating_text_view),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -6162,7 +6162,7 @@ fun PdfViewerScreen(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( 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, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer color = MaterialTheme.colorScheme.onSecondaryContainer
) )
@ -6358,7 +6358,7 @@ fun PdfViewerScreen(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.slider), 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( Icon(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
contentDescription = "Search" contentDescription = stringResource(R.string.action_search)
) )
} }
} }
@ -6438,7 +6438,7 @@ fun PdfViewerScreen(
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ai), painter = painterResource(id = R.drawable.ai),
contentDescription = "AI Features" contentDescription = stringResource(R.string.tooltip_ai)
) )
} }
DropdownMenu( DropdownMenu(
@ -6446,7 +6446,7 @@ fun PdfViewerScreen(
onDismissRequest = { showAiFeaturesMenu = false }) { onDismissRequest = { showAiFeaturesMenu = false }) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text("Summarize Page (Page ${currentPage + 1})") Text(stringResource(R.string.action_summarize_page))
}, onClick = { }, onClick = {
showAiFeaturesMenu = false showAiFeaturesMenu = false
if (isProUser) { if (isProUser) {
@ -6526,7 +6526,7 @@ fun PdfViewerScreen(
else painterResource( else painterResource(
id = R.drawable.text_to_speech 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( painter = painterResource(
id = if (ttsState.isPlaying) R.drawable.pause id = if (ttsState.isPlaying) R.drawable.pause
else R.drawable.play else R.drawable.play
), contentDescription = if (ttsState.isPlaying) "Pause TTS" ), contentDescription = if (ttsState.isPlaying) stringResource(R.string.content_desc_pause_tts)
else "Resume TTS" else stringResource(R.string.content_desc_resume_tts)
) )
} }
// Tune button for BASE mode // Tune button for BASE mode
if (currentTtsMode == TtsPlaybackManager.TtsMode.BASE) { if (currentTtsMode == TtsPlaybackManager.TtsMode.BASE) {
TooltipIconButton( TooltipIconButton(
text = "Voice Adjustments", text = stringResource(R.string.tts_voice_adjustments),
description = "Adjust voice speed and pitch", description = "Adjust voice speed and pitch",
onClick = { showTtsControlsSheet = true } onClick = { showTtsControlsSheet = true }
) { ) {
Icon( Icon(
imageVector = Icons.Default.Tune, imageVector = Icons.Default.Tune,
contentDescription = "Voice Adjustments" contentDescription = stringResource(R.string.tts_voice_adjustments)
) )
} }
} }
@ -6721,19 +6721,13 @@ fun PdfViewerScreen(
.fillMaxSize() .fillMaxSize()
.then( .then(
when { when {
isDockDragging -> Modifier // Positioned manually isDockDragging -> Modifier
// via offset during dockLocation == DockLocation.TOP -> Modifier
// drag dockLocation == DockLocation.BOTTOM -> Modifier
dockLocation == DockLocation.TOP -> Modifier // Aligned via Box else -> Modifier
// Scope
dockLocation == DockLocation.BOTTOM -> Modifier // Aligned via Box
// Scope
else -> Modifier // Positioned manually
// via offset
} }
) )
) { ) {
// Calculate drag offset to apply if floating/dragging
val dragModifier = val dragModifier =
if (isDockDragging || dockLocation == DockLocation.FLOATING) { if (isDockDragging || dockLocation == DockLocation.FLOATING) {
Modifier.offset { Modifier.offset {
@ -6742,10 +6736,9 @@ fun PdfViewerScreen(
) )
} }
} else { } else {
Modifier // Sticky positions use alignment below Modifier
} }
// Calculate Alignment for Sticky states
val alignModifier = when { val alignModifier = when {
isDockDragging || dockLocation == DockLocation.FLOATING -> Modifier isDockDragging || dockLocation == DockLocation.FLOATING -> Modifier
dockLocation == DockLocation.TOP -> Modifier.align(Alignment.TopCenter) dockLocation == DockLocation.TOP -> Modifier.align(Alignment.TopCenter)
@ -6759,7 +6752,7 @@ fun PdfViewerScreen(
} else { } else {
Modifier.padding( Modifier.padding(
horizontal = 16.dp horizontal = 16.dp
) // Original padding for floating capsule )
} }
val paddingModifier = val paddingModifier =
@ -7209,10 +7202,10 @@ fun PdfViewerScreen(
if (showPermissionRationaleDialog) { if (showPermissionRationaleDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showPermissionRationaleDialog = false }, onDismissRequest = { showPermissionRationaleDialog = false },
title = { Text("Permission Required") }, title = { Text(stringResource(R.string.dialog_permission_required)) },
text = { text = {
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 = { confirmButton = {
@ -7222,14 +7215,14 @@ fun PdfViewerScreen(
permissionLauncher.launch( permissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) )
}) { Text("Continue") } }) { Text(stringResource(R.string.action_continue)) }
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
showPermissionRationaleDialog = false showPermissionRationaleDialog = false
startTts() startTts()
}) { Text("Not now") } }) { Text(stringResource(R.string.action_not_now)) }
}) })
} }
if (showSummarizationUpsellDialog) { if (showSummarizationUpsellDialog) {
@ -7252,11 +7245,11 @@ fun PdfViewerScreen(
onClick = { onClick = {
showSummarizationUpsellDialog = false showSummarizationUpsellDialog = false
onNavigateToPro() onNavigateToPro()
}) { Text("Learn More") } }) { Text(stringResource(R.string.action_learn_more)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showSummarizationUpsellDialog = false }) { TextButton(onClick = { showSummarizationUpsellDialog = false }) {
Text("Not Now") Text(stringResource(R.string.action_not_now))
} }
}) })
} }
@ -7287,13 +7280,13 @@ fun PdfViewerScreen(
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
Text( Text(
text = "Add PDF to Tab", text = stringResource(R.string.title_add_pdf_to_tab),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
if (pdfFiles.isEmpty()) { if (pdfFiles.isEmpty()) {
Text( 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), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -7345,7 +7338,7 @@ fun PdfViewerScreen(
if (!selectedDictPackage.isNullOrEmpty()) { if (!selectedDictPackage.isNullOrEmpty()) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text) ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} else { } 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 showDictionarySettingsSheet = true
} }
} }
@ -7358,19 +7351,19 @@ fun PdfViewerScreen(
painter = painterResource(id = R.drawable.ai), painter = painterResource(id = R.drawable.ai),
contentDescription = null contentDescription = null
) )
}, title = { Text("Unlock Smart Dictionary") }, text = { }, title = { Text(stringResource(R.string.ai_unlock_smart_dict)) }, text = {
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 = { }, confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
showDictionaryUpsellDialog = false showDictionaryUpsellDialog = false
onNavigateToPro() onNavigateToPro()
}) { Text("Learn More") } }) { Text(stringResource(R.string.action_learn_more)) }
}, dismissButton = { }, dismissButton = {
TextButton(onClick = { showDictionaryUpsellDialog = false }) { TextButton(onClick = { showDictionaryUpsellDialog = false }) {
Text("Not Now") Text(stringResource(R.string.action_not_now))
} }
}) })
} }
@ -7380,12 +7373,10 @@ fun PdfViewerScreen(
AlertDialog( AlertDialog(
onDismissRequest = { showReindexDialog = null }, onDismissRequest = { showReindexDialog = null },
icon = { Icon(Icons.Default.Info, contentDescription = null) }, icon = { Icon(Icons.Default.Info, contentDescription = null) },
title = { Text("Re-index Document?") }, title = { Text(stringResource(R.string.title_reindex_document)) },
text = { text = {
Text( Text(
"You are changing the OCR script to ${newLanguage.displayName}.\n\n" + stringResource(R.string.desc_reindex_document_warning)
"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."
) )
}, },
confirmButton = { confirmButton = {
@ -7412,11 +7403,11 @@ fun PdfViewerScreen(
showOcrLanguageDialog = false showOcrLanguageDialog = false
} }
} }
) { Text("Re-index") } ) { Text(stringResource(R.string.action_reindex)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showReindexDialog = null }) { TextButton(onClick = { showReindexDialog = null }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
) )
@ -7625,8 +7616,8 @@ fun PdfViewerScreen(
val url = clickedLinkUrl!! val url = clickedLinkUrl!!
AlertDialog( AlertDialog(
onDismissRequest = { clickedLinkUrl = null }, onDismissRequest = { clickedLinkUrl = null },
title = { Text("External Link") }, title = { Text(stringResource(R.string.dialog_external_link_title)) },
text = { Text("You are about to navigate to:\n$url") }, text = { Text(stringResource(R.string.desc_external_link_warning)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -7636,7 +7627,7 @@ fun PdfViewerScreen(
Timber.e(e, "Failed to open URI") Timber.e(e, "Failed to open URI")
} }
clickedLinkUrl = null clickedLinkUrl = null
}) { Text("Visit") } }) { Text(stringResource(R.string.action_visit)) }
}, },
dismissButton = { dismissButton = {
Row { Row {
@ -7644,9 +7635,9 @@ fun PdfViewerScreen(
onClick = { onClick = {
clipboardManager.setText(AnnotatedString(url)) clipboardManager.setText(AnnotatedString(url))
clickedLinkUrl = null clickedLinkUrl = null
}) { Text("Copy") } }) { Text(stringResource(R.string.action_copy)) }
TextButton(onClick = { clickedLinkUrl = null }) { TextButton(onClick = { clickedLinkUrl = null }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
}) })
@ -7655,8 +7646,8 @@ fun PdfViewerScreen(
if (showSaveDialog) { if (showSaveDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showSaveDialog = false }, onDismissRequest = { showSaveDialog = false },
title = { Text("Save to Device") }, title = { Text(stringResource(R.string.title_save_to_device)) },
text = { Text("Choose format to save:") }, text = { Text(stringResource(R.string.desc_choose_format_save)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -7666,7 +7657,7 @@ fun PdfViewerScreen(
originalFileName, isAnnotated = true originalFileName, isAnnotated = true
) )
saveLauncher.launch(suggestedName) saveLauncher.launch(suggestedName)
}) { Text("With Annotations") } }) { Text(stringResource(R.string.action_with_annotations)) }
}, },
dismissButton = { dismissButton = {
Row { Row {
@ -7678,7 +7669,7 @@ fun PdfViewerScreen(
originalFileName, isAnnotated = false originalFileName, isAnnotated = false
) )
saveLauncher.launch(suggestedName) saveLauncher.launch(suggestedName)
}) { Text("Original") } }) { Text(stringResource(R.string.action_original)) }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@ -7686,7 +7677,7 @@ fun PdfViewerScreen(
onClick = { onClick = {
showSaveDialog = false showSaveDialog = false
pendingSaveMode = null pendingSaveMode = null
}) { Text("Cancel") } }) { Text(stringResource(R.string.action_cancel)) }
} }
}) })
} }
@ -7694,8 +7685,8 @@ fun PdfViewerScreen(
if (showShareDialog) { if (showShareDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showShareDialog = false }, onDismissRequest = { showShareDialog = false },
title = { Text("Share PDF") }, title = { Text(stringResource(R.string.share_chooser_title)) },
text = { Text("Choose format to share:") }, text = { Text(stringResource(R.string.desc_choose_format_share)) },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
@ -7721,7 +7712,7 @@ fun PdfViewerScreen(
) )
isShareLoading = false isShareLoading = false
} }
}) { Text("With Annotations") } }) { Text(stringResource(R.string.action_with_annotations)) }
}, },
dismissButton = { dismissButton = {
Row { Row {
@ -7742,10 +7733,10 @@ fun PdfViewerScreen(
) )
isShareLoading = false isShareLoading = false
} }
}) { Text("Original") } }) { Text(stringResource(R.string.action_original)) }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
TextButton(onClick = { showShareDialog = false }) { TextButton(onClick = { showShareDialog = false }) {
Text("Cancel") Text(stringResource(R.string.action_cancel))
} }
} }
}) })
@ -7773,7 +7764,7 @@ fun PdfViewerScreen(
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Preparing PDF...", text = stringResource(R.string.msg_preparing_pdf),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
} }
@ -8087,27 +8078,27 @@ private fun PasswordDialog(isError: Boolean, onDismiss: () -> Unit, onConfirm: (
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) } 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 { 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)) Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField( OutlinedTextField(
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
label = { Text("Password") }, label = { Text(stringResource(R.string.password)) },
singleLine = true, singleLine = true,
visualTransformation = if (passwordVisible) VisualTransformation.None visualTransformation = if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(), else PasswordVisualTransformation(),
keyboardActions = KeyboardActions(onDone = { onConfirm(password) }), keyboardActions = KeyboardActions(onDone = { onConfirm(password) }),
isError = isError, isError = isError,
supportingText = if (isError) { supportingText = if (isError) {
{ Text("Incorrect password") } { Text(stringResource(R.string.error_incorrect_password)) }
} else null, } else null,
trailingIcon = { trailingIcon = {
val image = if (passwordVisible) Icons.Filled.Visibility val image = if (passwordVisible) Icons.Filled.Visibility
else Icons.Filled.VisibilityOff 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 }) { IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, description) Icon(imageVector = image, description)
@ -8118,9 +8109,9 @@ private fun PasswordDialog(isError: Boolean, onDismiss: () -> Unit, onConfirm: (
} }
}, confirmButton = { }, confirmButton = {
Button(onClick = { onConfirm(password) }, enabled = password.isNotBlank()) { 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 @Composable
@ -8137,13 +8128,12 @@ fun PenPlayground(onClose: () -> Unit) {
Color.Black // Black Color.Black // Black
) )
// Dark Card Background
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.95f) .fillMaxWidth(0.95f)
.padding(16.dp), .padding(16.dp),
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
color = Color(0xFF1E1E1E), // Deep Matte Dark Grey color = Color(0xFF1E1E1E),
shadowElevation = 16.dp, shadowElevation = 16.dp,
tonalElevation = 0.dp tonalElevation = 0.dp
) { ) {
@ -8151,13 +8141,11 @@ fun PenPlayground(onClose: () -> Unit) {
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp), modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Header with Close Button
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Placeholder icon (Star) on left
Icon( Icon(
imageVector = Icons.Default.Star, imageVector = Icons.Default.Star,
contentDescription = null, contentDescription = null,
@ -8169,7 +8157,7 @@ fun PenPlayground(onClose: () -> Unit) {
Icon( Icon(
painter = painterResource( painter = painterResource(
id = R.drawable.close id = R.drawable.close
), // Ensure you have a close icon or use Icons.Default.Close ),
contentDescription = "Close", tint = Color.Gray contentDescription = "Close", tint = Color.Gray
) )
} }
@ -8181,19 +8169,17 @@ fun PenPlayground(onClose: () -> Unit) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(140.dp), // Height for pens + ink stroke space .height(140.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
PenType.entries.forEach { type -> PenType.entries.forEach { type ->
val isSelected = selectedPen == type val isSelected = selectedPen == type
// Selected pens float up slightly
val offsetY by animateDpAsState( val offsetY by animateDpAsState(
targetValue = if (isSelected) (-20).dp else 0.dp, label = "offset" targetValue = if (isSelected) (-20).dp else 0.dp, label = "offset"
) )
// Selected pens scale up
val scale by animateFloatAsState( val scale by animateFloatAsState(
targetValue = if (isSelected) 1.2f else 1.0f, label = "scale" targetValue = if (isSelected) 1.2f else 1.0f, label = "scale"
) )
@ -8205,13 +8191,13 @@ fun PenPlayground(onClose: () -> Unit) {
.scale(scale) .scale(scale)
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null // Remove ripple for cleaner look indication = null
) { selectedPen = type }) { ) { selectedPen = type }) {
// Drawing Area // Drawing Area
Box( Box(
modifier = Modifier modifier = Modifier
.width(40.dp) .width(40.dp)
.height(120.dp), // Tall enough for stroke + pen .height(120.dp),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
PenIcon( PenIcon(
@ -8229,7 +8215,6 @@ fun PenPlayground(onClose: () -> Unit) {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
// Subtle Divider
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp),
color = Color.White.copy(alpha = 0.1f), color = Color.White.copy(alpha = 0.1f),
@ -8292,10 +8277,10 @@ private fun OcrLanguageSelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onLanguageSelected: (OcrLanguage) -> 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()) { Column(Modifier.selectableGroup()) {
Text( 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, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
@ -8317,7 +8302,7 @@ private fun OcrLanguageSelectionDialog(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"You can change this later in More Options > OCR Language.", stringResource(R.string.desc_ocr_language_change_later),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer 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 @Composable
@ -8454,7 +8439,7 @@ fun PdfSearchResultsPanel(
} else { } else {
Column { Column {
Text( Text(
text = "Results found on ${totalPageCount}+ pages", text = stringResource(R.string.msg_results_found_pages),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
) )
@ -8506,7 +8491,7 @@ fun PdfSearchResultsList(
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
if (results.isEmpty()) { if (results.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No results found.", style = MaterialTheme.typography.bodyLarge) Text(stringResource(R.string.search_no_results_simple), style = MaterialTheme.typography.bodyLarge)
} }
} else { } else {
Column { Column {
@ -8555,14 +8540,14 @@ fun PdfCustomizeToolsSheet(
) { ) {
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) { Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
Text( Text(
text = "Customize Toolbar", text = stringResource(R.string.title_customize_toolbar),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( 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, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )

View file

@ -97,6 +97,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -211,7 +212,6 @@ fun TextAnnotationDock(
) { ) {
when (activePopup) { when (activePopup) {
ActivePopup.FONT_FAMILY -> { ActivePopup.FONT_FAMILY -> {
// Re-using the logic from your EPUB FontSelectionSheetContent but adapted for a Popup
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = Color(0xFF1E1E1E), color = Color(0xFF1E1E1E),
@ -229,12 +229,12 @@ fun TextAnnotationDock(
Tab( Tab(
selected = selectedTabIndex == 0, selected = selectedTabIndex == 0,
onClick = { selectedTabIndex = 0 }, onClick = { selectedTabIndex = 0 },
text = { Text("Presets", fontSize = 12.sp) } text = { Text(stringResource(R.string.tab_presets), fontSize = 12.sp) }
) )
Tab( Tab(
selected = selectedTabIndex == 1, selected = selectedTabIndex == 1,
onClick = { 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 { item {
val isSelected = currentFontName == "Default" || currentFontName == null val isSelected = currentFontName == "Default" || currentFontName == null
FontItem( FontItem(
name = "Default System Font", name = stringResource(R.string.font_default_system),
isSelected = isSelected, isSelected = isSelected,
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
onClick = { onClick = {
@ -281,12 +281,12 @@ fun TextAnnotationDock(
) { ) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Import", fontSize = 12.sp) Text(stringResource(R.string.action_import), fontSize = 12.sp)
} }
if (customFonts.isEmpty()) { if (customFonts.isEmpty()) {
Text( Text(
"No fonts imported", stringResource(R.string.msg_no_fonts_imported),
color = Color.Gray, color = Color.Gray,
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier.fillMaxWidth().padding(16.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -320,7 +320,7 @@ fun TextAnnotationDock(
ActivePopup.COLOR -> { ActivePopup.COLOR -> {
if (activeMenuMode == ColorMenuMode.PALETTE) { if (activeMenuMode == ColorMenuMode.PALETTE) {
ColorPickerBubble( ColorPickerBubble(
title = "Font color", title = stringResource(R.string.label_font_color),
currentColor = currentStyle.color.takeIf { it != Color.Unspecified } currentColor = currentStyle.color.takeIf { it != Color.Unspecified }
?: Color.Black, ?: Color.Black,
palette = textColorPalette, palette = textColorPalette,
@ -359,7 +359,7 @@ fun TextAnnotationDock(
ActivePopup.BACKGROUND -> { ActivePopup.BACKGROUND -> {
if (activeMenuMode == ColorMenuMode.PALETTE) { if (activeMenuMode == ColorMenuMode.PALETTE) {
ColorPickerBubble( ColorPickerBubble(
title = "Highlight", title = stringResource(R.string.label_highlight_color),
currentColor = when (currentStyle.background) { currentColor = when (currentStyle.background) {
Color.Unspecified, Color.Transparent -> Color.Transparent Color.Unspecified, Color.Transparent -> Color.Transparent
else -> currentStyle.background else -> currentStyle.background

View file

@ -70,6 +70,7 @@ import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight 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.BrightnessSlider
import com.aryan.reader.ColorComparePill import com.aryan.reader.ColorComparePill
import com.aryan.reader.HexInput import com.aryan.reader.HexInput
import com.aryan.reader.R
import com.aryan.reader.RgbInputColumn import com.aryan.reader.RgbInputColumn
import com.aryan.reader.SpectrumBox import com.aryan.reader.SpectrumBox
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -262,7 +264,7 @@ fun ToolSettingsPopup(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = "Straight Line", text = stringResource(R.string.label_straight_line),
color = Color.White, color = Color.White,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
@ -465,7 +467,7 @@ private fun ColorPickerDialog(
.padding(horizontal = 24.dp, vertical = 8.dp) .padding(horizontal = 24.dp, vertical = 8.dp)
) { ) {
Text( Text(
text = "Spectrum", text = stringResource(R.string.label_spectrum),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White color = Color.White
@ -520,7 +522,7 @@ private fun ColorPickerDialog(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
"Hex", stringResource(R.string.theme_color_hex),
color = Color.Gray, color = Color.Gray,
fontSize = 12.sp, fontSize = 12.sp,
maxLines = 1 maxLines = 1
@ -537,19 +539,19 @@ private fun ColorPickerDialog(
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
RgbInputColumn( RgbInputColumn(
label = "Red", label = stringResource(R.string.color_red),
value = currentColor.red, value = currentColor.red,
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) }, onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
RgbInputColumn( RgbInputColumn(
label = "Green", label = stringResource(R.string.color_green),
value = currentColor.green, value = currentColor.green,
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) }, onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
RgbInputColumn( RgbInputColumn(
label = "Blue", label = stringResource(R.string.color_blue),
value = currentColor.blue, value = currentColor.blue,
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) }, onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -565,7 +567,7 @@ private fun ColorPickerDialog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel", color = Color.Gray) Text(stringResource(R.string.action_cancel), color = Color.Gray)
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Button( Button(
@ -575,7 +577,7 @@ private fun ColorPickerDialog(
contentColor = Color.Black contentColor = Color.Black
) )
) { ) {
Text("Done") Text(stringResource(R.string.action_done))
} }
} }
} }

View file

@ -1,17 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Shown on book cards and in various dialogs. %1$d = number of books. -->
<plurals name="book_count"> <plurals name="book_count">
<item quantity="one">%1$d book</item> <item quantity="one">%1$d book</item>
<item quantity="other">%1$d books</item> <item quantity="other">%1$d books</item>
</plurals> </plurals>
<!-- Word-only plural (no number prefix) used in sentences. Example: "remove 3 books from shelf". -->
<plurals name="book_word"> <plurals name="book_word">
<item quantity="one">book</item> <item quantity="one">book</item>
<item quantity="other">books</item> <item quantity="other">books</item>
</plurals> </plurals>
<!-- Shown when deleting multiple shelves. %1$d = number of shelves. -->
<plurals name="shelf_count"> <plurals name="shelf_count">
<item quantity="one">%1$d shelf</item> <item quantity="one">%1$d shelf</item>
<item quantity="other">%1$d shelves</item> <item quantity="other">%1$d shelves</item>
</plurals> </plurals>
<!-- Shown in the search results summary bar. %1$d = total number of results found. -->
<plurals name="search_results_count"> <plurals name="search_results_count">
<item quantity="one">%1$d result found</item> <item quantity="one">%1$d result found</item>
<item quantity="other">%1$d results found</item> <item quantity="other">%1$d results found</item>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- This instruction strips the internet permission specifically for the releaseOffline build type -->
<uses-permission android:name="android.permission.INTERNET" tools:node="remove" />
</manifest>