package org.terst.feedreader.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api 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.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.terst.feedreader.data.AppDatabase import org.terst.feedreader.data.FilterAction import org.terst.feedreader.data.FilterRuleEntity import org.terst.feedreader.data.Net import org.terst.feedreader.data.Settings import org.terst.feedreader.stash.StashClient import org.terst.feedreader.stash.StashVerifyResult @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(onBack: () -> Unit) { val context = LocalContext.current val db = remember { AppDatabase.get(context) } val settings = remember { Settings(context) } val scope = rememberCoroutineScope() var stashUrl by remember { mutableStateOf("") } var stashKey by remember { mutableStateOf("") } var stashKeyVisible by remember { mutableStateOf(false) } var isVerifyingStash by remember { mutableStateOf(false) } var stashStatus by remember { mutableStateOf(null) } LaunchedEffect(Unit) { stashUrl = settings.stashBaseUrlNow() stashKey = settings.stashApiKeyNow() } var newRulePattern by remember { mutableStateOf("") } var newRuleAction by remember { mutableStateOf(FilterAction.MUTE) } var actionMenuExpanded by remember { mutableStateOf(false) } val rules by db.filterRuleDao().observeAll().collectAsState(initial = emptyList()) Scaffold( topBar = { TopAppBar( title = { Text("Settings") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Default.ArrowBack, contentDescription = "Back") } } ) } ) { padding -> LazyColumn(modifier = Modifier.fillMaxWidth().padding(padding)) { item { Column(modifier = Modifier.padding(16.dp)) { Text("FeeDreader", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(4.dp)) Text( "If a site paywalls its own feed, that's the site's call. But this reader " + "won't go out of its way to help a soft paywall block you from reading " + "something you already subscribed to get -- reader mode extracts the " + "article you were sent, nothing more, nothing sneaky.", style = MaterialTheme.typography.bodyMedium ) } HorizontalDivider() } item { Column(modifier = Modifier.padding(16.dp)) { Text("Stash", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = stashUrl, onValueChange = { stashUrl = it }, label = { Text("Base URL") }, placeholder = { Text("https://stash.example.com") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = stashKey, onValueChange = { stashKey = it; stashStatus = null }, label = { Text("API key") }, singleLine = true, visualTransformation = if (stashKeyVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { IconButton(onClick = { stashKeyVisible = !stashKeyVisible }) { Icon( if (stashKeyVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = if (stashKeyVisible) "Hide API key" else "Show API key" ) } }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) Button( enabled = !isVerifyingStash, onClick = { scope.launch { isVerifyingStash = true stashStatus = null val client = StashClient(Net.client, stashUrl.trim(), stashKey.trim()) when (val result = client.verifyCredentials()) { is StashVerifyResult.Valid -> { settings.setStash(stashUrl.trim(), stashKey.trim()) stashStatus = "Verified and saved" } is StashVerifyResult.Invalid -> { stashStatus = result.message } } isVerifyingStash = false } }, modifier = Modifier.fillMaxWidth() ) { if (isVerifyingStash) { CircularProgressIndicator(modifier = Modifier.height(20.dp).width(20.dp)) } else { Text("Verify and save") } } stashStatus?.let { Spacer(modifier = Modifier.height(8.dp)) Text(it, style = MaterialTheme.typography.bodySmall) } } HorizontalDivider() } item { Column(modifier = Modifier.padding(16.dp)) { Text("Filter rules", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) Row(modifier = Modifier.fillMaxWidth()) { OutlinedTextField( value = newRulePattern, onValueChange = { newRulePattern = it }, label = { Text("Keyword") }, modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(8.dp)) Box { Button(onClick = { actionMenuExpanded = true }) { Text(if (newRuleAction == FilterAction.MUTE) "Mute" else "Highlight") } DropdownMenu(expanded = actionMenuExpanded, onDismissRequest = { actionMenuExpanded = false }) { DropdownMenuItem(text = { Text("Mute") }, onClick = { newRuleAction = FilterAction.MUTE; actionMenuExpanded = false }) DropdownMenuItem(text = { Text("Highlight") }, onClick = { newRuleAction = FilterAction.HIGHLIGHT; actionMenuExpanded = false }) } } } Spacer(modifier = Modifier.height(8.dp)) Button( enabled = newRulePattern.isNotBlank(), onClick = { scope.launch { db.filterRuleDao().insert( FilterRuleEntity( pattern = newRulePattern.trim(), action = newRuleAction, createdAt = System.currentTimeMillis() ) ) newRulePattern = "" } }, modifier = Modifier.fillMaxWidth() ) { Text("Add rule") } } } items(rules, key = { it.id }) { rule -> ListItem( headlineContent = { Text(rule.pattern) }, supportingContent = { Text(if (rule.action == FilterAction.MUTE) "Mute" else "Highlight") }, trailingContent = { IconButton(onClick = { scope.launch { db.filterRuleDao().delete(rule.id) } }) { Icon(Icons.Default.Delete, contentDescription = "Delete rule") } } ) HorizontalDivider() } } } }