package com.vgmlr.shim
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
class ShimSettings : ComponentActivity() {
private lateinit var themeManager: ShimThemeManager
private lateinit var db: ShimDatabase
private val btnShape = RoundedCornerShape(8.dp)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT))
themeManager = ShimThemeManager(this)
db = ShimDatabase(this)
setContent {
ShimTheme(themeManager) {
SettingsScreen()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var bgColor by remember { mutableStateOf(themeManager.hexBgColor) }
var textColor by remember { mutableStateOf(themeManager.hexTextColor) }
var hashColor by remember { mutableStateOf(themeManager.hexHashColor) }
var fontSize by remember { mutableStateOf(themeManager.fontSize.floatValue.toString()) }
var lineHeight by remember { mutableStateOf(themeManager.lineHeight.floatValue.toString()) }
var lineSpacing by remember { mutableStateOf(themeManager.lineSpacing.floatValue.toString()) }
var isMonospace by remember { mutableStateOf(themeManager.isMonospace.value) }
val backup = remember { ShimBackUp.loadSettings(context) }
var bkInterval by remember { mutableIntStateOf(backup.intervalMinutes) }
var bkLocation by remember { mutableStateOf(backup.location) }
var bkEncrypt by remember { mutableStateOf(backup.encrypt) }
var bkPhrase by remember { mutableStateOf(backup.phrase) }
val bkPhraseMissing = bkEncrypt && bkPhrase.isBlank()
var restoreStatus by remember { mutableStateOf("") }
var showDeleteDialog by remember { mutableStateOf(false) }
val archived = remember(backup.stamp) {
if (backup.stamp == 0L) "none"
else OTCClock.getOTCTime(
LocalDateTime.ofInstant(Instant.ofEpochMilli(backup.stamp), ZoneId.systemDefault())
)
}
val folderPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree()
) { uri: Uri? ->
uri?.let {
try {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
bkLocation = it.toString()
} catch (_: Exception) { }
}
}
LaunchedEffect(bgColor, textColor, hashColor, fontSize, lineHeight, lineSpacing, isMonospace) {
delay(500)
if (!isHex(bgColor) || !isHex(textColor) || !isHex(hashColor)) return@LaunchedEffect
themeManager.saveTheme(bgColor, textColor, hashColor, isMonospace, fontSize, lineHeight, lineSpacing)
}
LaunchedEffect(bkInterval, bkLocation, bkEncrypt, bkPhrase) {
if (bkPhraseMissing) return@LaunchedEffect
delay(500)
ShimBackUp.saveSettings(context, bkInterval, bkLocation, bkEncrypt, bkPhrase)
ShimBackUp.schedule(context, bkInterval)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Settings", fontSize = 18.sp, color = Color.White) },
navigationIcon = {
IconButton(onClick = { finish() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.White)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF486860))
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0),
containerColor = themeManager.backgroundColor.value
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.imePadding()
.background(themeManager.backgroundColor.value)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
SectionHeader("Appearance")
SettingItem("Ground Color", bgColor, true) { bgColor = it }
SettingItem("Font Color", textColor, true) { textColor = it }
SettingItem("Hash Tag Color", hashColor, true) { hashColor = it }
SettingItem("Font Size", fontSize, false) { fontSize = it }
SettingItem("Line Height", lineHeight, false) { lineHeight = it }
SettingItem("Letter Spacing", lineSpacing, false) { lineSpacing = it }
DropdownItem("Monospace", isMonospace.toString(), listOf("true", "false")) {
isMonospace = it.toBoolean()
}
SectionHeader("Archive", topPad = 6.dp)
DropdownItem(
"Interval",
ShimBackUp.intervalLabel(bkInterval),
ShimBackUp.INTERVALS.map { ShimBackUp.intervalLabel(it) }
) { label ->
bkInterval = ShimBackUp.INTERVALS.first { ShimBackUp.intervalLabel(it) == label }
}
val locationName = remember(bkLocation) {
if (bkLocation.isEmpty()) "" else try {
DocumentFile.fromTreeUri(context, bkLocation.toUri())?.name ?: bkLocation
} catch (_: Exception) { bkLocation }
}
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) { SetLabel("Location") }
Box(
modifier = Modifier.weight(1f).clickable { folderPicker.launch(null) }
) {
OutlinedTextField(
value = if (locationName.length > 25) locationName.take(22) + "..." else locationName,
onValueChange = {},
readOnly = true,
enabled = false,
singleLine = true,
placeholder = { Text("app/", color = labelCol().copy(alpha = 0.5f)) },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
disabledTextColor = themeManager.textColor.value,
disabledPlaceholderColor = themeManager.textColor.value.copy(alpha = 0.5f),
disabledBorderColor = themeManager.textColor.value.copy(alpha = 0.2f)
)
)
}
}
Spacer(modifier = Modifier.height(12.dp))
DropdownItem("Encrypt", if (bkEncrypt) "True" else "False", listOf("True", "False")) {
bkEncrypt = it == "True"
}
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) { SetLabel("Phrase") }
OutlinedTextField(
value = bkPhrase,
onValueChange = { bkPhrase = it },
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { if (bkEncrypt) Text("Required", color = labelCol().copy(alpha = 0.5f)) },
colors = fieldCol(),
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
}
Spacer(modifier = Modifier.height(20.dp))
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) { SetLabel("Archived") }
Text(
text = archived,
color = themeManager.textColor.value,
fontSize = 15.sp,
fontFamily = FontFamily.Monospace
)
}
Spacer(modifier = Modifier.height(20.dp))
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) { SetLabel("Restore") }
Button(
onClick = {
scope.launch {
restoreStatus = if (ShimBackUp.restore(context)) "Restored" else "Failed"
delay(2000)
restoreStatus = ""
}
},
enabled = !bkPhraseMissing,
modifier = Modifier.weight(1f).height(48.dp),
shape = btnShape,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Gray,
contentColor = Color.Black,
disabledContainerColor = Color.Gray.copy(alpha = 0.4f),
disabledContentColor = Color.Black.copy(alpha = 0.5f)
)
) {
Text(
text = restoreStatus.ifEmpty { "Restore Archive" },
fontSize = 16.sp
)
}
}
SectionHeader("Delete")
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(modifier = Modifier.width(140.dp)) { SetLabel("Delete Shims") }
Button(
onClick = { showDeleteDialog = true },
modifier = Modifier.weight(1f).height(48.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF8B0000)),
shape = btnShape
) {
Text(text = "Delete", color = Color(0xFFEEEEEE), fontSize = 16.sp)
}
}
Spacer(modifier = Modifier.height(80.dp))
if (showDeleteDialog) {
ConfirmDialog(
message = "Delete Shims?",
onDismiss = { showDeleteDialog = false },
onConfirm = {
db.deleteAllShims()
showDeleteDialog = false
}
)
}
}
}
}
private fun isHex(value: String): Boolean =
runCatching { (if (value.startsWith("#")) value else "#$value").toColorInt() }.isSuccess
@Composable
private fun labelCol(): Color = themeManager.textColor.value.copy(alpha = 0.6f)
@Composable
private fun fieldCol() = OutlinedTextFieldDefaults.colors(
focusedTextColor = themeManager.textColor.value,
unfocusedTextColor = themeManager.textColor.value,
focusedBorderColor = themeManager.textColor.value.copy(alpha = 0.5f),
unfocusedBorderColor = themeManager.textColor.value.copy(alpha = 0.2f),
cursorColor = themeManager.textColor.value
)
@Composable
fun SetLabel(text: String) {
Text(text, color = labelCol(), fontSize = 16.sp, fontWeight = FontWeight.Bold)
}
@Composable
fun SectionHeader(title: String, topPad: Dp = 20.dp) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = topPad, bottom = 20.dp)
.background(colorResource(id = R.color.search_bg_color)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = title,
modifier = Modifier.padding(vertical = 7.dp),
color = labelCol(),
style = MaterialTheme.typography.bodyMedium.copy(letterSpacing = 1.5.sp)
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropdownItem(label: String, value: String, options: List<String>, onSelect: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) { SetLabel(label) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
value = value,
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth(),
colors = fieldCol()
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option) },
onClick = { onSelect(option); expanded = false }
)
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
}
@Composable
fun SettingItem(label: String, value: String, isColor: Boolean, onValueChange: (String) -> Unit) {
val labelCol = labelCol()
val bCol = themeManager.textColor.value.copy(alpha = 0.2f)
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(140.dp)) {
Text(
label,
color = labelCol,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
if (isColor) {
val previewColor = try { Color((if (value.startsWith("#")) value else "#$value").toColorInt()) } catch (_: Exception) { Color.Gray }
Box(
modifier = Modifier
.size(56.dp)
.background(previewColor, RoundedCornerShape(4.dp))
.border(1.dp, bCol, RoundedCornerShape(4.dp))
)
Spacer(Modifier.width(12.dp))
}
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.weight(1f),
colors = fieldCol(),
placeholder = {
Text(
when(label) {
"Font Size" -> "15.5"
"Line Height" -> "1.35"
"Letter Spacing" -> "0.2"
else -> "#000000"
},
color = labelCol
)
},
singleLine = true,
)
}
Spacer(modifier = Modifier.height(12.dp))
}
}
}