package com.vgmlr.kiln
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val ROW_PAD_H = 18.dp
private val FACE_SIZE = 96.dp
private val FACE_GAP = 10.dp
private val FIELD_TEXT = 17.sp
private val GRID_PAD = 14.dp
private val GRID_GAP = 12.dp
private val CARD_SHAPE = RoundedCornerShape(14.dp)
private val CARD_PAD = 14.dp
private val CARD_BORDER = 1.dp
private val CARD_ELEV = 6.dp
private val HEART_SIZE = 18.dp
private val STAT_GAP = 3.dp
private val LINE_GAP = 3.dp
private const val EMPTY_TEXT =
"\"..ayh don't trust any thing\nthat bleeds for five days\nand doesn't die.\""
@Composable
fun HomeScreen(
summaries: List<AccountSummary>,
onOpen: (KilnAccount) -> Unit,
onAdd: (String) -> Unit,
onSettings: () -> Unit,
onImport: (Uri, (Boolean) -> Unit) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
var showVersion by remember { mutableStateOf(false) }
var importFailed by remember { mutableStateOf(false) }
val uriHandler = LocalUriHandler.current
val importer = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri -> uri?.let { u -> onImport(u) { ok -> importFailed = !ok } } }
Scaffold(
topBar = {
KilnTopBar(
"kiln",
onSettings = onSettings,
onVersion = { showVersion = true },
onImport = { importer.launch(arrayOf("*/*")) },
action = {
IconButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, "New Account", tint = TopBarFg)
}
}
)
}
) { pad ->
Box(Modifier.fillMaxSize().padding(top = pad.calculateTopPadding())) {
if (summaries.isEmpty()) {
Text(
EMPTY_TEXT,
Modifier.align(Alignment.Center).padding(horizontal = ROW_PAD_H),
fontSize = 14.sp,
lineHeight = 22.sp,
textAlign = TextAlign.Center,
color = Color.White.copy(alpha = 0.4f)
)
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = GRID_PAD,
end = GRID_PAD,
top = GRID_PAD,
bottom = GRID_PAD + pad.calculateBottomPadding()
),
horizontalArrangement = Arrangement.spacedBy(GRID_GAP),
verticalArrangement = Arrangement.spacedBy(GRID_GAP)
) {
items(summaries, key = { it.kilnAccount.id }) { s ->
AccountCard(s) { onOpen(s.kilnAccount) }
}
}
}
}
}
if (showDialog) NewAccountDialog(
onDismiss = { showDialog = false },
onConfirm = { onAdd(it); showDialog = false }
)
VersionDialog(showVersion, onDismiss = { showVersion = false }, uriHandler = uriHandler)
if (importFailed) AlertDialog(
onDismissRequest = { importFailed = false },
shape = RoundedCornerShape(14.dp),
containerColor = CCard,
confirmButton = {
Button(
onClick = { importFailed = false },
modifier = Modifier.height(44.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CChip, contentColor = CText
),
shape = RoundedCornerShape(8.dp)
) { Text("Fine", color = CText, fontSize = 15.sp) }
},
text = { Text("Not a kiln_[timestamp].json file.", fontSize = 17.sp) }
)
}
@Composable
private fun AccountCard(summary: AccountSummary, onClick: () -> Unit) {
val accent = KilnGradientPalette.colorFor(summary.todayHue)
val bucket = KilnGradientPalette.bucket(summary.todayHue)
Column(
Modifier.fillMaxWidth()
.shadow(CARD_ELEV, CARD_SHAPE, ambientColor = accent, spotColor = accent)
.background(CCard, CARD_SHAPE)
.border(CARD_BORDER, accent, CARD_SHAPE)
.clickable(onClick = onClick)
.padding(CARD_PAD)
) {
Image(
painterResource(
KilnFaces.forBucket(KilnHair.of(summary.kilnAccount.hair), bucket)
),
null,
Modifier.size(FACE_SIZE).align(Alignment.CenterHorizontally)
)
Spacer(Modifier.height(FACE_GAP))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
summary.kilnAccount.name,
Modifier.weight(1f),
fontSize = 16.sp,
lineHeight = 16.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = 0.2.sp,
color = Color.White.copy(alpha = 0.92f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (summary.isDefault) {
Spacer(Modifier.width(6.dp))
Icon(
Icons.Filled.Favorite,
"Default Account",
Modifier.size(HEART_SIZE),
tint = colorResource(ICON_BG[bucket])
)
}
}
Spacer(Modifier.height(STAT_GAP))
StatLine("Next PMS:", pmsLabel(summary.daysToPreMenstrual))
Spacer(Modifier.height(LINE_GAP))
StatLine("Next Period:", daysLabel(summary.daysToPeriod))
}
}
@Composable
private fun StatLine(label: String, value: String) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
label,
fontSize = 12.sp,
lineHeight = 12.sp,
color = Color.White.copy(alpha = 0.50f)
)
Text(
value,
fontSize = 12.sp,
lineHeight = 12.sp,
fontWeight = FontWeight.Medium,
color = Color.White.copy(alpha = 0.85f),
maxLines = 1
)
}
}
private fun daysLabel(days: Long?): String = when (days) {
null -> "—"
0L -> "Today"
1L -> "1 Day"
else -> "$days Days"
}
private fun pmsLabel(days: Long?): String = if (days == 0L) "Now" else daysLabel(days)
private val ICON_BG = intArrayOf(
R.color.icon_bg_g0, R.color.icon_bg_g1, R.color.icon_bg_g2, R.color.icon_bg_g3,
R.color.icon_bg_g4, R.color.icon_bg_g5, R.color.icon_bg_g6, R.color.icon_bg_g7
)
@Composable
private fun NewAccountDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
var name by remember { mutableStateOf("") }
val btnShape = RoundedCornerShape(8.dp)
val btnColors = ButtonDefaults.buttonColors(
containerColor = CChip,
contentColor = CText
)
AlertDialog(
onDismissRequest = onDismiss,
shape = RoundedCornerShape(14.dp),
containerColor = CCard,
text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
singleLine = true,
textStyle = TextStyle(fontSize = FIELD_TEXT),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = CText,
unfocusedTextColor = CText,
focusedBorderColor = Color.White.copy(alpha = 0.45f),
unfocusedBorderColor = Color.White.copy(alpha = 0.15f),
focusedLabelColor = Color.White.copy(alpha = 0.60f),
unfocusedLabelColor = Color.White.copy(alpha = 0.40f),
cursorColor = Color.White.copy(alpha = 0.80f)
)
)
},
confirmButton = {
Button(
onClick = { onConfirm(name) },
enabled = name.isNotBlank(),
modifier = Modifier.height(44.dp),
colors = btnColors,
shape = btnShape
) {
Text(
"Create",
color = CText.copy(alpha = if (name.isNotBlank()) 1f else 0.3f),
fontSize = 15.sp
)
}
},
dismissButton = {
Button(
onClick = onDismiss,
modifier = Modifier.height(44.dp),
colors = btnColors,
shape = btnShape
) { Text("Cancel", color = CText, fontSize = 15.sp) }
}
)
}