feat: day preview popup, smoother month scrolling, UI polish
- Long-press on a day now opens a preview popup (ported from the iOS DayContextPreviewView): all-day events as colored bars with tapered ends when they continue beyond the day, timed events as dot+time+title rows, plus quick actions (new event, week view, day view) - Month scroll performance: stable item keys + contentType in the LazyColumn, remembered Instant.now()/theme colors, remembered callbacks so visible week rows stay skippable during recomposition - Fix "+N" and "KW" labels overlapping the lowest event lane (row bottom padding 6dp -> 16dp, mirrors the iOS fix) - Event detail: delete is now a quiet red text button instead of a full-width filled container - Top bar: hairline divider so the bar/calendar boundary is visible - FAB: explicit circular shape Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,9 @@ object L10n {
|
|||||||
"cal.no_events_title" to "Keine Termine",
|
"cal.no_events_title" to "Keine Termine",
|
||||||
"cal.no_events_body" to "In den nächsten 90 Tagen sind keine Termine vorhanden.",
|
"cal.no_events_body" to "In den nächsten 90 Tagen sind keine Termine vorhanden.",
|
||||||
"cal.loading_more" to "Lade weitere Wochen…", "cal.new_event" to "Neues Ereignis",
|
"cal.loading_more" to "Lade weitere Wochen…", "cal.new_event" to "Neues Ereignis",
|
||||||
|
"cal.show_in_day_view" to "In Tagesansicht öffnen",
|
||||||
|
"cal.show_in_week_view" to "In Wochenansicht öffnen",
|
||||||
|
"cal.no_events_day" to "Keine Termine an diesem Tag",
|
||||||
"menu.section.settings" to "Einstellungen", "menu.profile" to "Profil",
|
"menu.section.settings" to "Einstellungen", "menu.profile" to "Profil",
|
||||||
"menu.appearance" to "Darstellung", "menu.accounts" to "Konten & Kalender",
|
"menu.appearance" to "Darstellung", "menu.accounts" to "Konten & Kalender",
|
||||||
"menu.server" to "Server", "menu.logout" to "Abmelden", "menu.admin" to "Admin",
|
"menu.server" to "Server", "menu.logout" to "Abmelden", "menu.admin" to "Admin",
|
||||||
@@ -165,6 +168,9 @@ object L10n {
|
|||||||
"cal.no_events_title" to "No events",
|
"cal.no_events_title" to "No events",
|
||||||
"cal.no_events_body" to "No events in the next 90 days.",
|
"cal.no_events_body" to "No events in the next 90 days.",
|
||||||
"cal.loading_more" to "Loading more weeks…", "cal.new_event" to "New event",
|
"cal.loading_more" to "Loading more weeks…", "cal.new_event" to "New event",
|
||||||
|
"cal.show_in_day_view" to "Open in day view",
|
||||||
|
"cal.show_in_week_view" to "Open in week view",
|
||||||
|
"cal.no_events_day" to "No events on this day",
|
||||||
"menu.section.settings" to "Settings", "menu.profile" to "Profile",
|
"menu.section.settings" to "Settings", "menu.profile" to "Profile",
|
||||||
"menu.appearance" to "Appearance", "menu.accounts" to "Accounts & Calendars",
|
"menu.appearance" to "Appearance", "menu.accounts" to "Accounts & Calendars",
|
||||||
"menu.server" to "Server", "menu.logout" to "Sign out", "menu.admin" to "Admin",
|
"menu.server" to "Server", "menu.logout" to "Sign out", "menu.admin" to "Admin",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -94,6 +95,7 @@ fun CalendarScreen(
|
|||||||
var detailEvent by remember { mutableStateOf<CalEvent?>(null) }
|
var detailEvent by remember { mutableStateOf<CalEvent?>(null) }
|
||||||
var editor by remember { mutableStateOf<EditorRequest?>(null) }
|
var editor by remember { mutableStateOf<EditorRequest?>(null) }
|
||||||
var overlay by remember { mutableStateOf(Overlay.NONE) }
|
var overlay by remember { mutableStateOf(Overlay.NONE) }
|
||||||
|
var dayPreview by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
|
|
||||||
// Continuous month scrolling
|
// Continuous month scrolling
|
||||||
val monthListState = rememberLazyListState()
|
val monthListState = rememberLazyListState()
|
||||||
@@ -137,6 +139,7 @@ fun CalendarScreen(
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { editor = EditorRequest(null, if (isMonth) visibleMonth else state.currentDate) },
|
onClick = { editor = EditorRequest(null, if (isMonth) visibleMonth else state.currentDate) },
|
||||||
|
shape = CircleShape,
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
) {
|
) {
|
||||||
@@ -152,6 +155,8 @@ fun CalendarScreen(
|
|||||||
GroupBanner(group = g, onExit = { vm.switchGroup(null) })
|
GroupBanner(group = g, onExit = { vm.switchGroup(null) })
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
// remember: stable callbacks keep the lazily composed week rows
|
||||||
|
// skippable during scroll (fresh lambdas would recompose them all).
|
||||||
CalendarBody(
|
CalendarBody(
|
||||||
state = state,
|
state = state,
|
||||||
vm = vm,
|
vm = vm,
|
||||||
@@ -159,10 +164,10 @@ fun CalendarScreen(
|
|||||||
scrollToTodaySignal = todaySignal,
|
scrollToTodaySignal = todaySignal,
|
||||||
monthJumpSignal = monthJumpSignal,
|
monthJumpSignal = monthJumpSignal,
|
||||||
monthJumpTarget = monthJumpTarget,
|
monthJumpTarget = monthJumpTarget,
|
||||||
onVisibleMonthChange = { visibleMonth = it },
|
onVisibleMonthChange = remember { { visibleMonth = it } },
|
||||||
onEventClick = { detailEvent = it },
|
onEventClick = remember { { detailEvent = it } },
|
||||||
onDayClick = { date -> vm.goToDate(date, CalViewType.DAY) },
|
onDayClick = remember(vm) { { date -> vm.goToDate(date, CalViewType.DAY) } },
|
||||||
onDayLongPress = { date -> editor = EditorRequest(null, date) },
|
onDayLongPress = remember { { date -> dayPreview = date } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,18 @@ fun CalendarScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dayPreview?.let { date ->
|
||||||
|
DayPreviewDialog(
|
||||||
|
date = date,
|
||||||
|
events = remember(state.events, date) { vm.eventsOn(date, state.events) },
|
||||||
|
onDismiss = { dayPreview = null },
|
||||||
|
onEventClick = { ev -> dayPreview = null; detailEvent = ev },
|
||||||
|
onCreateEvent = { dayPreview = null; editor = EditorRequest(null, date) },
|
||||||
|
onOpenDay = { dayPreview = null; vm.goToDate(date, CalViewType.DAY) },
|
||||||
|
onOpenWeek = { dayPreview = null; vm.goToDate(date, CalViewType.WEEK) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the last event during the close animation.
|
// Keep the last event during the close animation.
|
||||||
var lastDetail by remember { mutableStateOf<CalEvent?>(null) }
|
var lastDetail by remember { mutableStateOf<CalEvent?>(null) }
|
||||||
detailEvent?.let { lastDetail = it }
|
detailEvent?.let { lastDetail = it }
|
||||||
@@ -330,6 +347,7 @@ private fun CompactTopBar(
|
|||||||
) {
|
) {
|
||||||
val twoLine = viewType == CalViewType.WEEK || viewType == CalViewType.DAY
|
val twoLine = viewType == CalViewType.WEEK || viewType == CalViewType.DAY
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().height(50.dp).padding(horizontal = 2.dp),
|
Modifier.fillMaxWidth().height(50.dp).padding(horizontal = 2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -373,6 +391,11 @@ private fun CompactTopBar(
|
|||||||
}
|
}
|
||||||
CompactIcon(Icons.Filled.Menu, onMenu, tr("nav.menu"))
|
CompactIcon(Icons.Filled.Menu, onMenu, tr("nav.menu"))
|
||||||
}
|
}
|
||||||
|
Divider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package com.scarriffle.calendarr.ui.calendar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.CalendarViewWeek
|
||||||
|
import androidx.compose.material.icons.filled.WbSunny
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.geometry.toRect
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||||
|
import com.scarriffle.calendarr.ui.L10n
|
||||||
|
import com.scarriffle.calendarr.ui.LocalLang
|
||||||
|
import com.scarriffle.calendarr.ui.tr
|
||||||
|
import com.scarriffle.calendarr.util.colorFromHex
|
||||||
|
import com.scarriffle.calendarr.util.contrastingTextColor
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Long-press day preview (mirrors the iOS DayContextPreviewView): all-day
|
||||||
|
* events as colored bars with tapered ends when they continue beyond this day,
|
||||||
|
* timed events as dot + time + title rows, followed by quick actions.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DayPreviewDialog(
|
||||||
|
date: LocalDate,
|
||||||
|
events: List<CalEvent>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onEventClick: (CalEvent) -> Unit,
|
||||||
|
onCreateEvent: () -> Unit,
|
||||||
|
onOpenDay: () -> Unit,
|
||||||
|
onOpenWeek: () -> Unit,
|
||||||
|
) {
|
||||||
|
val lang = LocalLang.current
|
||||||
|
val sorted = remember(events) {
|
||||||
|
events.sortedWith(compareByDescending<CalEvent> { it.isAllDay }.thenBy { it.startDate })
|
||||||
|
}
|
||||||
|
val weekdayAbbr = remember(date, lang) {
|
||||||
|
date.dayOfWeek.getDisplayName(TextStyle.SHORT, L10n.locale(lang)).uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.width(290.dp)
|
||||||
|
.padding(vertical = 14.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
weekdayAbbr,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(end = 6.dp, bottom = 3.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${date.dayOfMonth}",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Divider(Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
|
||||||
|
|
||||||
|
if (sorted.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
tr("cal.no_events_day"),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sorted.forEach { ev ->
|
||||||
|
if (ev.isAllDay) {
|
||||||
|
AllDayPreviewBar(event = ev, date = date, onClick = { onEventClick(ev) })
|
||||||
|
} else {
|
||||||
|
TimedPreviewRow(event = ev, lang = lang, onClick = { onEventClick(ev) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
|
||||||
|
PreviewAction(Icons.Filled.Add, tr("cal.new_event"), onCreateEvent)
|
||||||
|
PreviewAction(Icons.Filled.CalendarViewWeek, tr("cal.show_in_week_view"), onOpenWeek)
|
||||||
|
PreviewAction(Icons.Filled.WbSunny, tr("cal.show_in_day_view"), onOpenDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AllDayPreviewBar(event: CalEvent, date: LocalDate, onClick: () -> Unit) {
|
||||||
|
val color = colorFromHex(event.effectiveColor)
|
||||||
|
val startDay = localDate(event.startDate)
|
||||||
|
val lastDay = localDate(event.endDate.minusSeconds(1)) // all-day end is exclusive
|
||||||
|
val cLeft = startDay.isBefore(date)
|
||||||
|
val cRight = lastDay.isAfter(date)
|
||||||
|
val shape = remember(cLeft, cRight) { ChevronBarShape(cLeft, cRight) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 2.dp)
|
||||||
|
.clip(shape)
|
||||||
|
.background(color)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
start = if (cLeft) 14.dp else 8.dp,
|
||||||
|
end = if (cRight) 14.dp else 8.dp,
|
||||||
|
top = 4.dp,
|
||||||
|
bottom = 4.dp,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
event.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = color.contrastingTextColor(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TimedPreviewRow(event: CalEvent, lang: String, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(7.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colorFromHex(event.effectiveColor)),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
timeLabel(event.startDate, lang),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(start = 8.dp).width(42.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
event.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreviewAction(icon: ImageVector, label: String, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 9.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bar shape with tapered (pointed) ends on the sides where the event continues
|
||||||
|
* beyond the shown day — port of the iOS ChevronBarShape.
|
||||||
|
*/
|
||||||
|
private class ChevronBarShape(
|
||||||
|
private val continuesLeft: Boolean,
|
||||||
|
private val continuesRight: Boolean,
|
||||||
|
) : Shape {
|
||||||
|
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
|
||||||
|
val r = with(density) { 4.dp.toPx() }
|
||||||
|
if (!continuesLeft && !continuesRight) {
|
||||||
|
return Outline.Rounded(RoundRect(size.toRect(), CornerRadius(r)))
|
||||||
|
}
|
||||||
|
val t = with(density) { 8.dp.toPx() }.coerceAtMost(size.width / 2)
|
||||||
|
val p = Path()
|
||||||
|
if (continuesLeft && continuesRight) {
|
||||||
|
p.moveTo(t, 0f)
|
||||||
|
p.lineTo(size.width - t, 0f)
|
||||||
|
p.lineTo(size.width, size.height / 2)
|
||||||
|
p.lineTo(size.width - t, size.height)
|
||||||
|
p.lineTo(t, size.height)
|
||||||
|
p.lineTo(0f, size.height / 2)
|
||||||
|
} else if (continuesLeft) {
|
||||||
|
p.moveTo(t, 0f)
|
||||||
|
p.lineTo(size.width, 0f)
|
||||||
|
p.lineTo(size.width, size.height)
|
||||||
|
p.lineTo(t, size.height)
|
||||||
|
p.lineTo(0f, size.height / 2)
|
||||||
|
} else {
|
||||||
|
p.moveTo(0f, 0f)
|
||||||
|
p.lineTo(size.width - t, 0f)
|
||||||
|
p.lineTo(size.width, size.height / 2)
|
||||||
|
p.lineTo(size.width - t, size.height)
|
||||||
|
p.lineTo(0f, size.height)
|
||||||
|
}
|
||||||
|
p.close()
|
||||||
|
return Outline.Generic(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,8 @@ private const val MAX_LANES = 4
|
|||||||
private val DAY_NUM_H = 22.dp
|
private val DAY_NUM_H = 22.dp
|
||||||
private val LANE_H = 15.dp
|
private val LANE_H = 15.dp
|
||||||
private val LANE_SPACE = 2.dp
|
private val LANE_SPACE = 2.dp
|
||||||
private val ROW_HEIGHT = DAY_NUM_H + (LANE_H + LANE_SPACE) * MAX_LANES + 6.dp
|
// Bottom strip must fit the 8sp "+N"/"KW" labels without event bars overlapping them.
|
||||||
|
private val ROW_HEIGHT = DAY_NUM_H + (LANE_H + LANE_SPACE) * MAX_LANES + 16.dp
|
||||||
|
|
||||||
private enum class DividerEdge { NONE, TOP, BOTTOM }
|
private enum class DividerEdge { NONE, TOP, BOTTOM }
|
||||||
|
|
||||||
@@ -97,11 +98,14 @@ fun MonthView(
|
|||||||
val mondayFirst = state.weekStartsOnMonday
|
val mondayFirst = state.weekStartsOnMonday
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
|
|
||||||
val dividerColor = colorFromHex(settings.monthDividerColor, Color(0xFF7090C0))
|
val fallbackLabel = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
val labelColor = colorFromHex(settings.monthLabelColor, MaterialTheme.colorScheme.onSurfaceVariant)
|
val outline = MaterialTheme.colorScheme.outline
|
||||||
val gridColor = MaterialTheme.colorScheme.outline.copy(alpha = gridLineOpacity(settings.lineContrast))
|
val onBackground = MaterialTheme.colorScheme.onBackground
|
||||||
val secondaryText = MaterialTheme.colorScheme.onBackground.copy(alpha = secondaryTextOpacity(settings.textContrast))
|
val dividerColor = remember(settings.monthDividerColor) { colorFromHex(settings.monthDividerColor, Color(0xFF7090C0)) }
|
||||||
val todayColor = colorFromHex(settings.todayColor)
|
val labelColor = remember(settings.monthLabelColor, fallbackLabel) { colorFromHex(settings.monthLabelColor, fallbackLabel) }
|
||||||
|
val gridColor = remember(outline, settings.lineContrast) { outline.copy(alpha = gridLineOpacity(settings.lineContrast)) }
|
||||||
|
val secondaryText = remember(onBackground, settings.textContrast) { onBackground.copy(alpha = secondaryTextOpacity(settings.textContrast)) }
|
||||||
|
val todayColor = remember(settings.todayColor) { colorFromHex(settings.todayColor) }
|
||||||
|
|
||||||
// Column width measured once → no per-row BoxWithConstraints (smooth scrolling).
|
// Column width measured once → no per-row BoxWithConstraints (smooth scrolling).
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -144,7 +148,10 @@ fun MonthView(
|
|||||||
|
|
||||||
val eventsByWeek = remember(state.events, mondayFirst) { buildEventsByWeek(state.events, mondayFirst) }
|
val eventsByWeek = remember(state.events, mondayFirst) { buildEventsByWeek(state.events, mondayFirst) }
|
||||||
val dimPast = settings.dimPastEvents
|
val dimPast = settings.dimPastEvents
|
||||||
val now = java.time.Instant.now()
|
val cwLabel = tr("cal.cw")
|
||||||
|
// remember: a fresh Instant per recomposition would invalidate every visible
|
||||||
|
// WeekRow (the main source of scroll jank); minute precision is plenty here.
|
||||||
|
val now = remember { java.time.Instant.now() }
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 3.dp)) {
|
Row(Modifier.fillMaxWidth().padding(vertical = 3.dp)) {
|
||||||
@@ -163,7 +170,7 @@ fun MonthView(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize().onSizeChanged { gridWidthPx = it.width },
|
modifier = Modifier.fillMaxSize().onSizeChanged { gridWidthPx = it.width },
|
||||||
) {
|
) {
|
||||||
items(weekCount) { index ->
|
items(weekCount, key = { it }, contentType = { "week" }) { index ->
|
||||||
val weekStart = firstVisible.plusWeeks(index.toLong())
|
val weekStart = firstVisible.plusWeeks(index.toLong())
|
||||||
WeekRow(
|
WeekRow(
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
@@ -173,6 +180,7 @@ fun MonthView(
|
|||||||
dimPast = dimPast,
|
dimPast = dimPast,
|
||||||
now = now,
|
now = now,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
|
cwLabel = cwLabel,
|
||||||
dividerColor = dividerColor,
|
dividerColor = dividerColor,
|
||||||
gridColor = gridColor,
|
gridColor = gridColor,
|
||||||
labelColor = labelColor,
|
labelColor = labelColor,
|
||||||
@@ -196,6 +204,7 @@ private fun WeekRow(
|
|||||||
dimPast: Boolean,
|
dimPast: Boolean,
|
||||||
now: java.time.Instant,
|
now: java.time.Instant,
|
||||||
lang: String,
|
lang: String,
|
||||||
|
cwLabel: String,
|
||||||
dividerColor: Color,
|
dividerColor: Color,
|
||||||
gridColor: Color,
|
gridColor: Color,
|
||||||
labelColor: Color,
|
labelColor: Color,
|
||||||
@@ -208,7 +217,6 @@ private fun WeekRow(
|
|||||||
val days = remember(weekStart) { (0 until 7).map { weekStart.plusDays(it.toLong()) } }
|
val days = remember(weekStart) { (0 until 7).map { weekStart.plusDays(it.toLong()) } }
|
||||||
val boundaryCol = (1 until 7).firstOrNull { days[it].dayOfMonth == 1 }
|
val boundaryCol = (1 until 7).firstOrNull { days[it].dayOfMonth == 1 }
|
||||||
val rowStartsNewMonth = days[0].dayOfMonth == 1
|
val rowStartsNewMonth = days[0].dayOfMonth == 1
|
||||||
val cwLabel = tr("cal.cw")
|
|
||||||
|
|
||||||
val packed = remember(weekStart, weekEvents) { packEvents(weekStart, weekEvents) }
|
val packed = remember(weekStart, weekEvents) { packEvents(weekStart, weekEvents) }
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.compose.material.icons.filled.Notes
|
|||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.Schedule
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -126,13 +125,15 @@ fun EventDetailScreen(
|
|||||||
Text(tr("event.copy_title"))
|
Text(tr("event.copy_title"))
|
||||||
}
|
}
|
||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Button(
|
// Deliberately low-key (plain text button): destructive action
|
||||||
|
// should be findable, not the loudest element on the screen.
|
||||||
|
TextButton(
|
||||||
onClick = { confirmDelete = true },
|
onClick = { confirmDelete = true },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer),
|
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Delete, contentDescription = null)
|
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(tr("common.delete"))
|
Text(tr("common.delete"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user