Pith - kiln
kiln/app/src/main/java/com/vgmlr/kiln/KilnHomeScreen.kt [9.9 kb]
Modified: 20:13:54 158 026 (24 Aug 026)
5 Days Ago
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) }
        }
    )
}
Updates
Stave - Android 158.026
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00249
260,717 (+1,131)