feat: HA/Google edit, full-page event detail, KW+zigzag divider, HSV picker, splash
- Home Assistant & Google events now editable/deletable (server PUT/DELETE endpoints wired; saveEvent had no HA/Google update branch -> created dupes) - Event detail is now a full screen (Scaffold) instead of a bottom sheet: long notes scroll, action buttons no longer hidden behind the system nav bar; distinct calendar vs notes icons - Month view: calendar-week number on Mondays, month-boundary divider with zigzag step (month_divider_color), month label in month_label_color, text/line contrast applied - Compact top bar with smaller title; prev/next chevrons present in month view and scroll one month at a time - Real HSV colour picker (SV field + hue slider + hex) for primary, accent, today, month-divider and month-label colours - Black status/navigation bars; portrait-locked; crisp custom splash with app name, high-res icon and © Scarriffle Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Calendarr">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -305,31 +305,56 @@ class CalendarRepository @Inject constructor(
|
||||
calendarDbId: Int, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String,
|
||||
) = guarded {
|
||||
api.createGoogleEvent(
|
||||
jsonBody(
|
||||
"calendar_db_id" to calendarDbId, "title" to title,
|
||||
"start" to Dates.format(start, isAllDay), "end" to Dates.format(end, isAllDay),
|
||||
"allDay" to isAllDay, "location" to location, "description" to description,
|
||||
)
|
||||
).ensureSuccess()
|
||||
api.createGoogleEvent(simpleEventBody("calendar_db_id", calendarDbId, title, start, end, isAllDay, location, description))
|
||||
.ensureSuccess()
|
||||
}
|
||||
|
||||
suspend fun updateGoogleEvent(
|
||||
gcalDbId: Int, eventId: String, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String,
|
||||
) = guarded {
|
||||
api.updateGoogleEvent(gcalDbId, eventId, simpleEventBody(null, null, title, start, end, isAllDay, location, description))
|
||||
.ensureSuccess()
|
||||
}
|
||||
|
||||
suspend fun deleteGoogleEvent(gcalDbId: Int, eventId: String) =
|
||||
guarded { api.deleteGoogleEvent(gcalDbId, eventId).ensureSuccess() }
|
||||
|
||||
suspend fun createHAEvent(
|
||||
calendarId: Int, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String,
|
||||
) = guarded {
|
||||
api.createHAEvent(
|
||||
jsonBody(
|
||||
"calendar_id" to calendarId, "title" to title,
|
||||
"start" to Dates.format(start, isAllDay), "end" to Dates.format(end, isAllDay),
|
||||
"allDay" to isAllDay, "location" to location, "description" to description,
|
||||
)
|
||||
).ensureSuccess()
|
||||
api.createHAEvent(simpleEventBody("calendar_id", calendarId, title, start, end, isAllDay, location, description))
|
||||
.ensureSuccess()
|
||||
}
|
||||
|
||||
suspend fun updateHAEvent(
|
||||
calendarId: Int, uid: String, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String,
|
||||
) = guarded {
|
||||
api.updateHAEvent(calendarId, uid, simpleEventBody(null, null, title, start, end, isAllDay, location, description))
|
||||
.ensureSuccess()
|
||||
}
|
||||
|
||||
suspend fun deleteHAEvent(calendarId: Int, uid: String) =
|
||||
guarded { api.deleteHAEvent(calendarId, uid).ensureSuccess() }
|
||||
|
||||
/** Body for Google/HA events (no per-event colour). */
|
||||
private fun simpleEventBody(
|
||||
calKey: String?, calId: Int?, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String,
|
||||
) = jsonBody(
|
||||
buildMap {
|
||||
if (calKey != null && calId != null) put(calKey, calId)
|
||||
put("title", title)
|
||||
put("start", Dates.format(start, isAllDay))
|
||||
put("end", Dates.format(end, isAllDay))
|
||||
put("allDay", isAllDay)
|
||||
put("location", location)
|
||||
put("description", description)
|
||||
}
|
||||
)
|
||||
|
||||
private fun eventBody(
|
||||
calendarId: Int?, title: String, start: Instant, end: Instant,
|
||||
isAllDay: Boolean, location: String, description: String, color: String?,
|
||||
|
||||
@@ -170,10 +170,30 @@ interface CalendarrApi {
|
||||
@POST("api/google/events")
|
||||
suspend fun createGoogleEvent(@Body body: RequestBody): Response<ResponseBody>
|
||||
|
||||
@PUT("api/google/events/{gcalDbId}/{eventId}")
|
||||
suspend fun updateGoogleEvent(
|
||||
@Path("gcalDbId") gcalDbId: Int,
|
||||
@Path("eventId") eventId: String,
|
||||
@Body body: RequestBody,
|
||||
): Response<ResponseBody>
|
||||
|
||||
@HTTP(method = "DELETE", path = "api/google/events/{gcalDbId}/{eventId}", hasBody = false)
|
||||
suspend fun deleteGoogleEvent(
|
||||
@Path("gcalDbId") gcalDbId: Int,
|
||||
@Path("eventId") eventId: String,
|
||||
): Response<ResponseBody>
|
||||
|
||||
@POST("api/homeassistant/events")
|
||||
suspend fun createHAEvent(@Body body: RequestBody): Response<ResponseBody>
|
||||
|
||||
@DELETE("api/homeassistant/events/{calendarId}/{uid}")
|
||||
@PUT("api/homeassistant/events/{calendarId}/{uid}")
|
||||
suspend fun updateHAEvent(
|
||||
@Path("calendarId") calendarId: Int,
|
||||
@Path("uid") uid: String,
|
||||
@Body body: RequestBody,
|
||||
): Response<ResponseBody>
|
||||
|
||||
@HTTP(method = "DELETE", path = "api/homeassistant/events/{calendarId}/{uid}", hasBody = false)
|
||||
suspend fun deleteHAEvent(
|
||||
@Path("calendarId") calendarId: Int,
|
||||
@Path("uid") uid: String,
|
||||
|
||||
@@ -5,8 +5,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.scarriffle.calendarr.ui.auth.LoginScreen
|
||||
@@ -19,12 +23,22 @@ fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) {
|
||||
val route by vm.route.collectAsState()
|
||||
val settings by vm.settings.collectAsState()
|
||||
|
||||
var showSplash by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(1200)
|
||||
showSplash = false
|
||||
}
|
||||
|
||||
CalendarrTheme(settings) {
|
||||
CompositionLocalProvider(
|
||||
LocalLang provides L10n.resolved(settings.language),
|
||||
LocalAppSettings provides settings,
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
if (showSplash) {
|
||||
SplashScreen()
|
||||
return@Surface
|
||||
}
|
||||
when (route) {
|
||||
AppRoute.SETUP -> ServerSetupScreen(onConfigured = vm::onServerConfigured)
|
||||
AppRoute.LOGIN -> LoginScreen(
|
||||
|
||||
@@ -95,6 +95,7 @@ object L10n {
|
||||
"event.color" to "Terminfarbe", "event.reset_color" to "Zurücksetzen",
|
||||
"event.edit_title" to "Termin bearbeiten", "event.new_title" to "Neuer Termin",
|
||||
"event.copy_title" to "Termin kopieren", "event.copy_to" to "In Kalender kopieren",
|
||||
"event.detail_title" to "Termin", "event.source" to "Quelle",
|
||||
"event.save" to "Sichern", "event.add" to "Hinzufügen",
|
||||
"event.delete_confirm" to "Diesen Termin löschen?",
|
||||
"accounts.title" to "Konten", "accounts.loading" to "Lade Konten…",
|
||||
@@ -195,6 +196,7 @@ object L10n {
|
||||
"event.color" to "Event color", "event.reset_color" to "Reset",
|
||||
"event.edit_title" to "Edit event", "event.new_title" to "New event",
|
||||
"event.copy_title" to "Copy event", "event.copy_to" to "Copy to calendar",
|
||||
"event.detail_title" to "Event", "event.source" to "Source",
|
||||
"event.save" to "Save", "event.add" to "Add",
|
||||
"event.delete_confirm" to "Delete this event?",
|
||||
"accounts.title" to "Accounts", "accounts.loading" to "Loading accounts…",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.scarriffle.calendarr.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.scarriffle.calendarr.R
|
||||
|
||||
/** Custom startup screen: app name on top, crisp icon centered, copyright at the bottom. */
|
||||
@Composable
|
||||
fun SplashScreen() {
|
||||
Box(Modifier.fillMaxSize().background(Color.Black)) {
|
||||
Text(
|
||||
"Calendarr",
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 96.dp),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_splash_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.Center).size(148.dp).clip(RoundedCornerShape(32.dp)),
|
||||
)
|
||||
Text(
|
||||
"© Scarriffle",
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 40.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,22 @@ fun weekdayLabels(mondayFirst: Boolean, lang: String): List<String> {
|
||||
fun timeLabel(instant: Instant, lang: String): String =
|
||||
DateTimeFormatter.ofPattern("HH:mm", L10n.locale(lang)).withZone(zone).format(instant)
|
||||
|
||||
/** Opacity for secondary text (week number, overflow count) per text_contrast 1..4. */
|
||||
fun secondaryTextOpacity(contrast: Int): Float = when (contrast.coerceIn(1, 4)) {
|
||||
1 -> 0.40f
|
||||
2 -> 0.55f
|
||||
3 -> 0.72f
|
||||
else -> 0.92f
|
||||
}
|
||||
|
||||
/** Opacity for regular grid lines per line_contrast 1..4. */
|
||||
fun gridLineOpacity(contrast: Int): Float = when (contrast.coerceIn(1, 4)) {
|
||||
1 -> 0.15f
|
||||
2 -> 0.30f
|
||||
3 -> 0.50f
|
||||
else -> 0.80f
|
||||
}
|
||||
|
||||
fun localTime(instant: Instant): LocalTime = LocalTime.ofInstant(instant, zone)
|
||||
|
||||
fun localDate(instant: Instant): LocalDate = LocalDate.ofInstant(instant, zone)
|
||||
|
||||
@@ -2,9 +2,13 @@ package com.scarriffle.calendarr.ui.calendar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
@@ -22,9 +26,9 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -35,15 +39,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.scarriffle.calendarr.domain.model.AppSettings
|
||||
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||
import com.scarriffle.calendarr.domain.model.CalViewType
|
||||
import com.scarriffle.calendarr.ui.LocalLang
|
||||
import com.scarriffle.calendarr.ui.accounts.AccountsScreen
|
||||
import com.scarriffle.calendarr.ui.event.EventDetailSheet
|
||||
import com.scarriffle.calendarr.ui.event.EventDetailScreen
|
||||
import com.scarriffle.calendarr.ui.event.EventEditorSheet
|
||||
import com.scarriffle.calendarr.ui.menu.MenuSheet
|
||||
import com.scarriffle.calendarr.ui.profile.ProfileScreen
|
||||
@@ -77,57 +83,36 @@ fun CalendarScreen(
|
||||
// Continuous month scrolling
|
||||
val monthListState = rememberLazyListState()
|
||||
var todaySignal by remember { mutableIntStateOf(0) }
|
||||
var monthJumpSignal by remember { mutableIntStateOf(0) }
|
||||
var monthJumpTarget by remember { mutableStateOf<LocalDate?>(null) }
|
||||
var visibleMonth by remember { mutableStateOf(state.currentDate) }
|
||||
val isMonth = state.viewType == CalViewType.MONTH
|
||||
val barTitle = if (isMonth) titleForView(CalViewType.MONTH, visibleMonth, lang)
|
||||
else titleForView(state.viewType, state.currentDate, lang)
|
||||
|
||||
fun goPrev() {
|
||||
if (isMonth) { monthJumpTarget = visibleMonth.minusMonths(1); monthJumpSignal++ } else vm.navigatePrev()
|
||||
}
|
||||
fun goNext() {
|
||||
if (isMonth) { monthJumpTarget = visibleMonth.plusMonths(1); monthJumpSignal++ } else vm.navigateNext()
|
||||
}
|
||||
fun goToday() {
|
||||
if (isMonth) todaySignal++ else vm.moveToToday()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(barTitle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(Icons.Filled.Menu, contentDescription = tr("nav.menu"))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (!isMonth) {
|
||||
IconButton(onClick = vm::navigatePrev) {
|
||||
Icon(Icons.Filled.ChevronLeft, contentDescription = null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { if (isMonth) todaySignal++ else vm.moveToToday() }) {
|
||||
Icon(Icons.Filled.Today, contentDescription = tr("nav.today"))
|
||||
}
|
||||
if (!isMonth) {
|
||||
IconButton(onClick = vm::navigateNext) {
|
||||
Icon(Icons.Filled.ChevronRight, contentDescription = null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showFilter = true }) {
|
||||
Icon(Icons.Filled.FilterList, contentDescription = tr("filter.button"))
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { viewMenuOpen = true }) {
|
||||
Icon(state.viewType.icon, contentDescription = tr("view.change"))
|
||||
}
|
||||
DropdownMenu(expanded = viewMenuOpen, onDismissRequest = { viewMenuOpen = false }) {
|
||||
CalViewType.entries.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(tr("view.${type.key}")) },
|
||||
leadingIcon = { Icon(type.icon, contentDescription = null) },
|
||||
onClick = {
|
||||
viewMenuOpen = false
|
||||
vm.setViewType(type)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
CompactTopBar(
|
||||
title = barTitle,
|
||||
viewType = state.viewType,
|
||||
viewMenuOpen = viewMenuOpen,
|
||||
onMenu = { showMenu = true },
|
||||
onPrev = { goPrev() },
|
||||
onToday = { goToday() },
|
||||
onNext = { goNext() },
|
||||
onFilter = { showFilter = true },
|
||||
onViewMenuToggle = { viewMenuOpen = it },
|
||||
onSelectView = { vm.setViewType(it) },
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
@@ -153,6 +138,8 @@ fun CalendarScreen(
|
||||
vm = vm,
|
||||
monthListState = monthListState,
|
||||
scrollToTodaySignal = todaySignal,
|
||||
monthJumpSignal = monthJumpSignal,
|
||||
monthJumpTarget = monthJumpTarget,
|
||||
onVisibleMonthChange = { visibleMonth = it },
|
||||
onEventClick = { detailEvent = it },
|
||||
onDayClick = { date -> vm.goToDate(date, CalViewType.DAY) },
|
||||
@@ -186,21 +173,21 @@ fun CalendarScreen(
|
||||
}
|
||||
|
||||
detailEvent?.let { ev ->
|
||||
EventDetailSheet(
|
||||
EventDetailScreen(
|
||||
event = ev,
|
||||
onDismiss = { detailEvent = null },
|
||||
onClose = { detailEvent = null },
|
||||
onEdit = {
|
||||
detailEvent = null
|
||||
editor = EditorRequest(ev, localDate(ev.startDate))
|
||||
},
|
||||
onDelete = {
|
||||
vm.deleteEvent(ev) {}
|
||||
detailEvent = null
|
||||
},
|
||||
onCopy = {
|
||||
detailEvent = null
|
||||
editor = EditorRequest(existing = null, date = localDate(ev.startDate), prefill = ev)
|
||||
},
|
||||
onDelete = {
|
||||
vm.deleteEvent(ev) {}
|
||||
detailEvent = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -238,6 +225,8 @@ private fun CalendarBody(
|
||||
vm: CalendarViewModel,
|
||||
monthListState: androidx.compose.foundation.lazy.LazyListState,
|
||||
scrollToTodaySignal: Int,
|
||||
monthJumpSignal: Int,
|
||||
monthJumpTarget: LocalDate?,
|
||||
onVisibleMonthChange: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
@@ -249,6 +238,8 @@ private fun CalendarBody(
|
||||
vm = vm,
|
||||
listState = monthListState,
|
||||
scrollToTodaySignal = scrollToTodaySignal,
|
||||
monthJumpSignal = monthJumpSignal,
|
||||
monthJumpTarget = monthJumpTarget,
|
||||
onVisibleMonthChange = onVisibleMonthChange,
|
||||
onDayClick = onDayClick,
|
||||
onDayLongPress = onDayLongPress,
|
||||
@@ -284,6 +275,66 @@ private fun loadingPlaceholder() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactTopBar(
|
||||
title: String,
|
||||
viewType: CalViewType,
|
||||
viewMenuOpen: Boolean,
|
||||
onMenu: () -> Unit,
|
||||
onPrev: () -> Unit,
|
||||
onToday: () -> Unit,
|
||||
onNext: () -> Unit,
|
||||
onFilter: () -> Unit,
|
||||
onViewMenuToggle: (Boolean) -> Unit,
|
||||
onSelectView: (CalViewType) -> Unit,
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(50.dp).padding(horizontal = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CompactIcon(Icons.Filled.ChevronLeft, onPrev)
|
||||
TextButton(onClick = onToday, contentPadding = PaddingValues(horizontal = 6.dp)) {
|
||||
Text(tr("nav.today"), fontSize = 13.sp)
|
||||
}
|
||||
CompactIcon(Icons.Filled.ChevronRight, onNext)
|
||||
Text(
|
||||
title,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
CompactIcon(Icons.Filled.FilterList, onFilter, tr("filter.button"))
|
||||
Box {
|
||||
CompactIcon(viewType.icon, { onViewMenuToggle(true) }, tr("view.change"))
|
||||
DropdownMenu(expanded = viewMenuOpen, onDismissRequest = { onViewMenuToggle(false) }) {
|
||||
CalViewType.entries.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(tr("view.${type.key}")) },
|
||||
leadingIcon = { Icon(type.icon, contentDescription = null) },
|
||||
onClick = { onViewMenuToggle(false); onSelectView(type) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
CompactIcon(Icons.Filled.Menu, onMenu, tr("nav.menu"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactIcon(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
onClick: () -> Unit,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
IconButton(onClick = onClick, modifier = Modifier.size(40.dp)) {
|
||||
Icon(icon, contentDescription = contentDescription, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
|
||||
/** Distinct calendars currently present in the cache, for the filter sheet. */
|
||||
private fun allKnownCalendars(vm: CalendarViewModel): List<CalendarFilterEntry> {
|
||||
val st = vm.state.value
|
||||
|
||||
@@ -295,6 +295,8 @@ class CalendarViewModel @Inject constructor(
|
||||
when (existing.source) {
|
||||
"local" -> repository.updateLocalEvent(existing.id, title, start, end, isAllDay, location, description, color)
|
||||
"caldav" -> repository.updateCalDAVEvent(existing.id, existing.url, calendar.numericId, title, start, end, isAllDay, location, description, color)
|
||||
"homeassistant" -> repository.updateHAEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description)
|
||||
"google" -> repository.updateGoogleEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description)
|
||||
else -> createForSource(calendar, title, start, end, isAllDay, location, description, color)
|
||||
}
|
||||
} else {
|
||||
@@ -325,6 +327,7 @@ class CalendarViewModel @Inject constructor(
|
||||
"local" -> repository.deleteLocalEvent(event.id)
|
||||
"caldav" -> repository.deleteCalDAVEvent(event.id, event.url, calendarNumericId(event))
|
||||
"homeassistant" -> repository.deleteHAEvent(calendarNumericId(event) ?: 0, event.id)
|
||||
"google" -> repository.deleteGoogleEvent(calendarNumericId(event) ?: 0, event.id)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,19 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -27,6 +28,9 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -35,37 +39,47 @@ import androidx.compose.ui.unit.sp
|
||||
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||
import com.scarriffle.calendarr.ui.LocalAppSettings
|
||||
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.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.TextStyle
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.IsoFields
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val MONTHS_BACK = 18L
|
||||
private const val MONTHS_AHEAD = 18L
|
||||
private val ROW_HEIGHT = 82.dp
|
||||
|
||||
/**
|
||||
* Continuous, vertically scrolling month calendar (matches the iOS app).
|
||||
* There are no prev/next buttons — the user scrolls through weeks and the
|
||||
* top-bar title follows the currently visible month.
|
||||
*/
|
||||
private enum class DividerEdge { NONE, TOP, BOTTOM }
|
||||
|
||||
/** Continuous, vertically scrolling month calendar (matches the iOS app). */
|
||||
@Composable
|
||||
fun MonthView(
|
||||
state: CalendarUiState,
|
||||
vm: CalendarViewModel,
|
||||
listState: LazyListState,
|
||||
scrollToTodaySignal: Int,
|
||||
monthJumpSignal: Int,
|
||||
monthJumpTarget: LocalDate?,
|
||||
onVisibleMonthChange: (LocalDate) -> Unit,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
onDayLongPress: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
) {
|
||||
val lang = LocalLang.current
|
||||
val settings = LocalAppSettings.current
|
||||
val mondayFirst = state.weekStartsOnMonday
|
||||
val today = LocalDate.now()
|
||||
|
||||
val dividerColor = colorFromHex(settings.monthDividerColor, Color(0xFF7090C0))
|
||||
val labelColor = colorFromHex(settings.monthLabelColor, MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
val gridColor = MaterialTheme.colorScheme.outline.copy(alpha = gridLineOpacity(settings.lineContrast))
|
||||
val secondaryText = MaterialTheme.colorScheme.onBackground.copy(alpha = secondaryTextOpacity(settings.textContrast))
|
||||
|
||||
val firstVisible = remember(mondayFirst) {
|
||||
startOfWeek(today.withDayOfMonth(1).minusMonths(MONTHS_BACK), mondayFirst)
|
||||
}
|
||||
@@ -75,19 +89,20 @@ fun MonthView(
|
||||
val weekCount = remember(firstVisible, end) {
|
||||
(ChronoUnit.WEEKS.between(firstVisible, end).toInt() + 1).coerceAtLeast(1)
|
||||
}
|
||||
val todayIndex = remember(firstVisible) {
|
||||
ChronoUnit.WEEKS.between(firstVisible, startOfWeek(today, mondayFirst)).toInt()
|
||||
}
|
||||
fun weekIndexOf(date: LocalDate): Int =
|
||||
ChronoUnit.WEEKS.between(firstVisible, startOfWeek(date, mondayFirst)).toInt().coerceIn(0, weekCount - 1)
|
||||
|
||||
// Initial scroll to today's week.
|
||||
LaunchedEffect(Unit) {
|
||||
listState.scrollToItem((todayIndex - 1).coerceAtLeast(0))
|
||||
}
|
||||
// "Today" button.
|
||||
val todayIndex = remember(firstVisible) { weekIndexOf(today) }
|
||||
|
||||
LaunchedEffect(Unit) { listState.scrollToItem((todayIndex - 1).coerceAtLeast(0)) }
|
||||
LaunchedEffect(scrollToTodaySignal) {
|
||||
if (scrollToTodaySignal > 0) listState.animateScrollToItem((todayIndex - 1).coerceAtLeast(0))
|
||||
}
|
||||
// Track visible month + trigger on-demand loads (only when the month changes).
|
||||
LaunchedEffect(monthJumpSignal) {
|
||||
if (monthJumpSignal > 0 && monthJumpTarget != null) {
|
||||
listState.animateScrollToItem(weekIndexOf(monthJumpTarget.withDayOfMonth(1)))
|
||||
}
|
||||
}
|
||||
LaunchedEffect(listState, weekCount) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.map { firstVisible.plusWeeks(it.toLong()).plusDays(3).withDayOfMonth(1) }
|
||||
@@ -98,13 +113,10 @@ fun MonthView(
|
||||
}
|
||||
}
|
||||
|
||||
// Precompute a day → events index once per event-list change (avoids
|
||||
// filtering the whole list for every cell on every recomposition → smooth scroll).
|
||||
val eventsByDay = remember(state.events) { buildEventsByDay(state.events) }
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
// Fixed weekday header
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 3.dp)) {
|
||||
weekdayLabels(mondayFirst, lang).forEach { label ->
|
||||
Text(
|
||||
label,
|
||||
@@ -115,16 +127,19 @@ fun MonthView(
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f))
|
||||
|
||||
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
||||
items(weekCount) { index ->
|
||||
val weekStart = firstVisible.plusWeeks(index.toLong())
|
||||
WeekRow(
|
||||
weekStart = weekStart,
|
||||
weekStart = firstVisible.plusWeeks(index.toLong()),
|
||||
today = today,
|
||||
eventsByDay = eventsByDay,
|
||||
lang = lang,
|
||||
dividerColor = dividerColor,
|
||||
gridColor = gridColor,
|
||||
labelColor = labelColor,
|
||||
secondaryText = secondaryText,
|
||||
todayColor = colorFromHex(settings.todayColor),
|
||||
onDayClick = onDayClick,
|
||||
onDayLongPress = onDayLongPress,
|
||||
onEventClick = onEventClick,
|
||||
@@ -134,37 +149,67 @@ fun MonthView(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun WeekRow(
|
||||
weekStart: LocalDate,
|
||||
today: LocalDate,
|
||||
eventsByDay: Map<LocalDate, List<CalEvent>>,
|
||||
lang: String,
|
||||
dividerColor: Color,
|
||||
gridColor: Color,
|
||||
labelColor: Color,
|
||||
secondaryText: Color,
|
||||
todayColor: Color,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
onDayLongPress: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
) {
|
||||
val days = (0 until 7).map { weekStart.plusDays(it.toLong()) }
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(78.dp),
|
||||
) {
|
||||
days.forEach { day ->
|
||||
val boundaryCol = (1 until 7).firstOrNull { days[it].dayOfMonth == 1 }
|
||||
val rowStartsNewMonth = days[0].dayOfMonth == 1
|
||||
val cwLabel = tr("cal.cw")
|
||||
|
||||
BoxWithConstraints(Modifier.fillMaxWidth().height(ROW_HEIGHT)) {
|
||||
val cellW = maxWidth / 7
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
days.forEachIndexed { idx, day ->
|
||||
val edge = when {
|
||||
boundaryCol != null -> if (idx < boundaryCol) DividerEdge.BOTTOM else DividerEdge.TOP
|
||||
rowStartsNewMonth -> DividerEdge.TOP
|
||||
else -> DividerEdge.NONE
|
||||
}
|
||||
DayCell(
|
||||
day = day,
|
||||
isToday = day == today,
|
||||
isMonday = day.dayOfWeek == DayOfWeek.MONDAY,
|
||||
weekNumber = day.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR),
|
||||
cwLabel = cwLabel,
|
||||
events = eventsByDay[day] ?: emptyList(),
|
||||
lang = lang,
|
||||
edge = edge,
|
||||
dividerColor = dividerColor,
|
||||
gridColor = gridColor,
|
||||
labelColor = labelColor,
|
||||
secondaryText = secondaryText,
|
||||
todayColor = todayColor,
|
||||
onClick = { onDayClick(day) },
|
||||
onLongClick = { onDayLongPress(day) },
|
||||
onEventClick = onEventClick,
|
||||
modifier = Modifier.weight(1f).fillMaxSize(),
|
||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Vertical connector at the month boundary column (the "step").
|
||||
if (boundaryCol != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.offset(x = cellW * boundaryCol)
|
||||
.width(1.5.dp)
|
||||
.fillMaxHeight()
|
||||
.background(dividerColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -172,54 +217,85 @@ private fun WeekRow(
|
||||
private fun DayCell(
|
||||
day: LocalDate,
|
||||
isToday: Boolean,
|
||||
isMonday: Boolean,
|
||||
weekNumber: Int,
|
||||
cwLabel: String,
|
||||
events: List<CalEvent>,
|
||||
lang: String,
|
||||
edge: DividerEdge,
|
||||
dividerColor: Color,
|
||||
gridColor: Color,
|
||||
labelColor: Color,
|
||||
secondaryText: Color,
|
||||
todayColor: Color,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val settings = LocalAppSettings.current
|
||||
val todayColor = colorFromHex(settings.todayColor)
|
||||
val monthLabelColor = colorFromHex(settings.monthLabelColor, MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
val isFirst = day.dayOfMonth == 1
|
||||
val onBg = MaterialTheme.colorScheme.onBackground
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
val gridW = 0.5.dp.toPx()
|
||||
val divW = 1.5.dp.toPx()
|
||||
// top border (thick divider on a month boundary, otherwise thin grid)
|
||||
if (edge == DividerEdge.TOP) {
|
||||
drawLine(dividerColor, Offset(0f, 0f), Offset(size.width, 0f), divW)
|
||||
} else {
|
||||
drawLine(gridColor, Offset(0f, 0f), Offset(size.width, 0f), gridW)
|
||||
}
|
||||
// bottom border only when the month ends inside this row
|
||||
if (edge == DividerEdge.BOTTOM) {
|
||||
drawLine(dividerColor, Offset(0f, size.height), Offset(size.width, size.height), divW)
|
||||
}
|
||||
// right grid line
|
||||
drawLine(gridColor, Offset(size.width, 0f), Offset(size.width, size.height), gridW)
|
||||
}
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.padding(horizontal = 1.dp, vertical = 2.dp),
|
||||
.padding(horizontal = 1.dp),
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(top = 3.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isFirst) {
|
||||
Text(
|
||||
day.month.getDisplayName(TextStyle.SHORT, com.scarriffle.calendarr.ui.L10n.locale(lang)),
|
||||
day.month.getDisplayName(TextStyle.SHORT, com.scarriffle.calendarr.ui.L10n.locale(lang)).uppercase(),
|
||||
fontSize = 8.sp,
|
||||
color = monthLabelColor,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = labelColor,
|
||||
modifier = Modifier.padding(end = 2.dp),
|
||||
)
|
||||
}
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (isToday) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(18.dp)
|
||||
.width(18.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
.background(todayColor),
|
||||
)
|
||||
Box(Modifier.height(18.dp).width(18.dp).clip(RoundedCornerShape(9.dp)).background(todayColor))
|
||||
}
|
||||
Text(
|
||||
"${day.dayOfMonth}",
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (isToday) todayColor.contrastingTextColor() else MaterialTheme.colorScheme.onBackground,
|
||||
color = if (isToday) todayColor.contrastingTextColor() else onBg,
|
||||
)
|
||||
}
|
||||
}
|
||||
events.take(3).forEach { ev -> EventChip(ev) { onEventClick(ev) } }
|
||||
if (events.size > 3) {
|
||||
Text("+${events.size - 3}", fontSize = 8.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text("+${events.size - 3}", fontSize = 8.sp, color = secondaryText)
|
||||
}
|
||||
}
|
||||
// Calendar week number, bottom-right of the Monday cell.
|
||||
if (isMonday) {
|
||||
Text(
|
||||
"$cwLabel $weekNumber",
|
||||
fontSize = 8.sp,
|
||||
color = secondaryText,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 3.dp, bottom = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +333,6 @@ private fun buildEventsByDay(events: List<CalEvent>): Map<LocalDate, List<CalEve
|
||||
val map = HashMap<LocalDate, MutableList<CalEvent>>()
|
||||
for (ev in events) {
|
||||
val first = localDate(ev.startDate)
|
||||
// end is exclusive; the last covered day is the instant just before it
|
||||
val last = localDate(ev.endDate.minusSeconds(1)).coerceAtLeast(first)
|
||||
var d = first
|
||||
var guard = 0
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.scarriffle.calendarr.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
import com.scarriffle.calendarr.util.toHex
|
||||
|
||||
/**
|
||||
* Full HSV colour picker: saturation/value field + hue slider + hex input.
|
||||
* Returns a "#RRGGBB" string via [onConfirm].
|
||||
*/
|
||||
@Composable
|
||||
fun ColorPickerDialog(
|
||||
initial: String,
|
||||
title: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String) -> Unit,
|
||||
) {
|
||||
val initHsv = remember(initial) {
|
||||
FloatArray(3).also { android.graphics.Color.colorToHSV(colorFromHex(initial).toArgb(), it) }
|
||||
}
|
||||
var hue by remember { mutableFloatStateOf(initHsv[0]) }
|
||||
var sat by remember { mutableFloatStateOf(initHsv[1]) }
|
||||
var value by remember { mutableFloatStateOf(initHsv[2]) }
|
||||
var hexText by remember { mutableStateOf(colorFromHex(initial).toHex()) }
|
||||
|
||||
val current = Color.hsv(hue, sat, value)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
// Saturation / Value field
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(170.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Brush.horizontalGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))))
|
||||
.background(Brush.verticalGradient(listOf(Color.Transparent, Color.Black)))
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { o ->
|
||||
sat = (o.x / size.width).coerceIn(0f, 1f)
|
||||
value = (1f - o.y / size.height).coerceIn(0f, 1f)
|
||||
hexText = Color.hsv(hue, sat, value).toHex()
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures { change, _ ->
|
||||
change.consume()
|
||||
sat = (change.position.x / size.width).coerceIn(0f, 1f)
|
||||
value = (1f - change.position.y / size.height).coerceIn(0f, 1f)
|
||||
hexText = Color.hsv(hue, sat, value).toHex()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Canvas(Modifier.matchParentSize()) {
|
||||
val cx = sat * size.width
|
||||
val cy = (1f - value) * size.height
|
||||
drawCircle(Color.White, radius = 9f, center = Offset(cx, cy), style = Stroke(width = 3f))
|
||||
drawCircle(Color.Black, radius = 12f, center = Offset(cx, cy), style = Stroke(width = 1f))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
// Hue slider
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(26.dp)
|
||||
.clip(RoundedCornerShape(13.dp))
|
||||
.background(Brush.horizontalGradient((0..6).map { Color.hsv(it * 60f, 1f, 1f) }))
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { o ->
|
||||
hue = (o.x / size.width * 360f).coerceIn(0f, 360f)
|
||||
hexText = Color.hsv(hue, sat, value).toHex()
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures { change, _ ->
|
||||
change.consume()
|
||||
hue = (change.position.x / size.width * 360f).coerceIn(0f, 360f)
|
||||
hexText = Color.hsv(hue, sat, value).toHex()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Canvas(Modifier.matchParentSize()) {
|
||||
val cx = (hue / 360f) * size.width
|
||||
drawCircle(Color.White, radius = 10f, center = Offset(cx, size.height / 2f), style = Stroke(width = 3f))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(40.dp).clip(CircleShape).background(current).border(1.dp, MaterialTheme.colorScheme.outline, CircleShape))
|
||||
Spacer(Modifier.size(12.dp))
|
||||
OutlinedTextField(
|
||||
value = hexText,
|
||||
onValueChange = { raw ->
|
||||
hexText = raw
|
||||
val parsed = colorFromHex(raw, Color.Unspecified)
|
||||
if (parsed != Color.Unspecified) {
|
||||
val hsv = FloatArray(3)
|
||||
android.graphics.Color.colorToHSV(parsed.toArgb(), hsv)
|
||||
hue = hsv[0]; sat = hsv[1]; value = hsv[2]
|
||||
}
|
||||
},
|
||||
label = { Text("Hex") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = { onConfirm(current.toHex()) }) { Text("OK") } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text("Abbrechen") } },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.scarriffle.calendarr.ui.event
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Dns
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Notes
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||
import com.scarriffle.calendarr.ui.LocalLang
|
||||
import com.scarriffle.calendarr.ui.calendar.eventDateRange
|
||||
import com.scarriffle.calendarr.ui.tr
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
|
||||
/** Full-screen event detail (mirrors the iOS detail page). */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EventDetailScreen(
|
||||
event: CalEvent,
|
||||
onClose: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val lang = LocalLang.current
|
||||
var confirmDelete by remember { mutableStateOf(false) }
|
||||
val canEdit = event.source in setOf("local", "caldav", "homeassistant", "google")
|
||||
val canDelete = canEdit
|
||||
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(tr("event.detail_title")) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onClose) { Icon(Icons.Filled.Close, contentDescription = tr("common.close")) }
|
||||
},
|
||||
actions = {
|
||||
if (canEdit) {
|
||||
TextButton(onClick = onEdit) { Text(tr("event.edit_title")) }
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(20.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
Box(
|
||||
Modifier.width(6.dp).height(46.dp).clip(RoundedCornerShape(3.dp))
|
||||
.background(colorFromHex(event.effectiveColor)),
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(event.title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold)
|
||||
if (event.calendarName.isNotBlank()) {
|
||||
Text(event.calendarName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
DetailRow(Icons.Filled.Schedule, eventDateRange(event, lang))
|
||||
if (event.location.isNotBlank()) DetailRow(Icons.Filled.LocationOn, event.location)
|
||||
if (event.notes.isNotBlank()) DetailRow(Icons.Filled.Notes, event.notes)
|
||||
if (event.calendarName.isNotBlank()) DetailRow(Icons.Filled.CalendarMonth, event.calendarName)
|
||||
DetailRow(Icons.Filled.Dns, event.source.replaceFirstChar { it.uppercase() })
|
||||
|
||||
Spacer(Modifier.height(28.dp))
|
||||
OutlinedButton(onClick = onCopy, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(tr("event.copy_title"))
|
||||
}
|
||||
if (canDelete) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = { confirmDelete = true },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(tr("common.delete"))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmDelete) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmDelete = false },
|
||||
title = { Text(tr("common.delete")) },
|
||||
text = { Text(tr("event.delete_confirm")) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { confirmDelete = false; onDelete() }) {
|
||||
Text(tr("common.delete"), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { confirmDelete = false }) { Text(tr("common.cancel")) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(icon: ImageVector, text: String) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.Top) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package com.scarriffle.calendarr.ui.event
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Notes
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||
import com.scarriffle.calendarr.ui.LocalLang
|
||||
import com.scarriffle.calendarr.ui.calendar.eventDateRange
|
||||
import com.scarriffle.calendarr.ui.tr
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun EventDetailSheet(
|
||||
event: CalEvent,
|
||||
onDismiss: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
) {
|
||||
val lang = LocalLang.current
|
||||
var confirmDelete by remember { mutableStateOf(false) }
|
||||
val canEdit = event.source == "local" || event.source == "caldav"
|
||||
val canDelete = event.source == "local" || event.source == "caldav" || event.source == "homeassistant"
|
||||
|
||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||
Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 28.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(16.dp).clip(CircleShape).background(colorFromHex(event.effectiveColor)))
|
||||
Spacer(Modifier.size(10.dp))
|
||||
Text(
|
||||
event.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.size(16.dp))
|
||||
DetailRow(Icons.Filled.Schedule, eventDateRange(event, lang))
|
||||
if (event.calendarName.isNotBlank()) {
|
||||
DetailRow(Icons.Filled.CalendarMonth, event.calendarName)
|
||||
}
|
||||
if (event.location.isNotBlank()) {
|
||||
DetailRow(Icons.Filled.LocationOn, event.location)
|
||||
}
|
||||
if (event.notes.isNotBlank()) {
|
||||
DetailRow(Icons.Filled.Notes, event.notes)
|
||||
}
|
||||
Spacer(Modifier.size(20.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (canEdit) {
|
||||
OutlinedButton(onClick = onEdit) {
|
||||
Icon(Icons.Filled.Edit, contentDescription = null)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(tr("event.edit_title"))
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = onCopy) {
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = null)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(tr("event.copy_title"))
|
||||
}
|
||||
if (canDelete) {
|
||||
OutlinedButton(onClick = { confirmDelete = true }) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = null, tint = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(tr("common.delete"), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmDelete) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmDelete = false },
|
||||
title = { Text(tr("common.delete")) },
|
||||
text = { Text(tr("event.delete_confirm")) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { confirmDelete = false; onDelete() }) {
|
||||
Text(tr("common.delete"), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmDelete = false }) { Text(tr("common.cancel")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(icon: ImageVector, text: String) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), verticalAlignment = Alignment.Top) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.scarriffle.calendarr.domain.model.AppSettings
|
||||
import com.scarriffle.calendarr.domain.model.CalViewType
|
||||
import com.scarriffle.calendarr.ui.LocalAppSettings
|
||||
import com.scarriffle.calendarr.ui.components.ColorPickerDialog
|
||||
import com.scarriffle.calendarr.ui.tr
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
import com.scarriffle.calendarr.util.contrastingTextColor
|
||||
import com.scarriffle.calendarr.util.toHex
|
||||
|
||||
private val PALETTE = listOf(
|
||||
"#4285f4", "#ea4335", "#34a853", "#fbbc05", "#46bdc6", "#9c27b0", "#ff7043", "#7090c0",
|
||||
@@ -116,9 +117,11 @@ fun SettingsScreen(
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Section(tr("settings.colors"))
|
||||
ColorChooser(tr("settings.color.primary"), settings.primaryColor) { update(settings.copy(primaryColor = it)) }
|
||||
ColorChooser(tr("settings.color.accent"), settings.accentColor) { update(settings.copy(accentColor = it)) }
|
||||
ColorChooser(tr("settings.color.today"), settings.todayColor) { update(settings.copy(todayColor = it)) }
|
||||
ColorRow(tr("settings.color.primary"), settings.primaryColor) { update(settings.copy(primaryColor = it)) }
|
||||
ColorRow(tr("settings.color.accent"), settings.accentColor) { update(settings.copy(accentColor = it)) }
|
||||
ColorRow(tr("settings.color.today"), settings.todayColor) { update(settings.copy(todayColor = it)) }
|
||||
ColorRow(tr("settings.color.divider"), settings.monthDividerColor) { update(settings.copy(monthDividerColor = it)) }
|
||||
ColorRow(tr("settings.color.label"), settings.monthLabelColor) { update(settings.copy(monthLabelColor = it)) }
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Section(tr("settings.hourheight"))
|
||||
@@ -182,35 +185,24 @@ private fun ChipRow(options: List<Pair<String, String>>, selected: String, onSel
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun ColorChooser(label: String, current: String, onPick: (String) -> Unit) {
|
||||
// Show the user's current colour even if it isn't one of the presets.
|
||||
val swatches = remember(current) {
|
||||
(if (PALETTE.any { it.equals(current, ignoreCase = true) }) PALETTE else listOf(current) + PALETTE)
|
||||
}
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(18.dp).clip(CircleShape).background(colorFromHex(current)))
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
FlowRow(
|
||||
Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
private fun ColorRow(label: String, current: String, onPick: (String) -> Unit) {
|
||||
var showPicker by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
Modifier.fillMaxWidth().clickable { showPicker = true }.padding(vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
swatches.forEach { hex ->
|
||||
val selected = hex.equals(current, ignoreCase = true)
|
||||
Box(
|
||||
Modifier.size(34.dp).clip(CircleShape).background(colorFromHex(hex))
|
||||
.then(if (selected) Modifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else Modifier)
|
||||
.clickable { onPick(hex) },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (selected) Icon(Icons.Filled.Check, contentDescription = null, tint = colorFromHex(hex).contrastingTextColor(), modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
Box(Modifier.size(28.dp).clip(CircleShape).background(colorFromHex(current)).border(1.dp, MaterialTheme.colorScheme.outline, CircleShape))
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Text(colorFromHex(current).toHex(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
if (showPicker) {
|
||||
ColorPickerDialog(
|
||||
initial = current,
|
||||
title = label,
|
||||
onDismiss = { showPicker = false },
|
||||
onConfirm = { showPicker = false; onPick(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable-nodpi/ic_splash_logo.png
Normal file
BIN
app/src/main/res/drawable-nodpi/ic_splash_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The launcher icon inset so it sits comfortably inside the round splash mask. -->
|
||||
<!-- High-res logo inset so it sits comfortably inside the round splash mask. -->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@mipmap/ic_launcher"
|
||||
android:drawable="@drawable/ic_splash_logo"
|
||||
android:inset="18%" />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<resources>
|
||||
<style name="Theme.Calendarr" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">@color/splash_bg</item>
|
||||
<item name="android:statusBarColor">@color/splash_bg</item>
|
||||
<item name="android:navigationBarColor">@color/splash_bg</item>
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<!-- Android 12+ system splash screen -->
|
||||
<item name="android:windowSplashScreenBackground">@color/splash_bg</item>
|
||||
<item name="android:windowSplashScreenBackground">@android:color/black</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<resources>
|
||||
<color name="splash_bg">#0B1220</color>
|
||||
<color name="splash_bg">#000000</color>
|
||||
|
||||
<style name="Theme.Calendarr" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Dark window background avoids a white flash before Compose draws. -->
|
||||
<!-- Black window/system bars (matches the app background). -->
|
||||
<item name="android:windowBackground">@color/splash_bg</item>
|
||||
<item name="android:statusBarColor">@color/splash_bg</item>
|
||||
<item name="android:navigationBarColor">@color/splash_bg</item>
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user