diff --git a/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt b/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt index 8af7e9e..54ee865 100644 --- a/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt +++ b/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt @@ -3,6 +3,7 @@ package com.scarriffle.calendarr import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.scarriffle.calendarr.ui.CalendarrRoot import dagger.hilt.android.AndroidEntryPoint @@ -13,6 +14,10 @@ class MainActivity : ComponentActivity() { // Covers the window from the first frame (incl. warm-start), then hands // off to the in-app branded splash which stays until data is loaded. installSplashScreen() + // Draw edge-to-edge so system-bar insets reach every window, including + // ModalBottomSheets — otherwise navigationBarsPadding() inside the sheets + // resolves to 0 and the bottom menu rows hide behind the nav bar. + enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { CalendarrRoot() diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt b/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt index 75ff9b6..4ef00b3 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt @@ -1,6 +1,8 @@ package com.scarriffle.calendarr.ui +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -48,12 +50,18 @@ fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) { } when (route) { - AppRoute.SETUP -> ServerSetupScreen(onConfigured = vm::onServerConfigured) - AppRoute.LOGIN -> LoginScreen( - serverUrl = vm.serverUrl, - onLoggedIn = vm::onLoggedIn, - onBack = vm::switchServer, - ) + // Auth screens draw full-screen; inset them from the system + // bars (edge-to-edge) so their content isn't under status/nav. + AppRoute.SETUP -> Box(Modifier.systemBarsPadding()) { + ServerSetupScreen(onConfigured = vm::onServerConfigured) + } + AppRoute.LOGIN -> Box(Modifier.systemBarsPadding()) { + LoginScreen( + serverUrl = vm.serverUrl, + onLoggedIn = vm::onLoggedIn, + onBack = vm::switchServer, + ) + } AppRoute.MAIN -> calendarVm?.let { cvm -> CalendarScreen( vm = cvm, diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt index 554ddbb..66f905a 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt @@ -11,6 +11,7 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -430,7 +431,7 @@ private fun SharingSheet(vm: AccountsViewModel, calendarId: Int, onDismiss: () - ModalBottomSheet(onDismissRequest = onDismiss) { Column( - Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 24.dp).verticalScroll(rememberScrollState()), + Modifier.fillMaxWidth().navigationBarsPadding().padding(horizontal = 20.dp).padding(bottom = 24.dp).verticalScroll(rememberScrollState()), ) { Text(tr("share.title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) Spacer(Modifier.size(16.dp)) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt index 2959ca4..167da4b 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt @@ -6,8 +6,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Archive @@ -60,7 +63,12 @@ fun CalendarFilterSheet( val hiddenSet = if (groupMode) state.hiddenGroupKeys else state.hiddenKeys ModalBottomSheet(onDismissRequest = onDismiss) { - Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { + Column( + Modifier.fillMaxWidth() + .verticalScroll(rememberScrollState()) + .navigationBarsPadding() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index b968596..dd79b8f 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -11,11 +11,14 @@ 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.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ChevronLeft @@ -169,12 +172,17 @@ fun CalendarScreen( shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.navigationBarsPadding(), ) { Icon(Icons.Filled.Add, contentDescription = tr("cal.new_event")) } }, + // Edge-to-edge: we place the system-bar insets ourselves (top bar gets + // statusBarsPadding, the content column gets navigationBarsPadding) so + // nothing hides behind the status/nav bars. + contentWindowInsets = WindowInsets(0), ) { padding -> - Column(Modifier.fillMaxSize().padding(padding)) { + Column(Modifier.fillMaxSize().padding(padding).navigationBarsPadding()) { state.error?.let { err -> ErrorBanner(err, onRetry = { vm.loadVisible(force = true) }, onDismiss = vm::clearError) } @@ -223,8 +231,9 @@ fun CalendarScreen( } if (showFilter) { + androidx.compose.runtime.LaunchedEffect(Unit) { vm.loadAllCalendars() } CalendarFilterSheet( - events = remember(state.events, state.banishedKeys) { allKnownCalendars(vm) }, + events = remember(state.events, state.banishedKeys, state.allCalendars) { allKnownCalendars(vm) }, vm = vm, onDismiss = { showFilter = false }, ) @@ -405,8 +414,9 @@ private fun CompactTopBar( onSelectView: (CalViewType) -> Unit, ) { val twoLine = viewType == CalViewType.WEEK || viewType == CalViewType.DAY + // Surface fills behind the status bar; the content column is inset below it. Surface(color = MaterialTheme.colorScheme.background) { - Column { + Column(Modifier.statusBarsPadding()) { Row( Modifier.fillMaxWidth().height(50.dp).padding(horizontal = 2.dp), verticalAlignment = Alignment.CenterVertically, @@ -490,8 +500,12 @@ private fun GroupBanner(group: Group, onExit: () -> Unit) { * cache (minus banished) so a locally quick-hidden calendar still shows up and * can be toggled back on. */ private fun allKnownCalendars(vm: CalendarViewModel): List { - return vm.knownCalendars() + val fromEvents = vm.knownCalendars() .map { CalendarFilterEntry(calendarKey(it.source, it.calendarId), it.calendarName.ifBlank { it.source }, it.effectiveColor, it.source, it.readOnly) } + // Event-derived entries first (current server colour / owner name), then the + // full source list so calendars WITHOUT events in range still appear; + // distinctBy keeps the event-derived entry when a calendar has both. + return (fromEvents + vm.state.value.allCalendars) .distinctBy { it.key } .sortedBy { it.name.lowercase() } } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index 3df4b2c..15b5ea7 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -55,6 +55,9 @@ data class CalendarUiState( // hidden keys ("gm:" / "gc"). In-memory; reset when switching group. val activeGroupMembers: List = emptyList(), val hiddenGroupKeys: Set = emptySet(), + // Full calendar list across all sources (loaded on demand) so the filter + // shows every calendar, including ones with no events in the loaded range. + val allCalendars: List = emptyList(), ) fun groupMemberKey(ownerId: Int): String = "gm:$ownerId" @@ -449,6 +452,47 @@ class CalendarViewModel @Inject constructor( .filter { calendarKey(it.source, it.calendarId) !in banished } } + /** + * Load the full calendar list across all sources into [CalendarUiState.allCalendars] + * so the filter shows every calendar, even ones with no events in the loaded + * range. Called when the filter sheet opens. Read-only flag for shared local + * calendars from owned/permission; banished calendars are dropped. + */ + fun loadAllCalendars() { + viewModelScope.launch { + val banished = _state.value.banishedKeys + val entries = mutableListOf() + runCatching { repository.getLocalCalendars() }.getOrDefault(emptyList()).forEach { c -> + entries += CalendarFilterEntry( + key = calendarKey("local", c.id.toString()), + name = if (c.owned) c.name else (c.sharedBy ?: c.name), + color = c.color, + source = "local", + readOnly = !c.owned && c.permission != "read_write", + ) + } + runCatching { repository.getCalDAVAccounts() }.getOrDefault(emptyList()).forEach { acc -> + acc.calendars.orEmpty().forEach { c -> + entries += CalendarFilterEntry(calendarKey("caldav", c.id.toString()), c.name, c.color ?: acc.color, "caldav") + } + } + runCatching { repository.getGoogleAccounts() }.getOrDefault(emptyList()).forEach { acc -> + acc.calendars.orEmpty().forEach { c -> + entries += CalendarFilterEntry(calendarKey("google", c.id.toString()), c.name, c.color ?: "#4285f4", "google") + } + } + runCatching { repository.getHomeAssistantAccounts() }.getOrDefault(emptyList()).forEach { acc -> + acc.calendars.orEmpty().forEach { c -> + entries += CalendarFilterEntry(calendarKey("homeassistant", c.id.toString()), c.name, c.color ?: "#46bdc6", "homeassistant") + } + } + runCatching { repository.getICalSubscriptions() }.getOrDefault(emptyList()).forEach { s -> + entries += CalendarFilterEntry(calendarKey("ical", s.id.toString()), s.name, s.color, "ical") + } + _state.update { st -> st.copy(allCalendars = entries.filter { it.key !in banished }) } + } + } + /** * Banish ("permanently hide") a calendar, or lift the banish. Unlike the * quick-hide, this DOES sync to the server (`sidebar_hidden`/`enabled`) for diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt index 61021d5..73cf958 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt @@ -11,6 +11,8 @@ 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.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -149,6 +151,8 @@ fun EventEditorSheet( Column( Modifier .fillMaxWidth() + .navigationBarsPadding() + .imePadding() .verticalScroll(rememberScrollState()) .padding(horizontal = 20.dp) .padding(bottom = 32.dp), diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt index d57c822..92cfb05 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt @@ -11,6 +11,7 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -232,7 +233,7 @@ private fun GroupEditSheet( } ModalBottomSheet(onDismissRequest = onDismiss) { - Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 24.dp).verticalScroll(rememberScrollState())) { + Column(Modifier.fillMaxWidth().navigationBarsPadding().padding(horizontal = 20.dp).padding(bottom = 24.dp).verticalScroll(rememberScrollState())) { Text( if (existing == null) tr("groups.create") else tr("groups.manage"), style = MaterialTheme.typography.titleLarge,