v1.0.47 (#279)
* Add performance and stylus debugging logs * Refactor and decouple UI models from `MainViewModel` * Refactor library state management and projection logic * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Introduce ReaderEngine and enhance EPUB reader features in windows app * Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support. * Implement PDF rendering and text extraction for desktop using Pdfium * Add `NonReaderScreens.kt` and UI dependencies * Refactor and centralize library state management and models to improve cross-platform consistency * Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing * Implement PDF annotation system and enhanced zoom controls for the desktop viewer * Implement WebView-based EPUB rendering for desktop using CEF and embedded resources * Optimize UI state projection, navigation state handling, and main screen pager performance * Implement Bring Your Own Key (BYOK) support for AI features in OSS version * Support Gemini-based Cloud TTS with BYOK support for OSS builds * Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling. * crash fixes * Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6 * Implement dynamic bottom padding for the page info bar to account for device rounded corners * Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar` * Optimize PDF tiling performance and refine pan-and-fling gesture handling * Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers * Updated UI for customize toolbar * Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls * restructure PDF viewer UI component hierarchy to fix verifier crash * Implement separate text dimming factors for light and dark themes * Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers * Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning * Centralize file type resolution logic and implement HTML sanitization during import * Introduce vertical margin customization and configurable progress bar positioning * texture support in epub reader * Enhance TTS session management, progress tracking, and diagnostic logging * Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic. * Refine TTS page mapping for PDF and overhaul TTS control UI * Implement natural session completion logic in `TtsPlaybackManager` for cloud tts * Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen` * Refine TTS playback continuity across PDF pages and improve state management for session transitions * Implement global texture transparency and enhance textured theme support across PDF and EPUB readers. * Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode * Add Support Project screen * Optimize library performance via projection caching, batch database updates, and scoped folder synchronization. * Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic * Bump version to 1.0.47 (51)
This commit is contained in:
parent
f42de6b462
commit
d7a9cae9e1
126 changed files with 15287 additions and 3154 deletions
314
app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
Normal file
314
app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
package com.aryan.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AiSettingsScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var settings by remember { mutableStateOf(loadAiByokSettings(context)) }
|
||||
var selectedProvider by remember { mutableStateOf("gemini") }
|
||||
var providerMenuExpanded by remember { mutableStateOf(false) }
|
||||
var pendingKey by remember { mutableStateOf("") }
|
||||
var showSaveConfirm by remember { mutableStateOf(false) }
|
||||
var providerToDelete by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
fun refresh() {
|
||||
settings = loadAiByokSettings(context)
|
||||
}
|
||||
|
||||
fun updateModels(newSettings: AiByokSettings) {
|
||||
saveAiByokSettings(context, newSettings)
|
||||
settings = loadAiByokSettings(context)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
topBar = {
|
||||
CustomTopAppBar(
|
||||
title = { Text("AI keys and models") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Saved keys", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
SavedKeyRow("Gemini", maskedAiByokKey(context, "gemini"), onDelete = { providerToDelete = "gemini" })
|
||||
SavedKeyRow("Groq", maskedAiByokKey(context, "groq"), onDelete = { providerToDelete = "groq" })
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Add or replace key", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = providerMenuExpanded,
|
||||
onExpandedChange = { providerMenuExpanded = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedProvider.replaceFirstChar { it.titlecase() },
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Provider") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = providerMenuExpanded) },
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = providerMenuExpanded,
|
||||
onDismissRequest = { providerMenuExpanded = false }
|
||||
) {
|
||||
listOf("gemini", "groq").forEach { provider ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(provider.replaceFirstChar { it.titlecase() }) },
|
||||
onClick = {
|
||||
selectedProvider = provider
|
||||
providerMenuExpanded = false
|
||||
},
|
||||
trailingIcon = if (provider == selectedProvider) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = pendingKey,
|
||||
onValueChange = { pendingKey = it },
|
||||
label = { Text("API key") },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = { showSaveConfirm = true },
|
||||
enabled = pendingKey.isNotBlank(),
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Save key")
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("Use one model for all features", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"When off, each reader AI feature uses its own selected model.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = settings.useOneModel,
|
||||
onCheckedChange = { updateModels(settings.copy(useOneModel = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.useOneModel) {
|
||||
ModelSelector(
|
||||
title = "All AI features",
|
||||
description = "Smart dictionary, summaries, and recaps all use this model.",
|
||||
selectedId = settings.modelForAll,
|
||||
onSelected = { updateModels(settings.copy(modelForAll = it)) }
|
||||
)
|
||||
} else {
|
||||
ModelSelector(
|
||||
title = "Smart dictionary",
|
||||
description = "Used when defining selected words or phrases.",
|
||||
selectedId = settings.defineModel,
|
||||
onSelected = { updateModels(settings.copy(defineModel = it)) }
|
||||
)
|
||||
ModelSelector(
|
||||
title = "Summaries",
|
||||
description = "Used for EPUB summaries and PDF page summaries. PDF/image summaries need Gemini.",
|
||||
selectedId = settings.summarizeModel,
|
||||
onSelected = { updateModels(settings.copy(summarizeModel = it)) }
|
||||
)
|
||||
ModelSelector(
|
||||
title = "Recaps",
|
||||
description = "Used for story recap generation.",
|
||||
selectedId = settings.recapModel,
|
||||
onSelected = { updateModels(settings.copy(recapModel = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
ModelSelector(
|
||||
title = "Cloud TTS",
|
||||
description = "Uses the saved Gemini key. Only $GEMINI_CLOUD_TTS_MODEL is supported for now.",
|
||||
selectedId = settings.ttsModel,
|
||||
options = listOf(AiModelOption("gemini", GEMINI_CLOUD_TTS_MODEL)),
|
||||
onSelected = { updateModels(settings.copy(ttsModel = it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSaveConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSaveConfirm = false },
|
||||
title = { Text("Save ${selectedProvider.replaceFirstChar { it.titlecase() }} key?") },
|
||||
text = { Text("After saving, only the first 3 and last 3 characters will be visible. To change it later, replace or delete it.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
saveAiByokKey(context, selectedProvider, pendingKey)
|
||||
pendingKey = ""
|
||||
showSaveConfirm = false
|
||||
refresh()
|
||||
}) { Text("Save") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showSaveConfirm = false }) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
providerToDelete?.let { provider ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { providerToDelete = null },
|
||||
title = { Text("Delete ${provider.replaceFirstChar { it.titlecase() }} key?") },
|
||||
text = { Text("Features using this provider will stop working until a new key is saved.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
deleteAiByokKey(context, provider)
|
||||
providerToDelete = null
|
||||
refresh()
|
||||
}) { Text("Delete") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { providerToDelete = null }) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedKeyRow(
|
||||
label: String,
|
||||
maskedKey: String,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(label) },
|
||||
supportingContent = {
|
||||
Text(maskedKey.ifBlank { "No key saved" })
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = onDelete, enabled = maskedKey.isNotBlank()) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete $label key")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ModelSelector(
|
||||
title: String,
|
||||
description: String,
|
||||
selectedId: String,
|
||||
options: List<AiModelOption> = aiByokModelOptions,
|
||||
onSelected: (String) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val selected = options.firstOrNull { it.id == selectedId }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selected?.label ?: "No model selected",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Model") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("No model selected") },
|
||||
onClick = {
|
||||
onSelected("")
|
||||
expanded = false
|
||||
},
|
||||
trailingIcon = if (selectedId.isBlank()) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null
|
||||
)
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option.label) },
|
||||
onClick = {
|
||||
onSelected(option.id)
|
||||
expanded = false
|
||||
},
|
||||
trailingIcon = if (option.id == selected?.id) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue