phase 0: fork Episteme, rename package to org.dueattendant149.bookreader
- Cloned from Aryan-Raj3112/episteme (AGPL-3.0) - Package: com.aryan.reader → org.dueattendant149.bookreader - Application ID: org.dueattendant149.bookreader - Added AGENTS.md with migration plan - Upstream: github.com/Aryan-Raj3112/episteme - Origin: git.dueattendant149.org/Atte149/book-reader
This commit is contained in:
parent
e615128a23
commit
5f64f3d722
631 changed files with 3082 additions and 3006 deletions
|
|
@ -0,0 +1,322 @@
|
|||
package org.dueattendant149.bookreader
|
||||
|
||||
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.res.stringResource
|
||||
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) }
|
||||
val providerLabels = mapOf(
|
||||
"gemini" to stringResource(R.string.provider_gemini),
|
||||
"groq" to stringResource(R.string.provider_groq),
|
||||
)
|
||||
|
||||
fun refresh() {
|
||||
settings = loadAiByokSettings(context)
|
||||
}
|
||||
|
||||
fun updateModels(newSettings: AiByokSettings) {
|
||||
saveAiByokSettings(context, newSettings)
|
||||
settings = loadAiByokSettings(context)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
topBar = {
|
||||
CustomTopAppBar(
|
||||
title = { Text(stringResource(R.string.ai_settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.ai_settings_saved_keys), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
SavedKeyRow(providerLabels.getValue("gemini"), maskedAiByokKey(context, "gemini"), onDelete = { providerToDelete = "gemini" })
|
||||
SavedKeyRow(providerLabels.getValue("groq"), maskedAiByokKey(context, "groq"), onDelete = { providerToDelete = "groq" })
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text(stringResource(R.string.ai_settings_add_or_replace_key), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = providerMenuExpanded,
|
||||
onExpandedChange = { providerMenuExpanded = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = providerLabels[selectedProvider].orEmpty(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.label_provider)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = providerMenuExpanded) },
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = providerMenuExpanded,
|
||||
onDismissRequest = { providerMenuExpanded = false }
|
||||
) {
|
||||
listOf("gemini", "groq").forEach { provider ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(providerLabels[provider].orEmpty()) },
|
||||
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(stringResource(R.string.label_api_key)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = { showSaveConfirm = true },
|
||||
enabled = pendingKey.isNotBlank(),
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text(stringResource(R.string.ai_settings_save_key))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.ai_settings_use_one_model), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
stringResource(R.string.ai_settings_use_one_model_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = settings.useOneModel,
|
||||
onCheckedChange = { updateModels(settings.copy(useOneModel = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.useOneModel) {
|
||||
ModelSelector(
|
||||
title = stringResource(R.string.ai_settings_all_features),
|
||||
description = stringResource(R.string.ai_settings_all_features_desc),
|
||||
selectedId = settings.modelForAll,
|
||||
onSelected = { updateModels(settings.copy(modelForAll = it)) }
|
||||
)
|
||||
} else {
|
||||
ModelSelector(
|
||||
title = stringResource(R.string.ai_settings_smart_dictionary),
|
||||
description = stringResource(R.string.ai_settings_smart_dictionary_desc),
|
||||
selectedId = settings.defineModel,
|
||||
onSelected = { updateModels(settings.copy(defineModel = it)) }
|
||||
)
|
||||
ModelSelector(
|
||||
title = stringResource(R.string.ai_settings_summaries),
|
||||
description = stringResource(R.string.ai_settings_summaries_desc),
|
||||
selectedId = settings.summarizeModel,
|
||||
onSelected = { updateModels(settings.copy(summarizeModel = it)) }
|
||||
)
|
||||
ModelSelector(
|
||||
title = stringResource(R.string.ai_settings_recaps),
|
||||
description = stringResource(R.string.ai_settings_recaps_desc),
|
||||
selectedId = settings.recapModel,
|
||||
onSelected = { updateModels(settings.copy(recapModel = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
ModelSelector(
|
||||
title = stringResource(R.string.credits_cloud_tts_title),
|
||||
description = stringResource(R.string.ai_settings_cloud_tts_desc, GEMINI_CLOUD_TTS_MODEL),
|
||||
selectedId = settings.ttsModel,
|
||||
options = listOf(AiModelOption("gemini", GEMINI_CLOUD_TTS_MODEL)),
|
||||
onSelected = { updateModels(settings.copy(ttsModel = it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSaveConfirm) {
|
||||
val providerLabel = providerLabels[selectedProvider].orEmpty()
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSaveConfirm = false },
|
||||
title = { Text(stringResource(R.string.dialog_save_provider_key, providerLabel)) },
|
||||
text = { Text(stringResource(R.string.dialog_save_key_desc)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
saveAiByokKey(context, selectedProvider, pendingKey)
|
||||
pendingKey = ""
|
||||
showSaveConfirm = false
|
||||
refresh()
|
||||
}) { Text(stringResource(R.string.action_save)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showSaveConfirm = false }) { Text(stringResource(R.string.action_cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
providerToDelete?.let { provider ->
|
||||
val providerLabel = providerLabels[provider].orEmpty()
|
||||
AlertDialog(
|
||||
onDismissRequest = { providerToDelete = null },
|
||||
title = { Text(stringResource(R.string.dialog_delete_provider_key, providerLabel)) },
|
||||
text = { Text(stringResource(R.string.dialog_delete_key_desc)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
deleteAiByokKey(context, provider)
|
||||
providerToDelete = null
|
||||
refresh()
|
||||
}) { Text(stringResource(R.string.action_delete)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { providerToDelete = null }) { Text(stringResource(R.string.action_cancel)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SavedKeyRow(
|
||||
label: String,
|
||||
maskedKey: String,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val noKeySaved = stringResource(R.string.ai_settings_no_key_saved)
|
||||
ListItem(
|
||||
headlineContent = { Text(label) },
|
||||
supportingContent = {
|
||||
Text(maskedKey.ifBlank { noKeySaved })
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = onDelete, enabled = maskedKey.isNotBlank()) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.content_desc_delete_provider_key, label))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 ?: stringResource(R.string.ai_settings_no_model_selected),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.label_model)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.ai_settings_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