Pith - wedge_android
wedge_android/app/src/main/java/com/vgmlr/wedge/WedgeCalendarPicker.kt [7.9 kb]
Modified: 00:00:28 153 026 (19 Aug 026)
5 Days Ago
package com.vgmlr.wedge

import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
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.graphics.vector.ImageVector
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.ui.input.pointer.pointerInput

private const val MONTH_SPAN = 5
private const val GRID_ROWS = 6

private val TITLE_WIDTH = 132.dp
private val SWIPE_THRESHOLD = 48.dp

private data class DayCell(
    val timestamp: Long,
    val dayOfMonth: Int,
    val otcDay: Long,
    val inMonth: Boolean,
    val isPast: Boolean,
    val isToday: Boolean
)

private fun buildMonth(today: Long, monthOffset: Int): Pair<String, List<DayCell>> {
    val monthStart = Calendar.getInstance().apply {
        timeInMillis = today
        set(Calendar.DAY_OF_MONTH, 1)
        add(Calendar.MONTH, monthOffset)
    }
    val title = SimpleDateFormat("MMMM", Locale.US)
        .format(monthStart.time)
        .replaceFirstChar { it.uppercase(Locale.US) }
    val lead = monthStart.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY
    val month = monthStart.get(Calendar.MONTH)

    val cursor = (monthStart.clone() as Calendar).apply { add(Calendar.DAY_OF_MONTH, -lead) }
    val cells = ArrayList<DayCell>(GRID_ROWS * 7)
    repeat(GRID_ROWS * 7) {
        val ts = cursor.timeInMillis
        cells.add(
            DayCell(
                timestamp = ts,
                dayOfMonth = cursor.get(Calendar.DAY_OF_MONTH),
                otcDay = WedgeDate.getOtcParts(ts).first,
                inMonth = cursor.get(Calendar.MONTH) == month,
                isPast = ts < today,
                isToday = ts == today
            )
        )
        cursor.add(Calendar.DAY_OF_MONTH, 1)
    }
    return title to cells
}

@Composable
fun WedgeCalendarPicker(
    onDateSelected: (Long) -> Unit,
    onDismiss: () -> Unit
) {
    val fg = colorResource(id = R.color.dialog_color)
    val today = remember { WedgeDate.startOfDay(System.currentTimeMillis()) }
    var monthOffset by remember { mutableIntStateOf(0) }
    val (title, cells) = remember(today, monthOffset) { buildMonth(today, monthOffset) }

    Dialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(usePlatformDefaultWidth = false)
    ) {
        Surface(
            shape = RoundedCornerShape(8.dp),
            modifier = Modifier
                .padding(horizontal = 20.dp)
                .fillMaxWidth(),
            color = colorResource(id = R.color.dialog_bg_color)
        ) {
            Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    NavArrow(
                        Icons.AutoMirrored.Filled.KeyboardArrowLeft,
                        monthOffset > 0, fg
                    ) { monthOffset-- }
                    Text(
                        text = title,
                        color = fg,
                        fontSize = 16.sp,
                        fontFamily = FontFamily.Monospace,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.width(TITLE_WIDTH)
                    )
                    NavArrow(
                        Icons.AutoMirrored.Filled.KeyboardArrowRight,
                        monthOffset < MONTH_SPAN - 1, fg
                    ) { monthOffset++ }
                }

                Spacer(Modifier.height(14.dp))

                Row(modifier = Modifier.fillMaxWidth()) {
                    listOf("S", "M", "T", "W", "T", "F", "S").forEach { d ->
                        Text(
                            text = d,
                            color = fg.copy(alpha = 0.8f),
                            fontSize = 13.sp,
                            fontFamily = FontFamily.Monospace,
                            textAlign = TextAlign.Center,
                            modifier = Modifier.weight(1f)
                        )
                    }
                }

                Spacer(Modifier.height(6.dp))

                Column(
                    modifier = Modifier.pointerInput(Unit) {
                        val threshold = SWIPE_THRESHOLD.toPx()
                        var travel = 0f
                        detectHorizontalDragGestures(
                            onDragStart = { travel = 0f },
                            onDragCancel = { travel = 0f },
                            onDragEnd = {
                                when {
                                    travel <= -threshold && monthOffset < MONTH_SPAN - 1 -> monthOffset++
                                    travel >= threshold && monthOffset > 0 -> monthOffset--
                                }
                            }
                        ) { change, amount ->
                            travel += amount
                            change.consume()
                        }
                    }
                ) {
                    for (row in 0 until GRID_ROWS) {
                        Row(modifier = Modifier.fillMaxWidth()) {
                            for (col in 0 until 7) {
                                DayCellView(
                                    cell = cells[row * 7 + col],
                                    fg = fg,
                                    modifier = Modifier.weight(1f),
                                    onPick = onDateSelected
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun NavArrow(icon: ImageVector, enabled: Boolean, fg: Color, onClick: () -> Unit) {
    IconButton(onClick = onClick, enabled = enabled, modifier = Modifier.size(34.dp)) {
        Icon(icon, null, tint = fg.copy(alpha = if (enabled) 1f else 0.2f))
    }
}

@Composable
private fun DayCellView(
    cell: DayCell,
    fg: Color,
    modifier: Modifier,
    onPick: (Long) -> Unit
) {
    val tappable = !cell.isPast
    val alpha = when {
        !cell.inMonth -> 0.22f
        cell.isPast -> 0.3f
        else -> 1f
    }
    Box(
        modifier = modifier
            .height(44.dp)
            .then(
                if (cell.isToday)
                    Modifier.border(1.dp, fg.copy(alpha = 0.6f), RoundedCornerShape(6.dp))
                else Modifier
            )
            .clickable(enabled = tappable) { onPick(cell.timestamp) },
        contentAlignment = Alignment.Center
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(
                text = cell.dayOfMonth.toString(),
                color = fg.copy(alpha = alpha),
                fontSize = 15.sp,
                lineHeight = 16.sp,
                fontFamily = FontFamily.Monospace
            )
            Text(
                text = cell.otcDay.toString(),
                color = fg.copy(alpha = alpha * 0.7f),
                fontSize = 12.sp,
                lineHeight = 13.sp,
                fontFamily = FontFamily.Monospace
            )
        }
    }
}
Updates
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026
Grit - Firefox/Chrome 154.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00319
257,054 (+308)