New languages (#256)
* Remove custom-onnxruntime-arm64.aar * Refactor remaining, hardcoded UI strings into localizable resources and added French and Russian to language list * Fix background auto-advance for Text-to-Speech (TTS)
This commit is contained in:
parent
224d12d1fb
commit
4582b506f3
33 changed files with 940 additions and 488 deletions
|
|
@ -33,7 +33,7 @@ android {
|
||||||
versionCode = 46
|
versionCode = 46
|
||||||
versionName = "1.0.46"
|
versionName = "1.0.46"
|
||||||
|
|
||||||
resourceConfigurations += setOf("en", "ar", "de", "tr")
|
resourceConfigurations += setOf("en", "ar", "de", "tr", "fr", "ru")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
|
@ -258,12 +259,12 @@ fun AppNavigation(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text("Error: $errorMessage", color = MaterialTheme.colorScheme.error)
|
Text(stringResource(R.string.error_message_format, errorMessage), color = MaterialTheme.colorScheme.error)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
viewModel.clearSelectedFile()
|
viewModel.clearSelectedFile()
|
||||||
}) {
|
}) {
|
||||||
Text("Go Back")
|
Text(stringResource(R.string.action_go_back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1126,10 +1126,13 @@ fun TtsSettingsSheet(
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
DeviceVoicesTab(isTtsActive, context, TtsPlaybackManager.TtsMode.BASE)
|
DeviceVoicesTab(isTtsActive, context, TtsPlaybackManager.TtsMode.BASE)
|
||||||
} else {
|
} else {
|
||||||
Text("Active TTS Engine", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.tts_active_engine), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, RoundedCornerShape(24.dp)).padding(4.dp)) {
|
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, RoundedCornerShape(24.dp)).padding(4.dp)) {
|
||||||
val modes = listOf(TtsPlaybackManager.TtsMode.CLOUD to "Cloud AI", TtsPlaybackManager.TtsMode.BASE to "Device Native")
|
val modes = listOf(
|
||||||
|
TtsPlaybackManager.TtsMode.CLOUD to stringResource(R.string.tts_mode_cloud_ai),
|
||||||
|
TtsPlaybackManager.TtsMode.BASE to stringResource(R.string.tts_mode_device_native)
|
||||||
|
)
|
||||||
modes.forEach { (mode, title) ->
|
modes.forEach { (mode, title) ->
|
||||||
val isSelected = currentMode == mode
|
val isSelected = currentMode == mode
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -1150,9 +1153,9 @@ fun TtsSettingsSheet(
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
TabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent, divider = {}) {
|
TabRow(selectedTabIndex = selectedTabIndex, containerColor = Color.Transparent, divider = {}) {
|
||||||
Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text("Cloud Voices", maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }, text = { Text(stringResource(R.string.tts_tab_cloud_voices), maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
||||||
Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text("Device Voices", maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }, text = { Text(stringResource(R.string.tts_tab_device_voices), maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
||||||
Tab(selected = selectedTabIndex == 2, onClick = { selectedTabIndex = 2 }, text = { Text("Cloud Cache", maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
Tab(selected = selectedTabIndex == 2, onClick = { selectedTabIndex = 2 }, text = { Text(stringResource(R.string.tts_tab_cloud_cache), maxLines = 1, overflow = TextOverflow.Ellipsis) })
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
@ -1180,10 +1183,10 @@ fun AiVoicesTab(
|
||||||
val isCloudMode = currentMode == TtsPlaybackManager.TtsMode.CLOUD
|
val isCloudMode = currentMode == TtsPlaybackManager.TtsMode.CLOUD
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Select High-Quality Cloud Voice", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.tts_select_cloud_voice), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
if (samplePlayer.cachedSpeakers.isNotEmpty()) {
|
if (samplePlayer.cachedSpeakers.isNotEmpty()) {
|
||||||
TextButton(onClick = { samplePlayer.clearSamples() }, modifier = Modifier.heightIn(min = 24.dp)) {
|
TextButton(onClick = { samplePlayer.clearSamples() }, modifier = Modifier.heightIn(min = 24.dp)) {
|
||||||
Text("Clear Samples", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelMedium)
|
Text(stringResource(R.string.tts_clear_samples), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1244,7 +1247,8 @@ fun DeviceVoicesTab(
|
||||||
var allVoices by remember { mutableStateOf<List<Voice>>(emptyList()) }
|
var allVoices by remember { mutableStateOf<List<Voice>>(emptyList()) }
|
||||||
var isTtsLoading by remember { mutableStateOf(true) }
|
var isTtsLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
var selectedLanguage by remember { mutableStateOf("All") }
|
val allLanguagesLabel = stringResource(R.string.filter_all)
|
||||||
|
var selectedLanguage by remember { mutableStateOf(allLanguagesLabel) }
|
||||||
var languageMenuExpanded by remember { mutableStateOf(false) }
|
var languageMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
|
|
@ -1264,13 +1268,13 @@ fun DeviceVoicesTab(
|
||||||
}
|
}
|
||||||
|
|
||||||
val languages = remember(allVoices) {
|
val languages = remember(allVoices) {
|
||||||
val list = listOf("All") + allVoices.map { it.locale.displayLanguage }.filter { it.isNotBlank() }.distinct().sorted()
|
val list = listOf(allLanguagesLabel) + allVoices.map { it.locale.displayLanguage }.filter { it.isNotBlank() }.distinct().sorted()
|
||||||
Timber.tag("TTS_DIAGNOSE").d("Languages list updated: size=${list.size}, items=$list")
|
Timber.tag("TTS_DIAGNOSE").d("Languages list updated: size=${list.size}, items=$list")
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredVoices = remember(allVoices, selectedLanguage) {
|
val filteredVoices = remember(allVoices, selectedLanguage) {
|
||||||
if (selectedLanguage == "All") allVoices
|
if (selectedLanguage == allLanguagesLabel) allVoices
|
||||||
else allVoices.filter { it.locale.displayLanguage == selectedLanguage }
|
else allVoices.filter { it.locale.displayLanguage == selectedLanguage }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1310,8 +1314,8 @@ fun DeviceVoicesTab(
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text("System Default Voice", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.tts_system_default_voice), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
Text("Uses device settings", style = MaterialTheme.typography.bodySmall)
|
Text(stringResource(R.string.tts_uses_device_settings), style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
if (isBaseMode && savedVoiceName == null) Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
|
if (isBaseMode && savedVoiceName == null) Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
|
|
@ -1326,7 +1330,7 @@ fun DeviceVoicesTab(
|
||||||
value = selectedLanguage,
|
value = selectedLanguage,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("Language Filter") },
|
label = { Text(stringResource(R.string.tts_language_filter)) },
|
||||||
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = languageMenuExpanded) },
|
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = languageMenuExpanded) },
|
||||||
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
modifier = Modifier.fillMaxWidth().menuAnchor(),
|
modifier = Modifier.fillMaxWidth().menuAnchor(),
|
||||||
|
|
@ -1359,7 +1363,7 @@ fun DeviceVoicesTab(
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(voice.locale.displayName, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal) },
|
headlineContent = { Text(voice.locale.displayName, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal) },
|
||||||
supportingContent = { Text(if (voice.isNetworkConnectionRequired) "Online" else "Offline") },
|
supportingContent = { Text(if (voice.isNetworkConnectionRequired) stringResource(R.string.tts_online) else stringResource(R.string.tts_offline)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.primary)
|
||||||
|
|
@ -1378,11 +1382,11 @@ fun DeviceVoicesTab(
|
||||||
onClick = {
|
onClick = {
|
||||||
ttsEngine?.apply {
|
ttsEngine?.apply {
|
||||||
this.voice = voice
|
this.voice = voice
|
||||||
speak("This is a voice sample.", TextToSpeech.QUEUE_FLUSH, null, "sample_${voice.name}")
|
speak(context.getString(R.string.tts_voice_sample_generic), TextToSpeech.QUEUE_FLUSH, null, "sample_${voice.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.PlayArrow, contentDescription = "Play Sample", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.PlayArrow, contentDescription = stringResource(R.string.tts_play_sample), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -1415,7 +1419,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Cloud TTS Cache", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
|
Text(stringResource(R.string.tts_tab_cloud_cache), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
|
||||||
Surface(color = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(8.dp)) {
|
Surface(color = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(8.dp)) {
|
||||||
Text(formatBytes(totalSize), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
|
Text(formatBytes(totalSize), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -1431,7 +1435,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
value = selectedSpeakerFilter,
|
value = selectedSpeakerFilter,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("Voice Filter") },
|
label = { Text(stringResource(R.string.tts_voice_filter)) },
|
||||||
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterMenuExpanded) },
|
trailingIcon = { androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterMenuExpanded) },
|
||||||
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
colors = androidx.compose.material3.ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
modifier = Modifier.fillMaxWidth().menuAnchor()
|
modifier = Modifier.fillMaxWidth().menuAnchor()
|
||||||
|
|
@ -1459,7 +1463,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
|
|
||||||
if (chapters.isEmpty()) {
|
if (chapters.isEmpty()) {
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(150.dp), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxWidth().height(150.dp), contentAlignment = Alignment.Center) {
|
||||||
Text("No audio cached for this voice.", style = MaterialTheme.typography.bodyMedium)
|
Text(stringResource(R.string.tts_no_audio_cached_for_voice), style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 240.dp).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp))) {
|
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 240.dp).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp))) {
|
||||||
|
|
@ -1467,7 +1471,16 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
val chapter = chapters[index]
|
val chapter = chapters[index]
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text("${chapter.chapterTitle} (${chapter.chunkCount} chunks)", fontWeight = FontWeight.Medium)
|
Text(
|
||||||
|
"${chapter.chapterTitle} ${
|
||||||
|
context.resources.getQuantityString(
|
||||||
|
R.plurals.tts_cache_chunk_count_parenthetical,
|
||||||
|
chapter.chunkCount,
|
||||||
|
chapter.chunkCount
|
||||||
|
)
|
||||||
|
}",
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
},
|
},
|
||||||
supportingContent = { Text(formatBytes(chapter.sizeBytes)) },
|
supportingContent = { Text(formatBytes(chapter.sizeBytes)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
|
|
@ -1475,7 +1488,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
cacheManager.deleteSpecificFiles(chapter.matchingFiles, chapter.directory)
|
cacheManager.deleteSpecificFiles(chapter.matchingFiles, chapter.directory)
|
||||||
chapters = cacheManager.getChapterCaches(bookTitle, selectedSpeakerFilter)
|
chapters = cacheManager.getChapterCaches(bookTitle, selectedSpeakerFilter)
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete), tint = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -1495,7 +1508,7 @@ fun TtsCacheTab(bookTitle: String, context: Context, currentSpeakerId: String) {
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = null)
|
Icon(Icons.Default.Delete, contentDescription = null)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Clear Cache for $selectedSpeakerFilter")
|
Text(stringResource(R.string.tts_clear_cache_for_voice, selectedSpeakerFilter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1958,8 +1971,8 @@ fun ReaderThemePanel(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text("Preserve Image Colors", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.theme_preserve_image_colors), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
|
||||||
Text("Keep original image colors when theme changes", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
Text(stringResource(R.string.theme_preserve_image_colors_desc), style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||||
}
|
}
|
||||||
androidx.compose.material3.Switch(
|
androidx.compose.material3.Switch(
|
||||||
checked = excludeImages,
|
checked = excludeImages,
|
||||||
|
|
@ -1980,7 +1993,7 @@ fun ReaderThemePanel(
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.theme_my_themes), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.theme_my_themes), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
IconButton(onClick = { editingTheme = null; showBuilder = true }, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = { editingTheme = null; showBuilder = true }, modifier = Modifier.size(24.dp)) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Create Theme", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.theme_new), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
@ -2462,7 +2475,7 @@ fun HighlightColorPickerDialog(
|
||||||
.padding(horizontal = 24.dp, vertical = 8.dp)
|
.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Customize Highlights",
|
text = stringResource(R.string.highlight_customize_title),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
|
|
@ -2494,7 +2507,7 @@ fun HighlightColorPickerDialog(
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
contentDescription = "Selected",
|
contentDescription = stringResource(R.string.content_desc_selected),
|
||||||
tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White
|
tint = if (slotColor.luminance() > 0.5f) Color.Black else Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2539,7 +2552,7 @@ fun HighlightColorPickerDialog(
|
||||||
modifier = Modifier.weight(1.6f),
|
modifier = Modifier.weight(1.6f),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("HEX", color = Color.Gray, fontSize = 12.sp, maxLines = 1)
|
Text(stringResource(R.string.theme_color_hex), color = Color.Gray, fontSize = 12.sp, maxLines = 1)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
|
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
|
||||||
}
|
}
|
||||||
|
|
@ -2548,15 +2561,15 @@ fun HighlightColorPickerDialog(
|
||||||
modifier = Modifier.weight(2.4f),
|
modifier = Modifier.weight(2.4f),
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
RgbInputColumn(label = "R", value = currentColor.red,
|
RgbInputColumn(label = stringResource(R.string.color_r), value = currentColor.red,
|
||||||
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
|
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
RgbInputColumn(label = "G", value = currentColor.green,
|
RgbInputColumn(label = stringResource(R.string.color_g), value = currentColor.green,
|
||||||
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
|
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
RgbInputColumn(label = "B", value = currentColor.blue,
|
RgbInputColumn(label = stringResource(R.string.color_b), value = currentColor.blue,
|
||||||
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
|
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
@ -2571,11 +2584,11 @@ fun HighlightColorPickerDialog(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { updateFromColor(selectedSlot.color) }) {
|
TextButton(onClick = { updateFromColor(selectedSlot.color) }) {
|
||||||
Text("Reset", color = Color(0xFFFF5252))
|
Text(stringResource(R.string.action_reset), color = Color(0xFFFF5252))
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
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(
|
||||||
|
|
@ -2584,7 +2597,7 @@ fun HighlightColorPickerDialog(
|
||||||
containerColor = Color.White
|
containerColor = Color.White
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Save", color = Color.Black, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.action_save), color = Color.Black, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2647,7 +2660,7 @@ fun AiHubBottomSheet(
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.weight(1f))
|
Box(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
text = "AI Features",
|
text = stringResource(R.string.ai_features_title),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|
@ -2671,9 +2684,12 @@ fun AiHubBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs = mutableListOf("Summary")
|
val summaryTab = stringResource(R.string.ai_tab_summary)
|
||||||
if (onGenerateRecap != null) tabs.add("Recap")
|
val recapTab = stringResource(R.string.ai_tab_recap)
|
||||||
if (summaryCacheManager != null) tabs.add("Cache")
|
val cacheTab = stringResource(R.string.ai_tab_cache)
|
||||||
|
val tabs = mutableListOf(summaryTab)
|
||||||
|
if (onGenerateRecap != null) tabs.add(recapTab)
|
||||||
|
if (summaryCacheManager != null) tabs.add(cacheTab)
|
||||||
|
|
||||||
TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.padding(bottom = 16.dp)) {
|
TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.padding(bottom = 16.dp)) {
|
||||||
tabs.forEachIndexed { index, title ->
|
tabs.forEachIndexed { index, title ->
|
||||||
|
|
@ -2683,11 +2699,11 @@ fun AiHubBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeTab = tabs.getOrNull(selectedTabIndex) ?: "Summary"
|
val activeTab = tabs.getOrNull(selectedTabIndex) ?: summaryTab
|
||||||
var cacheRefreshTrigger by remember { mutableIntStateOf(0) }
|
var cacheRefreshTrigger by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
when (activeTab) {
|
when (activeTab) {
|
||||||
"Summary" -> {
|
summaryTab -> {
|
||||||
val cachedSummary = remember(currentChapterIndex, cacheRefreshTrigger) { summaryCacheManager?.getSummary(bookTitle, currentChapterIndex) }
|
val cachedSummary = remember(currentChapterIndex, cacheRefreshTrigger) { summaryCacheManager?.getSummary(bookTitle, currentChapterIndex) }
|
||||||
val effectiveResult = summarizationResult ?: if (cachedSummary != null) SummarizationResult(summary = cachedSummary, isCacheHit = true) else null
|
val effectiveResult = summarizationResult ?: if (cachedSummary != null) SummarizationResult(summary = cachedSummary, isCacheHit = true) else null
|
||||||
|
|
||||||
|
|
@ -2696,7 +2712,7 @@ fun AiHubBottomSheet(
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Icon(painterResource(R.drawable.summarize), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
|
Icon(painterResource(R.drawable.summarize), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text("No summary for ${chapterTitle.lowercase()} yet.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.ai_no_summary_for_chapter, chapterTitle.lowercase()), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { onGenerateSummary(false) },
|
onClick = { onGenerateSummary(false) },
|
||||||
|
|
@ -2704,7 +2720,7 @@ fun AiHubBottomSheet(
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Generate Summary for $chapterTitle")
|
Text(stringResource(R.string.ai_generate_summary_for_chapter, chapterTitle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2721,14 +2737,14 @@ fun AiHubBottomSheet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Recap" -> {
|
recapTab -> {
|
||||||
// Recap Tab
|
// Recap Tab
|
||||||
if (recapResult == null && !isRecapLoading) {
|
if (recapResult == null && !isRecapLoading) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
|
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text("Get a recap of the story up to your current position.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
Text(stringResource(R.string.ai_recap_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { onGenerateRecap?.invoke() },
|
onClick = { onGenerateRecap?.invoke() },
|
||||||
|
|
@ -2736,13 +2752,13 @@ fun AiHubBottomSheet(
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(painterResource(R.drawable.ai), contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Generate Story Recap")
|
Text(stringResource(R.string.ai_generate_story_recap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AiResultContentView(
|
AiResultContentView(
|
||||||
title = "Story Recap",
|
title = stringResource(R.string.ai_story_recap),
|
||||||
result = recapResult,
|
result = recapResult,
|
||||||
isLoading = isRecapLoading,
|
isLoading = isRecapLoading,
|
||||||
isMainTtsActive = isMainTtsActive,
|
isMainTtsActive = isMainTtsActive,
|
||||||
|
|
@ -2754,7 +2770,7 @@ fun AiHubBottomSheet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Cache" -> {
|
cacheTab -> {
|
||||||
if (summaryCacheManager != null) {
|
if (summaryCacheManager != null) {
|
||||||
ManageCacheTab(bookTitle, summaryCacheManager, onCacheChanged = {
|
ManageCacheTab(bookTitle, summaryCacheManager, onCacheChanged = {
|
||||||
cacheRefreshTrigger++
|
cacheRefreshTrigger++
|
||||||
|
|
@ -2804,15 +2820,15 @@ fun AiResultContentView(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (result.isCacheHit) {
|
text = if (result.isCacheHit) {
|
||||||
"⚡ Cache Hit • Free"
|
stringResource(R.string.ai_cache_hit_free)
|
||||||
} else if (result.cost != null) {
|
} else if (result.cost != null) {
|
||||||
if (result.cost == 0.0 && result.freeRemaining != null) {
|
if (result.cost == 0.0 && result.freeRemaining != null) {
|
||||||
"✨ Generated • Free (${result.freeRemaining}/10 left)"
|
stringResource(R.string.ai_generated_free_remaining, result.freeRemaining ?: 0)
|
||||||
} else {
|
} else {
|
||||||
"✨ Generated • Cost: ${result.cost} credits"
|
stringResource(R.string.ai_generated_cost, result.cost.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"✨ Generating... • Cost: Calculating"
|
stringResource(R.string.ai_generating_cost_calculating)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = if (result.isCacheHit || (result.cost == 0.0 && result.freeRemaining != null)) Color(
|
color = if (result.isCacheHit || (result.cost == 0.0 && result.freeRemaining != null)) Color(
|
||||||
|
|
@ -2828,7 +2844,7 @@ fun AiResultContentView(
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
Text("Thinking...", modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge)
|
Text(stringResource(R.string.ai_thinking), modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (result != null) {
|
} else if (result != null) {
|
||||||
|
|
@ -2856,7 +2872,7 @@ fun AiResultContentView(
|
||||||
IconButton(onClick = onClear) {
|
IconButton(onClick = onClear) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Delete,
|
imageVector = Icons.Default.Delete,
|
||||||
contentDescription = "Clear",
|
contentDescription = stringResource(R.string.action_clear),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2865,7 +2881,7 @@ fun AiResultContentView(
|
||||||
|
|
||||||
if (onRegenerate != null) {
|
if (onRegenerate != null) {
|
||||||
TextButton(onClick = onRegenerate) {
|
TextButton(onClick = onRegenerate) {
|
||||||
Text("Regenerate")
|
Text(stringResource(R.string.ai_regenerate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2881,7 +2897,7 @@ fun AiResultContentView(
|
||||||
ttsController.start(
|
ttsController.start(
|
||||||
chunks = chunks,
|
chunks = chunks,
|
||||||
bookTitle = title,
|
bookTitle = title,
|
||||||
chapterTitle = "AI Output",
|
chapterTitle = context.getString(R.string.ai_output_title),
|
||||||
coverImageUri = null,
|
coverImageUri = null,
|
||||||
ttsMode = loadTtsMode(context),
|
ttsMode = loadTtsMode(context),
|
||||||
playbackSource = "POPUP",
|
playbackSource = "POPUP",
|
||||||
|
|
@ -2895,7 +2911,7 @@ fun AiResultContentView(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
|
imageVector = if (isTtsSessionActive) Icons.Default.Stop else Icons.Default.PlayArrow,
|
||||||
contentDescription = "Read Aloud"
|
contentDescription = stringResource(R.string.action_read_aloud)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
|
@ -2903,7 +2919,7 @@ fun AiResultContentView(
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ContentCopy,
|
imageVector = Icons.Default.ContentCopy,
|
||||||
contentDescription = "Copy"
|
contentDescription = stringResource(R.string.action_copy)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2964,7 +2980,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
|
||||||
|
|
||||||
if (cachedItems.isEmpty()) {
|
if (cachedItems.isEmpty()) {
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||||
Text("No cached summaries for this book.", style = MaterialTheme.typography.bodyMedium)
|
Text(stringResource(R.string.ai_no_cached_summaries), style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
@ -2990,7 +3006,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Delete,
|
imageVector = Icons.Default.Delete,
|
||||||
contentDescription = "Delete",
|
contentDescription = stringResource(R.string.action_delete),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -3016,7 +3032,7 @@ fun ManageCacheTab(bookTitle: String, summaryCacheManager: SummaryCacheManager,
|
||||||
},
|
},
|
||||||
modifier = Modifier.align(Alignment.End)
|
modifier = Modifier.align(Alignment.End)
|
||||||
) {
|
) {
|
||||||
Text("Clear All", color = MaterialTheme.colorScheme.error)
|
Text(stringResource(R.string.clear_all), color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,3 @@
|
||||||
/*
|
|
||||||
* Episteme Reader - A native Android document reader.
|
|
||||||
* Copyright (C) 2026 Episteme
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
* mail: epistemereader@gmail.com
|
|
||||||
*/
|
|
||||||
// FontsScreen.kt
|
// FontsScreen.kt
|
||||||
@file:Suppress("KotlinConstantConditions")
|
@file:Suppress("KotlinConstantConditions")
|
||||||
|
|
||||||
|
|
@ -93,6 +74,8 @@ fun FontsScreen(
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val showGoogleFontsOption = !(BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE)
|
||||||
|
|
||||||
// Dialog state
|
// Dialog state
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) }
|
var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) }
|
||||||
|
|
@ -116,7 +99,7 @@ fun FontsScreen(
|
||||||
title = { Text(stringResource(R.string.custom_fonts)) },
|
title = { Text(stringResource(R.string.custom_fonts)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackClick) {
|
IconButton(onClick = onBackClick) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -127,13 +110,15 @@ fun FontsScreen(
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
if (showGoogleFontsOption) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = { showGoogleFontsSheet = true },
|
onClick = { showGoogleFontsSheet = true },
|
||||||
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
|
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
|
||||||
text = { Text("Google Fonts") },
|
text = { Text(stringResource(R.string.google_fonts)) },
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = { pickFontLauncher.launch(fontMimeTypes) },
|
onClick = { pickFontLauncher.launch(fontMimeTypes) },
|
||||||
|
|
@ -146,13 +131,16 @@ fun FontsScreen(
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||||
if (fonts.isEmpty()) {
|
if (fonts.isEmpty()) {
|
||||||
|
val secondaryText = if (showGoogleFontsOption) stringResource(R.string.action_browse_google_fonts) else null
|
||||||
|
val secondaryClick: (() -> Unit)? = if (showGoogleFontsOption) { { showGoogleFontsSheet = true } } else null
|
||||||
|
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = stringResource(R.string.no_custom_fonts),
|
title = stringResource(R.string.no_custom_fonts),
|
||||||
message = stringResource(R.string.import_fonts_desc),
|
message = stringResource(R.string.import_fonts_desc),
|
||||||
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
|
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
secondaryButtonText = "Browse Google Fonts",
|
secondaryButtonText = secondaryText,
|
||||||
onSecondaryClick = { showGoogleFontsSheet = true }
|
onSecondaryClick = secondaryClick
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -254,7 +242,7 @@ fun GoogleFontsBottomSheet(
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Browse Google Fonts",
|
text = stringResource(R.string.action_browse_google_fonts),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 12.dp)
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
|
@ -264,7 +252,7 @@ fun GoogleFontsBottomSheet(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = { searchQuery = it },
|
onValueChange = { searchQuery = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = { Text("Search 1900+ fonts...") },
|
placeholder = { Text(stringResource(R.string.google_fonts_search_placeholder)) },
|
||||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
|
@ -280,7 +268,7 @@ fun GoogleFontsBottomSheet(
|
||||||
if (searchQuery.isBlank()) {
|
if (searchQuery.isBlank()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Popular Choices",
|
text = stringResource(R.string.google_fonts_popular_choices),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
|
@ -289,7 +277,7 @@ fun GoogleFontsBottomSheet(
|
||||||
} else if (displayList.isEmpty()) {
|
} else if (displayList.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "No fonts found matching '$searchQuery'",
|
text = stringResource(R.string.google_fonts_no_matches, searchQuery),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
|
|
@ -335,7 +323,7 @@ fun GoogleFontsBottomSheet(
|
||||||
isDownloaded -> {
|
isDownloaded -> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = "Already Downloaded",
|
contentDescription = stringResource(R.string.content_desc_already_downloaded),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -350,7 +338,7 @@ fun GoogleFontsBottomSheet(
|
||||||
else -> {
|
else -> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CloudDownload,
|
Icons.Default.CloudDownload,
|
||||||
contentDescription = "Download",
|
contentDescription = stringResource(R.string.action_download),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -396,7 +384,7 @@ fun FontListItem(
|
||||||
IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
contentDescription = "Delete",
|
contentDescription = stringResource(R.string.action_delete),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -853,7 +853,7 @@ fun RecentFileCard(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = "Selected",
|
contentDescription = stringResource(R.string.content_desc_selected),
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
|
|
@ -1009,13 +1009,13 @@ fun DefaultTopAppBar(
|
||||||
Badge()
|
Badge()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Default.Menu, contentDescription = "Open Drawer")
|
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_open_drawer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, actions = {
|
}, actions = {
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = onAppThemeClick) {
|
IconButton(onClick = onAppThemeClick) {
|
||||||
Icon(painterResource(id = R.drawable.palette), contentDescription = "App Theme")
|
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.content_desc_app_theme))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Recent Files Limit Menu
|
// Recent Files Limit Menu
|
||||||
|
|
@ -1035,7 +1035,7 @@ fun DefaultTopAppBar(
|
||||||
showLimitMenu = false
|
showLimitMenu = false
|
||||||
},
|
},
|
||||||
trailingIcon = if (uiState.recentFilesLimit == limit) {
|
trailingIcon = if (uiState.recentFilesLimit == limit) {
|
||||||
{ Icon(Icons.Default.Check, contentDescription = "Selected") }
|
{ Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected)) }
|
||||||
} else null
|
} else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1045,7 +1045,7 @@ fun DefaultTopAppBar(
|
||||||
// Options Menu (MoreVert)
|
// Options Menu (MoreVert)
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { showOptionsMenu = true }) {
|
IconButton(onClick = { showOptionsMenu = true }) {
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
|
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_more_options))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) {
|
expanded = showOptionsMenu, onDismissRequest = { showOptionsMenu = false }) {
|
||||||
|
|
@ -1056,12 +1056,12 @@ fun DefaultTopAppBar(
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
DropdownMenuItem(text = { Text("Enable Multi-Tab Reading") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_enable_multi_tab_reading)) }, onClick = {
|
||||||
onTabsToggle(!uiState.isTabsEnabled)
|
onTabsToggle(!uiState.isTabsEnabled)
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
}, trailingIcon = {
|
}, trailingIcon = {
|
||||||
if (uiState.isTabsEnabled) {
|
if (uiState.isTabsEnabled) {
|
||||||
Icon(Icons.Default.Check, contentDescription = "Enabled")
|
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1070,18 +1070,18 @@ fun DefaultTopAppBar(
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
})
|
})
|
||||||
|
|
||||||
DropdownMenuItem(text = { Text("Use Strict File Filter") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_use_strict_file_filter)) }, onClick = {
|
||||||
onStrictFilterToggleClick()
|
onStrictFilterToggleClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
}, trailingIcon = {
|
}, trailingIcon = {
|
||||||
if (uiState.useStrictFileFilter) {
|
if (uiState.useStrictFileFilter) {
|
||||||
Icon(Icons.Default.Check, contentDescription = "Enabled")
|
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
DropdownMenuItem(text = { Text("Language") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_language)) }, onClick = {
|
||||||
onLanguageClick()
|
onLanguageClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
})
|
})
|
||||||
|
|
@ -1098,17 +1098,17 @@ fun DefaultTopAppBar(
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(text = { Text("Test Panel ML Detection") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_test_panel_ml_detection)) }, onClick = {
|
||||||
onTestPanelDetectionClick()
|
onTestPanelDetectionClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
})
|
})
|
||||||
|
|
||||||
DropdownMenuItem(text = { Text("Test Speech Bubble ML Detection") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_test_speech_bubble_ml_detection)) }, onClick = {
|
||||||
onTestSpeechBubbleDetectionClick()
|
onTestSpeechBubbleDetectionClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
})
|
})
|
||||||
|
|
||||||
DropdownMenuItem(text = { Text("Export Logs (Last 5000 lines)") }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_export_logs_last_lines, 5000)) }, onClick = {
|
||||||
onExportLogsClick()
|
onExportLogsClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
})
|
})
|
||||||
|
|
@ -1163,7 +1163,7 @@ private fun AppDrawerContent(
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current).data(photoUrl)
|
model = ImageRequest.Builder(LocalContext.current).data(photoUrl)
|
||||||
.crossfade(true).build(),
|
.crossfade(true).build(),
|
||||||
contentDescription = "Profile picture",
|
contentDescription = stringResource(R.string.content_desc_profile_picture),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(80.dp)
|
.size(80.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape),
|
||||||
|
|
@ -1172,7 +1172,7 @@ private fun AppDrawerContent(
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.AccountCircle,
|
imageVector = Icons.Outlined.AccountCircle,
|
||||||
contentDescription = "Profile",
|
contentDescription = stringResource(R.string.content_desc_profile),
|
||||||
modifier = Modifier.size(80.dp)
|
modifier = Modifier.size(80.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1189,9 +1189,9 @@ private fun AppDrawerContent(
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Default.FormatListNumbered, contentDescription = "Credits", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer)
|
Icon(Icons.Default.FormatListNumbered, contentDescription = stringResource(R.string.credits_tab), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("${uiState.credits} Credits", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onTertiaryContainer)
|
Text(stringResource(R.string.credits_count, uiState.credits), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onTertiaryContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1239,7 +1239,7 @@ private fun AppDrawerContent(
|
||||||
if (!uiState.isProUser) {
|
if (!uiState.isProUser) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.VerifiedUser,
|
imageVector = Icons.Default.VerifiedUser,
|
||||||
contentDescription = "Pro Feature",
|
contentDescription = stringResource(R.string.content_desc_pro_feature),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
@ -1294,7 +1294,7 @@ private fun AppDrawerContent(
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = R.mipmap.ic_launcher,
|
model = R.mipmap.ic_launcher,
|
||||||
contentDescription = "App Icon",
|
contentDescription = stringResource(R.string.content_desc_app_icon),
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier.size(64.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
@ -1467,7 +1467,7 @@ fun DeviceManagementScreen(
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.PhoneAndroid, contentDescription = "Device")
|
Icon(Icons.Default.PhoneAndroid, contentDescription = stringResource(R.string.content_desc_device))
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(device.deviceName, fontWeight = FontWeight.SemiBold)
|
Text(device.deviceName, fontWeight = FontWeight.SemiBold)
|
||||||
|
|
@ -1679,9 +1679,9 @@ fun CloseAllTabsDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||||
fun StrictFilterConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
fun StrictFilterConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Enable Strict File Filter") },
|
title = { Text(stringResource(R.string.dialog_strict_file_filter_title)) },
|
||||||
text = { Text("If you enable this, some supported file types like AZW3, CB7, and FB2 might not show up depending on your file manager.\n\nAre you sure you want to enable this filter?") },
|
text = { Text(stringResource(R.string.dialog_strict_file_filter_desc)) },
|
||||||
confirmButton = { TextButton(onClick = onConfirm) { Text("Enable") } },
|
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_enable)) } },
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }
|
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1714,13 +1714,13 @@ fun AppThemeBottomSheet(
|
||||||
.padding(bottom = 24.dp)
|
.padding(bottom = 24.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "App Theme",
|
text = stringResource(R.string.app_theme_title),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text("Appearance", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.app_theme_appearance), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
|
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
|
||||||
AppThemeMode.entries.forEach { mode ->
|
AppThemeMode.entries.forEach { mode ->
|
||||||
|
|
@ -1731,14 +1731,14 @@ fun AppThemeBottomSheet(
|
||||||
.clickable { onThemeModeChanged(mode) },
|
.clickable { onThemeModeChanged(mode) },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(mode.displayName, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
|
Text(stringResource(mode.labelRes), color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text("Contrast", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.app_theme_contrast), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
|
Row(modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colorScheme.surfaceContainerHigh, androidx.compose.foundation.shape.RoundedCornerShape(24.dp)).padding(4.dp)) {
|
||||||
AppContrastOption.entries.forEach { option ->
|
AppContrastOption.entries.forEach { option ->
|
||||||
|
|
@ -1749,14 +1749,14 @@ fun AppThemeBottomSheet(
|
||||||
.clickable { onContrastOptionChanged(option) },
|
.clickable { onContrastOptionChanged(option) },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(option.displayName, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
|
Text(stringResource(option.labelRes), color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text("Text Brightness", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.app_theme_text_brightness), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -1778,32 +1778,33 @@ fun AppThemeBottomSheet(
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text("Color Scheme", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.app_theme_color_scheme), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
val presets = listOf(
|
||||||
|
R.string.app_theme_preset_ocean to Color(0xFF00668B),
|
||||||
|
R.string.app_theme_preset_mint to Color(0xFF006C4C),
|
||||||
|
R.string.app_theme_preset_rose to Color(0xFF9C4146),
|
||||||
|
R.string.app_theme_preset_sepia to Color(0xFF705D49),
|
||||||
|
R.string.app_theme_preset_amethyst to Color(0xFF9B59B6),
|
||||||
|
R.string.app_theme_preset_amber to Color(0xFFFFC107),
|
||||||
|
R.string.app_theme_preset_sapphire to Color(0xFF0F52BA)
|
||||||
|
)
|
||||||
|
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
item {
|
item {
|
||||||
ThemeSwatch(
|
ThemeSwatch(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
isSelected = uiState.appSeedColor == null,
|
isSelected = uiState.appSeedColor == null,
|
||||||
label = "Dynamic",
|
label = stringResource(R.string.app_theme_dynamic),
|
||||||
onClick = { onSeedColorChanged(null) }
|
onClick = { onSeedColorChanged(null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val presets = listOf(
|
|
||||||
"Ocean" to Color(0xFF00668B),
|
|
||||||
"Mint" to Color(0xFF006C4C),
|
|
||||||
"Rose" to Color(0xFF9C4146),
|
|
||||||
"Sepia" to Color(0xFF705D49),
|
|
||||||
"Amethyst" to Color(0xFF9B59B6),
|
|
||||||
"Amber" to Color(0xFFFFC107),
|
|
||||||
"Sapphire" to Color(0xFF0F52BA)
|
|
||||||
)
|
|
||||||
items(presets.size) { i ->
|
items(presets.size) { i ->
|
||||||
val (label, color) = presets[i]
|
val (labelRes, color) = presets[i]
|
||||||
ThemeSwatch(
|
ThemeSwatch(
|
||||||
color = color,
|
color = color,
|
||||||
isSelected = uiState.appSeedColor == color,
|
isSelected = uiState.appSeedColor == color,
|
||||||
label = label,
|
label = stringResource(labelRes),
|
||||||
onClick = { onSeedColorChanged(color) }
|
onClick = { onSeedColorChanged(color) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1812,15 +1813,15 @@ fun AppThemeBottomSheet(
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("My Themes", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.theme_my_themes), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
IconButton(onClick = { showCreateDialog = true }, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = { showCreateDialog = true }, modifier = Modifier.size(24.dp)) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Add Custom Theme", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_add_custom_theme), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
if (uiState.customAppThemes.isEmpty()) {
|
if (uiState.customAppThemes.isEmpty()) {
|
||||||
Text("No custom themes yet.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.theme_no_custom), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
} else {
|
} else {
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
items(uiState.customAppThemes) { theme ->
|
items(uiState.customAppThemes) { theme ->
|
||||||
|
|
@ -1873,7 +1874,7 @@ fun ThemeSwatch(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(text = label, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 64.dp))
|
Text(text = label, style = MaterialTheme.typography.labelSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 64.dp))
|
||||||
if (onDelete != null) {
|
if (onDelete != null) {
|
||||||
Icon(Icons.Default.Close, contentDescription = "Delete", modifier = Modifier.size(16.dp).clickable { onDelete() }, tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_delete), modifier = Modifier.size(16.dp).clickable { onDelete() }, tint = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1887,6 +1888,7 @@ fun CreateAppThemeDialog(
|
||||||
onSave: (String, Color) -> Unit
|
onSave: (String, Color) -> Unit
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val initialHsv = remember(initialColor) {
|
val initialHsv = remember(initialColor) {
|
||||||
val hsv = FloatArray(3)
|
val hsv = FloatArray(3)
|
||||||
|
|
@ -1931,7 +1933,7 @@ fun CreateAppThemeDialog(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Create App Theme",
|
text = stringResource(R.string.app_theme_create_title),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
|
|
@ -1942,7 +1944,7 @@ fun CreateAppThemeDialog(
|
||||||
androidx.compose.material3.OutlinedTextField(
|
androidx.compose.material3.OutlinedTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = { Text("Theme Name") },
|
label = { Text(stringResource(R.string.theme_name)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
|
colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
|
||||||
|
|
@ -1990,7 +1992,7 @@ fun CreateAppThemeDialog(
|
||||||
modifier = Modifier.weight(1.6f),
|
modifier = Modifier.weight(1.6f),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("HEX", color = Color.Gray, fontSize = 12.sp, maxLines = 1)
|
Text(stringResource(R.string.theme_color_hex), color = Color.Gray, fontSize = 12.sp, maxLines = 1)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
|
HexInput(color = currentColor, onHexChanged = { updateFromColor(it) })
|
||||||
}
|
}
|
||||||
|
|
@ -1999,15 +2001,15 @@ fun CreateAppThemeDialog(
|
||||||
modifier = Modifier.weight(2.4f),
|
modifier = Modifier.weight(2.4f),
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
RgbInputColumn(label = "R", value = currentColor.red,
|
RgbInputColumn(label = stringResource(R.string.color_r), value = currentColor.red,
|
||||||
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
|
onValueChange = { r -> updateFromColor(currentColor.copy(red = r)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
RgbInputColumn(label = "G", value = currentColor.green,
|
RgbInputColumn(label = stringResource(R.string.color_g), value = currentColor.green,
|
||||||
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
|
onValueChange = { g -> updateFromColor(currentColor.copy(green = g)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
RgbInputColumn(label = "B", value = currentColor.blue,
|
RgbInputColumn(label = stringResource(R.string.color_b), value = currentColor.blue,
|
||||||
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
|
onValueChange = { b -> updateFromColor(currentColor.copy(blue = b)) },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
@ -2022,14 +2024,14 @@ fun CreateAppThemeDialog(
|
||||||
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))
|
||||||
androidx.compose.material3.Button(
|
androidx.compose.material3.Button(
|
||||||
onClick = { onSave(name.ifBlank { "Custom Theme" }, currentColor) },
|
onClick = { onSave(name.ifBlank { context.getString(R.string.app_theme_custom_default_name) }, currentColor) },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = currentColor)
|
colors = ButtonDefaults.buttonColors(containerColor = currentColor)
|
||||||
) {
|
) {
|
||||||
Text("Save", color = if (currentColor.luminance() > 0.5f) Color.Black else Color.White, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.action_save), color = if (currentColor.luminance() > 0.5f) Color.Black else Color.White, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2043,18 +2045,20 @@ fun LanguageSelectionDialog(onDismiss: () -> Unit) {
|
||||||
val currentTag = if (!currentLocales.isEmpty) currentLocales.get(0)?.language ?: "en" else "en"
|
val currentTag = if (!currentLocales.isEmpty) currentLocales.get(0)?.language ?: "en" else "en"
|
||||||
|
|
||||||
val languages = listOf(
|
val languages = listOf(
|
||||||
"en" to "English (Default)",
|
"en" to R.string.language_english_default,
|
||||||
"ar" to "العربية (Arabic)",
|
"ar" to R.string.language_arabic,
|
||||||
"de" to "Deutsch (German)",
|
"de" to R.string.language_german,
|
||||||
"tr" to "Türkçe (Turkish)"
|
"tr" to R.string.language_turkish,
|
||||||
|
"fr" to R.string.language_french,
|
||||||
|
"ru" to R.string.language_russian
|
||||||
)
|
)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Language") },
|
title = { Text(stringResource(R.string.options_language)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
languages.forEach { (tag, name) ->
|
languages.forEach { (tag, nameRes) ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -2069,7 +2073,7 @@ fun LanguageSelectionDialog(onDismiss: () -> Unit) {
|
||||||
) {
|
) {
|
||||||
RadioButton(selected = currentTag == tag, onClick = null)
|
RadioButton(selected = currentTag == tag, onClick = null)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Text(name)
|
Text(stringResource(nameRes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -630,7 +630,7 @@ fun LibraryScreenContent(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { onSearchActiveChange(false) }) {
|
IconButton(onClick = { onSearchActiveChange(false) }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_close_search))
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = textFieldValue,
|
value = textFieldValue,
|
||||||
|
|
@ -654,7 +654,7 @@ fun LibraryScreenContent(
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (searchQuery.isNotEmpty()) {
|
if (searchQuery.isNotEmpty()) {
|
||||||
IconButton(onClick = { onSearchQueryChange("") }) {
|
IconButton(onClick = { onSearchQueryChange("") }) {
|
||||||
Icon(Icons.Default.Close, contentDescription = "Clear query")
|
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_clear_query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -666,17 +666,17 @@ fun LibraryScreenContent(
|
||||||
actions = {
|
actions = {
|
||||||
if (pagerState.currentPage == 0) {
|
if (pagerState.currentPage == 0) {
|
||||||
IconButton(onClick = onFilterClick) {
|
IconButton(onClick = onFilterClick) {
|
||||||
Icon(Icons.Default.FilterList, contentDescription = "Filter")
|
Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.content_desc_filter))
|
||||||
}
|
}
|
||||||
Box {
|
Box {
|
||||||
TextButton(onClick = { showSortMenu = true }) {
|
TextButton(onClick = { showSortMenu = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.sort),
|
painter = painterResource(id = R.drawable.sort),
|
||||||
contentDescription = "Sort",
|
contentDescription = stringResource(R.string.content_desc_sort),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(sortOrder.displayName)
|
Text(stringResource(sortOrder.labelRes))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showSortMenu,
|
expanded = showSortMenu,
|
||||||
|
|
@ -684,7 +684,7 @@ fun LibraryScreenContent(
|
||||||
) {
|
) {
|
||||||
SortOrder.entries.forEach { order ->
|
SortOrder.entries.forEach { order ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(order.displayName) },
|
text = { Text(stringResource(order.labelRes)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onSortOrderChange(order)
|
onSortOrderChange(order)
|
||||||
showSortMenu = false
|
showSortMenu = false
|
||||||
|
|
@ -693,7 +693,7 @@ fun LibraryScreenContent(
|
||||||
if (order == sortOrder) {
|
if (order == sortOrder) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = "Selected"
|
contentDescription = stringResource(R.string.content_desc_selected)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -702,7 +702,7 @@ fun LibraryScreenContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = { onSearchActiveChange(true) }) {
|
IconButton(onClick = { onSearchActiveChange(true) }) {
|
||||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -733,34 +733,34 @@ fun LibraryScreenContent(
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
|
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
|
||||||
label = { Text(stringResource(R.string.filter_types, libraryFilters.fileTypes.joinToString { it.name })) },
|
label = { Text(stringResource(R.string.filter_types, libraryFilters.fileTypes.joinToString { it.name })) },
|
||||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (libraryFilters.sourceFolders.isNotEmpty()) {
|
if (libraryFilters.sourceFolders.isNotEmpty()) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) },
|
onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) },
|
||||||
label = { Text(stringResource(R.string.filter_folders, libraryFilters.sourceFolders.size)) },
|
label = { Text(stringResource(R.string.filter_folders, libraryFilters.sourceFolders.size)) },
|
||||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (libraryFilters.readStatus != ReadStatusFilter.ALL) {
|
if (libraryFilters.readStatus != ReadStatusFilter.ALL) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) },
|
onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) },
|
||||||
label = { Text(stringResource(R.string.filter_status, libraryFilters.readStatus.displayName)) },
|
label = { Text(stringResource(R.string.filter_status, stringResource(libraryFilters.readStatus.labelRes))) },
|
||||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (libraryFilters.tagIds.isNotEmpty()) {
|
if (libraryFilters.tagIds.isNotEmpty()) {
|
||||||
val selectedTags = allTags.filter { it.id in libraryFilters.tagIds }
|
val selectedTags = allTags.filter { it.id in libraryFilters.tagIds }
|
||||||
val tagLabel = when {
|
val tagLabel = when {
|
||||||
selectedTags.isEmpty() -> "${libraryFilters.tagIds.size} tags"
|
selectedTags.isEmpty() -> pluralStringResource(R.plurals.tag_count, libraryFilters.tagIds.size, libraryFilters.tagIds.size)
|
||||||
selectedTags.size <= 2 -> selectedTags.joinToString { it.name }
|
selectedTags.size <= 2 -> selectedTags.joinToString { it.name }
|
||||||
else -> "${selectedTags.size} tags"
|
else -> pluralStringResource(R.plurals.tag_count, selectedTags.size, selectedTags.size)
|
||||||
}
|
}
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onRemoveFilter(libraryFilters.copy(tagIds = emptySet())) },
|
onClick = { onRemoveFilter(libraryFilters.copy(tagIds = emptySet())) },
|
||||||
label = { Text("Tags: $tagLabel") },
|
label = { Text(stringResource(R.string.filter_tags, tagLabel)) },
|
||||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
trailingIcon = { Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_clear), modifier = Modifier.size(16.dp)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -775,7 +775,7 @@ fun LibraryScreenContent(
|
||||||
if (recentFiles.isNotEmpty()) {
|
if (recentFiles.isNotEmpty()) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.fab_add_file)) },
|
text = { Text(stringResource(R.string.fab_add_file)) },
|
||||||
icon = { Icon(Icons.Default.Add, contentDescription = "Add file") },
|
icon = { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.fab_add_file)) },
|
||||||
onClick = onSelectFileClick,
|
onClick = onSelectFileClick,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -784,7 +784,7 @@ fun LibraryScreenContent(
|
||||||
1 -> {
|
1 -> {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.fab_new_shelf)) },
|
text = { Text(stringResource(R.string.fab_new_shelf)) },
|
||||||
icon = { Icon(Icons.Default.Add, contentDescription = "New shelf") },
|
icon = { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.fab_new_shelf)) },
|
||||||
onClick = onNewShelfClick,
|
onClick = onNewShelfClick,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -896,7 +896,7 @@ private fun ShelvesScreen(
|
||||||
item {
|
item {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Browse by tag",
|
text = stringResource(R.string.section_browse_by_tag),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
|
|
@ -1085,7 +1085,7 @@ private fun ShelfDetailScreen(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { closeShelfSearch() }) {
|
IconButton(onClick = { closeShelfSearch() }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_close_search))
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = searchFieldValue,
|
value = searchFieldValue,
|
||||||
|
|
@ -1109,7 +1109,7 @@ private fun ShelfDetailScreen(
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (searchQuery.isNotEmpty()) {
|
if (searchQuery.isNotEmpty()) {
|
||||||
IconButton(onClick = { clearShelfSearchQuery() }) {
|
IconButton(onClick = { clearShelfSearchQuery() }) {
|
||||||
Icon(Icons.Default.Close, contentDescription = "Clear query")
|
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.content_desc_clear_query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1128,9 +1128,9 @@ private fun ShelfDetailScreen(
|
||||||
Text(
|
Text(
|
||||||
text = when {
|
text = when {
|
||||||
isFolderShelf && shelf.childShelfCount > 0 && shelf.directBookCount > 0 ->
|
isFolderShelf && shelf.childShelfCount > 0 && shelf.directBookCount > 0 ->
|
||||||
"${shelf.childShelfCount} folders • ${getBookCountString(shelf.directBookCount)}"
|
"${pluralStringResource(R.plurals.folder_count, shelf.childShelfCount, shelf.childShelfCount)} • ${getBookCountString(shelf.directBookCount)}"
|
||||||
isFolderShelf && shelf.childShelfCount > 0 ->
|
isFolderShelf && shelf.childShelfCount > 0 ->
|
||||||
"${shelf.childShelfCount} folders"
|
pluralStringResource(R.plurals.folder_count, shelf.childShelfCount, shelf.childShelfCount)
|
||||||
isFolderShelf -> getBookCountString(shelf.directBookCount)
|
isFolderShelf -> getBookCountString(shelf.directBookCount)
|
||||||
else -> getBookCountString(shelf.bookCount)
|
else -> getBookCountString(shelf.bookCount)
|
||||||
},
|
},
|
||||||
|
|
@ -1141,7 +1141,7 @@ private fun ShelfDetailScreen(
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
|
@ -1149,11 +1149,11 @@ private fun ShelfDetailScreen(
|
||||||
TextButton(onClick = { showSortMenu = true }) {
|
TextButton(onClick = { showSortMenu = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.sort),
|
painter = painterResource(id = R.drawable.sort),
|
||||||
contentDescription = "Sort",
|
contentDescription = stringResource(R.string.content_desc_sort),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(sortOrder.displayName)
|
Text(stringResource(sortOrder.labelRes))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showSortMenu,
|
expanded = showSortMenu,
|
||||||
|
|
@ -1161,7 +1161,7 @@ private fun ShelfDetailScreen(
|
||||||
) {
|
) {
|
||||||
SortOrder.entries.forEach { order ->
|
SortOrder.entries.forEach { order ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(order.displayName) },
|
text = { Text(stringResource(order.labelRes)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onSortOrderChange(order)
|
onSortOrderChange(order)
|
||||||
showSortMenu = false
|
showSortMenu = false
|
||||||
|
|
@ -1170,7 +1170,7 @@ private fun ShelfDetailScreen(
|
||||||
if (order == sortOrder) {
|
if (order == sortOrder) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = "Selected"
|
contentDescription = stringResource(R.string.content_desc_selected)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1182,7 +1182,7 @@ private fun ShelfDetailScreen(
|
||||||
IconButton(onClick = { isSearchActive = true }) {
|
IconButton(onClick = { isSearchActive = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Search,
|
imageVector = Icons.Default.Search,
|
||||||
contentDescription = "Search shelf"
|
contentDescription = stringResource(R.string.content_desc_search_shelf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1191,7 +1191,7 @@ private fun ShelfDetailScreen(
|
||||||
IconButton(onClick = { showMoreMenu = true }) {
|
IconButton(onClick = { showMoreMenu = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = "More options"
|
contentDescription = stringResource(R.string.content_desc_more_options)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
@ -1252,7 +1252,7 @@ private fun ShelfDetailScreen(
|
||||||
if (isFolderShelf) {
|
if (isFolderShelf) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Folders",
|
text = stringResource(R.string.section_folders),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
@ -1275,7 +1275,7 @@ private fun ShelfDetailScreen(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Files",
|
text = stringResource(R.string.section_files),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
@ -1321,7 +1321,7 @@ private fun AddBooksModeScreen(
|
||||||
title = { Text(stringResource(R.string.add_to_shelf, shelfName)) },
|
title = { Text(stringResource(R.string.add_to_shelf, shelfName)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
|
@ -1329,11 +1329,11 @@ private fun AddBooksModeScreen(
|
||||||
TextButton(onClick = { showSortMenu = true }) {
|
TextButton(onClick = { showSortMenu = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.sort),
|
painter = painterResource(id = R.drawable.sort),
|
||||||
contentDescription = "Sort",
|
contentDescription = stringResource(R.string.content_desc_sort),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(sortOrder.displayName)
|
Text(stringResource(sortOrder.labelRes))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showSortMenu,
|
expanded = showSortMenu,
|
||||||
|
|
@ -1341,14 +1341,14 @@ private fun AddBooksModeScreen(
|
||||||
) {
|
) {
|
||||||
SortOrder.entries.forEach { order ->
|
SortOrder.entries.forEach { order ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(order.displayName) },
|
text = { Text(stringResource(order.labelRes)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onSortOrderChange(order)
|
onSortOrderChange(order)
|
||||||
showSortMenu = false
|
showSortMenu = false
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (order == sortOrder) {
|
if (order == sortOrder) {
|
||||||
Icon(Icons.Default.Check, contentDescription = "Selected")
|
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -1368,7 +1368,7 @@ private fun AddBooksModeScreen(
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = source == currentSource,
|
selected = source == currentSource,
|
||||||
onClick = { onSourceChange(source) },
|
onClick = { onSourceChange(source) },
|
||||||
label = { Text(source.displayName) }
|
label = { Text(stringResource(source.labelRes)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1378,7 +1378,7 @@ private fun AddBooksModeScreen(
|
||||||
if (selectedBookUris.isNotEmpty()) {
|
if (selectedBookUris.isNotEmpty()) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.fab_add_count, selectedBookUris.size)) },
|
text = { Text(stringResource(R.string.fab_add_count, selectedBookUris.size)) },
|
||||||
icon = { Icon(Icons.Default.Check, contentDescription = "Add books") },
|
icon = { Icon(Icons.Default.Check, contentDescription = stringResource(R.string.fab_add_books)) },
|
||||||
onClick = onAddSelectedBooks
|
onClick = onAddSelectedBooks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1447,7 +1447,7 @@ private fun ShelfCover(shelf: Shelf) {
|
||||||
.fallback(placeholder)
|
.fallback(placeholder)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = "${shelf.name} shelf cover",
|
contentDescription = stringResource(R.string.content_desc_shelf_cover, shelf.name),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = coverWidth, height = coverHeight)
|
.size(width = coverWidth, height = coverHeight)
|
||||||
|
|
@ -1637,7 +1637,7 @@ private fun LibraryListItem(
|
||||||
modifier = Modifier.matchParentSize().background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
|
modifier = Modifier.matchParentSize().background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Check, contentDescription = "Selected", modifier = Modifier.size(36.dp).background(MaterialTheme.colorScheme.primary, CircleShape).padding(6.dp), tint = MaterialTheme.colorScheme.onPrimary)
|
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_selected), modifier = Modifier.size(36.dp).background(MaterialTheme.colorScheme.primary, CircleShape).padding(6.dp), tint = MaterialTheme.colorScheme.onPrimary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2239,13 +2239,13 @@ fun LibraryFilterSheet(
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = currentFilters.readStatus == status,
|
selected = currentFilters.readStatus == status,
|
||||||
onClick = { currentFilters = currentFilters.copy(readStatus = status) },
|
onClick = { currentFilters = currentFilters.copy(readStatus = status) },
|
||||||
label = { Text(status.displayName) }
|
label = { Text(stringResource(status.labelRes)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allTags.isNotEmpty()) {
|
if (allTags.isNotEmpty()) {
|
||||||
Text("Tags", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleMedium)
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
@ -2740,12 +2740,12 @@ fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Un
|
||||||
}
|
}
|
||||||
if (onEdit != null) {
|
if (onEdit != null) {
|
||||||
IconButton(onClick = onEdit) {
|
IconButton(onClick = onEdit) {
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Edit")
|
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.label_edit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onDelete != null) {
|
if (onDelete != null) {
|
||||||
IconButton(onClick = onDelete) {
|
IconButton(onClick = onDelete) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Remove")
|
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_remove))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2997,7 +2997,7 @@ fun OpdsBookDetailsSheet(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Check, contentDescription = "Read")
|
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.action_read))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.action_read), fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
* mail: epistemereader@gmail.com
|
* mail: epistemereader@gmail.com
|
||||||
*/
|
*/
|
||||||
// MainViewModel.kt
|
// MainViewModel.kt
|
||||||
@file:Suppress("DEPRECATION")
|
@file:Suppress("DEPRECATION", "ANNOTATION_WILL_BE_APPLIED_ALSO_TO_PROPERTY_OR_FIELD")
|
||||||
|
|
||||||
package com.aryan.reader
|
package com.aryan.reader
|
||||||
|
|
||||||
|
|
@ -31,8 +31,14 @@ import android.database.Cursor
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.aryan.reader.tts.TtsController
|
||||||
|
import com.aryan.reader.tts.TtsPlaybackManager
|
||||||
|
import com.aryan.reader.paginatedreader.LocatorConverter
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import com.aryan.reader.paginatedreader.semanticBlockModule
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
@ -181,20 +187,21 @@ private data class CachedSpeechBubble(
|
||||||
val maskBitmap: Bitmap?
|
val maskBitmap: Bitmap?
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class AddBooksSource(val displayName: String) {
|
enum class AddBooksSource(@StringRes val labelRes: Int) {
|
||||||
UNSHELVED("Unshelved"), ALL_BOOKS("All Books")
|
UNSHELVED(R.string.add_books_source_unshelved),
|
||||||
|
ALL_BOOKS(R.string.add_books_source_all_books)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class AppThemeMode(val displayName: String) {
|
enum class AppThemeMode(@StringRes val labelRes: Int) {
|
||||||
SYSTEM("System"),
|
SYSTEM(R.string.app_theme_mode_system),
|
||||||
LIGHT("Light"),
|
LIGHT(R.string.app_theme_mode_light),
|
||||||
DARK("Dark")
|
DARK(R.string.app_theme_mode_dark)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class AppContrastOption(val displayName: String, val value: Double) {
|
enum class AppContrastOption(@StringRes val labelRes: Int, val value: Double) {
|
||||||
STANDARD("Standard", 0.0),
|
STANDARD(R.string.app_contrast_standard, 0.0),
|
||||||
MEDIUM("Medium", 0.5),
|
MEDIUM(R.string.app_contrast_medium, 0.5),
|
||||||
HIGH("High", 1.0)
|
HIGH(R.string.app_contrast_high, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CustomAppTheme(
|
data class CustomAppTheme(
|
||||||
|
|
@ -240,18 +247,21 @@ data class Shelf(
|
||||||
val childShelfCount: Int get() = childShelfIds.size
|
val childShelfCount: Int get() = childShelfIds.size
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SortOrder(val displayName: String) {
|
enum class SortOrder(@StringRes val labelRes: Int) {
|
||||||
RECENT("Recent"),
|
RECENT(R.string.sort_recent),
|
||||||
TITLE_ASC("Title A-Z"),
|
TITLE_ASC(R.string.sort_title_az),
|
||||||
AUTHOR_ASC("Author A-Z"),
|
AUTHOR_ASC(R.string.sort_author_az),
|
||||||
PERCENT_ASC("Percent complete 0-100"),
|
PERCENT_ASC(R.string.sort_percent_asc),
|
||||||
PERCENT_DESC("Percent complete 100-0"),
|
PERCENT_DESC(R.string.sort_percent_desc),
|
||||||
SIZE_ASC("Size (Smallest)"),
|
SIZE_ASC(R.string.sort_size_smallest),
|
||||||
SIZE_DESC("Size (Biggest)")
|
SIZE_DESC(R.string.sort_size_biggest)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ReadStatusFilter(val displayName: String) {
|
enum class ReadStatusFilter(@StringRes val labelRes: Int) {
|
||||||
ALL("All"), UNREAD("Unread"), IN_PROGRESS("In Progress"), COMPLETED("Completed")
|
ALL(R.string.read_status_all),
|
||||||
|
UNREAD(R.string.read_status_unread),
|
||||||
|
IN_PROGRESS(R.string.read_status_in_progress),
|
||||||
|
COMPLETED(R.string.read_status_completed)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LibraryFilters(
|
data class LibraryFilters(
|
||||||
|
|
@ -652,6 +662,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
|
|
||||||
val proUpgradeState = billingClientWrapper.proUpgradeState
|
val proUpgradeState = billingClientWrapper.proUpgradeState
|
||||||
|
|
||||||
|
val ttsController by lazy { TtsController(appContext).apply { connect() } }
|
||||||
|
|
||||||
|
private var backgroundTtsBook: EpubBook? = null
|
||||||
|
private var backgroundTtsBookId: String? = null
|
||||||
|
private var backgroundTtsCoverPath: String? = null
|
||||||
|
|
||||||
private val _internalState = MutableStateFlow(
|
private val _internalState = MutableStateFlow(
|
||||||
ReaderScreenState(
|
ReaderScreenState(
|
||||||
renderMode = try {
|
renderMode = try {
|
||||||
|
|
@ -1264,7 +1280,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = fontsRepository.importFont(android.net.Uri.fromFile(tempFile))
|
val result = fontsRepository.importFont(Uri.fromFile(tempFile))
|
||||||
result.onSuccess { font ->
|
result.onSuccess { font ->
|
||||||
if (uiState.value.isSyncEnabled) {
|
if (uiState.value.isSyncEnabled) {
|
||||||
uploadNewFont(font)
|
uploadNewFont(font)
|
||||||
|
|
@ -1512,6 +1528,37 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
init {
|
init {
|
||||||
Timber.d("ViewModel instance created.")
|
Timber.d("ViewModel instance created.")
|
||||||
WorkManager.getInstance(application).cancelUniqueWork(FolderSyncWorker.WORK_NAME)
|
WorkManager.getInstance(application).cancelUniqueWork(FolderSyncWorker.WORK_NAME)
|
||||||
|
|
||||||
|
// --- ADD THIS BLOCK ---
|
||||||
|
val locatorConverter = LocatorConverter(
|
||||||
|
bookCacheDao,
|
||||||
|
ProtoBuf { serializersModule = semanticBlockModule },
|
||||||
|
appContext
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasSessionFinished = false
|
||||||
|
ttsController.ttsState.collect { state ->
|
||||||
|
val isPlaying = state.isPlaying
|
||||||
|
val sessionFinished = state.sessionFinished
|
||||||
|
val isReaderSource = state.playbackSource == "READER"
|
||||||
|
|
||||||
|
if (isReaderSource) {
|
||||||
|
if (sessionFinished && !wasSessionFinished) {
|
||||||
|
if (_internalState.value.selectedEpubBook == null) {
|
||||||
|
Timber.tag("TTS_BG_ADVANCE").i("Reader is closed. Handling auto-advance in background.")
|
||||||
|
advanceTtsChapterInBackground(state, locatorConverter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.sessionEndedByStop) {
|
||||||
|
backgroundTtsBook = null
|
||||||
|
backgroundTtsBookId = null
|
||||||
|
backgroundTtsCoverPath = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasSessionFinished = sessionFinished
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
recentFilesRepository.migrateLegacyShelvesToRoom()
|
recentFilesRepository.migrateLegacyShelvesToRoom()
|
||||||
if (!prefs.getBoolean(KEY_DEFAULT_TAGS_SEEDED, false)) {
|
if (!prefs.getBoolean(KEY_DEFAULT_TAGS_SEEDED, false)) {
|
||||||
|
|
@ -2531,6 +2578,20 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
val uriString = _internalState.value.selectedPdfUri?.toString()
|
val uriString = _internalState.value.selectedPdfUri?.toString()
|
||||||
?: _internalState.value.selectedEpubUri?.toString()
|
?: _internalState.value.selectedEpubUri?.toString()
|
||||||
|
|
||||||
|
val ttsState = ttsController.ttsState.value
|
||||||
|
val isTtsActive = ttsState.playbackSource == "READER" &&
|
||||||
|
(ttsState.isPlaying || ttsState.isLoading || ttsState.sessionFinished || ttsState.currentText != null)
|
||||||
|
|
||||||
|
if (isTtsActive && _internalState.value.selectedEpubBook != null) {
|
||||||
|
backgroundTtsBook = _internalState.value.selectedEpubBook
|
||||||
|
backgroundTtsBookId = _internalState.value.selectedBookId
|
||||||
|
backgroundTtsCoverPath = uiState.value.recentFiles.find { it.bookId == backgroundTtsBookId }?.coverImagePath
|
||||||
|
} else if (!isTtsActive) {
|
||||||
|
backgroundTtsBook = null
|
||||||
|
backgroundTtsBookId = null
|
||||||
|
backgroundTtsCoverPath = null
|
||||||
|
}
|
||||||
|
|
||||||
_internalState.update {
|
_internalState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedPdfUri = null,
|
selectedPdfUri = null,
|
||||||
|
|
@ -2600,6 +2661,68 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun advanceTtsChapterInBackground(state: TtsPlaybackManager.TtsState, locatorConverter: LocatorConverter) {
|
||||||
|
val currentChapterIndex = state.chapterIndex ?: return
|
||||||
|
val book = backgroundTtsBook ?: return
|
||||||
|
val bookId = backgroundTtsBookId ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
var nextIdx = currentChapterIndex + 1
|
||||||
|
val totalChapters = book.chapters.size
|
||||||
|
var foundContent = false
|
||||||
|
|
||||||
|
while (nextIdx < totalChapters) {
|
||||||
|
Timber.tag("TTS_BG_ADVANCE").d("Trying chapter $nextIdx natively.")
|
||||||
|
val nativeChunks = locatorConverter.getTtsChunksForChapter(book, nextIdx)
|
||||||
|
|
||||||
|
if (!nativeChunks.isNullOrEmpty()) {
|
||||||
|
val token = getAuthToken()
|
||||||
|
val mode = try {
|
||||||
|
TtsPlaybackManager.TtsMode.valueOf(state.ttsMode)
|
||||||
|
} catch(e: Exception) {
|
||||||
|
TtsPlaybackManager.TtsMode.CLOUD
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
ttsController.start(
|
||||||
|
chunks = nativeChunks,
|
||||||
|
bookTitle = book.title,
|
||||||
|
chapterTitle = book.chapters.getOrNull(nextIdx)?.title,
|
||||||
|
coverImageUri = backgroundTtsCoverPath?.let { Uri.fromFile(File(it)).toString() },
|
||||||
|
chapterIndex = nextIdx,
|
||||||
|
ttsMode = mode,
|
||||||
|
playbackSource = "READER",
|
||||||
|
authToken = token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foundContent = true
|
||||||
|
|
||||||
|
// Save reading position locally
|
||||||
|
val cfi = nativeChunks.firstOrNull()?.sourceCfi
|
||||||
|
if (cfi != null) {
|
||||||
|
val locator = locatorConverter.getLocatorFromCfi(book, nextIdx, cfi)
|
||||||
|
if (locator != null) {
|
||||||
|
recentFilesRepository.getFileByBookId(bookId)?.uriString?.let { uriString ->
|
||||||
|
recentFilesRepository.updateEpubReadingPosition(uriString, locator, cfi, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Timber.tag("TTS_BG_ADVANCE").d("Chapter $nextIdx is empty natively. Skipping to next.")
|
||||||
|
nextIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundContent) {
|
||||||
|
Timber.tag("TTS_BG_ADVANCE").d("Reached end of book or no content found.")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
ttsController.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadSyncedFoldersFromPrefs(): List<SyncedFolder> {
|
private fun loadSyncedFoldersFromPrefs(): List<SyncedFolder> {
|
||||||
val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null)
|
val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null)
|
||||||
val folders = mutableListOf<SyncedFolder>()
|
val folders = mutableListOf<SyncedFolder>()
|
||||||
|
|
@ -5408,6 +5531,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
speechBubbleCache.clear()
|
speechBubbleCache.clear()
|
||||||
speechBubbleDetectionJobs.clear()
|
speechBubbleDetectionJobs.clear()
|
||||||
|
|
||||||
|
ttsController.release()
|
||||||
|
|
||||||
Timber.d("ViewModel instance cleared (onCleared).")
|
Timber.d("ViewModel instance cleared (onCleared).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ fun ProScreen(
|
||||||
title = { },
|
title = { },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
|
@ -179,7 +179,7 @@ fun ProScreen(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.crown),
|
painter = painterResource(id = R.drawable.crown),
|
||||||
contentDescription = "Pro",
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
tint = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +209,7 @@ fun ProScreen(
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
),
|
),
|
||||||
text = {
|
text = {
|
||||||
AutoSizeText("Credits",
|
AutoSizeText(stringResource(R.string.credits_tab),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
|
|
@ -312,7 +312,7 @@ private fun ProTierCard(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.crown),
|
painter = painterResource(id = R.drawable.crown),
|
||||||
contentDescription = "Pro Badge",
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
@ -422,7 +422,7 @@ private fun ProTierCard(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.crown),
|
painter = painterResource(id = R.drawable.crown),
|
||||||
contentDescription = "Unlocked",
|
contentDescription = stringResource(R.string.pro_unlocked),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
@ -477,7 +477,7 @@ private fun ProTierCard(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Info,
|
imageVector = Icons.Default.Info,
|
||||||
contentDescription = "Info",
|
contentDescription = stringResource(R.string.info),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
|
@ -499,7 +499,7 @@ private fun ProTierCard(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.crown),
|
painter = painterResource(id = R.drawable.crown),
|
||||||
contentDescription = "Pro",
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
|
@ -539,7 +539,7 @@ private fun ProTierCard(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LegalText(prefixText = "By purchasing,")
|
LegalText(prefixText = stringResource(R.string.legal_by_purchasing))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -631,7 +631,7 @@ private fun CreditTierCard(
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("AI & Cloud Credits", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.credits_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "$credits",
|
text = "$credits",
|
||||||
|
|
@ -639,7 +639,7 @@ private fun CreditTierCard(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text("Credits Available", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.credits_available), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
|
@ -698,7 +698,7 @@ private fun CreditTierCard(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"Estimated Cost Breakdown",
|
stringResource(R.string.credits_estimated_cost_breakdown),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.align(Alignment.Start)
|
modifier = Modifier.align(Alignment.Start)
|
||||||
|
|
@ -707,13 +707,13 @@ private fun CreditTierCard(
|
||||||
|
|
||||||
CostBreakdownItem(
|
CostBreakdownItem(
|
||||||
iconRes = R.drawable.text_to_speech,
|
iconRes = R.drawable.text_to_speech,
|
||||||
title = "Cloud TTS",
|
title = stringResource(R.string.credits_cloud_tts_title),
|
||||||
description = "Cost: ~3-4 credits per minute of audio generated.\nTo enable: Reader Screen > More > TTS Voice Settings."
|
description = stringResource(R.string.credits_cloud_tts_desc)
|
||||||
)
|
)
|
||||||
CostBreakdownItem(
|
CostBreakdownItem(
|
||||||
iconRes = R.drawable.summarize,
|
iconRes = R.drawable.summarize,
|
||||||
title = "AI Summaries & Recap",
|
title = stringResource(R.string.credits_ai_summaries_title),
|
||||||
description = "Cost: ~1-4 credits per request based on chapter length.\nPro Users get 10 free summaries daily."
|
description = stringResource(R.string.credits_ai_summaries_desc)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ fun ContextualTopAppBar(
|
||||||
actions = {
|
actions = {
|
||||||
if (onTagClick != null) {
|
if (onTagClick != null) {
|
||||||
IconButton(onClick = onTagClick) {
|
IconButton(onClick = onTagClick) {
|
||||||
Icon(painterResource(id = R.drawable.tag), contentDescription = "Tag")
|
Icon(painterResource(id = R.drawable.tag), contentDescription = stringResource(R.string.content_desc_tag))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onPinClick != null) {
|
if (onPinClick != null) {
|
||||||
|
|
@ -516,14 +516,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Tags", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.section_tags), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||||
TextButton(onClick = onOpenTags) { Text("+ Add / Edit") }
|
TextButton(onClick = onOpenTags) { Text(stringResource(R.string.action_add_edit)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.tags.isNotEmpty()) {
|
if (item.tags.isNotEmpty()) {
|
||||||
BookTagChipsRow(tags = item.tags, compact = false)
|
BookTagChipsRow(tags = item.tags, compact = false)
|
||||||
} else {
|
} else {
|
||||||
Text("No tags assigned.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.msg_no_tags_assigned), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -599,7 +599,7 @@ private fun InfoRowDetailed(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ContentCopy,
|
imageVector = Icons.Default.ContentCopy,
|
||||||
contentDescription = "Copy $label",
|
contentDescription = stringResource(R.string.content_desc_copy_value, label),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
||||||
)
|
)
|
||||||
|
|
@ -827,7 +827,7 @@ fun EmptyState(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.FileOpen,
|
imageVector = Icons.Outlined.FileOpen,
|
||||||
contentDescription = "No files icon",
|
contentDescription = stringResource(R.string.content_desc_no_files_icon),
|
||||||
modifier = Modifier.size(80.dp),
|
modifier = Modifier.size(80.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -1154,13 +1154,13 @@ fun TagSelectionBottomSheet(
|
||||||
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).heightIn(max = 500.dp)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).heightIn(max = 500.dp)) {
|
||||||
Text("Apply Tags", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp))
|
Text(stringResource(R.string.title_apply_tags), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp))
|
||||||
|
|
||||||
androidx.compose.material3.OutlinedTextField(
|
androidx.compose.material3.OutlinedTextField(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = { searchQuery = it },
|
onValueChange = { searchQuery = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = { Text("Search or create tag...") },
|
placeholder = { Text(stringResource(R.string.placeholder_search_create_tag)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
leadingIcon = { Icon(Icons.Default.Search, null) }
|
leadingIcon = { Icon(Icons.Default.Search, null) }
|
||||||
|
|
@ -1180,7 +1180,7 @@ fun TagSelectionBottomSheet(
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Text("Create \"${searchQuery.trim()}\"", color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.action_create_tag, searchQuery.trim()), color = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,7 @@ fun ChapterWebView(
|
||||||
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(context.getString(R.string.clip_label_copied_link), urlToShow)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
showExternalLinkDialog = null
|
showExternalLinkDialog = null
|
||||||
}) { Text(stringResource(R.string.action_copy)) }
|
}) { Text(stringResource(R.string.action_copy)) }
|
||||||
|
|
@ -1066,7 +1066,7 @@ fun ChapterWebView(
|
||||||
onCopy = {
|
onCopy = {
|
||||||
val clipboard =
|
val clipboard =
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Copied Text", state.selectedText)
|
val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), state.selectedText)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
state.finishActionModeCallback()
|
state.finishActionModeCallback()
|
||||||
localWebViewRef?.clearFocus()
|
localWebViewRef?.clearFocus()
|
||||||
|
|
|
||||||
|
|
@ -577,7 +577,7 @@ fun AnnotationBottomSheet(
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Delete")
|
Text(stringResource(R.string.action_delete))
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = { onSave(noteText) },
|
onClick = { onSave(noteText) },
|
||||||
|
|
@ -586,7 +586,7 @@ fun AnnotationBottomSheet(
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Save Note")
|
Text(stringResource(R.string.action_save_note))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ fun EpubReaderTopBar(
|
||||||
description = stringResource(R.string.tooltip_back_desc),
|
description = stringResource(R.string.tooltip_back_desc),
|
||||||
onClick = onNavigateBack
|
onClick = onNavigateBack
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -583,7 +583,7 @@ fun EpubReaderBottomBar(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ai),
|
painter = painterResource(id = R.drawable.ai),
|
||||||
contentDescription = "AI Features"
|
contentDescription = stringResource(R.string.ai_features_title)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1091,7 +1091,7 @@ fun AutoScrollControls(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowUpward,
|
imageVector = Icons.Default.ArrowUpward,
|
||||||
contentDescription = "Scroll to Top",
|
contentDescription = stringResource(R.string.action_scroll_to_top),
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -1440,7 +1440,7 @@ fun TtsOverlayControls(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
|
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
|
||||||
"Play/Pause",
|
stringResource(R.string.content_desc_play_pause),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1514,10 +1514,10 @@ fun TtsOverlayControls(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { onCollapseChange(true) }, modifier = Modifier.size(32.dp)) {
|
IconButton(onClick = { onCollapseChange(true) }, modifier = Modifier.size(32.dp)) {
|
||||||
Icon(Icons.Default.ChevronRight, "Collapse", modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(Icons.Default.ChevronRight, stringResource(R.string.content_desc_collapse), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) {
|
IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) {
|
||||||
Icon(Icons.Default.Close, "Stop TTS", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
|
Icon(Icons.Default.Close, stringResource(R.string.content_desc_stop_tts), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1541,7 +1541,7 @@ fun TtsOverlayControls(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
|
painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
|
||||||
"Play/Pause",
|
stringResource(R.string.content_desc_play_pause),
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1557,7 +1557,7 @@ fun TtsOverlayControls(
|
||||||
// Unified Sliders Block
|
// Unified Sliders Block
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Spd: %.1fx".format(rate), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
|
Text(stringResource(R.string.tts_speed_short, "%.1f".format(rate)), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
|
||||||
Slider(
|
Slider(
|
||||||
value = rate,
|
value = rate,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
|
|
@ -1571,11 +1571,11 @@ fun TtsOverlayControls(
|
||||||
modifier = Modifier.weight(1f).height(24.dp)
|
modifier = Modifier.weight(1f).height(24.dp)
|
||||||
)
|
)
|
||||||
IconButton(onClick = { rate = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
|
IconButton(onClick = { rate = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
|
||||||
Icon(Icons.Default.Refresh, "Reset Speed", modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.Refresh, stringResource(R.string.content_desc_reset_speed), modifier = Modifier.size(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Ptch: %.1fx".format(pitch), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
|
Text(stringResource(R.string.tts_pitch_short, "%.1f".format(pitch)), style = MaterialTheme.typography.labelSmall, modifier = Modifier.width(62.dp))
|
||||||
Slider(
|
Slider(
|
||||||
value = pitch,
|
value = pitch,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
|
|
@ -1589,7 +1589,7 @@ fun TtsOverlayControls(
|
||||||
modifier = Modifier.weight(1f).height(24.dp)
|
modifier = Modifier.weight(1f).height(24.dp)
|
||||||
)
|
)
|
||||||
IconButton(onClick = { pitch = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
|
IconButton(onClick = { pitch = 1.0f; saveAndApply() }, modifier = Modifier.size(32.dp)) {
|
||||||
Icon(Icons.Default.Refresh, "Reset Pitch", modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.Refresh, stringResource(R.string.content_desc_reset_pitch), modifier = Modifier.size(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ fun EpubReaderDrawerSheet(
|
||||||
Tab(
|
Tab(
|
||||||
selected = drawerPagerState.currentPage == 2,
|
selected = drawerPagerState.currentPage == 2,
|
||||||
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
|
onClick = { drawerScope.launch { drawerPagerState.animateScrollToPage(2) } },
|
||||||
text = { Text("Annotations") }
|
text = { Text(stringResource(R.string.tab_annotations)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,13 +408,13 @@ private fun ChaptersList(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { expandedEntryIndices = effectiveToc.indices.toSet() }) {
|
TextButton(onClick = { expandedEntryIndices = effectiveToc.indices.toSet() }) {
|
||||||
Text("Expand All")
|
Text(stringResource(R.string.action_expand_all))
|
||||||
}
|
}
|
||||||
TextButton(onClick = { expandedEntryIndices = emptySet() }) {
|
TextButton(onClick = { expandedEntryIndices = emptySet() }) {
|
||||||
Text("Collapse All")
|
Text(stringResource(R.string.action_collapse_all))
|
||||||
}
|
}
|
||||||
TextButton(onClick = onScrollToCurrent) {
|
TextButton(onClick = onScrollToCurrent) {
|
||||||
Text("Locate")
|
Text(stringResource(R.string.action_locate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -905,7 +905,7 @@ fun EpubReaderHost(
|
||||||
})
|
})
|
||||||
var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) }
|
var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) }
|
||||||
|
|
||||||
val ttsController = rememberTtsController()
|
val ttsController = viewModel.ttsController
|
||||||
val ttsState by ttsController.ttsState.collectAsState()
|
val ttsState by ttsController.ttsState.collectAsState()
|
||||||
|
|
||||||
val totalBookLengthChars = remember(chapters) {
|
val totalBookLengthChars = remember(chapters) {
|
||||||
|
|
@ -4979,7 +4979,7 @@ fun EpubReaderHost(
|
||||||
},
|
},
|
||||||
onCopy = {
|
onCopy = {
|
||||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Copied Text", targetHighlight.text)
|
val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), targetHighlight.text)
|
||||||
clipboardManager.setPrimaryClip(clip)
|
clipboardManager.setPrimaryClip(clip)
|
||||||
highlightToNoteCfi = null
|
highlightToNoteCfi = null
|
||||||
},
|
},
|
||||||
|
|
@ -5188,13 +5188,13 @@ fun EpubReaderHost(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showInsufficientCreditsDialog = false },
|
onDismissRequest = { showInsufficientCreditsDialog = false },
|
||||||
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
|
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
|
||||||
title = { Text("Out of Credits") },
|
title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
|
||||||
text = { Text("You don't have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.") },
|
text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showInsufficientCreditsDialog = false
|
showInsufficientCreditsDialog = false
|
||||||
onNavigateToPro()
|
onNavigateToPro()
|
||||||
}) { Text("Get Pro / Add Credits") }
|
}) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showInsufficientCreditsDialog = false }) {
|
TextButton(onClick = { showInsufficientCreditsDialog = false }) {
|
||||||
|
|
|
||||||
|
|
@ -912,9 +912,9 @@ fun VisualOptionsSheet(
|
||||||
AnimatedVisibility(visible = pullToTurnEnabled) {
|
AnimatedVisibility(visible = pullToTurnEnabled) {
|
||||||
Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) {
|
Column(modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) {
|
||||||
HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f))
|
HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f))
|
||||||
Text("Pull Distance to Change Chapter", style = MaterialTheme.typography.titleSmall)
|
Text(stringResource(R.string.setting_pull_distance_change_chapter), style = MaterialTheme.typography.titleSmall)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Short", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.label_short), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Slider(
|
Slider(
|
||||||
value = pullToTurnMultiplier,
|
value = pullToTurnMultiplier,
|
||||||
onValueChange = onPullToTurnMultiplierChange,
|
onValueChange = onPullToTurnMultiplierChange,
|
||||||
|
|
@ -922,7 +922,7 @@ fun VisualOptionsSheet(
|
||||||
steps = 14,
|
steps = 14,
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 12.dp)
|
modifier = Modifier.weight(1f).padding(horizontal = 12.dp)
|
||||||
)
|
)
|
||||||
Text("Long", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(stringResource(R.string.label_long), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1079,7 +1079,7 @@ fun FormatSlider(
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(32.dp) // Slimmer buttons
|
modifier = Modifier.size(32.dp) // Slimmer buttons
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Remove, contentDescription = "Decrease", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Remove, contentDescription = stringResource(R.string.content_desc_decrease), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using our new CustomCanvasSlider here!
|
// Using our new CustomCanvasSlider here!
|
||||||
|
|
@ -1097,7 +1097,7 @@ fun FormatSlider(
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(32.dp) // Slimmer buttons
|
modifier = Modifier.size(32.dp) // Slimmer buttons
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Increase", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_increase), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ fun FeedbackScreen(
|
||||||
title = { Text(stringResource(R.string.drawer_help_feedback)) },
|
title = { Text(stringResource(R.string.drawer_help_feedback)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { navController.popBackStack() }) {
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -132,7 +132,7 @@ fun FeedbackScreen(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.github),
|
painter = painterResource(id = R.drawable.github),
|
||||||
contentDescription = "GitHub",
|
contentDescription = stringResource(R.string.github_issues),
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
@ -150,7 +150,7 @@ fun FeedbackScreen(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Email,
|
imageVector = Icons.Outlined.Email,
|
||||||
contentDescription = "Email",
|
contentDescription = stringResource(R.string.email_support),
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
@ -201,7 +201,7 @@ private fun FeedbackOptionCard(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
contentDescription = "Open",
|
contentDescription = stringResource(R.string.action_open),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
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.PlatformTextStyle
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
|
@ -2002,10 +2003,10 @@ internal fun PaginatedReaderContent(
|
||||||
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 = {
|
||||||
Text(
|
Text(
|
||||||
"You clicked on an external link:\n\n$urlToShow\n\nWhat would you like to do?"
|
stringResource(R.string.dialog_external_link_desc, urlToShow)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|
@ -2014,10 +2015,10 @@ internal fun PaginatedReaderContent(
|
||||||
onClick = {
|
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(context.getString(R.string.clip_label_copied_link), urlToShow)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
showExternalLinkDialog = null
|
showExternalLinkDialog = null
|
||||||
}) { Text("Copy") }
|
}) { Text(stringResource(R.string.action_copy)) }
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, urlToShow.toUri())
|
val intent = Intent(Intent.ACTION_VIEW, urlToShow.toUri())
|
||||||
|
|
@ -2028,15 +2029,15 @@ internal fun PaginatedReaderContent(
|
||||||
e, "No activity found to handle intent for URL: $urlToShow"
|
e, "No activity found to handle intent for URL: $urlToShow"
|
||||||
)
|
)
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context, "No browser found to open the link.", Toast.LENGTH_LONG
|
context, context.getString(R.string.error_no_browser), Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
showExternalLinkDialog = null
|
showExternalLinkDialog = null
|
||||||
}) { Text("Open") }
|
}) { Text(stringResource(R.string.action_open)) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showExternalLinkDialog = null }) { Text("Cancel") }
|
TextButton(onClick = { showExternalLinkDialog = null }) { Text(stringResource(R.string.action_cancel)) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2797,7 +2798,7 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageRequest,
|
model = imageRequest,
|
||||||
contentDescription = "List item marker",
|
contentDescription = stringResource(R.string.content_desc_list_item_marker),
|
||||||
modifier = markerAreaModifier.height(
|
modifier = markerAreaModifier.height(
|
||||||
imageSize
|
imageSize
|
||||||
),
|
),
|
||||||
|
|
@ -3555,7 +3556,7 @@ internal fun PaginatedReaderContent(
|
||||||
onCopy = {
|
onCopy = {
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Copied Text", sel.text)
|
val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), sel.text)
|
||||||
clipboardManager.setPrimaryClip(clip)
|
clipboardManager.setPrimaryClip(clip)
|
||||||
activeSelection = null
|
activeSelection = null
|
||||||
},
|
},
|
||||||
|
|
@ -3926,7 +3927,7 @@ internal fun PaginatedReaderContent(
|
||||||
if (showColorPickerDialog != null) {
|
if (showColorPickerDialog != null) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showColorPickerDialog = null },
|
onDismissRequest = { showColorPickerDialog = null },
|
||||||
title = { Text("Select Color") },
|
title = { Text(stringResource(R.string.dialog_select_color)) },
|
||||||
text = {
|
text = {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 48.dp),
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
|
|
@ -3953,7 +3954,7 @@ internal fun PaginatedReaderContent(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showColorPickerDialog = null
|
showColorPickerDialog = null
|
||||||
}) { Text("Close") }
|
}) { Text(stringResource(R.string.action_close)) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3972,7 +3973,7 @@ internal fun PaginatedReaderContent(
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Book has no pages to display.")
|
Timber.w("Book has no pages to display.")
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("This book has no content to display.")
|
Text(stringResource(R.string.msg_book_no_content))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4109,7 +4110,7 @@ private fun RenderFlexChildBlock(
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageRequest,
|
model = imageRequest,
|
||||||
contentDescription = "List item marker",
|
contentDescription = stringResource(R.string.content_desc_list_item_marker),
|
||||||
modifier = markerAreaModifier.height(imageSize),
|
modifier = markerAreaModifier.height(imageSize),
|
||||||
alignment = Alignment.CenterEnd,
|
alignment = Alignment.CenterEnd,
|
||||||
contentScale = ContentScale.FillHeight
|
contentScale = ContentScale.FillHeight
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,7 @@ fun PenPlayground(onClose: () -> Unit) {
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = R.drawable.close
|
id = R.drawable.close
|
||||||
),
|
),
|
||||||
contentDescription = "Close", tint = Color.Gray
|
contentDescription = stringResource(R.string.action_close), tint = Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ internal fun OcrLanguageSelectionDialog(
|
||||||
) {
|
) {
|
||||||
RadioButton(selected = (language == currentLanguage), onClick = null)
|
RadioButton(selected = (language == currentLanguage), onClick = null)
|
||||||
Text(
|
Text(
|
||||||
text = language.displayName,
|
text = stringResource(language.displayNameRes),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,14 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aryan.reader.R
|
||||||
import io.legere.pdfiumandroid.api.Bookmark
|
import io.legere.pdfiumandroid.api.Bookmark
|
||||||
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -255,7 +257,9 @@ internal fun PdfTocTreeItem(
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
contentDescription = stringResource(
|
||||||
|
if (isExpanded) R.string.content_desc_collapse else R.string.content_desc_expand
|
||||||
|
),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -301,13 +305,13 @@ internal fun PdfNavigationDrawerContent(
|
||||||
) {
|
) {
|
||||||
Tab(selected = drawerPagerState.currentPage == 0, onClick = {
|
Tab(selected = drawerPagerState.currentPage == 0, onClick = {
|
||||||
drawerScope.launch { drawerPagerState.animateScrollToPage(0) }
|
drawerScope.launch { drawerPagerState.animateScrollToPage(0) }
|
||||||
}, text = { Text("Chapters") })
|
}, text = { Text(stringResource(R.string.tab_chapters)) })
|
||||||
Tab(
|
Tab(
|
||||||
selected = drawerPagerState.currentPage == 1,
|
selected = drawerPagerState.currentPage == 1,
|
||||||
onClick = {
|
onClick = {
|
||||||
drawerScope.launch { drawerPagerState.animateScrollToPage(1) }
|
drawerScope.launch { drawerPagerState.animateScrollToPage(1) }
|
||||||
},
|
},
|
||||||
text = { Text("Bookmarks") },
|
text = { Text(stringResource(R.string.tab_bookmarks)) },
|
||||||
modifier = Modifier.testTag("BookmarksTab")
|
modifier = Modifier.testTag("BookmarksTab")
|
||||||
)
|
)
|
||||||
Tab(
|
Tab(
|
||||||
|
|
@ -315,7 +319,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
drawerScope.launch { drawerPagerState.animateScrollToPage(2) }
|
drawerScope.launch { drawerPagerState.animateScrollToPage(2) }
|
||||||
},
|
},
|
||||||
text = { Text("Highlights") },
|
text = { Text(stringResource(R.string.tab_highlights)) },
|
||||||
modifier = Modifier.testTag("HighlightsTab")
|
modifier = Modifier.testTag("HighlightsTab")
|
||||||
)
|
)
|
||||||
Tab(
|
Tab(
|
||||||
|
|
@ -323,7 +327,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
drawerScope.launch { drawerPagerState.animateScrollToPage(3) }
|
drawerScope.launch { drawerPagerState.animateScrollToPage(3) }
|
||||||
},
|
},
|
||||||
text = { Text("Pages") },
|
text = { Text(stringResource(R.string.tab_pages)) },
|
||||||
modifier = Modifier.testTag("PagesTab")
|
modifier = Modifier.testTag("PagesTab")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -340,7 +344,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Chapters are not available for this book.",
|
stringResource(R.string.msg_chapters_not_available),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
@ -434,13 +438,13 @@ internal fun PdfNavigationDrawerContent(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { expandedEntryIndices = flatTableOfContents.indices.toSet() }) {
|
TextButton(onClick = { expandedEntryIndices = flatTableOfContents.indices.toSet() }) {
|
||||||
Text("Expand All")
|
Text(stringResource(R.string.action_expand_all))
|
||||||
}
|
}
|
||||||
TextButton(onClick = { expandedEntryIndices = emptySet() }) {
|
TextButton(onClick = { expandedEntryIndices = emptySet() }) {
|
||||||
Text("Collapse All")
|
Text(stringResource(R.string.action_collapse_all))
|
||||||
}
|
}
|
||||||
TextButton(onClick = onScrollToCurrent) {
|
TextButton(onClick = onScrollToCurrent) {
|
||||||
Text("Locate")
|
Text(stringResource(R.string.action_locate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -503,7 +507,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"You haven't added any bookmarks yet.",
|
stringResource(R.string.no_bookmarks_yet),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
@ -531,7 +535,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
)
|
)
|
||||||
}, supportingContent = {
|
}, supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
"Page ${bookmark.pageIndex + 1} of ${bookmark.totalPages}",
|
stringResource(R.string.page_of_pages, bookmark.pageIndex + 1, bookmark.totalPages),
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}, trailingContent = {
|
}, trailingContent = {
|
||||||
|
|
@ -542,7 +546,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = "More options for bookmark"
|
contentDescription = stringResource(R.string.content_desc_more_options_bookmark)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
@ -551,13 +555,13 @@ internal fun PdfNavigationDrawerContent(
|
||||||
bookmarkMenuExpandedFor = null
|
bookmarkMenuExpandedFor = null
|
||||||
}) {
|
}) {
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Text("Rename")
|
Text(stringResource(R.string.action_rename))
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
showRenameBookmarkDialog = bookmark
|
showRenameBookmarkDialog = bookmark
|
||||||
bookmarkMenuExpandedFor = null
|
bookmarkMenuExpandedFor = null
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Text("Delete")
|
Text(stringResource(R.string.action_delete))
|
||||||
}, onClick = {
|
}, onClick = {
|
||||||
showDeleteConfirmDialogFor = bookmark
|
showDeleteConfirmDialogFor = bookmark
|
||||||
bookmarkMenuExpandedFor = null
|
bookmarkMenuExpandedFor = null
|
||||||
|
|
@ -581,11 +585,11 @@ internal fun PdfNavigationDrawerContent(
|
||||||
|
|
||||||
AlertDialog(onDismissRequest = {
|
AlertDialog(onDismissRequest = {
|
||||||
showRenameBookmarkDialog = null
|
showRenameBookmarkDialog = null
|
||||||
}, title = { Text("Rename Bookmark") }, text = {
|
}, title = { Text(stringResource(R.string.dialog_rename_bookmark)) }, text = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = newTitle,
|
value = newTitle,
|
||||||
onValueChange = { newTitle = it },
|
onValueChange = { newTitle = it },
|
||||||
label = { Text("New Title") },
|
label = { Text(stringResource(R.string.label_new_title)) },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = bookmarkToRename.title,
|
text = bookmarkToRename.title,
|
||||||
|
|
@ -605,33 +609,33 @@ internal fun PdfNavigationDrawerContent(
|
||||||
onClick = {
|
onClick = {
|
||||||
onRenameBookmark(bookmarkToRename, newTitle)
|
onRenameBookmark(bookmarkToRename, newTitle)
|
||||||
showRenameBookmarkDialog = null
|
showRenameBookmarkDialog = null
|
||||||
}) { Text("Save") }
|
}) { Text(stringResource(R.string.action_save)) }
|
||||||
}, dismissButton = {
|
}, dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showRenameBookmarkDialog = null
|
showRenameBookmarkDialog = null
|
||||||
}) { Text("Cancel") }
|
}) { Text(stringResource(R.string.action_cancel)) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
showDeleteConfirmDialogFor?.let { bookmarkToDelete ->
|
showDeleteConfirmDialogFor?.let { bookmarkToDelete ->
|
||||||
AlertDialog(onDismissRequest = {
|
AlertDialog(onDismissRequest = {
|
||||||
showDeleteConfirmDialogFor = null
|
showDeleteConfirmDialogFor = null
|
||||||
}, title = { Text("Delete Bookmark?") }, text = {
|
}, title = { Text(stringResource(R.string.dialog_delete_bookmark)) }, text = {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to permanently delete this bookmark?"
|
stringResource(R.string.dialog_delete_bookmark_desc)
|
||||||
)
|
)
|
||||||
}, confirmButton = {
|
}, confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onDeleteBookmark(bookmarkToDelete)
|
onDeleteBookmark(bookmarkToDelete)
|
||||||
showDeleteConfirmDialogFor = null
|
showDeleteConfirmDialogFor = null
|
||||||
}) { Text("Delete") }
|
}) { Text(stringResource(R.string.action_delete)) }
|
||||||
}, dismissButton = {
|
}, dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showDeleteConfirmDialogFor = null
|
showDeleteConfirmDialogFor = null
|
||||||
}) { Text("Cancel") }
|
}) { Text(stringResource(R.string.action_cancel)) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -645,7 +649,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"You haven't added any highlights yet.",
|
stringResource(R.string.no_highlights_yet),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
@ -662,12 +666,12 @@ internal fun PdfNavigationDrawerContent(
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = !filterWithNotesOnly,
|
selected = !filterWithNotesOnly,
|
||||||
onClick = { filterWithNotesOnly = false },
|
onClick = { filterWithNotesOnly = false },
|
||||||
label = { Text("All") }
|
label = { Text(stringResource(R.string.read_status_all)) }
|
||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = filterWithNotesOnly,
|
selected = filterWithNotesOnly,
|
||||||
onClick = { filterWithNotesOnly = true },
|
onClick = { filterWithNotesOnly = true },
|
||||||
label = { Text("With Notes") }
|
label = { Text(stringResource(R.string.filter_with_notes)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -689,7 +693,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
text = highlight.text.ifBlank { "Highlighted section" },
|
text = highlight.text.ifBlank { stringResource(R.string.msg_highlighted_section_default) },
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
|
|
@ -707,7 +711,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
"Page ${highlight.pageIndex + 1}",
|
stringResource(R.string.pdf_page_short, highlight.pageIndex + 1),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -733,14 +737,20 @@ internal fun PdfNavigationDrawerContent(
|
||||||
Box {
|
Box {
|
||||||
var highlightMenuExpanded by remember { mutableStateOf(false) }
|
var highlightMenuExpanded by remember { mutableStateOf(false) }
|
||||||
IconButton(onClick = { highlightMenuExpanded = true }) {
|
IconButton(onClick = { highlightMenuExpanded = true }) {
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "Options")
|
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.content_desc_options))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = highlightMenuExpanded,
|
expanded = highlightMenuExpanded,
|
||||||
onDismissRequest = { highlightMenuExpanded = false }
|
onDismissRequest = { highlightMenuExpanded = false }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(if (highlight.note.isNullOrBlank()) "Add Note" else "Edit Note") },
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (highlight.note.isNullOrBlank()) R.string.menu_add_note else R.string.menu_edit_note
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onNoteRequested(highlight.id)
|
onNoteRequested(highlight.id)
|
||||||
highlightMenuExpanded = false
|
highlightMenuExpanded = false
|
||||||
|
|
@ -748,7 +758,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Delete") },
|
text = { Text(stringResource(R.string.action_delete)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
showDeleteConfirmDialogFor = highlight
|
showDeleteConfirmDialogFor = highlight
|
||||||
highlightMenuExpanded = false
|
highlightMenuExpanded = false
|
||||||
|
|
@ -770,20 +780,20 @@ internal fun PdfNavigationDrawerContent(
|
||||||
showDeleteConfirmDialogFor?.let { highlightToDelete ->
|
showDeleteConfirmDialogFor?.let { highlightToDelete ->
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showDeleteConfirmDialogFor = null },
|
onDismissRequest = { showDeleteConfirmDialogFor = null },
|
||||||
title = { Text("Delete Highlight?") },
|
title = { Text(stringResource(R.string.dialog_delete_highlight)) },
|
||||||
text = { Text("Are you sure you want to permanently delete this highlight?") },
|
text = { Text(stringResource(R.string.dialog_delete_highlight_desc)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onDeleteHighlight(highlightToDelete)
|
onDeleteHighlight(highlightToDelete)
|
||||||
showDeleteConfirmDialogFor = null
|
showDeleteConfirmDialogFor = null
|
||||||
}
|
}
|
||||||
) { Text("Delete") }
|
) { Text(stringResource(R.string.action_delete)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { showDeleteConfirmDialogFor = null }
|
onClick = { showDeleteConfirmDialogFor = null }
|
||||||
) { Text("Cancel") }
|
) { Text(stringResource(R.string.action_cancel)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -809,7 +819,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("Locate")
|
Text(stringResource(R.string.action_locate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -880,7 +890,7 @@ internal fun PdfNavigationDrawerContent(
|
||||||
if (thumb != null) {
|
if (thumb != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = thumb!!.asImageBitmap(),
|
bitmap = thumb!!.asImageBitmap(),
|
||||||
contentDescription = "Page ${pageIdx + 1}",
|
contentDescription = stringResource(R.string.pdf_page_short, pageIdx + 1),
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ package com.aryan.reader.pdf
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
|
@ -78,7 +79,9 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -95,12 +98,12 @@ import com.aryan.reader.pdf.ocr.OcrSymbol
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
enum class OcrLanguage(val displayName: String) {
|
enum class OcrLanguage(@StringRes val displayNameRes: Int) {
|
||||||
LATIN("English, Spanish, French, etc."),
|
LATIN(R.string.ocr_language_latin),
|
||||||
DEVANAGARI("Hindi, Marathi, Sanskrit + English"),
|
DEVANAGARI(R.string.ocr_language_devanagari),
|
||||||
CHINESE("Chinese + English"),
|
CHINESE(R.string.ocr_language_chinese),
|
||||||
JAPANESE("Japanese + English"),
|
JAPANESE(R.string.ocr_language_japanese),
|
||||||
KOREAN("Korean + English")
|
KOREAN(R.string.ocr_language_korean)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class OcrSymbolInfo(
|
internal data class OcrSymbolInfo(
|
||||||
|
|
@ -232,6 +235,8 @@ internal fun PdfSelectionMenuPopup(
|
||||||
onTts: (() -> Unit)? = null,
|
onTts: (() -> Unit)? = null,
|
||||||
onNote: (() -> Unit)? = null
|
onNote: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Popup(
|
Popup(
|
||||||
popupPositionProvider = popupPositionProvider,
|
popupPositionProvider = popupPositionProvider,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
|
@ -266,7 +271,7 @@ internal fun PdfSelectionMenuPopup(
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|
@ -321,7 +326,7 @@ internal fun PdfSelectionMenuPopup(
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CopyAll, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(Icons.Default.CopyAll, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Copy Thread")
|
Text(stringResource(R.string.action_copy_thread))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -360,23 +365,23 @@ internal fun PdfSelectionMenuPopup(
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
val actions = mutableListOf<MenuActionItem>()
|
val actions = mutableListOf<MenuActionItem>()
|
||||||
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = "Copy", onClick = { onCopy(menuState.selectedText) }))
|
actions.add(MenuActionItem(iconRes = R.drawable.copy, label = context.getString(R.string.action_copy), onClick = { onCopy(menuState.selectedText) }))
|
||||||
if (onTts != null) {
|
if (onTts != null) {
|
||||||
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = "Speak", onClick = onTts))
|
actions.add(MenuActionItem(imageVector = Icons.AutoMirrored.Filled.VolumeUp, label = context.getString(R.string.label_speak), onClick = onTts))
|
||||||
}
|
}
|
||||||
if (menuState.selectedText.length <= 2000) {
|
if (menuState.selectedText.length <= 2000) {
|
||||||
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = "Dict", onClick = { onAiDefine(menuState.selectedText) }))
|
actions.add(MenuActionItem(iconRes = R.drawable.dictionary, label = context.getString(R.string.label_dict), onClick = { onAiDefine(menuState.selectedText) }))
|
||||||
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = "Translate", onClick = { onTranslate(menuState.selectedText) }))
|
actions.add(MenuActionItem(iconRes = R.drawable.translate, label = context.getString(R.string.action_translate), onClick = { onTranslate(menuState.selectedText) }))
|
||||||
actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = "Search", onClick = { onSearch(menuState.selectedText) }))
|
actions.add(MenuActionItem(imageVector = Icons.Default.Search, label = context.getString(R.string.action_search), onClick = { onSearch(menuState.selectedText) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onNote != null) {
|
if (onNote != null) {
|
||||||
val noteLabel = if (menuState.note.isNullOrBlank()) "Note" else "Edit"
|
val noteLabel = context.getString(if (menuState.note.isNullOrBlank()) R.string.label_note else R.string.label_edit)
|
||||||
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
|
actions.add(MenuActionItem(imageVector = Icons.Default.Edit, label = noteLabel, onClick = onNote))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!menuState.isExistingHighlight) {
|
if (!menuState.isExistingHighlight) {
|
||||||
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = "Select All", onClick = { onSelectAll() }))
|
actions.add(MenuActionItem(iconRes = R.drawable.select_all, label = context.getString(R.string.select_all), onClick = { onSelectAll() }))
|
||||||
}
|
}
|
||||||
if (menuState.isExistingHighlight) {
|
if (menuState.isExistingHighlight) {
|
||||||
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = { onDelete() }, isError = true))
|
actions.add(MenuActionItem(imageVector = Icons.Default.Delete, label = "Remove", onClick = { onDelete() }, isError = true))
|
||||||
|
|
@ -659,7 +664,7 @@ fun PdfHighlightColorRow(
|
||||||
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 (displayColor.luminance() > 0.5f) Color.Black else Color.White,
|
tint = if (displayColor.luminance() > 0.5f) Color.Black else Color.White,
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -752,10 +757,10 @@ fun PdfAnnotationBottomSheet(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
PdfBottomSheetToolButton(icon = R.drawable.copy, label = "Copy", effectiveText = effectiveText, onClick = onCopy)
|
PdfBottomSheetToolButton(icon = R.drawable.copy, label = stringResource(R.string.action_copy), effectiveText = effectiveText, onClick = onCopy)
|
||||||
PdfBottomSheetToolButton(icon = R.drawable.dictionary, label = "Dict", effectiveText = effectiveText, onClick = onDictionary)
|
PdfBottomSheetToolButton(icon = R.drawable.dictionary, label = stringResource(R.string.label_dict), effectiveText = effectiveText, onClick = onDictionary)
|
||||||
PdfBottomSheetToolButton(icon = R.drawable.translate, label = "Translate", effectiveText = effectiveText, onClick = onTranslate)
|
PdfBottomSheetToolButton(icon = R.drawable.translate, label = stringResource(R.string.action_translate), effectiveText = effectiveText, onClick = onTranslate)
|
||||||
PdfBottomSheetToolButton(icon = R.drawable.search, label = "Search", effectiveText = effectiveText, onClick = onSearch)
|
PdfBottomSheetToolButton(icon = R.drawable.search, label = stringResource(R.string.action_search), effectiveText = effectiveText, onClick = onSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
@ -763,7 +768,7 @@ fun PdfAnnotationBottomSheet(
|
||||||
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.fillMaxWidth().heightIn(min = 100.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp),
|
||||||
maxLines = 5,
|
maxLines = 5,
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
|
@ -793,7 +798,7 @@ fun PdfAnnotationBottomSheet(
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(Icons.Default.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Delete")
|
Text(stringResource(R.string.action_delete))
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = { onSave(noteText) },
|
onClick = { onSave(noteText) },
|
||||||
|
|
@ -802,7 +807,7 @@ fun PdfAnnotationBottomSheet(
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Save Note")
|
Text(stringResource(R.string.action_save_note))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
|
@ -199,7 +200,7 @@ internal fun ThumbnailWithIndicator(
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = thumbnail.asImageBitmap(),
|
bitmap = thumbnail.asImageBitmap(),
|
||||||
contentDescription = "Start page thumbnail",
|
contentDescription = stringResource(R.string.content_desc_start_page_thumbnail),
|
||||||
contentScale = ContentScale.FillBounds,
|
contentScale = ContentScale.FillBounds,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
@ -230,7 +231,7 @@ internal fun BookmarkButton(
|
||||||
AnimatedVisibility(visible = isBookmarked, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = isBookmarked, enter = fadeIn(), exit = fadeOut()) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.bookmark),
|
painter = painterResource(id = R.drawable.bookmark),
|
||||||
contentDescription = "Bookmark",
|
contentDescription = stringResource(R.string.content_desc_bookmark),
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
@ -271,7 +272,7 @@ internal fun ZoomPercentageIndicator(
|
||||||
// Reset Zoom Button
|
// Reset Zoom Button
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.zoom_out),
|
painter = painterResource(id = R.drawable.zoom_out),
|
||||||
contentDescription = "Reset Zoom",
|
contentDescription = stringResource(R.string.content_desc_reset_zoom),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -73,7 +74,7 @@ internal fun SearchNavigationPill(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (mode == SearchHighlightMode.ALL) Icons.Default.Visibility
|
imageVector = if (mode == SearchHighlightMode.ALL) Icons.Default.Visibility
|
||||||
else Icons.Default.VisibilityOff,
|
else Icons.Default.VisibilityOff,
|
||||||
contentDescription = "Toggle Highlights",
|
contentDescription = stringResource(R.string.content_desc_toggle_search_highlights),
|
||||||
tint = if (mode == SearchHighlightMode.ALL) MaterialTheme.colorScheme.primary
|
tint = if (mode == SearchHighlightMode.ALL) MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
@ -95,7 +96,7 @@ internal fun SearchNavigationPill(
|
||||||
IconButton(onClick = onPrev, enabled = isPrevEnabled) {
|
IconButton(onClick = onPrev, enabled = isPrevEnabled) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.KeyboardArrowUp,
|
imageVector = Icons.Default.KeyboardArrowUp,
|
||||||
contentDescription = "Previous",
|
contentDescription = stringResource(R.string.tooltip_prev_result),
|
||||||
tint = if (isPrevEnabled) MaterialTheme.colorScheme.onSurface
|
tint = if (isPrevEnabled) MaterialTheme.colorScheme.onSurface
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
)
|
)
|
||||||
|
|
@ -122,7 +123,7 @@ internal fun SearchNavigationPill(
|
||||||
IconButton(onClick = onNext, enabled = isNextEnabled) {
|
IconButton(onClick = onNext, enabled = isNextEnabled) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.KeyboardArrowDown,
|
imageVector = Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = "Next",
|
contentDescription = stringResource(R.string.tooltip_next_result),
|
||||||
tint = if (isNextEnabled) MaterialTheme.colorScheme.onSurface
|
tint = if (isNextEnabled) MaterialTheme.colorScheme.onSurface
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
)
|
)
|
||||||
|
|
@ -141,12 +142,12 @@ fun PdfSearchResultsPanel(
|
||||||
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
if (lazyResults.itemCount == 0 && lazyResults.loadState.refresh !is LoadState.Loading) {
|
if (lazyResults.itemCount == 0 && lazyResults.loadState.refresh !is LoadState.Loading) {
|
||||||
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 {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.msg_results_found_pages),
|
text = stringResource(R.string.msg_results_found_pages, totalPageCount),
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|
@ -195,6 +196,7 @@ fun PdfSearchResultsList(
|
||||||
onResultClick: (SearchResult) -> Unit,
|
onResultClick: (SearchResult) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
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) {
|
||||||
|
|
@ -203,7 +205,11 @@ fun PdfSearchResultsList(
|
||||||
} else {
|
} else {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "${results.size} matches found",
|
text = context.resources.getQuantityString(
|
||||||
|
R.plurals.search_matches_count,
|
||||||
|
results.size,
|
||||||
|
results.size
|
||||||
|
),
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.positionChanged
|
import androidx.compose.ui.input.pointer.positionChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
|
@ -450,7 +451,7 @@ private fun DragPill(
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.drag_handle),
|
painter = painterResource(id = R.drawable.drag_handle),
|
||||||
contentDescription = "Drag to move text box",
|
contentDescription = stringResource(R.string.content_desc_drag_text_box),
|
||||||
modifier = Modifier.size((20f / scale).dp)
|
modifier = Modifier.size((20f / scale).dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,14 +136,14 @@ internal fun PdfTopBar(
|
||||||
description = stringResource(R.string.tooltip_back_desc),
|
description = stringResource(R.string.tooltip_back_desc),
|
||||||
onClick = onNavigateBack
|
onClick = onNavigateBack
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||||
}
|
}
|
||||||
val titleText = when {
|
val titleText = when {
|
||||||
isLoadingDocument -> stringResource(R.string.loading_pdf)
|
isLoadingDocument -> stringResource(R.string.loading_pdf)
|
||||||
errorMessage != null -> stringResource(R.string.error_loading_pdf)
|
errorMessage != null -> stringResource(R.string.error_loading_pdf)
|
||||||
totalPages > 0 && pagerStatePageCount > 0 -> "Page ${currentPageForDisplay + 1} of $totalPages"
|
totalPages > 0 && pagerStatePageCount > 0 -> stringResource(R.string.page_of_pages, currentPageForDisplay + 1, totalPages)
|
||||||
totalPages > 0 && pagerStatePageCount == 0 -> stringResource(R.string.loading_page)
|
totalPages > 0 && pagerStatePageCount == 0 -> stringResource(R.string.loading_page)
|
||||||
else -> "PDF Viewer"
|
else -> stringResource(R.string.pdf_viewer)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = titleText,
|
text = titleText,
|
||||||
|
|
@ -179,16 +179,16 @@ internal fun PdfTopBar(
|
||||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||||
onClick = onShowDictionarySettings
|
onClick = onShowDictionarySettings
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(id = R.drawable.dictionary), contentDescription = "Dictionary Settings", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
TooltipIconButton(text = "Demo Annotations", onClick = onGenerateDemoAnnotations) {
|
TooltipIconButton(text = stringResource(R.string.tooltip_demo_annotations), onClick = onGenerateDemoAnnotations) {
|
||||||
Icon(Icons.Default.BugReport, contentDescription = "Generate Demo Annotations", tint = MaterialTheme.colorScheme.secondary)
|
Icon(Icons.Default.BugReport, contentDescription = stringResource(R.string.content_desc_generate_demo_annotations), tint = MaterialTheme.colorScheme.secondary)
|
||||||
}
|
}
|
||||||
TooltipIconButton(text = stringResource(R.string.pen_playground), onClick = onShowPenPlayground) {
|
TooltipIconButton(text = stringResource(R.string.pen_playground), onClick = onShowPenPlayground) {
|
||||||
Icon(Icons.Default.Star, contentDescription = "Open Pen Playground", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Default.Star, contentDescription = stringResource(R.string.content_desc_open_pen_playground), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
TooltipIconButton(text = stringResource(R.string.import_svg), onClick = onImportSvg) {
|
TooltipIconButton(text = stringResource(R.string.import_svg), onClick = onImportSvg) {
|
||||||
Icon(Icons.Default.Brush, contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63))
|
Icon(Icons.Default.Brush, contentDescription = stringResource(R.string.import_svg), tint = Color(0xFFE91E63))
|
||||||
|
|
@ -392,7 +392,7 @@ internal fun PdfTopBar(
|
||||||
|
|
||||||
item {
|
item {
|
||||||
IconButton(onClick = onNewTabClick, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp).size(36.dp)) {
|
IconButton(onClick = onNewTabClick, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp).size(36.dp)) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "New Tab", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.content_desc_new_tab), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -492,8 +492,8 @@ fun PdfBottomBar(
|
||||||
) {
|
) {
|
||||||
if (jumpBackPage != null) {
|
if (jumpBackPage != null) {
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = "Jump Back to Page ${jumpBackPage + 1}",
|
text = stringResource(R.string.action_jump_back_to_page, jumpBackPage + 1),
|
||||||
description = "Return to previous page",
|
description = stringResource(R.string.desc_return_to_previous_page),
|
||||||
onClick = onJumpBack
|
onClick = onJumpBack
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -502,7 +502,7 @@ fun PdfBottomBar(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.Undo,
|
Icons.AutoMirrored.Filled.Undo,
|
||||||
contentDescription = "Jump Back",
|
contentDescription = stringResource(R.string.content_desc_jump_back),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -535,7 +535,7 @@ fun PdfBottomBar(
|
||||||
enabled = !isTtsPlayingOrLoading,
|
enabled = !isTtsPlayingOrLoading,
|
||||||
modifier = Modifier.testTag("TocButton")
|
modifier = Modifier.testTag("TocButton")
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Menu, contentDescription = "Table of Contents")
|
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,7 +557,7 @@ fun PdfBottomBar(
|
||||||
onClick = onToggleHighlights
|
onClick = onToggleHighlights
|
||||||
) {
|
) {
|
||||||
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
|
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
|
||||||
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = "Highlight all text", tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,7 +577,7 @@ fun PdfBottomBar(
|
||||||
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
|
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
|
||||||
onClick = onToggleEditMode
|
onClick = onToggleEditMode
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Toggle Editing Mode", tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,19 +587,19 @@ fun PdfBottomBar(
|
||||||
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
|
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
|
||||||
onClick = onToggleTts
|
onClick = onToggleTts
|
||||||
) {
|
) {
|
||||||
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS", tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.FLAVOR != "oss") {
|
if (BuildConfig.FLAVOR != "oss") {
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = if (isBubbleZoomModeActive) "Exit Smart Zoom" else "Smart Comic Zoom",
|
text = if (isBubbleZoomModeActive) stringResource(R.string.action_exit_smart_zoom) else stringResource(R.string.action_smart_comic_zoom),
|
||||||
description = "Toggle Smart Comic Zoom",
|
description = stringResource(R.string.desc_toggle_smart_comic_zoom),
|
||||||
onClick = onToggleBubbleZoom
|
onClick = onToggleBubbleZoom
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.comic_bubble),
|
painterResource(R.drawable.comic_bubble),
|
||||||
contentDescription = "Smart Comic Zoom",
|
contentDescription = stringResource(R.string.content_desc_smart_comic_zoom),
|
||||||
tint = if (isBubbleZoomModeActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
tint = if (isBubbleZoomModeActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2089,7 +2089,7 @@ fun PdfViewerScreen(
|
||||||
if (!selectedDictPackage.isNullOrEmpty()) {
|
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||||
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2102,7 +2102,7 @@ fun PdfViewerScreen(
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2113,7 +2113,7 @@ fun PdfViewerScreen(
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3386,7 +3386,7 @@ fun PdfViewerScreen(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = errorMessage ?: "Failed to load PDF.",
|
text = errorMessage ?: stringResource(R.string.error_failed_load_pdf),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -4380,7 +4380,10 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Downloading ${ocrLanguage.displayName.substringBefore("(")} language pack...",
|
text = stringResource(
|
||||||
|
R.string.msg_downloading_language_pack,
|
||||||
|
stringResource(ocrLanguage.displayNameRes).substringBefore("(").trim()
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
)
|
)
|
||||||
|
|
@ -4429,7 +4432,10 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Downloading Bubble Zoom model... ${(progress * 100).toInt()}%",
|
text = stringResource(
|
||||||
|
R.string.msg_downloading_bubble_zoom_model_progress,
|
||||||
|
(progress * 100).toInt()
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
)
|
)
|
||||||
|
|
@ -4473,7 +4479,7 @@ fun PdfViewerScreen(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Exit slider navigation"
|
contentDescription = stringResource(R.string.content_desc_exit_slider_navigation)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4913,15 +4919,23 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
else -1
|
else -1
|
||||||
val text =
|
val text =
|
||||||
if (index >= 0) "Result ${index + 1} / ${searchData.matches.size}"
|
if (index >= 0) context.getString(
|
||||||
else "${searchData.matches.size} Results"
|
R.string.pdf_search_result_position,
|
||||||
|
index + 1,
|
||||||
|
searchData.matches.size
|
||||||
|
)
|
||||||
|
else context.resources.getQuantityString(
|
||||||
|
R.plurals.search_results_count,
|
||||||
|
searchData.matches.size,
|
||||||
|
searchData.matches.size
|
||||||
|
)
|
||||||
Triple(text, index > 0, index < searchData.matches.size - 1)
|
Triple(text, index > 0, index < searchData.matches.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SmartSearchResult.Paged -> {
|
is SmartSearchResult.Paged -> {
|
||||||
val page = currentResult?.locationInSource
|
val page = currentResult?.locationInSource
|
||||||
val text = if (page != null) "Page ${page + 1}"
|
val text = if (page != null) context.getString(R.string.pdf_page_short, page + 1)
|
||||||
else "${searchData.totalPageCount}+ Pages"
|
else context.getString(R.string.msg_search_pages_count, searchData.totalPageCount)
|
||||||
Triple(text, true, true)
|
Triple(text, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5046,9 +5060,9 @@ fun PdfViewerScreen(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "Jump Back", modifier = Modifier.size(18.dp))
|
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = stringResource(R.string.content_desc_jump_back), modifier = Modifier.size(18.dp))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Back to Pg ${lastPage + 1}", style = MaterialTheme.typography.labelLarge)
|
Text(stringResource(R.string.pdf_back_to_page_short, lastPage + 1), style = MaterialTheme.typography.labelLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5535,7 +5549,7 @@ fun PdfViewerScreen(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isTtsPageBelow) Icons.Default.ArrowDownward
|
imageVector = if (isTtsPageBelow) Icons.Default.ArrowDownward
|
||||||
else Icons.Default.ArrowUpward,
|
else Icons.Default.ArrowUpward,
|
||||||
contentDescription = "Scroll to reading page"
|
contentDescription = stringResource(R.string.content_desc_scroll_to_reading_page)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5817,10 +5831,10 @@ fun PdfViewerScreen(
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = { Text("Unlock Page Summarization") },
|
title = { Text(stringResource(R.string.dialog_unlock_page_summarization)) },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
"Get concise summaries of any page with Episteme Pro. Upgrade to start using this feature."
|
stringResource(R.string.dialog_unlock_page_summarization_desc)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|
@ -5841,13 +5855,13 @@ fun PdfViewerScreen(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showInsufficientCreditsDialog = false },
|
onDismissRequest = { showInsufficientCreditsDialog = false },
|
||||||
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
|
icon = { Icon(painterResource(id = R.drawable.crown), contentDescription = null) },
|
||||||
title = { Text("Out of Credits") },
|
title = { Text(stringResource(R.string.dialog_out_of_credits_title)) },
|
||||||
text = { Text("You don't have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.") },
|
text = { Text(stringResource(R.string.dialog_out_of_credits_desc)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showInsufficientCreditsDialog = false
|
showInsufficientCreditsDialog = false
|
||||||
onNavigateToPro()
|
onNavigateToPro()
|
||||||
}) { Text("Get Pro / Add Credits") }
|
}) { Text(stringResource(R.string.action_get_pro_or_add_credits)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showInsufficientCreditsDialog = false }) {
|
TextButton(onClick = { showInsufficientCreditsDialog = false }) {
|
||||||
|
|
@ -5874,7 +5888,7 @@ fun PdfViewerScreen(
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = poppedUpPanelBitmap!!.asImageBitmap(),
|
bitmap = poppedUpPanelBitmap!!.asImageBitmap(),
|
||||||
contentDescription = "Annotated Page",
|
contentDescription = stringResource(R.string.content_desc_annotated_page),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
|
|
@ -5894,7 +5908,7 @@ fun PdfViewerScreen(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Close Image",
|
contentDescription = stringResource(R.string.content_desc_close_image),
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5913,16 +5927,16 @@ fun PdfViewerScreen(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showBubbleZoomDownloadDialog = false },
|
onDismissRequest = { showBubbleZoomDownloadDialog = false },
|
||||||
icon = { Icon(Icons.Default.Info, contentDescription = null) },
|
icon = { Icon(Icons.Default.Info, contentDescription = null) },
|
||||||
title = { Text("Download Bubble Zoom Model") },
|
title = { Text(stringResource(R.string.dialog_download_bubble_zoom_model)) },
|
||||||
text = {
|
text = {
|
||||||
Text("To use the Bubble Zoom feature, an AI model needs to be downloaded (~134 MB). Do you want to download it now?")
|
Text(stringResource(R.string.dialog_download_bubble_zoom_model_desc))
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
showBubbleZoomDownloadDialog = false
|
showBubbleZoomDownloadDialog = false
|
||||||
viewModel.downloadSpeechBubbleModel(context)
|
viewModel.downloadSpeechBubbleModel(context)
|
||||||
}) {
|
}) {
|
||||||
Text("Download")
|
Text(stringResource(R.string.action_download))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import com.aryan.reader.FileType
|
import com.aryan.reader.FileType
|
||||||
|
import com.aryan.reader.R
|
||||||
import com.aryan.reader.data.RecentFileItem
|
import com.aryan.reader.data.RecentFileItem
|
||||||
import com.aryan.reader.data.RecentFilesRepository
|
import com.aryan.reader.data.RecentFilesRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -32,7 +33,8 @@ class ReflowWorker(
|
||||||
Timber.tag("PdfToHtmlPerf").e("FAILURE: KEY_PDF_URI is null | bookId=$bookId")
|
Timber.tag("PdfToHtmlPerf").e("FAILURE: KEY_PDF_URI is null | bookId=$bookId")
|
||||||
return@withContext Result.failure()
|
return@withContext Result.failure()
|
||||||
}
|
}
|
||||||
val originalTitle = inputData.getString(KEY_ORIGINAL_TITLE) ?: "Document"
|
val originalTitle = inputData.getString(KEY_ORIGINAL_TITLE)
|
||||||
|
?: applicationContext.getString(R.string.default_document_title)
|
||||||
val reflowBookId = "${bookId}_reflow"
|
val reflowBookId = "${bookId}_reflow"
|
||||||
|
|
||||||
Timber.tag("PdfToHtmlPerf").d(
|
Timber.tag("PdfToHtmlPerf").d(
|
||||||
|
|
@ -67,11 +69,11 @@ class ReflowWorker(
|
||||||
bookId = reflowBookId,
|
bookId = reflowBookId,
|
||||||
uriString = destFile.toUri().toString(),
|
uriString = destFile.toUri().toString(),
|
||||||
type = FileType.HTML,
|
type = FileType.HTML,
|
||||||
displayName = "$originalTitle (Text View)",
|
displayName = applicationContext.getString(R.string.reflow_display_name_format, originalTitle),
|
||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
coverImagePath = null,
|
coverImagePath = null,
|
||||||
title = "$originalTitle (Reflow)",
|
title = applicationContext.getString(R.string.reflow_title_format, originalTitle),
|
||||||
author = "Generated",
|
author = applicationContext.getString(R.string.generated_author),
|
||||||
isAvailable = true,
|
isAvailable = true,
|
||||||
isRecent = true,
|
isRecent = true,
|
||||||
lastModifiedTimestamp = System.currentTimeMillis(),
|
lastModifiedTimestamp = System.currentTimeMillis(),
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = activePopup == ActivePopup.FONT_FAMILY,
|
isSelected = activePopup == ActivePopup.FONT_FAMILY,
|
||||||
iconRes = R.drawable.fonts,
|
iconRes = R.drawable.fonts,
|
||||||
contentDescription = "Select Font Family",
|
contentDescription = stringResource(R.string.content_desc_select_font_family),
|
||||||
onClick = {
|
onClick = {
|
||||||
activePopup =
|
activePopup =
|
||||||
if (activePopup == ActivePopup.FONT_FAMILY) ActivePopup.NONE else ActivePopup.FONT_FAMILY
|
if (activePopup == ActivePopup.FONT_FAMILY) ActivePopup.NONE else ActivePopup.FONT_FAMILY
|
||||||
|
|
@ -481,7 +481,7 @@ fun TextAnnotationDock(
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.KeyboardArrowDown,
|
imageVector = Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = "Select Font Size",
|
contentDescription = stringResource(R.string.content_desc_select_font_size),
|
||||||
tint = Color.Gray,
|
tint = Color.Gray,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -561,7 +561,7 @@ fun TextAnnotationDock(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.font_background),
|
painter = painterResource(id = R.drawable.font_background),
|
||||||
contentDescription = "Font Background",
|
contentDescription = stringResource(R.string.content_desc_font_background),
|
||||||
modifier = Modifier.size(17.dp),
|
modifier = Modifier.size(17.dp),
|
||||||
tint = Color.Black
|
tint = Color.Black
|
||||||
)
|
)
|
||||||
|
|
@ -587,7 +587,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = isBold,
|
isSelected = isBold,
|
||||||
iconRes = R.drawable.format_bold,
|
iconRes = R.drawable.format_bold,
|
||||||
contentDescription = "Bold",
|
contentDescription = stringResource(R.string.content_desc_bold),
|
||||||
onClick = {
|
onClick = {
|
||||||
val newW = if (isBold) FontWeight.Normal else FontWeight.Bold
|
val newW = if (isBold) FontWeight.Normal else FontWeight.Bold
|
||||||
onUpdateStyle(currentStyle.copy(fontWeight = newW, fontFamily = currentStyle.fontFamily))
|
onUpdateStyle(currentStyle.copy(fontWeight = newW, fontFamily = currentStyle.fontFamily))
|
||||||
|
|
@ -605,7 +605,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = isItalic,
|
isSelected = isItalic,
|
||||||
iconRes = R.drawable.format_italic,
|
iconRes = R.drawable.format_italic,
|
||||||
contentDescription = "Italic",
|
contentDescription = stringResource(R.string.content_desc_italic),
|
||||||
onClick = {
|
onClick = {
|
||||||
val newStyle = if (isItalic) FontStyle.Normal else FontStyle.Italic
|
val newStyle = if (isItalic) FontStyle.Normal else FontStyle.Italic
|
||||||
onUpdateStyle(currentStyle.copy(fontStyle = newStyle, fontFamily = currentStyle.fontFamily))
|
onUpdateStyle(currentStyle.copy(fontStyle = newStyle, fontFamily = currentStyle.fontFamily))
|
||||||
|
|
@ -625,7 +625,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = isUnderline,
|
isSelected = isUnderline,
|
||||||
iconRes = R.drawable.format_underlined,
|
iconRes = R.drawable.format_underlined,
|
||||||
contentDescription = "Underline",
|
contentDescription = stringResource(R.string.content_desc_underline),
|
||||||
onClick = {
|
onClick = {
|
||||||
val hasStrike = currentDec.contains(TextDecoration.LineThrough)
|
val hasStrike = currentDec.contains(TextDecoration.LineThrough)
|
||||||
val newDec = if (isUnderline) {
|
val newDec = if (isUnderline) {
|
||||||
|
|
@ -654,7 +654,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = isStrike,
|
isSelected = isStrike,
|
||||||
iconRes = R.drawable.format_strikethrough,
|
iconRes = R.drawable.format_strikethrough,
|
||||||
contentDescription = "Strikethrough",
|
contentDescription = stringResource(R.string.content_desc_strikethrough),
|
||||||
onClick = {
|
onClick = {
|
||||||
val hasUnd = currentDec.contains(TextDecoration.Underline)
|
val hasUnd = currentDec.contains(TextDecoration.Underline)
|
||||||
val newDec = if (isStrike) {
|
val newDec = if (isStrike) {
|
||||||
|
|
@ -682,7 +682,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
iconRes = R.drawable.text_box,
|
iconRes = R.drawable.text_box,
|
||||||
contentDescription = "Insert Text Box",
|
contentDescription = stringResource(R.string.content_desc_insert_text_box),
|
||||||
onClick = {
|
onClick = {
|
||||||
Timber.tag("PdfTextBoxDebug").d("Dock: Insert Text Box icon clicked")
|
Timber.tag("PdfTextBoxDebug").d("Dock: Insert Text Box icon clicked")
|
||||||
onInsertTextBox()
|
onInsertTextBox()
|
||||||
|
|
@ -698,7 +698,7 @@ fun TextAnnotationDock(
|
||||||
FormattingIconButton(
|
FormattingIconButton(
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
iconVector = Icons.Default.Close,
|
iconVector = Icons.Default.Close,
|
||||||
contentDescription = "Close",
|
contentDescription = stringResource(R.string.action_close),
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
onClearTextBoxSelection()
|
onClearTextBoxSelection()
|
||||||
|
|
@ -739,7 +739,7 @@ private fun FontItem(
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
contentDescription = "Selected",
|
contentDescription = stringResource(R.string.content_desc_selected),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -814,7 +814,7 @@ private fun ColorPickerBubble(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Close",
|
contentDescription = stringResource(R.string.action_close),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -1193,12 +1193,12 @@ private fun ColorPickerSpectrumContent(
|
||||||
IconButton(onClick = onBack, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = onBack, modifier = Modifier.size(24.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = stringResource(R.string.action_back),
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Spectrum",
|
text = stringResource(R.string.label_spectrum),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
|
@ -1264,7 +1264,7 @@ private fun ColorPickerSpectrumContent(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Hex",
|
stringResource(R.string.theme_color_hex),
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
|
|
@ -1282,19 +1282,19 @@ private fun ColorPickerSpectrumContent(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
RgbInputColumn(
|
RgbInputColumn(
|
||||||
label = "R",
|
label = stringResource(R.string.color_r),
|
||||||
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 = "G",
|
label = stringResource(R.string.color_g),
|
||||||
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 = "B",
|
label = stringResource(R.string.color_b),
|
||||||
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)
|
||||||
|
|
@ -1313,7 +1313,7 @@ private fun ColorPickerSpectrumContent(
|
||||||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||||||
contentPadding = PaddingValues(0.dp)
|
contentPadding = PaddingValues(0.dp)
|
||||||
) {
|
) {
|
||||||
Text("Done", fontSize = 14.sp)
|
Text(stringResource(R.string.action_done), fontSize = 14.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import android.graphics.RectF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.aryan.reader.FileType
|
import com.aryan.reader.FileType
|
||||||
|
import com.aryan.reader.R
|
||||||
import io.legere.pdfiumandroid.api.Bookmark
|
import io.legere.pdfiumandroid.api.Bookmark
|
||||||
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
||||||
import io.legere.pdfiumandroid.suspend.PdfPageKt
|
import io.legere.pdfiumandroid.suspend.PdfPageKt
|
||||||
|
|
@ -452,7 +453,7 @@ class OpdsStreamDocumentWrapper(
|
||||||
textSize = 40f
|
textSize = 40f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
}
|
}
|
||||||
canvas.drawText("Page Unavailable", 400f, 600f, paint)
|
canvas.drawText(context.getString(R.string.msg_page_unavailable), 400f, 600f, paint)
|
||||||
val stream = java.io.ByteArrayOutputStream()
|
val stream = java.io.ByteArrayOutputStream()
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream)
|
||||||
return stream.toByteArray()
|
return stream.toByteArray()
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@
|
||||||
<item quantity="other">%1$d results found</item>
|
<item quantity="other">%1$d results found</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Shown in the PDF search results summary bar. %1$d = total number of matches found. -->
|
||||||
|
<plurals name="search_matches_count">
|
||||||
|
<item quantity="one">%1$d match found</item>
|
||||||
|
<item quantity="other">%1$d matches found</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<plurals name="dialog_delete_permanently">
|
<plurals name="dialog_delete_permanently">
|
||||||
<item quantity="one">Delete File Permanently</item>
|
<item quantity="one">Delete File Permanently</item>
|
||||||
<item quantity="other">Delete Files Permanently</item>
|
<item quantity="other">Delete Files Permanently</item>
|
||||||
|
|
@ -52,4 +58,22 @@
|
||||||
<item quantity="one">%1$d book removed from library.</item>
|
<item quantity="one">%1$d book removed from library.</item>
|
||||||
<item quantity="other">%1$d books removed from library.</item>
|
<item quantity="other">%1$d books removed from library.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Word count for folders shown in shelf subtitles. %1$d = number of folders. -->
|
||||||
|
<plurals name="folder_count">
|
||||||
|
<item quantity="one">%1$d folder</item>
|
||||||
|
<item quantity="other">%1$d folders</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Word count for tags shown in filter chips. %1$d = number of tags. -->
|
||||||
|
<plurals name="tag_count">
|
||||||
|
<item quantity="one">%1$d tag</item>
|
||||||
|
<item quantity="other">%1$d tags</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Parenthetical cache chunk count in TTS cache entries. %1$d = number of cached audio chunks. -->
|
||||||
|
<plurals name="tts_cache_chunk_count_parenthetical">
|
||||||
|
<item quantity="one">(%1$d chunk)</item>
|
||||||
|
<item quantity="other">(%1$d chunks)</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -15,6 +15,10 @@
|
||||||
<string name="action_search">Search</string>
|
<string name="action_search">Search</string>
|
||||||
<string name="action_clear">Clear</string>
|
<string name="action_clear">Clear</string>
|
||||||
<string name="action_apply">Apply</string>
|
<string name="action_apply">Apply</string>
|
||||||
|
<string name="action_enable">Enable</string>
|
||||||
|
<!-- Generic error label. %1$s = the error message. -->
|
||||||
|
<string name="error_message_format">Error: %1$s</string>
|
||||||
|
<string name="action_go_back">Go Back</string>
|
||||||
<!-- Tab label in the multi-tab PDF reader — means the slot is available (unused), NOT "free of cost". -->
|
<!-- Tab label in the multi-tab PDF reader — means the slot is available (unused), NOT "free of cost". -->
|
||||||
<string name="tab_free">Free</string>
|
<string name="tab_free">Free</string>
|
||||||
<string name="active_tabs">Active Tabs</string>
|
<string name="active_tabs">Active Tabs</string>
|
||||||
|
|
@ -288,6 +292,13 @@
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<string name="custom_fonts">Custom Fonts</string>
|
<string name="custom_fonts">Custom Fonts</string>
|
||||||
<string name="import_font">Import Font</string>
|
<string name="import_font">Import Font</string>
|
||||||
|
<string name="google_fonts">Google Fonts</string>
|
||||||
|
<string name="action_browse_google_fonts">Browse Google Fonts</string>
|
||||||
|
<string name="google_fonts_search_placeholder">Search 1900+ fonts…</string>
|
||||||
|
<string name="google_fonts_popular_choices">Popular Choices</string>
|
||||||
|
<!-- Empty state in the Google Fonts browser. %1$s = the user-entered search query. -->
|
||||||
|
<string name="google_fonts_no_matches">No fonts found matching \'%1$s\'</string>
|
||||||
|
<string name="content_desc_already_downloaded">Already Downloaded</string>
|
||||||
<string name="no_custom_fonts">No Custom Fonts</string>
|
<string name="no_custom_fonts">No Custom Fonts</string>
|
||||||
<!-- TTF and OTF are font file format names — do not translate. -->
|
<!-- TTF and OTF are font file format names — do not translate. -->
|
||||||
<string name="import_fonts_desc">Import TTF or OTF files to use them in your books.</string>
|
<string name="import_fonts_desc">Import TTF or OTF files to use them in your books.</string>
|
||||||
|
|
@ -512,19 +523,20 @@
|
||||||
<string name="search_in_book">Search in book…</string>
|
<string name="search_in_book">Search in book…</string>
|
||||||
<string name="search_no_results_simple">No results found.</string>
|
<string name="search_no_results_simple">No results found.</string>
|
||||||
|
|
||||||
<!-- Common.kt: — AI feature, not translated. -->
|
<!-- Common.kt: AI feature status. -->
|
||||||
<string name="generating_summary" translatable="false">Generating summary…</string>
|
<string name="generating_summary">Generating summary…</string>
|
||||||
<string name="action_stop">Stop</string>
|
<string name="action_stop">Stop</string>
|
||||||
<string name="action_read_aloud">Read aloud</string>
|
<string name="action_read_aloud">Read aloud</string>
|
||||||
<string name="action_copy">Copy</string>
|
<string name="action_copy">Copy</string>
|
||||||
<string name="no_summary_generated" translatable="false">No summary could be generated.</string>
|
<string name="action_copy_thread">Copy Thread</string>
|
||||||
|
<string name="no_summary_generated">No summary could be generated.</string>
|
||||||
|
|
||||||
<!-- Common.kt: AiDefinitionPopup — AI feature, not translated. -->
|
<!-- Common.kt: AiDefinitionPopup — AI feature strings. -->
|
||||||
<string name="ai_thinking" translatable="false">Thinking…</string>
|
<string name="ai_thinking">Thinking…</string>
|
||||||
<string name="content_desc_open_dictionary">Open in Dictionary App</string>
|
<string name="content_desc_open_dictionary">Open in Dictionary App</string>
|
||||||
<string name="ai_no_definition" translatable="false">AI could not provide a definition.</string>
|
<string name="ai_no_definition">AI could not provide a definition.</string>
|
||||||
<!-- AI feature string. %1$s = the word or phrase the user selected. -->
|
<!-- AI feature string. %1$s = the word or phrase the user selected. -->
|
||||||
<string name="ai_asking_about" translatable="false">Asking AI about \'%1$s\'…</string>
|
<string name="ai_asking_about">Asking AI about \'%1$s\'…</string>
|
||||||
|
|
||||||
<!-- Common.kt: TtsSettingsSheet -->
|
<!-- Common.kt: TtsSettingsSheet -->
|
||||||
<!-- TTS = Text-to-Speech. -->
|
<!-- TTS = Text-to-Speech. -->
|
||||||
|
|
@ -534,8 +546,8 @@
|
||||||
<string name="tts_synthesis_mode">Synthesis Mode</string>
|
<string name="tts_synthesis_mode">Synthesis Mode</string>
|
||||||
<!-- On-Device TTS mode — speech is generated locally without internet. -->
|
<!-- On-Device TTS mode — speech is generated locally without internet. -->
|
||||||
<string name="tts_mode_on_device">On-Device</string>
|
<string name="tts_mode_on_device">On-Device</string>
|
||||||
<!-- Cloud TTS mode — speech is generated remotely for higher quality. Not translated (feature name). -->
|
<!-- Cloud TTS mode — speech is generated remotely for higher quality. -->
|
||||||
<string name="tts_mode_cloud_hq" translatable="false">Cloud (HQ)</string>
|
<string name="tts_mode_cloud_hq">Cloud (HQ)</string>
|
||||||
<string name="tts_voice_selection">Voice Selection</string>
|
<string name="tts_voice_selection">Voice Selection</string>
|
||||||
<string name="tts_play_sample">Play Sample</string>
|
<string name="tts_play_sample">Play Sample</string>
|
||||||
|
|
||||||
|
|
@ -586,17 +598,17 @@
|
||||||
<string name="color_g" translatable="false">G</string>
|
<string name="color_g" translatable="false">G</string>
|
||||||
<string name="color_b" translatable="false">B</string>
|
<string name="color_b" translatable="false">B</string>
|
||||||
|
|
||||||
<!-- Common.kt: fetchAiDefinition — AI feature errors, not translated. -->
|
<!-- Common.kt: fetchAiDefinition — AI feature errors. -->
|
||||||
<string name="error_text_empty" translatable="false">Text is empty.</string>
|
<string name="error_text_empty">Text is empty.</string>
|
||||||
<string name="error_ai_empty_definition" translatable="false">AI returned an empty definition.</string>
|
<string name="error_ai_empty_definition">AI returned an empty definition.</string>
|
||||||
<string name="error_could_not_get_definition" translatable="false">Could not get definition.</string>
|
<string name="error_could_not_get_definition">Could not get definition.</string>
|
||||||
<string name="error_unknown_server" translatable="false">An unknown server error occurred.</string>
|
<string name="error_unknown_server">An unknown server error occurred.</string>
|
||||||
<string name="error_network_check_connection" translatable="false">Network error. Check connection.</string>
|
<string name="error_network_check_connection">Network error. Check connection.</string>
|
||||||
|
|
||||||
<!-- Common.kt: fetchRecap — Recap feature, not translated. -->
|
<!-- Common.kt: fetchRecap — Recap feature errors. -->
|
||||||
<string name="error_not_enough_context" translatable="false">Not enough context for a recap.</string>
|
<string name="error_not_enough_context">Not enough context for a recap.</string>
|
||||||
<string name="error_parse_recap" translatable="false">Failed to parse recap.</string>
|
<string name="error_parse_recap">Failed to parse recap.</string>
|
||||||
<string name="error_network_recap" translatable="false">Network error during recap generation.</string>
|
<string name="error_network_recap">Network error during recap generation.</string>
|
||||||
|
|
||||||
<!-- DictionarySettingsDialog.kt -->
|
<!-- DictionarySettingsDialog.kt -->
|
||||||
<string name="dict_lookup_settings">Lookup Settings</string>
|
<string name="dict_lookup_settings">Lookup Settings</string>
|
||||||
|
|
@ -616,27 +628,27 @@
|
||||||
<string name="dict_search_app_description">App used for web searches.</string>
|
<string name="dict_search_app_description">App used for web searches.</string>
|
||||||
<string name="dict_none">None</string>
|
<string name="dict_none">None</string>
|
||||||
|
|
||||||
<!-- EpubReaderAi.kt — AI feature strings, not translated. -->
|
<!-- EpubReaderAi.kt — AI feature strings. -->
|
||||||
<string name="ai_error_book_content_empty" translatable="false">The book content is empty.</string>
|
<string name="ai_error_book_content_empty">The book content is empty.</string>
|
||||||
<string name="ai_error_parse_summary" translatable="false">Failed to parse summary from server response.</string>
|
<string name="ai_error_parse_summary">Failed to parse summary from server response.</string>
|
||||||
<string name="ai_error_fetch_summary" translatable="false">Could not fetch summary.</string>
|
<string name="ai_error_fetch_summary">Could not fetch summary.</string>
|
||||||
<!-- %1$d = HTTP or server error code; %2$s = human-readable error message from the server. -->
|
<!-- %1$d = HTTP or server error code; %2$s = human-readable error message from the server. -->
|
||||||
<string name="ai_error_with_code" translatable="false">Error: %1$d. %2$s</string>
|
<string name="ai_error_with_code">Error: %1$d. %2$s</string>
|
||||||
<string name="ai_error_network_server" translatable="false">Network error. Please check connection and server status.</string>
|
<string name="ai_error_network_server">Network error. Please check connection and server status.</string>
|
||||||
<!-- %1$d = chapter number being analyzed. -->
|
<!-- %1$d = chapter number being analyzed. -->
|
||||||
<string name="ai_analyzing_chapter" translatable="false">Analyzing Chapter %1$d…</string>
|
<string name="ai_analyzing_chapter">Analyzing Chapter %1$d…</string>
|
||||||
<string name="ai_reading_position" translatable="false">Reading current position…</string>
|
<string name="ai_reading_position">Reading current position…</string>
|
||||||
<string name="ai_generating_recap" translatable="false">Generating Recap…</string>
|
<string name="ai_generating_recap">Generating Recap…</string>
|
||||||
<string name="ai_chapter_summary" translatable="false">Chapter Summary</string>
|
<string name="ai_chapter_summary">Chapter Summary</string>
|
||||||
<!-- "(Beta)" indicates this feature is in beta — keep the label consistent. -->
|
<!-- "(Beta)" indicates this feature is in beta — keep the label consistent. -->
|
||||||
<string name="ai_story_recap_beta" translatable="false">Story Recap (Beta)</string>
|
<string name="ai_story_recap_beta">Story Recap (Beta)</string>
|
||||||
<string name="ai_unlock_summarization" translatable="false">Unlock Chapter Summarization</string>
|
<string name="ai_unlock_summarization">Unlock Chapter Summarization</string>
|
||||||
<!-- "Episteme Pro" is the product tier name. -->
|
<!-- "Episteme Pro" is the product tier name. -->
|
||||||
<string name="ai_unlock_summarization_desc" translatable="false">Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.</string>
|
<string name="ai_unlock_summarization_desc">Get concise summaries of any chapter with Episteme Pro. Upgrade to start using this feature.</string>
|
||||||
<string name="action_learn_more">Learn More</string>
|
<string name="action_learn_more">Learn More</string>
|
||||||
<string name="ai_unlock_smart_dict" translatable="false">Unlock Smart Dictionary</string>
|
<string name="ai_unlock_smart_dict">Unlock Smart Dictionary</string>
|
||||||
<!-- "Pro" refers to the Episteme Pro product tier. -->
|
<!-- "Pro" refers to the Episteme Pro product tier. -->
|
||||||
<string name="ai_unlock_smart_dict_desc" translatable="false">Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.</string>
|
<string name="ai_unlock_smart_dict_desc">Defining entire phrases and paragraphs up to 2000 characters is a Pro feature. Upgrade to get instant definitions for any selected text.</string>
|
||||||
|
|
||||||
<!-- EpubReaderAnnotations.kt -->
|
<!-- EpubReaderAnnotations.kt -->
|
||||||
<string name="content_desc_bookmark_icon">Bookmark</string>
|
<string name="content_desc_bookmark_icon">Bookmark</string>
|
||||||
|
|
@ -684,8 +696,8 @@
|
||||||
<string name="content_desc_text_formatting">Text Formatting</string>
|
<string name="content_desc_text_formatting">Text Formatting</string>
|
||||||
<!-- AI chapter summarization feature menu item. -->
|
<!-- AI chapter summarization feature menu item. -->
|
||||||
<string name="menu_chapter_summarization">Chapter Summarization</string>
|
<string name="menu_chapter_summarization">Chapter Summarization</string>
|
||||||
<!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. Not translated. -->
|
<!-- Recap is an AI feature. "(Beta)" indicates it is in beta — keep consistent. -->
|
||||||
<string name="menu_recap_beta" translatable="false">Recap (Beta)</string>
|
<string name="menu_recap_beta">Recap (Beta)</string>
|
||||||
<!-- TTS = Text-to-Speech. Accessibility label. -->
|
<!-- TTS = Text-to-Speech. Accessibility label. -->
|
||||||
<string name="content_desc_stop_tts">Stop TTS</string>
|
<string name="content_desc_stop_tts">Stop TTS</string>
|
||||||
<!-- TTS = Text-to-Speech. Accessibility label. -->
|
<!-- TTS = Text-to-Speech. Accessibility label. -->
|
||||||
|
|
@ -719,6 +731,8 @@
|
||||||
<string name="label_max">Max</string>
|
<string name="label_max">Max</string>
|
||||||
<string name="content_desc_slower">Slower</string>
|
<string name="content_desc_slower">Slower</string>
|
||||||
<string name="content_desc_faster">Faster</string>
|
<string name="content_desc_faster">Faster</string>
|
||||||
|
<string name="content_desc_decrease">Decrease</string>
|
||||||
|
<string name="content_desc_increase">Increase</string>
|
||||||
<!-- Shown in the PDF reader header. %1$d = current page number; %2$d = total page count. -->
|
<!-- Shown in the PDF reader header. %1$d = current page number; %2$d = total page count. -->
|
||||||
<string name="page_of_pages">Page %1$d of %2$d</string>
|
<string name="page_of_pages">Page %1$d of %2$d</string>
|
||||||
|
|
||||||
|
|
@ -726,10 +740,15 @@
|
||||||
<string name="tab_chapters">Chapters</string>
|
<string name="tab_chapters">Chapters</string>
|
||||||
<string name="tab_bookmarks">Bookmarks</string>
|
<string name="tab_bookmarks">Bookmarks</string>
|
||||||
<string name="tab_highlights">Highlights</string>
|
<string name="tab_highlights">Highlights</string>
|
||||||
|
<string name="tab_pages">Pages</string>
|
||||||
|
<string name="action_expand_all">Expand All</string>
|
||||||
|
<string name="action_collapse_all">Collapse All</string>
|
||||||
|
<string name="action_locate">Locate</string>
|
||||||
<string name="no_bookmarks_yet">You haven\'t added any bookmarks yet.</string>
|
<string name="no_bookmarks_yet">You haven\'t added any bookmarks yet.</string>
|
||||||
<string name="content_desc_more_options_bookmark">More options for bookmark</string>
|
<string name="content_desc_more_options_bookmark">More options for bookmark</string>
|
||||||
<string name="dialog_rename_bookmark">Rename Bookmark</string>
|
<string name="dialog_rename_bookmark">Rename Bookmark</string>
|
||||||
<string name="label_new_name">New Name</string>
|
<string name="label_new_name">New Name</string>
|
||||||
|
<string name="label_new_title">New Title</string>
|
||||||
<string name="dialog_delete_bookmark">Delete Bookmark?</string>
|
<string name="dialog_delete_bookmark">Delete Bookmark?</string>
|
||||||
<string name="dialog_delete_bookmark_desc">Are you sure you want to permanently delete this bookmark?</string>
|
<string name="dialog_delete_bookmark_desc">Are you sure you want to permanently delete this bookmark?</string>
|
||||||
<string name="no_highlights_yet">No highlights yet.</string>
|
<string name="no_highlights_yet">No highlights yet.</string>
|
||||||
|
|
@ -912,6 +931,14 @@
|
||||||
<string name="content_desc_undo">Undo</string>
|
<string name="content_desc_undo">Undo</string>
|
||||||
<string name="content_desc_redo">Redo</string>
|
<string name="content_desc_redo">Redo</string>
|
||||||
<string name="content_desc_show_dock">Show Dock</string>
|
<string name="content_desc_show_dock">Show Dock</string>
|
||||||
|
<string name="content_desc_select_font_family">Select Font Family</string>
|
||||||
|
<string name="content_desc_select_font_size">Select Font Size</string>
|
||||||
|
<string name="content_desc_font_background">Font Background</string>
|
||||||
|
<string name="content_desc_bold">Bold</string>
|
||||||
|
<string name="content_desc_italic">Italic</string>
|
||||||
|
<string name="content_desc_underline">Underline</string>
|
||||||
|
<string name="content_desc_strikethrough">Strikethrough</string>
|
||||||
|
<string name="content_desc_insert_text_box">Insert Text Box</string>
|
||||||
|
|
||||||
<!-- PdfPageComposable -->
|
<!-- PdfPageComposable -->
|
||||||
<!-- OCR = Optical Character Recognition. %1$s = error reason string. -->
|
<!-- OCR = Optical Character Recognition. %1$s = error reason string. -->
|
||||||
|
|
@ -1028,6 +1055,230 @@
|
||||||
<!-- Label for the highlight color picker in text annotations. -->
|
<!-- Label for the highlight color picker in text annotations. -->
|
||||||
<string name="label_highlight_color">Highlight</string>
|
<string name="label_highlight_color">Highlight</string>
|
||||||
|
|
||||||
|
<!-- Hardcoded UI String Cleanup -->
|
||||||
|
<!-- Home overflow menu item: enables the multi-tab reader mode. -->
|
||||||
|
<string name="options_enable_multi_tab_reading">Enable Multi-Tab Reading</string>
|
||||||
|
<!-- Home overflow menu item: uses stricter MIME filters when choosing local files. -->
|
||||||
|
<string name="options_use_strict_file_filter">Use Strict File Filter</string>
|
||||||
|
<string name="options_language">Language</string>
|
||||||
|
<!-- Debug-only menu item that runs local panel detection diagnostics. -->
|
||||||
|
<string name="options_test_panel_ml_detection">Test Panel ML Detection</string>
|
||||||
|
<!-- Debug-only menu item that runs local speech bubble detection diagnostics. -->
|
||||||
|
<string name="options_test_speech_bubble_ml_detection">Test Speech Bubble ML Detection</string>
|
||||||
|
<!-- Debug-only menu item. %1$d = maximum number of log lines included. -->
|
||||||
|
<string name="options_export_logs_last_lines">Export Logs (Last %1$d lines)</string>
|
||||||
|
<string name="dialog_strict_file_filter_title">Enable Strict File Filter</string>
|
||||||
|
<!-- Warning shown before enabling stricter Android file picker MIME filtering. File extensions should remain uppercase. -->
|
||||||
|
<string name="dialog_strict_file_filter_desc">If you enable this, some supported file types like AZW3, CB7, and FB2 might not show up depending on your file manager.\n\nAre you sure you want to enable this filter?</string>
|
||||||
|
|
||||||
|
<!-- App language picker. Language names are shown in their own language followed by an English hint. -->
|
||||||
|
<string name="language_english_default">English (Default)</string>
|
||||||
|
<string name="language_arabic">العربية (Arabic)</string>
|
||||||
|
<string name="language_german">Deutsch (German)</string>
|
||||||
|
<string name="language_turkish">Türkçe (Turkish)</string>
|
||||||
|
<string name="language_french">Français (French)</string>
|
||||||
|
<string name="language_russian">Русский (Russian)</string>
|
||||||
|
|
||||||
|
<!-- App-wide theme controls in HomeScreen.kt. -->
|
||||||
|
<string name="app_theme_title">App Theme</string>
|
||||||
|
<string name="app_theme_appearance">Appearance</string>
|
||||||
|
<string name="app_theme_contrast">Contrast</string>
|
||||||
|
<string name="app_theme_text_brightness">Text Brightness</string>
|
||||||
|
<string name="app_theme_color_scheme">Color Scheme</string>
|
||||||
|
<string name="app_theme_dynamic">Dynamic</string>
|
||||||
|
<string name="app_theme_create_title">Create App Theme</string>
|
||||||
|
<!-- Fallback name when the user saves a custom app theme without entering a name. -->
|
||||||
|
<string name="app_theme_custom_default_name">Custom Theme</string>
|
||||||
|
<string name="app_theme_preset_ocean">Ocean</string>
|
||||||
|
<string name="app_theme_preset_mint">Mint</string>
|
||||||
|
<string name="app_theme_preset_rose">Rose</string>
|
||||||
|
<string name="app_theme_preset_sepia">Sepia</string>
|
||||||
|
<string name="app_theme_preset_amethyst">Amethyst</string>
|
||||||
|
<string name="app_theme_preset_amber">Amber</string>
|
||||||
|
<string name="app_theme_preset_sapphire">Sapphire</string>
|
||||||
|
<string name="content_desc_add_custom_theme">Add Custom Theme</string>
|
||||||
|
<string name="content_desc_app_theme">App Theme</string>
|
||||||
|
<string name="content_desc_app_icon">App Icon</string>
|
||||||
|
<string name="content_desc_device">Device</string>
|
||||||
|
<string name="content_desc_open_drawer">Open Drawer</string>
|
||||||
|
<string name="content_desc_profile_picture">Profile picture</string>
|
||||||
|
<string name="content_desc_profile">Profile</string>
|
||||||
|
<string name="content_desc_pro_feature">Pro Feature</string>
|
||||||
|
<string name="content_desc_filter">Filter</string>
|
||||||
|
<string name="content_desc_sort">Sort</string>
|
||||||
|
<string name="content_desc_close_search">Close search</string>
|
||||||
|
<string name="content_desc_clear_query">Clear query</string>
|
||||||
|
<string name="content_desc_search_shelf">Search shelf</string>
|
||||||
|
<!-- Accessibility label. %1$s = shelf name. -->
|
||||||
|
<string name="content_desc_shelf_cover">%1$s shelf cover</string>
|
||||||
|
<string name="theme_preserve_image_colors">Preserve Image Colors</string>
|
||||||
|
<string name="theme_preserve_image_colors_desc">Keep original image colors when theme changes</string>
|
||||||
|
|
||||||
|
<!-- Enum labels used by library filters, sorting, and app theme controls. -->
|
||||||
|
<string name="add_books_source_unshelved">Unshelved</string>
|
||||||
|
<string name="add_books_source_all_books">All Books</string>
|
||||||
|
<string name="app_theme_mode_system">System</string>
|
||||||
|
<string name="app_theme_mode_light">Light</string>
|
||||||
|
<string name="app_theme_mode_dark">Dark</string>
|
||||||
|
<string name="app_contrast_standard">Standard</string>
|
||||||
|
<string name="app_contrast_medium">Medium</string>
|
||||||
|
<string name="app_contrast_high">High</string>
|
||||||
|
<string name="sort_recent">Recent</string>
|
||||||
|
<string name="sort_title_az">Title A-Z</string>
|
||||||
|
<string name="sort_author_az">Author A-Z</string>
|
||||||
|
<string name="sort_percent_asc">Percent complete 0-100</string>
|
||||||
|
<string name="sort_percent_desc">Percent complete 100-0</string>
|
||||||
|
<string name="sort_size_smallest">Size (Smallest)</string>
|
||||||
|
<string name="sort_size_biggest">Size (Biggest)</string>
|
||||||
|
<string name="read_status_all">All</string>
|
||||||
|
<string name="read_status_unread">Unread</string>
|
||||||
|
<string name="read_status_in_progress">In Progress</string>
|
||||||
|
<string name="read_status_completed">Completed</string>
|
||||||
|
<!-- Active tag filter chip label. %1$s = tag names or a count, already pluralized by code. -->
|
||||||
|
<string name="filter_tags">Tags: %1$s</string>
|
||||||
|
<string name="section_browse_by_tag">Browse by tag</string>
|
||||||
|
<string name="section_tags">Tags</string>
|
||||||
|
<string name="section_folders">Folders</string>
|
||||||
|
<string name="section_files">Files</string>
|
||||||
|
|
||||||
|
<!-- TTS settings sheet labels. TTS = Text-to-Speech. -->
|
||||||
|
<string name="tts_active_engine">Active TTS Engine</string>
|
||||||
|
<string name="tts_mode_cloud_ai">Cloud AI</string>
|
||||||
|
<string name="tts_mode_device_native">Device Native</string>
|
||||||
|
<string name="tts_tab_cloud_voices">Cloud Voices</string>
|
||||||
|
<string name="tts_tab_device_voices">Device Voices</string>
|
||||||
|
<string name="tts_tab_cloud_cache">Cloud Cache</string>
|
||||||
|
<string name="tts_select_cloud_voice">Select High-Quality Cloud Voice</string>
|
||||||
|
<string name="tts_clear_samples">Clear Samples</string>
|
||||||
|
<string name="tts_system_default_voice">System Default Voice</string>
|
||||||
|
<string name="tts_uses_device_settings">Uses device settings</string>
|
||||||
|
<string name="tts_language_filter">Language Filter</string>
|
||||||
|
<string name="tts_online">Online</string>
|
||||||
|
<string name="tts_offline">Offline</string>
|
||||||
|
<string name="tts_voice_filter">Voice Filter</string>
|
||||||
|
<string name="tts_no_audio_cached_for_voice">No audio cached for this voice.</string>
|
||||||
|
<!-- Button label. %1$s = selected cloud TTS voice identifier. -->
|
||||||
|
<string name="tts_clear_cache_for_voice">Clear Cache for %1$s</string>
|
||||||
|
<string name="tts_voice_sample_generic">This is a voice sample.</string>
|
||||||
|
|
||||||
|
<!-- AI hub and Pro credits. AI = Artificial Intelligence. -->
|
||||||
|
<string name="ai_features_title">AI Features</string>
|
||||||
|
<string name="ai_tab_summary">Summary</string>
|
||||||
|
<string name="ai_tab_recap">Recap</string>
|
||||||
|
<string name="ai_tab_cache">Cache</string>
|
||||||
|
<!-- %1$s = chapter title, lowercased by code. -->
|
||||||
|
<string name="ai_no_summary_for_chapter">No summary for %1$s yet.</string>
|
||||||
|
<!-- %1$s = chapter title. -->
|
||||||
|
<string name="ai_generate_summary_for_chapter">Generate Summary for %1$s</string>
|
||||||
|
<string name="ai_recap_desc">Get a recap of the story up to your current position.</string>
|
||||||
|
<string name="ai_generate_story_recap">Generate Story Recap</string>
|
||||||
|
<string name="ai_story_recap">Story Recap</string>
|
||||||
|
<string name="ai_cache_hit_free">Cache Hit • Free</string>
|
||||||
|
<!-- %1$d = number of free daily summaries remaining. -->
|
||||||
|
<string name="ai_generated_free_remaining">Generated • Free (%1$d/10 left)</string>
|
||||||
|
<!-- %1$s = credit cost returned by the server. -->
|
||||||
|
<string name="ai_generated_cost">Generated • Cost: %1$s credits</string>
|
||||||
|
<string name="ai_generating_cost_calculating">Generating… • Cost: Calculating</string>
|
||||||
|
<string name="ai_output_title">AI Output</string>
|
||||||
|
<string name="ai_regenerate">Regenerate</string>
|
||||||
|
<string name="ai_no_cached_summaries">No cached summaries for this book.</string>
|
||||||
|
<string name="credits_tab">Credits</string>
|
||||||
|
<string name="credits_title">AI & Cloud Credits</string>
|
||||||
|
<string name="credits_available">Credits Available</string>
|
||||||
|
<!-- Compact account badge. %1$d = remaining credit count. -->
|
||||||
|
<string name="credits_count">%1$d Credits</string>
|
||||||
|
<string name="credits_estimated_cost_breakdown">Estimated Cost Breakdown</string>
|
||||||
|
<string name="credits_cloud_tts_title">Cloud TTS</string>
|
||||||
|
<string name="credits_cloud_tts_desc">Cost: ~3-4 credits per minute of audio generated.\nTo enable: Reader Screen > More > TTS Voice Settings.</string>
|
||||||
|
<string name="credits_ai_summaries_title">AI Summaries & Recap</string>
|
||||||
|
<string name="credits_ai_summaries_desc">Cost: ~1-4 credits per request based on chapter length.\nPro Users get 10 free summaries daily.</string>
|
||||||
|
<string name="legal_by_purchasing">By purchasing,</string>
|
||||||
|
<string name="dialog_out_of_credits_title">Out of Credits</string>
|
||||||
|
<string name="dialog_out_of_credits_desc">You don\'t have enough credits. Get Episteme Pro for 10 free Summaries per day, or add more credits to use Summaries, Cloud TTS and Story Recap.</string>
|
||||||
|
<string name="action_get_pro_or_add_credits">Get Pro / Add Credits</string>
|
||||||
|
<string name="dialog_unlock_page_summarization">Unlock Page Summarization</string>
|
||||||
|
<string name="dialog_unlock_page_summarization_desc">Get concise summaries of any page with Episteme Pro. Upgrade to start using this feature.</string>
|
||||||
|
<string name="dialog_download_bubble_zoom_model">Download Bubble Zoom Model</string>
|
||||||
|
<string name="dialog_download_bubble_zoom_model_desc">To use the Bubble Zoom feature, an AI model needs to be downloaded (~134 MB). Do you want to download it now?</string>
|
||||||
|
|
||||||
|
<!-- PDF and annotation helpers. -->
|
||||||
|
<string name="action_translate">Translate</string>
|
||||||
|
<string name="pdf_back_to_page_short">Back to Pg %1$d</string>
|
||||||
|
<!-- Compact page label used in floating PDF search/navigation controls. %1$d = 1-indexed page number. -->
|
||||||
|
<string name="pdf_page_short">Page %1$d</string>
|
||||||
|
<!-- Floating PDF search/navigation control. %1$d = current result number; %2$d = total result count. -->
|
||||||
|
<string name="pdf_search_result_position">Result %1$d / %2$d</string>
|
||||||
|
<!-- Error shown when the PDF renderer cannot open a document and did not provide a specific error message. -->
|
||||||
|
<string name="error_failed_load_pdf">Failed to load PDF.</string>
|
||||||
|
<!-- Progress banner while downloading the optional Bubble Zoom AI model. %1$d = percentage complete (0-100). -->
|
||||||
|
<string name="msg_downloading_bubble_zoom_model_progress">Downloading Bubble Zoom model… %1$d%%</string>
|
||||||
|
<string name="content_desc_exit_slider_navigation">Exit slider navigation</string>
|
||||||
|
<string name="content_desc_jump_back">Jump Back</string>
|
||||||
|
<string name="content_desc_scroll_to_reading_page">Scroll to reading page</string>
|
||||||
|
<string name="content_desc_annotated_page">Annotated Page</string>
|
||||||
|
<string name="content_desc_close_image">Close Image</string>
|
||||||
|
<string name="content_desc_toggle_search_highlights">Toggle search highlights</string>
|
||||||
|
<string name="content_desc_drag_text_box">Drag to move text box</string>
|
||||||
|
<string name="content_desc_no_files_icon">No files icon</string>
|
||||||
|
<!-- Accessibility label. %1$s = visible field label being copied. -->
|
||||||
|
<string name="content_desc_copy_value">Copy %1$s</string>
|
||||||
|
<string name="content_desc_tag">Tag</string>
|
||||||
|
<string name="content_desc_list_item_marker">List item marker</string>
|
||||||
|
<string name="content_desc_reset_zoom">Reset Zoom</string>
|
||||||
|
<string name="content_desc_generate_demo_annotations">Generate Demo Annotations</string>
|
||||||
|
<string name="tooltip_demo_annotations">Demo Annotations</string>
|
||||||
|
<string name="content_desc_open_pen_playground">Open Pen Playground</string>
|
||||||
|
<string name="content_desc_new_tab">New Tab</string>
|
||||||
|
<string name="content_desc_highlight_all_text">Highlight all text</string>
|
||||||
|
<string name="content_desc_toggle_editing_mode">Toggle Editing Mode</string>
|
||||||
|
<string name="content_desc_smart_comic_zoom">Smart Comic Zoom</string>
|
||||||
|
<string name="highlight_customize_title">Customize Highlights</string>
|
||||||
|
<string name="ocr_language_latin">English, Spanish, French, etc.</string>
|
||||||
|
<string name="ocr_language_devanagari">Hindi, Marathi, Sanskrit + English</string>
|
||||||
|
<string name="ocr_language_chinese">Chinese + English</string>
|
||||||
|
<string name="ocr_language_japanese">Japanese + English</string>
|
||||||
|
<string name="ocr_language_korean">Korean + English</string>
|
||||||
|
|
||||||
<!-- UniversalDocument -->
|
<!-- UniversalDocument -->
|
||||||
<string name="msg_page_unavailable">Page Unavailable</string>
|
<string name="msg_page_unavailable">Page Unavailable</string>
|
||||||
|
|
||||||
|
<!-- ReflowWorker: generated Text View records. "Text View" is the PDF reflow feature name. -->
|
||||||
|
<string name="default_document_title">Document</string>
|
||||||
|
<string name="generated_author">Generated</string>
|
||||||
|
<string name="reflow_display_name_format">%1$s (Text View)</string>
|
||||||
|
<string name="reflow_title_format">%1$s (Reflow)</string>
|
||||||
|
|
||||||
|
<!-- Shared tag management UI. -->
|
||||||
|
<string name="action_add_edit">Add / Edit</string>
|
||||||
|
<string name="msg_no_tags_assigned">No tags assigned.</string>
|
||||||
|
<string name="title_apply_tags">Apply Tags</string>
|
||||||
|
<string name="placeholder_search_create_tag">Search or create tag…</string>
|
||||||
|
<!-- Tag creation row. %1$s = tag name typed by the user. -->
|
||||||
|
<string name="action_create_tag">Create \"%1$s\"</string>
|
||||||
|
|
||||||
|
<!-- EPUB/Paginated reader controls. -->
|
||||||
|
<string name="setting_pull_distance_change_chapter">Pull Distance to Change Chapter</string>
|
||||||
|
<string name="label_short">Short</string>
|
||||||
|
<string name="label_long">Long</string>
|
||||||
|
<!-- Compact TTS speed label. %1$s = formatted multiplier, e.g. 1.2. -->
|
||||||
|
<string name="tts_speed_short">Spd: %1$sx</string>
|
||||||
|
<!-- Compact TTS pitch label. %1$s = formatted multiplier, e.g. 1.0. -->
|
||||||
|
<string name="tts_pitch_short">Ptch: %1$sx</string>
|
||||||
|
<string name="content_desc_play_pause">Play/Pause</string>
|
||||||
|
<string name="content_desc_reset_speed">Reset Speed</string>
|
||||||
|
<string name="content_desc_reset_pitch">Reset Pitch</string>
|
||||||
|
<string name="dialog_select_color">Select Color</string>
|
||||||
|
<string name="msg_book_no_content">This book has no content to display.</string>
|
||||||
|
<string name="clip_label_copied_link">Copied Link</string>
|
||||||
|
<string name="clip_label_copied_text">Copied Text</string>
|
||||||
|
|
||||||
|
<!-- PDF toolbar labels. -->
|
||||||
|
<string name="content_desc_table_of_contents">Table of Contents</string>
|
||||||
|
<string name="content_desc_bookmark">Bookmark</string>
|
||||||
|
<!-- Tooltip/button text. %1$d = 1-indexed page number. -->
|
||||||
|
<string name="action_jump_back_to_page">Jump Back to Page %1$d</string>
|
||||||
|
<string name="desc_return_to_previous_page">Return to previous page</string>
|
||||||
|
<string name="action_exit_smart_zoom">Exit Smart Zoom</string>
|
||||||
|
<string name="action_smart_comic_zoom">Smart Comic Zoom</string>
|
||||||
|
<string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,7 @@
|
||||||
<locale android:name="en"/>
|
<locale android:name="en"/>
|
||||||
<locale android:name="ar"/>
|
<locale android:name="ar"/>
|
||||||
<locale android:name="de"/>
|
<locale android:name="de"/>
|
||||||
|
<locale android:name="fr"/>
|
||||||
|
<locale android:name="ru"/>
|
||||||
<locale android:name="tr"/>
|
<locale android:name="tr"/>
|
||||||
</locale-config>
|
</locale-config>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue